@sublang/playbook 0.9.0 → 1.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 (51) hide show
  1. package/README.md +190 -151
  2. package/package.json +50 -6
  3. package/reference/sdlc/captain.md +102 -0
  4. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +227 -0
  5. package/reference/sdlc/captain.playbook/captain.fsm.js +628 -0
  6. package/reference/sdlc/captain.playbook/captain.fsm.ts +851 -0
  7. package/reference/sdlc/captain.playbook/captain.gears.md +60 -0
  8. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +23 -0
  9. package/reference/sdlc/captain.playbook/captain.playbook.js +1053 -0
  10. package/reference/sdlc/captain.playbook/captain.playbook.ts +1144 -0
  11. package/reference/sdlc/code.playbook/bin/playbook.js +158 -12
  12. package/reference/sdlc/code.playbook/bin/run.js +999 -0
  13. package/reference/sdlc/code.playbook/code.fsm.d.ts +11 -4
  14. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +2 -2
  15. package/reference/sdlc/code.playbook/code.fsm.introspect.js +1 -1
  16. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +6 -6
  17. package/reference/sdlc/code.playbook/code.fsm.js +334 -102
  18. package/reference/sdlc/code.playbook/code.fsm.ts +467 -180
  19. package/reference/sdlc/code.playbook/code.gears.md +11 -10
  20. package/reference/sdlc/code.playbook/code.playbook.d.ts +16 -19
  21. package/reference/sdlc/code.playbook/code.playbook.js +199 -488
  22. package/reference/sdlc/code.playbook/code.playbook.ts +327 -566
  23. package/reference/sdlc/code.playbook/code.registry.d.ts +0 -3
  24. package/reference/sdlc/code.playbook/code.registry.js +0 -3
  25. package/reference/sdlc/code.playbook/code.registry.ts +0 -6
  26. package/reference/sdlc/code.playbook/playbook-captain.d.ts +9 -4
  27. package/reference/sdlc/code.playbook/playbook-captain.js +889 -210
  28. package/reference/sdlc/code.playbook/playbook-captain.ts +1136 -257
  29. package/reference/sdlc/code.playbook/playbook.config.template.yaml +21 -0
  30. package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +396 -0
  31. package/reference/sdlc/discuss.playbook/discuss.fsm.js +2066 -0
  32. package/reference/sdlc/discuss.playbook/discuss.fsm.ts +2464 -0
  33. package/reference/sdlc/discuss.playbook/discuss.gears.md +251 -0
  34. package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +113 -0
  35. package/reference/sdlc/discuss.playbook/discuss.playbook.js +1514 -0
  36. package/reference/sdlc/discuss.playbook/discuss.playbook.ts +1926 -0
  37. package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +58 -0
  38. package/reference/sdlc/discuss.playbook/discuss.registry.js +97 -0
  39. package/reference/sdlc/discuss.playbook/discuss.registry.ts +153 -0
  40. package/slc/gears2fsm.md +557 -57
  41. package/slc/link.md +1165 -89
  42. package/slc/optimize.md +92 -0
  43. package/slc/text2gears.md +255 -7
  44. package/src/runtime.d.ts +146 -3
  45. package/src/runtime.ts +201 -2
  46. package/src/xstate-playbook-runtime.d.ts +201 -0
  47. package/src/xstate-playbook-runtime.js +2058 -0
  48. package/src/xstate-playbook-runtime.ts +2792 -0
  49. package/src/xstate-runtime.d.ts +95 -0
  50. package/src/xstate-runtime.js +1258 -0
  51. package/src/xstate-runtime.ts +1816 -0
package/src/runtime.ts CHANGED
@@ -10,24 +10,223 @@
10
10
 
11
11
  export interface PlayerResult {
12
12
  status: 'ok' | 'aborted' | 'error';
13
+ resumeToken?: string;
13
14
  finalText?: string;
14
15
  error?: string;
15
16
  }
16
17
 
