@cleocode/playbooks 2026.4.93 → 2026.4.95

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.
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Playbook runtime — deterministic state machine executor for `.cantbook` flows.
3
+ *
4
+ * This module is the executable heart of CLEO's T910 "Orchestration Coherence v4"
5
+ * pipeline. It walks a validated {@link PlaybookDefinition} one node at a time,
6
+ * merging node outputs into a shared `context`, enforcing per-node iteration
7
+ * caps, and pausing for HITL approval gates via the signed resume-token
8
+ * protocol (see `approval.ts`).
9
+ *
10
+ * Design constraints (non-negotiable):
11
+ *
12
+ * 1. Pure dependency injection — the runtime never imports or instantiates
13
+ * subprocess code. Callers pass an {@link AgentDispatcher} for `agentic`
14
+ * nodes and an optional {@link DeterministicRunner} for `deterministic`
15
+ * nodes. Tests can therefore exercise every branch without mocking any
16
+ * `@cleocode/*` module.
17
+ * 2. Deterministic ordering — a topological traversal is computed up front
18
+ * from the {@link PlaybookDefinition.edges} graph (with `depends[]`
19
+ * treated as reverse edges, exactly as the parser's cycle check does).
20
+ * Execution order is stable across runs for the same definition.
21
+ * 3. Fail-closed policy — unknown node kinds, missing successors, unresolved
22
+ * `inject_into` targets, or dispatcher errors all terminate the run with
23
+ * a typed `terminalStatus`. The runtime never silently swallows failures.
24
+ * 4. HITL gates persist — when an `approval` node executes, the run is
25
+ * marked `paused` in `playbook_runs`, a {@link PlaybookApproval} row is
26
+ * written with the HMAC-signed resume token, and the returned
27
+ * {@link ExecutePlaybookResult.approvalToken} is what the human reviewer
28
+ * must present via `resumePlaybook` to continue.
29
+ *
30
+ * @task T930 — Playbook Runtime State Machine
31
+ */
32
+ import type { DatabaseSync } from 'node:sqlite';
33
+ import type { PlaybookDefinition } from '@cleocode/contracts';
34
+ /**
35
+ * Input payload handed to {@link AgentDispatcher.dispatch} on every
36
+ * `agentic` node execution. All fields are read-only from the dispatcher's
37
+ * perspective — the runtime owns the lifecycle.
38
+ */
39
+ export interface AgentDispatchInput {
40
+ /** Playbook run identifier (FK into `playbook_runs.run_id`). */
41
+ runId: string;
42
+ /** Node identifier within the run graph. */
43
+ nodeId: string;
44
+ /** Agent identity resolved from `node.agent` (falls back to `node.skill`). */
45
+ agentId: string;
46
+ /** Task identifier lifted from `context.taskId` if present, otherwise `runId`. */
47
+ taskId: string;
48
+ /** Snapshot of the accumulated bindings at dispatch time. */
49
+ context: Record<string, unknown>;
50
+ /** 1-based iteration counter for this specific node (retry-aware). */
51
+ iteration: number;
52
+ }
53
+ /**
54
+ * Terminal output returned by {@link AgentDispatcher.dispatch}. The runtime
55
+ * merges {@link output} into the run context on `status === 'success'` and
56
+ * triggers iteration-cap / escalation logic on `'failure'`.
57
+ */
58
+ export interface AgentDispatchResult {
59
+ status: 'success' | 'failure';
60
+ /** Key-value pairs merged into the run context on success. */
61
+ output: Record<string, unknown>;
62
+ /** Human-readable failure reason. Persisted to `playbook_runs.error_context`. */
63
+ error?: string;
64
+ }
65
+ /**
66
+ * Injected contract for spawning subagents. Implementations MUST NOT depend
67
+ * on any `@cleocode/*` module — the runtime passes all state through
68
+ * {@link AgentDispatchInput} so tests can provide deterministic stubs.
69
+ */
70
+ export interface AgentDispatcher {
71
+ /** Execute a single `agentic` node; return a success/failure envelope. */
72
+ dispatch(input: AgentDispatchInput): Promise<AgentDispatchResult>;
73
+ }
74
+ /**
75
+ * Input payload handed to {@link DeterministicRunner.run} on every
76
+ * `deterministic` node execution.
77
+ */
78
+ export interface DeterministicRunInput {
79
+ runId: string;
80
+ nodeId: string;
81
+ command: string;
82
+ args: readonly string[];
83
+ cwd?: string;
84
+ env?: Readonly<Record<string, string>>;
85
+ /** Timeout in milliseconds; `undefined` means the runner picks a default. */
86
+ timeout_ms?: number;
87
+ context: Record<string, unknown>;
88
+ iteration: number;
89
+ }
90
+ /**
91
+ * Terminal output returned by {@link DeterministicRunner.run}. Shape mirrors
92
+ * {@link AgentDispatchResult} for runtime uniformity.
93
+ */
94
+ export interface DeterministicRunResult {
95
+ status: 'success' | 'failure';
96
+ output: Record<string, unknown>;
97
+ error?: string;
98
+ }
99
+ /**
100
+ * Injected contract for running `deterministic` nodes (CLI tools, validators,
101
+ * decide scripts). If not supplied, the runtime delegates to
102
+ * {@link AgentDispatcher.dispatch} with `agentId = "deterministic:<command>"`
103
+ * so a single stub can cover both node kinds during unit testing.
104
+ */
105
+ export interface DeterministicRunner {
106
+ run(input: DeterministicRunInput): Promise<DeterministicRunResult>;
107
+ }
108
+ /**
109
+ * Options accepted by {@link executePlaybook}.
110
+ */
111
+ export interface ExecutePlaybookOptions {
112
+ /** Open `node:sqlite` handle with the T889 migration applied. */
113
+ db: DatabaseSync;
114
+ /** Validated playbook definition (output of {@link parsePlaybook}). */
115
+ playbook: PlaybookDefinition;
116
+ /** SHA-256 hex of the playbook source (output of {@link parsePlaybook}). */
117
+ playbookHash: string;
118
+ /** Starting bindings seeded into the run context (e.g. `{ taskId }`). */
119
+ initialContext: Record<string, unknown>;
120
+ /** Required dispatcher for `agentic` nodes. */
121
+ dispatcher: AgentDispatcher;
122
+ /** Optional runner for `deterministic` nodes. Defaults to `dispatcher`. */
123
+ deterministicRunner?: DeterministicRunner;
124
+ /** Override secret for HMAC resume-token signing. */
125
+ approvalSecret?: string;
126
+ /** Fallback per-node iteration cap when `on_failure.max_iterations` is unset. */
127
+ maxIterationsDefault?: number;
128
+ /** Epic id persisted to `playbook_runs.epic_id` for dashboard filtering. */
129
+ epicId?: string;
130
+ /** Session id persisted to `playbook_runs.session_id`. */
131
+ sessionId?: string;
132
+ /** Injectable clock for deterministic tests (defaults to `() => new Date()`). */
133
+ now?: () => Date;
134
+ }
135
+ /**
136
+ * Options accepted by {@link resumePlaybook}. The runtime validates that the
137
+ * supplied approval token resolves to an `approved` {@link PlaybookApproval}
138
+ * row before continuing execution.
139
+ */
140
+ export interface ResumePlaybookOptions {
141
+ db: DatabaseSync;
142
+ playbook: PlaybookDefinition;
143
+ /** The token previously returned in {@link ExecutePlaybookResult.approvalToken}. */
144
+ approvalToken: string;
145
+ dispatcher: AgentDispatcher;
146
+ deterministicRunner?: DeterministicRunner;
147
+ approvalSecret?: string;
148
+ maxIterationsDefault?: number;
149
+ now?: () => Date;
150
+ }
151
+ /**
152
+ * Terminal status values reported by the runtime.
153
+ */
154
+ export type PlaybookTerminalStatus = 'completed' | 'failed' | 'pending_approval' | 'exceeded_iteration_cap';
155
+ /**
156
+ * Final envelope returned by both {@link executePlaybook} and
157
+ * {@link resumePlaybook}.
158
+ */
159
+ export interface ExecutePlaybookResult {
160
+ runId: string;
161
+ terminalStatus: PlaybookTerminalStatus;
162
+ /** Fully-merged bindings at the point the runtime stopped. */
163
+ finalContext: Record<string, unknown>;
164
+ /** Set when `terminalStatus === 'pending_approval'`. */
165
+ approvalToken?: string;
166
+ /** Set when the run stopped because a specific node failed. */
167
+ failedNodeId?: string;
168
+ /** Set when the run stopped because a node hit its iteration cap. */
169
+ exceededNodeId?: string;
170
+ /** Human-readable error reason mirrored from the last failing node. */
171
+ errorContext?: string;
172
+ }
173
+ /**
174
+ * Error code stamped onto errors thrown by the runtime for invalid inputs.
175
+ * Exported for parity with the rest of the `@cleocode/playbooks` error codes.
176
+ */
177
+ export declare const E_PLAYBOOK_RUNTIME_INVALID: "E_PLAYBOOK_RUNTIME_INVALID";
178
+ /**
179
+ * Error code thrown when {@link resumePlaybook} is called with a token that
180
+ * does not resolve to an `approved` gate.
181
+ */
182
+ export declare const E_PLAYBOOK_RESUME_BLOCKED: "E_PLAYBOOK_RESUME_BLOCKED";
183
+ /**
184
+ * Execute a playbook from its entry node until a terminal state is reached
185
+ * (`completed`, `failed`, `exceeded_iteration_cap`, or `pending_approval`).
186
+ *
187
+ * Every execution is persisted to `playbook_runs` so that crashes or HITL
188
+ * pauses can resume via {@link resumePlaybook}. Returned
189
+ * {@link ExecutePlaybookResult.finalContext} is a fully-merged snapshot at
190
+ * the moment the runtime stopped.
191
+ *
192
+ * @param options - Runtime configuration, including the injected dispatcher.
193
+ * @returns Terminal envelope describing where the run stopped.
194
+ */
195
+ export declare function executePlaybook(options: ExecutePlaybookOptions): Promise<ExecutePlaybookResult>;
196
+ /**
197
+ * Resume a paused playbook run using a HITL approval token. The runtime
198
+ * validates that the token maps to an `approved` {@link PlaybookApproval}
199
+ * row and that the associated run is in `paused` state, then continues from
200
+ * the approval node's single successor.
201
+ *
202
+ * @throws Error stamped with {@link E_PLAYBOOK_RESUME_BLOCKED} if the token
203
+ * is unknown, the gate is still `pending`, the gate was `rejected`, the
204
+ * run is not `paused`, or the approval node has no successor.
205
+ */
206
+ export declare function resumePlaybook(options: ResumePlaybookOptions): Promise<ExecutePlaybookResult>;