18
+ export interface PlayerCallOptions {
19
+ resume: string | false;
20
+ }
21
+
22
+ export interface CaptainCallOptions {
23
+ visibility: 'visible' | 'hidden';
24
+ resume: string | false;
25
+ allowedTools?: readonly string[];
26
+ }
27
+
28
+ export interface CaptainResult {
29
+ status: 'ok' | 'aborted' | 'error';
30
+ finalText?: string;
31
+ error?: string;
32
+ }
33
+
34
+ export type JsonValue =
35
+ | null
36
+ | boolean
37
+ | number
38
+ | string
39
+ | readonly JsonValue[]
40
+ | { readonly [key: string]: JsonValue };
41
+
42
+ export interface NormalizedError {
43
+ name: string;
44
+ message: string;
45
+ stack?: string;
46
+ }
47
+
48
+ export type PlaybookStateValue =
49
+ | string
50
+ | { readonly [key: string]: PlaybookStateValue };
51
+
52
+ export interface PlaybookState {
53
+ value: PlaybookStateValue;
54
+ activeStateIds: readonly string[];
55
+ tags: readonly string[];
56
+ status: 'active' | 'done' | 'error' | 'stopped';
57
+ quiescent: boolean;
58
+ stateId?: string;
59
+ }
60
+
61
+ export interface PlaybookPendingCall {
62
+ callId: string;
63
+ playbookId: string;
64
+ childSessionId: string;
65
+ }
66
+
67
+ export interface PlaybookCallRequest {
68
+ callId: string;
69
+ playbookId: string;
70
+ text: string;
71
+ }
72
+
73
+ export type PlaybookCallResult =
74
+ | {
75
+ status: 'ok';
76
+ playbookId: string;
77
+ childSessionId: string;
78
+ state?: PlaybookState;
79
+ output?: JsonValue;
80
+ }
81
+ | {
82
+ status: 'aborted';
83
+ playbookId: string;
84
+ childSessionId?: string;
85
+ state?: PlaybookState;
86
+ error?: NormalizedError;
87
+ }
88
+ | {
89
+ status: 'error';
90
+ playbookId: string;
91
+ childSessionId?: string;
92
+ state?: PlaybookState;
93
+ error: NormalizedError;
94
+ };
95
+
96
+ export type PlaybookCallStart =
97
+ | { state: 'settled'; result: PlaybookCallResult }
98
+ | { state: 'suspended'; childSessionId: string };
99
+
100
+ export type PlaybookRunResult =
101
+ | { outcome: 'quiescent' | 'no-action'; state: PlaybookState }
102
+ | {
103
+ outcome: 'failed' | 'aborted';
104
+ state: PlaybookState;
105
+ error?: NormalizedError;
106
+ }
107
+ | {
108
+ outcome: 'terminal';
109
+ state: PlaybookState;
110
+ output?: JsonValue;
111
+ }
112
+ | {
113
+ outcome: 'suspended';
114
+ state: PlaybookState;
115
+ pendingCall: PlaybookPendingCall;
116
+ };
117
+
17
118
  export interface PlaybookPorts {
18
119
  callPlayer(
19
120
  playerId: string,
20
121
  prompt: string,
21
122
  signal: AbortSignal,
123
+ options: PlayerCallOptions,
22
124
  ): Promise<PlayerResult>;
125
+ callCaptain(
126
+ prompt: string,
127
+ signal: AbortSignal,
128
+ options: CaptainCallOptions,
129
+ ): Promise<CaptainResult>;
23
130
  callJudge(prompt: string, signal: AbortSignal): Promise<string>;
131
+ callPlaybook(
132
+ request: PlaybookCallRequest,
133
+ signal: AbortSignal,
134
+ ): Promise<PlaybookCallStart>;
24
135
  emitStatus(message: string, data?: unknown): Promise<void>;
25
136
  emitTelemetry(event: { topic: string; payload: unknown }): Promise<void>;
26
137
  }
27
138
 
139
+ export interface PlaybookSession {
140
+ sessionId: string;
141
+ playbookId: string;
142
+ rootSessionId: string;
143
+ parentSessionId?: string;
144
+ parentCallId?: string;
145
+ depth: number;
146
+ ports: PlaybookPorts;
147
+ }
148
+
149
+ export type PlaybookTraceType =
150
+ | 'session.started'
151
+ | 'boss.input.received'
152
+ | 'judge.call.started'
153
+ | 'judge.call.finished'
154
+ | 'player.call.started'
155
+ | 'player.call.finished'
156
+ | 'captain.call.started'
157
+ | 'captain.call.finished'
158
+ | 'playbook.call.started'
159
+ | 'playbook.call.finished'
160
+ | 'fsm.transition'
161
+ | 'status.emitted'
162
+ | 'boss.input.settled'
163
+ | 'session.disposed';
164
+
165
+ export interface PlaybookTraceEvent {
166
+ schemaVersion: 2;
167
+ sessionId: string;
168
+ playbookId: string;
169
+ rootSessionId: string;
170
+ parentSessionId?: string;
171
+ parentCallId?: string;
172
+ depth: number;
173
+ sequence: number;
174
+ timestamp: number;
175
+ type: PlaybookTraceType;
176
+ turnId?: number;
177
+ callId?: string;
178
+ payload: JsonValue;
179
+ }
180
+
181
+ export interface PlaybookPendingBossQuestion {
182
+ questionId: string;
183
+ player: string;
184
+ question: string;
185
+ sourceItem?: string;
186
+ }
187
+
188
+ // DR-014 §1: JSON-safe capture of a parked session. `machine` is the
189
+ // XState persisted snapshot and is opaque to hosts; the pending Boss
190
+ // questions are first-class so a host can surface what was asked
191
+ // without parsing status lines or telemetry.
192
+ export interface PlaybookRuntimeSnapshot {
193
+ schemaVersion: 1;
194
+ playbookId: string;
195
+ machine: JsonValue;
196
+ playerResumeTokens: { readonly [playerId: string]: string };
197
+ sequences: {
198
+ trace: number;
199
+ turn: number;
200
+ judgeCall: number;
201
+ playerCall: number;
202
+ playbookCall: number;
203
+ captainCall?: number;
204
+ };
205
+ state: PlaybookState;
206
+ pendingBossQuestions: readonly PlaybookPendingBossQuestion[];
207
+ }
208
+
28
209
  export interface PlaybookRuntime {
29
- init(ports: PlaybookPorts): Promise<void>;
30
- handleBossInput(turn: { text: string; signal: AbortSignal }): Promise<void>;
210
+ init(session: PlaybookSession): Promise<void>;
211
+ // DR-014 §1 optional durable-session capability: a runtime implements
212
+ // both members or neither. `exportSnapshot` returns undefined outside
213
+ // a safe capture point (parked quiescence between public boundaries);
214
+ // `restore` is an alternative to `init` that rehydrates the exported
215
+ // snapshot under the same immutable session identity.
216
+ exportSnapshot?(): PlaybookRuntimeSnapshot | undefined;
217
+ restore?(
218
+ session: PlaybookSession,
219
+ snapshot: PlaybookRuntimeSnapshot,
220
+ ): Promise<void>;
221
+ handleBossInput(turn: {
222
+ text: string;
223
+ signal: AbortSignal;
224
+ }): Promise<PlaybookRunResult>;
225
+ resumePlaybookCall(input: {
226
+ callId: string;
227
+ result: PlaybookCallResult;
228
+ signal: AbortSignal;
229
+ }): Promise<PlaybookRunResult>;
31
230
  dispose(): Promise<void>;
32
231
  }
33
232
 
@@ -0,0 +1,201 @@
1
+ import type { AnyStateMachine, EventObject, PromiseActorLogic } from 'xstate';
2
+ import type { CaptainResult, JsonValue, PlaybookPorts, PlaybookRuntimeFactory, PlaybookSession, PlaybookState, PlayerResult } from './runtime.js';
3
+ export interface PlaybookPendingBossQuestionContext {
4
+ questionId: string;
5
+ resumeStateId: string;
6
+ sourceItem: string;
7
+ player: string;
8
+ question: string;
9
+ }
10
+ export interface PlaybookPlayerInput {
11
+ stateId: string;
12
+ player: string;
13
+ sourceItem: string;
14
+ prompt: string;
15
+ result: Readonly<Record<string, string>>;
16
+ pendingBossQuestion?: {
17
+ readonly question: string;
18
+ };
19
+ bossReply?: string;
20
+ }
21
+ export interface PlaybookCaptainInput {
22
+ stateId: string;
23
+ sourceItem: string;
24
+ prompt: string;
25
+ result: Readonly<Record<string, string>>;
26
+ allowedTools?: readonly string[];
27
+ pendingBossQuestion?: {
28
+ readonly question: string;
29
+ };
30
+ bossReply?: string;
31
+ }
32
+ export interface PlaybookScriptInput {
33
+ stateId: string;
34
+ sourceItem: string;
35
+ command: string;
36
+ result: Readonly<Record<string, string>>;
37
+ }
38
+ /** Adjudicated actor output: the selected guard plus payload fields. */
39
+ export type PlaybookActorOutput = Record<string, unknown> & {
40
+ guard: string;
41
+ };
42
+ export type JudgePurpose = 'boss-input-classification' | 'player-output-adjudication' | 'captain-output-adjudication';
43
+ /**
44
+ * Traced runtime boundary used by the provided actors. The factory's runtime
45
+ * implements it; standalone helpers accept it optionally so verification can
46
+ * exercise composition/adjudication without a live runtime.
47
+ */
48
+ export interface RuntimeBoundaryCalls {
49
+ callPlayer(input: PlaybookPlayerInput, playerId: string, prompt: string, signal: AbortSignal): Promise<PlayerResult>;
50
+ callJudge(purpose: JudgePurpose, stateId: string | undefined, prompt: string, signal: AbortSignal): Promise<string>;
51
+ callCaptain?(input: PlaybookCaptainInput, prompt: string, signal: AbortSignal): Promise<CaptainResult>;
52
+ }
53
+ export interface ScheduledStatus {
54
+ message: string;
55
+ data?: JsonValue;
56
+ }
57
+ export interface XStateBossEventFieldSpec {
58
+ /** The judge supplies routing data; the runtime supplies exact Boss text. */
59
+ source: 'judge' | 'text';
60
+ /** Judge-authored fields are optional unless explicitly required. */
61
+ required?: boolean;
62
+ /** Optional closed set for a string-valued judge field. */
63
+ values?: readonly string[];
64
+ }
65
+ export interface XStateBossEventSpec {
66
+ type: string;
67
+ fields?: Readonly<Record<string, XStateBossEventFieldSpec>>;
68
+ }
69
+ export declare const BOSS_REPLY_ERRORS: {
70
+ readonly missingQuestion: "needsBossReply outcome missing 'question' field";
71
+ readonly unregisteredState: (stateId: string) => string;
72
+ };
73
+ export interface XStatePlaybookRuntimeSpec<TOptions> {
74
+ /** Diagnostic label used in internal invariant errors. Default 'playbook'. */
75
+ label?: string;
76
+ /** Validate and JSON-snapshot the caller's per-run options. */
77
+ snapshotOptions: (value: unknown) => TOptions;
78
+ /** Derive the FSM machine input from validated options. Default: identity. */
79
+ machineInput?: (options: TOptions, session: PlaybookSession) => unknown;
80
+ /**
81
+ * Deterministic textual entry event (slc/link.md §Boss-event mapping):
82
+ * where the ready or reconstructed terminal machine accepts exactly one
83
+ * ordinary textual entry event, send it without a judge call, carrying the
84
+ * exact Boss text in `textField`. Absent: every non-empty turn classifies.
85
+ */
86
+ entryEvent?: {
87
+ type: string;
88
+ textField: string;
89
+ };
90
+ /**
91
+ * Exact flat Boss-event contracts whose non-text fields the judge may
92
+ * select. `entryEvent` and scalar `BOSS_REPLY` contracts are supplied by
93
+ * the factory; linkers emit entries here for additional typed events such
94
+ * as `BOSS_INTERRUPT` when their erased payload cannot be recovered from
95
+ * the XState machine alone.
96
+ */
97
+ bossEvents?: readonly XStateBossEventSpec[];
98
+ /** Boss-input classifier override; default: generic parked-state classifier. */
99
+ classifyBossText?: (text: string, ports: PlaybookPorts, signal: AbortSignal, snapshotOrState: unknown, boundary?: RuntimeBoundaryCalls) => Promise<EventObject | undefined>;
100
+ /** Status line emitted after classification names an event. Default: none. */
101
+ classificationStatus?: (event: EventObject) => string | undefined;
102
+ /** Map a player-invoking state's input to the host player id. Default: lowercased player name. */
103
+ resolvePlayerId?: (input: PlaybookPlayerInput, options: TOptions) => string;
104
+ /** Compose the player prompt. Default: continuation blocks + `<field>` placeholder substitution. */
105
+ composePlayerPrompt?: (input: PlaybookPlayerInput) => string;
106
+ /** Compose the direct-Captain prompt. Default: continuation blocks + placeholder substitution with deterministic JSON rendering. */
107
+ composeCaptainPrompt?: (input: PlaybookCaptainInput) => string;
108
+ /** Linker-known exceptions to the default kebab-token → camel-field mapping. */
109
+ placeholderFields?: Readonly<Record<string, string>>;
110
+ /** Adjudicator prompt for delegated players. Default: generic guard menu. */
111
+ buildJudgePrompt?: (input: PlaybookPlayerInput, finalText: string) => string;
112
+ /** Required-payload-field extraction from a `result` description. Default: bilingual `Output shall include` clause scan. */
113
+ extractRequiredFields?: (description: string) => string[];
114
+ /** Required fields carried verbatim from the player's finalText instead of judge JSON. Default: none. */
115
+ verbatimPayloadFields?: ReadonlySet<string>;
116
+ /** States that may suspend for a Boss reply. Default: targets of the FSM's `awaitBossReply` BOSS_REPLY transitions. */
117
+ resumableStateIds?: ReadonlySet<string>;
118
+ /** Human status lines for a root transition. Default: entry lines with question/failure surfacing. */
119
+ statusesForState?: (state: PlaybookState, context: Record<string, unknown>, event: unknown) => readonly ScheduledStatus[];
120
+ /** Detached JSON-safe transition-event descriptor. Default: `type` + `transitionEventFields` strings + validated output + normalized error. */
121
+ normalizeTransitionEvent?: (event: unknown) => JsonValue | undefined;
122
+ /** String payload fields the default transition-event descriptor copies. */
123
+ transitionEventFields?: readonly string[];
124
+ /** Working directory for `script` actors. Default: the validated options' string `cwd`, else the process working directory. */
125
+ scriptCwd?: (options: TOptions) => string | undefined;
126
+ }
127
+ /** Strip a single Markdown code fence that wraps the whole string. */
128
+ export declare function stripCodeFence(text: string): string;
129
+ export declare function extractJsonValue(text: string, start: number, repair: boolean): string | undefined;
130
+ export declare function parseJudgeJson(raw: string): unknown;
131
+ export declare function normalizeErrorCompact(err: unknown): {
132
+ name: string;
133
+ message: string;
134
+ } | undefined;
135
+ export declare function normalizeErrorFull(err: unknown): {
136
+ name: string;
137
+ message: string;
138
+ stack?: string;
139
+ } | undefined;
140
+ /** Read the FSM context's single pending Boss question, when well-formed. */
141
+ export declare function pendingBossQuestionFromContext(context: Record<string, unknown>): PlaybookPendingBossQuestionContext | undefined;
142
+ /**
143
+ * Default player-prompt composer (slc/link.md §Player prompt composition).
144
+ * One callback-based pass substitutes each `<fieldName>` placeholder whose
145
+ * typed input field is a string; replacement text is literal, and
146
+ * placeholder-looking text inside a value is never re-substituted. The
147
+ * continuation preamble and Q/A blocks precede the domain body on resume.
148
+ */
149
+ export declare function defaultComposePlayerPrompt(input: PlaybookPlayerInput, placeholderFields?: Readonly<Record<string, string>>): string;
150
+ /**
151
+ * Default direct-Captain prompt composer (slc/link.md §Captain prompt
152
+ * composition). Placeholder substitution is presence-based: string fields
153
+ * substitute verbatim; JSON-safe arrays/objects render as deterministic JSON
154
+ * with lexicographically sorted keys at every depth.
155
+ */
156
+ export declare function defaultComposeCaptainPrompt(input: PlaybookCaptainInput, placeholderFields?: Readonly<Record<string, string>>): string;
157
+ /** Default player binding: each player to its lowercased name. */
158
+ export declare function defaultResolvePlayerId(input: PlaybookPlayerInput): string;
159
+ /**
160
+ * Default required-field extraction (slc/link.md §Captain adjudication).
161
+ * Limited to the description's `Output shall include` / `输出应包含` clause;
162
+ * recognizes both the bare backticked name and the annotated `name: <...>`
163
+ * form.
164
+ */
165
+ export declare function defaultExtractRequiredFields(description: string): string[];
166
+ /** Default delegated-player adjudicator prompt. */
167
+ export declare function defaultBuildJudgePrompt(input: PlaybookPlayerInput, finalText: string): string;
168
+ export interface PlayerAdjudicationSpec {
169
+ buildJudgePrompt?: (input: PlaybookPlayerInput, finalText: string) => string;
170
+ extractRequiredFields?: (description: string) => string[];
171
+ verbatimPayloadFields?: ReadonlySet<string>;
172
+ }
173
+ /**
174
+ * LLM-judge adjudicator for delegated players. Coerces the player's
175
+ * finalText into one of the state's declared guards, extracts every required
176
+ * payload field from the judge reply, and fails loudly (throws) on a missing
177
+ * JSON object, an undeclared guard, or a missing required field. Fields in
178
+ * `verbatimPayloadFields` carry `finalText.trim()` rather than round-tripping
179
+ * long-form prose through judge JSON.
180
+ */
181
+ export declare function adjudicatePlayerOutput(spec: PlayerAdjudicationSpec, input: PlaybookPlayerInput, finalText: string, ports: PlaybookPorts, signal: AbortSignal, boundary?: RuntimeBoundaryCalls): Promise<PlaybookActorOutput>;
182
+ export interface PlayerBridgeSpec {
183
+ resolvePlayerId: (input: PlaybookPlayerInput) => string;
184
+ composePlayerPrompt: (input: PlaybookPlayerInput) => string;
185
+ adjudication: PlayerAdjudicationSpec;
186
+ resumableStateIds: ReadonlySet<string>;
187
+ }
188
+ export declare function createPlayerBridge(spec: PlayerBridgeSpec, ports: PlaybookPorts, getActiveSignal?: () => AbortSignal | undefined, boundary?: RuntimeBoundaryCalls, onControlPlaneError?: (error: unknown) => void): PromiseActorLogic<PlaybookActorOutput, PlaybookPlayerInput>;
189
+ /** Targets of the FSM's `awaitBossReply` BOSS_REPLY transitions. */
190
+ export declare function resumableStateIdsFromMachine(machine: AnyStateMachine): ReadonlySet<string>;
191
+ /**
192
+ * Build a `PlaybookRuntimeFactory` that interprets the given FSM artifact
193
+ * under the slc/link.md contract. The factory provides every actor kind the
194
+ * machine declares — `player`, `script`, `captain`, and nested `playbook`
195
+ * (literal and dynamic) — and implements the full runtime lifecycle including
196
+ * the optional parked-session snapshot capability (DR-014).
197
+ *
198
+ * Scope: single-region root machines (each snapshot exposes exactly one
199
+ * playbook state id). Parallel-region FSMs keep their own linked runtimes.
200
+ */
201
+ export declare function createXStatePlaybookRuntime<TOptions>(machine: AnyStateMachine, spec: XStatePlaybookRuntimeSpec<TOptions>): PlaybookRuntimeFactory<TOptions>;