@cleocode/playbooks 2026.4.88 → 2026.4.91
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/README.md +207 -0
- package/package.json +3 -3
- package/dist/approval.d.ts +0 -113
- package/dist/approval.js +0 -244
- package/dist/index.d.ts +0 -29
- package/dist/index.js +0 -32
- package/dist/parser.d.ts +0 -60
- package/dist/parser.js +0 -509
- package/dist/policy.d.ts +0 -55
- package/dist/policy.js +0 -85
- package/dist/schema.d.ts +0 -374
- package/dist/schema.js +0 -34
- package/dist/state.d.ts +0 -96
- package/dist/state.js +0 -322
package/dist/parser.d.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* .cantbook YAML parser → PlaybookDefinition.
|
|
3
|
-
*
|
|
4
|
-
* Grammar (see contracts/playbook.ts):
|
|
5
|
-
* version: "1.0"
|
|
6
|
-
* name: <string>
|
|
7
|
-
* description?: <string>
|
|
8
|
-
* inputs?: [{name, required?, default?, description?}]
|
|
9
|
-
* nodes: [<agentic | deterministic | approval node>]
|
|
10
|
-
* edges: [{from, to, contract?:{requires?[], ensures?[]}}]
|
|
11
|
-
* error_handlers?: [{on, action, message?}]
|
|
12
|
-
*
|
|
13
|
-
* Validation:
|
|
14
|
-
* - version MUST be "1.0"
|
|
15
|
-
* - name MUST be non-empty
|
|
16
|
-
* - node ids MUST be unique
|
|
17
|
-
* - every edge.from + edge.to MUST reference a known node id
|
|
18
|
-
* - nodes form a DAG when combined with edges (no cycles)
|
|
19
|
-
* - agentic nodes MUST have skill OR agent (at least one)
|
|
20
|
-
* - deterministic nodes MUST have command + args
|
|
21
|
-
* - approval nodes MUST have prompt
|
|
22
|
-
* - depends[] entries MUST be valid node ids
|
|
23
|
-
* - iteration_cap (max_iterations) MUST be 0..10 (hard limit)
|
|
24
|
-
*
|
|
25
|
-
* @task T889 / T904 / W4-7
|
|
26
|
-
*/
|
|
27
|
-
import type { PlaybookDefinition } from '@cleocode/contracts';
|
|
28
|
-
/**
|
|
29
|
-
* Error thrown on any structural or semantic parse failure. Carries a
|
|
30
|
-
* `code`/`exitCode` pair so callers can bubble up consistent LAFS envelopes.
|
|
31
|
-
*/
|
|
32
|
-
export declare class PlaybookParseError extends Error {
|
|
33
|
-
readonly field?: string | undefined;
|
|
34
|
-
readonly value?: unknown | undefined;
|
|
35
|
-
/** Stable envelope error code for LAFS. */
|
|
36
|
-
readonly code = "E_PLAYBOOK_PARSE";
|
|
37
|
-
/** Process exit code used by CLI wrappers when a parse fails. */
|
|
38
|
-
readonly exitCode = 70;
|
|
39
|
-
/**
|
|
40
|
-
* @param message - Human-readable reason the playbook is invalid.
|
|
41
|
-
* @param field - Offending field path (e.g. `"nodes[0].id"`).
|
|
42
|
-
* @param value - Offending value for diagnostics (never re-thrown).
|
|
43
|
-
*/
|
|
44
|
-
constructor(message: string, field?: string | undefined, value?: unknown | undefined);
|
|
45
|
-
}
|
|
46
|
-
/** Result of a successful {@link parsePlaybook} call. */
|
|
47
|
-
export interface ParsePlaybookResult {
|
|
48
|
-
/** Validated, normalized definition ready for runtime execution. */
|
|
49
|
-
definition: PlaybookDefinition;
|
|
50
|
-
/** SHA-256 hex of the input source (for tamper detection). */
|
|
51
|
-
sourceHash: string;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Parse raw .cantbook YAML text into a validated {@link PlaybookDefinition}.
|
|
55
|
-
*
|
|
56
|
-
* @param source - Raw .cantbook YAML text.
|
|
57
|
-
* @returns The validated definition plus a deterministic SHA-256 source hash.
|
|
58
|
-
* @throws {PlaybookParseError} On any structural or semantic violation.
|
|
59
|
-
*/
|
|
60
|
-
export declare function parsePlaybook(source: string): ParsePlaybookResult;
|
package/dist/parser.js
DELETED
|
@@ -1,509 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* .cantbook YAML parser → PlaybookDefinition.
|
|
3
|
-
*
|
|
4
|
-
* Grammar (see contracts/playbook.ts):
|
|
5
|
-
* version: "1.0"
|
|
6
|
-
* name: <string>
|
|
7
|
-
* description?: <string>
|
|
8
|
-
* inputs?: [{name, required?, default?, description?}]
|
|
9
|
-
* nodes: [<agentic | deterministic | approval node>]
|
|
10
|
-
* edges: [{from, to, contract?:{requires?[], ensures?[]}}]
|
|
11
|
-
* error_handlers?: [{on, action, message?}]
|
|
12
|
-
*
|
|
13
|
-
* Validation:
|
|
14
|
-
* - version MUST be "1.0"
|
|
15
|
-
* - name MUST be non-empty
|
|
16
|
-
* - node ids MUST be unique
|
|
17
|
-
* - every edge.from + edge.to MUST reference a known node id
|
|
18
|
-
* - nodes form a DAG when combined with edges (no cycles)
|
|
19
|
-
* - agentic nodes MUST have skill OR agent (at least one)
|
|
20
|
-
* - deterministic nodes MUST have command + args
|
|
21
|
-
* - approval nodes MUST have prompt
|
|
22
|
-
* - depends[] entries MUST be valid node ids
|
|
23
|
-
* - iteration_cap (max_iterations) MUST be 0..10 (hard limit)
|
|
24
|
-
*
|
|
25
|
-
* @task T889 / T904 / W4-7
|
|
26
|
-
*/
|
|
27
|
-
import { createHash } from 'node:crypto';
|
|
28
|
-
import { load as yamlLoad } from 'js-yaml';
|
|
29
|
-
/** Supported playbook grammar version. Bump with migration plan only. */
|
|
30
|
-
const PLAYBOOK_VERSION = '1.0';
|
|
31
|
-
/** Hard ceiling on per-node retries to prevent runaway agents. */
|
|
32
|
-
const MAX_ITERATION_CAP = 10;
|
|
33
|
-
/** Allowed string literals for {@link PlaybookErrorHandler.on}. */
|
|
34
|
-
const ERROR_HANDLER_TRIGGERS = new Set([
|
|
35
|
-
'agentic_timeout',
|
|
36
|
-
'iteration_cap_exceeded',
|
|
37
|
-
'contract_violation',
|
|
38
|
-
]);
|
|
39
|
-
/** Allowed string literals for {@link PlaybookErrorHandler.action}. */
|
|
40
|
-
const ERROR_HANDLER_ACTIONS = new Set([
|
|
41
|
-
'inject_hint',
|
|
42
|
-
'hitl_escalate',
|
|
43
|
-
'abort',
|
|
44
|
-
]);
|
|
45
|
-
/** Allowed string literals for {@link PlaybookAgenticNode.role}. */
|
|
46
|
-
const AGENTIC_ROLES = new Set([
|
|
47
|
-
'orchestrator',
|
|
48
|
-
'lead',
|
|
49
|
-
'worker',
|
|
50
|
-
]);
|
|
51
|
-
/** Allowed string literals for {@link PlaybookApprovalNode.policy}. */
|
|
52
|
-
const APPROVAL_POLICIES = new Set(['conservative', 'permissive', 'custom']);
|
|
53
|
-
/**
|
|
54
|
-
* Error thrown on any structural or semantic parse failure. Carries a
|
|
55
|
-
* `code`/`exitCode` pair so callers can bubble up consistent LAFS envelopes.
|
|
56
|
-
*/
|
|
57
|
-
export class PlaybookParseError extends Error {
|
|
58
|
-
field;
|
|
59
|
-
value;
|
|
60
|
-
/** Stable envelope error code for LAFS. */
|
|
61
|
-
code = 'E_PLAYBOOK_PARSE';
|
|
62
|
-
/** Process exit code used by CLI wrappers when a parse fails. */
|
|
63
|
-
exitCode = 70;
|
|
64
|
-
/**
|
|
65
|
-
* @param message - Human-readable reason the playbook is invalid.
|
|
66
|
-
* @param field - Offending field path (e.g. `"nodes[0].id"`).
|
|
67
|
-
* @param value - Offending value for diagnostics (never re-thrown).
|
|
68
|
-
*/
|
|
69
|
-
constructor(message, field, value) {
|
|
70
|
-
super(message);
|
|
71
|
-
this.field = field;
|
|
72
|
-
this.value = value;
|
|
73
|
-
this.name = 'PlaybookParseError';
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Parse raw .cantbook YAML text into a validated {@link PlaybookDefinition}.
|
|
78
|
-
*
|
|
79
|
-
* @param source - Raw .cantbook YAML text.
|
|
80
|
-
* @returns The validated definition plus a deterministic SHA-256 source hash.
|
|
81
|
-
* @throws {PlaybookParseError} On any structural or semantic violation.
|
|
82
|
-
*/
|
|
83
|
-
export function parsePlaybook(source) {
|
|
84
|
-
// 1. YAML parse
|
|
85
|
-
let raw;
|
|
86
|
-
try {
|
|
87
|
-
raw = yamlLoad(source);
|
|
88
|
-
}
|
|
89
|
-
catch (err) {
|
|
90
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
91
|
-
throw new PlaybookParseError(`YAML syntax error: ${msg}`);
|
|
92
|
-
}
|
|
93
|
-
if (!isRecord(raw)) {
|
|
94
|
-
throw new PlaybookParseError('.cantbook must be a YAML map at top level');
|
|
95
|
-
}
|
|
96
|
-
// 2. Validate version
|
|
97
|
-
if (raw.version !== PLAYBOOK_VERSION) {
|
|
98
|
-
throw new PlaybookParseError(`Unsupported version: ${formatValue(raw.version)}. Only "${PLAYBOOK_VERSION}" is supported.`, 'version', raw.version);
|
|
99
|
-
}
|
|
100
|
-
const version = raw.version;
|
|
101
|
-
// 3. Validate name
|
|
102
|
-
if (typeof raw.name !== 'string' || raw.name.length === 0) {
|
|
103
|
-
throw new PlaybookParseError('name must be a non-empty string', 'name', raw.name);
|
|
104
|
-
}
|
|
105
|
-
const name = raw.name;
|
|
106
|
-
// 4. Parse nodes
|
|
107
|
-
if (!Array.isArray(raw.nodes) || raw.nodes.length === 0) {
|
|
108
|
-
throw new PlaybookParseError('nodes must be a non-empty array', 'nodes', raw.nodes);
|
|
109
|
-
}
|
|
110
|
-
const nodes = raw.nodes.map((n, i) => parseNode(n, i));
|
|
111
|
-
// 5. Check node id uniqueness
|
|
112
|
-
const ids = new Set();
|
|
113
|
-
for (const n of nodes) {
|
|
114
|
-
if (ids.has(n.id)) {
|
|
115
|
-
throw new PlaybookParseError(`duplicate node id: ${n.id}`, 'nodes', n.id);
|
|
116
|
-
}
|
|
117
|
-
ids.add(n.id);
|
|
118
|
-
}
|
|
119
|
-
// 6. Parse edges
|
|
120
|
-
const edgesRaw = raw.edges === undefined ? [] : raw.edges;
|
|
121
|
-
if (!Array.isArray(edgesRaw)) {
|
|
122
|
-
throw new PlaybookParseError('edges must be an array', 'edges', edgesRaw);
|
|
123
|
-
}
|
|
124
|
-
const edges = edgesRaw.map((e, i) => parseEdge(e, i, ids));
|
|
125
|
-
// 7. Validate depends[] references (must exist as known node ids)
|
|
126
|
-
for (const n of nodes) {
|
|
127
|
-
if (!n.depends)
|
|
128
|
-
continue;
|
|
129
|
-
for (const dep of n.depends) {
|
|
130
|
-
if (!ids.has(dep)) {
|
|
131
|
-
throw new PlaybookParseError(`node ${n.id} depends on unknown node ${dep}`, 'depends', dep);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
// 8. Iteration cap enforcement (0..MAX_ITERATION_CAP inclusive)
|
|
136
|
-
for (const n of nodes) {
|
|
137
|
-
const cap = n.on_failure?.max_iterations;
|
|
138
|
-
if (cap !== undefined && (cap < 0 || cap > MAX_ITERATION_CAP)) {
|
|
139
|
-
throw new PlaybookParseError(`node ${n.id} max_iterations must be 0..${MAX_ITERATION_CAP} (got ${cap})`, 'max_iterations', cap);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// 9. DAG check (edges + depends both contribute to the graph)
|
|
143
|
-
if (hasCycle(nodes, edges)) {
|
|
144
|
-
throw new PlaybookParseError('playbook contains a cycle in node graph');
|
|
145
|
-
}
|
|
146
|
-
// 10. Build definition
|
|
147
|
-
const definition = {
|
|
148
|
-
version,
|
|
149
|
-
name,
|
|
150
|
-
description: typeof raw.description === 'string' ? raw.description : undefined,
|
|
151
|
-
inputs: parseInputs(raw.inputs),
|
|
152
|
-
nodes,
|
|
153
|
-
edges,
|
|
154
|
-
error_handlers: parseErrorHandlers(raw.error_handlers),
|
|
155
|
-
};
|
|
156
|
-
const sourceHash = createHash('sha256').update(source).digest('hex');
|
|
157
|
-
return { definition, sourceHash };
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Parse a single node entry. Dispatches on `type` to the appropriate
|
|
161
|
-
* specialization validator.
|
|
162
|
-
*
|
|
163
|
-
* @param raw - Raw YAML node object.
|
|
164
|
-
* @param index - Zero-based index for error messages.
|
|
165
|
-
*/
|
|
166
|
-
function parseNode(raw, index) {
|
|
167
|
-
if (!isRecord(raw)) {
|
|
168
|
-
throw new PlaybookParseError(`nodes[${index}] must be an object`, `nodes[${index}]`, raw);
|
|
169
|
-
}
|
|
170
|
-
if (typeof raw.id !== 'string' || raw.id.length === 0) {
|
|
171
|
-
throw new PlaybookParseError(`nodes[${index}].id must be a non-empty string`, `nodes[${index}].id`, raw.id);
|
|
172
|
-
}
|
|
173
|
-
const id = raw.id;
|
|
174
|
-
if (typeof raw.type !== 'string') {
|
|
175
|
-
throw new PlaybookParseError(`nodes[${index}].type must be a string`, `nodes[${index}].type`, raw.type);
|
|
176
|
-
}
|
|
177
|
-
const type = raw.type;
|
|
178
|
-
const description = typeof raw.description === 'string' ? raw.description : undefined;
|
|
179
|
-
const depends = parseStringArray(raw.depends, `nodes[${index}].depends`);
|
|
180
|
-
const requires = parseRequires(raw.requires, `nodes[${index}].requires`);
|
|
181
|
-
const ensures = parseEnsures(raw.ensures, `nodes[${index}].ensures`);
|
|
182
|
-
const on_failure = parseOnFailure(raw.on_failure, `nodes[${index}].on_failure`);
|
|
183
|
-
const base = {
|
|
184
|
-
id,
|
|
185
|
-
description,
|
|
186
|
-
depends,
|
|
187
|
-
requires,
|
|
188
|
-
ensures,
|
|
189
|
-
on_failure,
|
|
190
|
-
};
|
|
191
|
-
switch (type) {
|
|
192
|
-
case 'agentic':
|
|
193
|
-
return parseAgenticNode(raw, base, index);
|
|
194
|
-
case 'deterministic':
|
|
195
|
-
return parseDeterministicNode(raw, base, index);
|
|
196
|
-
case 'approval':
|
|
197
|
-
return parseApprovalNode(raw, base, index);
|
|
198
|
-
default:
|
|
199
|
-
throw new PlaybookParseError(`nodes[${index}].type must be one of agentic | deterministic | approval (got ${formatValue(raw.type)})`, `nodes[${index}].type`, raw.type);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
function parseAgenticNode(raw, base, index) {
|
|
203
|
-
const skill = typeof raw.skill === 'string' ? raw.skill : undefined;
|
|
204
|
-
const agent = typeof raw.agent === 'string' ? raw.agent : undefined;
|
|
205
|
-
if (!skill && !agent) {
|
|
206
|
-
throw new PlaybookParseError(`nodes[${index}] (agentic) must define at least one of 'skill' or 'agent'`, `nodes[${index}]`, raw);
|
|
207
|
-
}
|
|
208
|
-
let role;
|
|
209
|
-
if (raw.role !== undefined) {
|
|
210
|
-
if (typeof raw.role !== 'string' ||
|
|
211
|
-
!AGENTIC_ROLES.has(raw.role)) {
|
|
212
|
-
throw new PlaybookParseError(`nodes[${index}].role must be one of orchestrator | lead | worker (got ${formatValue(raw.role)})`, `nodes[${index}].role`, raw.role);
|
|
213
|
-
}
|
|
214
|
-
role = raw.role;
|
|
215
|
-
}
|
|
216
|
-
let inputs;
|
|
217
|
-
if (raw.inputs !== undefined) {
|
|
218
|
-
if (!isRecord(raw.inputs)) {
|
|
219
|
-
throw new PlaybookParseError(`nodes[${index}].inputs must be an object`, `nodes[${index}].inputs`, raw.inputs);
|
|
220
|
-
}
|
|
221
|
-
const acc = {};
|
|
222
|
-
for (const [k, v] of Object.entries(raw.inputs)) {
|
|
223
|
-
if (typeof v !== 'string') {
|
|
224
|
-
throw new PlaybookParseError(`nodes[${index}].inputs.${k} must be a string`, `nodes[${index}].inputs.${k}`, v);
|
|
225
|
-
}
|
|
226
|
-
acc[k] = v;
|
|
227
|
-
}
|
|
228
|
-
inputs = acc;
|
|
229
|
-
}
|
|
230
|
-
return {
|
|
231
|
-
...base,
|
|
232
|
-
type: 'agentic',
|
|
233
|
-
skill,
|
|
234
|
-
agent,
|
|
235
|
-
role,
|
|
236
|
-
inputs,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
function parseDeterministicNode(raw, base, index) {
|
|
240
|
-
if (typeof raw.command !== 'string' || raw.command.length === 0) {
|
|
241
|
-
throw new PlaybookParseError(`nodes[${index}] (deterministic) must have a non-empty 'command'`, `nodes[${index}].command`, raw.command);
|
|
242
|
-
}
|
|
243
|
-
const args = parseStringArray(raw.args, `nodes[${index}].args`) ?? [];
|
|
244
|
-
const cwd = typeof raw.cwd === 'string' ? raw.cwd : undefined;
|
|
245
|
-
let env;
|
|
246
|
-
if (raw.env !== undefined) {
|
|
247
|
-
if (!isRecord(raw.env)) {
|
|
248
|
-
throw new PlaybookParseError(`nodes[${index}].env must be an object`, `nodes[${index}].env`, raw.env);
|
|
249
|
-
}
|
|
250
|
-
const acc = {};
|
|
251
|
-
for (const [k, v] of Object.entries(raw.env)) {
|
|
252
|
-
if (typeof v !== 'string') {
|
|
253
|
-
throw new PlaybookParseError(`nodes[${index}].env.${k} must be a string`, `nodes[${index}].env.${k}`, v);
|
|
254
|
-
}
|
|
255
|
-
acc[k] = v;
|
|
256
|
-
}
|
|
257
|
-
env = acc;
|
|
258
|
-
}
|
|
259
|
-
let timeout_ms;
|
|
260
|
-
if (raw.timeout_ms !== undefined) {
|
|
261
|
-
if (typeof raw.timeout_ms !== 'number' ||
|
|
262
|
-
!Number.isFinite(raw.timeout_ms) ||
|
|
263
|
-
raw.timeout_ms < 0) {
|
|
264
|
-
throw new PlaybookParseError(`nodes[${index}].timeout_ms must be a non-negative number`, `nodes[${index}].timeout_ms`, raw.timeout_ms);
|
|
265
|
-
}
|
|
266
|
-
timeout_ms = raw.timeout_ms;
|
|
267
|
-
}
|
|
268
|
-
return {
|
|
269
|
-
...base,
|
|
270
|
-
type: 'deterministic',
|
|
271
|
-
command: raw.command,
|
|
272
|
-
args,
|
|
273
|
-
cwd,
|
|
274
|
-
env,
|
|
275
|
-
timeout_ms,
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
function parseApprovalNode(raw, base, index) {
|
|
279
|
-
if (typeof raw.prompt !== 'string' || raw.prompt.length === 0) {
|
|
280
|
-
throw new PlaybookParseError(`nodes[${index}] (approval) must have a non-empty 'prompt'`, `nodes[${index}].prompt`, raw.prompt);
|
|
281
|
-
}
|
|
282
|
-
let policy;
|
|
283
|
-
if (raw.policy !== undefined) {
|
|
284
|
-
if (typeof raw.policy !== 'string' || !APPROVAL_POLICIES.has(raw.policy)) {
|
|
285
|
-
throw new PlaybookParseError(`nodes[${index}].policy must be one of conservative | permissive | custom (got ${formatValue(raw.policy)})`, `nodes[${index}].policy`, raw.policy);
|
|
286
|
-
}
|
|
287
|
-
policy = raw.policy;
|
|
288
|
-
}
|
|
289
|
-
return {
|
|
290
|
-
...base,
|
|
291
|
-
type: 'approval',
|
|
292
|
-
prompt: raw.prompt,
|
|
293
|
-
policy,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Parse a single edge entry. Edges reference existing node ids by the time
|
|
298
|
-
* this is called (the `ids` set is fully populated before edge parsing).
|
|
299
|
-
*/
|
|
300
|
-
function parseEdge(raw, index, ids) {
|
|
301
|
-
if (!isRecord(raw)) {
|
|
302
|
-
throw new PlaybookParseError(`edges[${index}] must be an object`, `edges[${index}]`, raw);
|
|
303
|
-
}
|
|
304
|
-
if (typeof raw.from !== 'string' || raw.from.length === 0) {
|
|
305
|
-
throw new PlaybookParseError(`edges[${index}].from must be a non-empty string`, `edges[${index}].from`, raw.from);
|
|
306
|
-
}
|
|
307
|
-
if (typeof raw.to !== 'string' || raw.to.length === 0) {
|
|
308
|
-
throw new PlaybookParseError(`edges[${index}].to must be a non-empty string`, `edges[${index}].to`, raw.to);
|
|
309
|
-
}
|
|
310
|
-
if (!ids.has(raw.from)) {
|
|
311
|
-
throw new PlaybookParseError(`edges[${index}].from references unknown node ${raw.from}`, `edges[${index}].from`, raw.from);
|
|
312
|
-
}
|
|
313
|
-
if (!ids.has(raw.to)) {
|
|
314
|
-
throw new PlaybookParseError(`edges[${index}].to references unknown node ${raw.to}`, `edges[${index}].to`, raw.to);
|
|
315
|
-
}
|
|
316
|
-
let contract;
|
|
317
|
-
if (raw.contract !== undefined) {
|
|
318
|
-
if (!isRecord(raw.contract)) {
|
|
319
|
-
throw new PlaybookParseError(`edges[${index}].contract must be an object`, `edges[${index}].contract`, raw.contract);
|
|
320
|
-
}
|
|
321
|
-
const requires = parseStringArray(raw.contract.requires, `edges[${index}].contract.requires`);
|
|
322
|
-
const ensures = parseStringArray(raw.contract.ensures, `edges[${index}].contract.ensures`);
|
|
323
|
-
contract = { requires, ensures };
|
|
324
|
-
}
|
|
325
|
-
return { from: raw.from, to: raw.to, contract };
|
|
326
|
-
}
|
|
327
|
-
function parseInputs(raw) {
|
|
328
|
-
if (raw === undefined)
|
|
329
|
-
return undefined;
|
|
330
|
-
if (!Array.isArray(raw)) {
|
|
331
|
-
throw new PlaybookParseError('inputs must be an array', 'inputs', raw);
|
|
332
|
-
}
|
|
333
|
-
return raw.map((r, i) => {
|
|
334
|
-
if (!isRecord(r)) {
|
|
335
|
-
throw new PlaybookParseError(`inputs[${i}] must be an object`, `inputs[${i}]`, r);
|
|
336
|
-
}
|
|
337
|
-
if (typeof r.name !== 'string' || r.name.length === 0) {
|
|
338
|
-
throw new PlaybookParseError(`inputs[${i}].name must be a non-empty string`, `inputs[${i}].name`, r.name);
|
|
339
|
-
}
|
|
340
|
-
let required;
|
|
341
|
-
if (r.required !== undefined) {
|
|
342
|
-
if (typeof r.required !== 'boolean') {
|
|
343
|
-
throw new PlaybookParseError(`inputs[${i}].required must be boolean`, `inputs[${i}].required`, r.required);
|
|
344
|
-
}
|
|
345
|
-
required = r.required;
|
|
346
|
-
}
|
|
347
|
-
const description = typeof r.description === 'string' ? r.description : undefined;
|
|
348
|
-
const input = { name: r.name };
|
|
349
|
-
if (required !== undefined)
|
|
350
|
-
input.required = required;
|
|
351
|
-
if (Object.hasOwn(r, 'default'))
|
|
352
|
-
input.default = r.default;
|
|
353
|
-
if (description !== undefined)
|
|
354
|
-
input.description = description;
|
|
355
|
-
return input;
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
function parseErrorHandlers(raw) {
|
|
359
|
-
if (raw === undefined)
|
|
360
|
-
return undefined;
|
|
361
|
-
if (!Array.isArray(raw)) {
|
|
362
|
-
throw new PlaybookParseError('error_handlers must be an array', 'error_handlers', raw);
|
|
363
|
-
}
|
|
364
|
-
return raw.map((r, i) => {
|
|
365
|
-
if (!isRecord(r)) {
|
|
366
|
-
throw new PlaybookParseError(`error_handlers[${i}] must be an object`, `error_handlers[${i}]`, r);
|
|
367
|
-
}
|
|
368
|
-
if (typeof r.on !== 'string' ||
|
|
369
|
-
!ERROR_HANDLER_TRIGGERS.has(r.on)) {
|
|
370
|
-
throw new PlaybookParseError(`error_handlers[${i}].on must be one of agentic_timeout | iteration_cap_exceeded | contract_violation (got ${formatValue(r.on)})`, `error_handlers[${i}].on`, r.on);
|
|
371
|
-
}
|
|
372
|
-
if (typeof r.action !== 'string' ||
|
|
373
|
-
!ERROR_HANDLER_ACTIONS.has(r.action)) {
|
|
374
|
-
throw new PlaybookParseError(`error_handlers[${i}].action must be one of inject_hint | hitl_escalate | abort (got ${formatValue(r.action)})`, `error_handlers[${i}].action`, r.action);
|
|
375
|
-
}
|
|
376
|
-
const message = typeof r.message === 'string' ? r.message : undefined;
|
|
377
|
-
return {
|
|
378
|
-
on: r.on,
|
|
379
|
-
action: r.action,
|
|
380
|
-
message,
|
|
381
|
-
};
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
function parseRequires(raw, field) {
|
|
385
|
-
if (raw === undefined)
|
|
386
|
-
return undefined;
|
|
387
|
-
if (!isRecord(raw)) {
|
|
388
|
-
throw new PlaybookParseError(`${field} must be an object`, field, raw);
|
|
389
|
-
}
|
|
390
|
-
const from = typeof raw.from === 'string' ? raw.from : undefined;
|
|
391
|
-
const fields = parseStringArray(raw.fields, `${field}.fields`);
|
|
392
|
-
const schema = typeof raw.schema === 'string' ? raw.schema : undefined;
|
|
393
|
-
return { from, fields, schema };
|
|
394
|
-
}
|
|
395
|
-
function parseEnsures(raw, field) {
|
|
396
|
-
if (raw === undefined)
|
|
397
|
-
return undefined;
|
|
398
|
-
if (!isRecord(raw)) {
|
|
399
|
-
throw new PlaybookParseError(`${field} must be an object`, field, raw);
|
|
400
|
-
}
|
|
401
|
-
const outputFiles = parseStringArray(raw.outputFiles, `${field}.outputFiles`);
|
|
402
|
-
let exitCode;
|
|
403
|
-
if (raw.exitCode !== undefined) {
|
|
404
|
-
if (typeof raw.exitCode !== 'number' || !Number.isInteger(raw.exitCode)) {
|
|
405
|
-
throw new PlaybookParseError(`${field}.exitCode must be an integer`, `${field}.exitCode`, raw.exitCode);
|
|
406
|
-
}
|
|
407
|
-
exitCode = raw.exitCode;
|
|
408
|
-
}
|
|
409
|
-
const schema = typeof raw.schema === 'string' ? raw.schema : undefined;
|
|
410
|
-
return { outputFiles, exitCode, schema };
|
|
411
|
-
}
|
|
412
|
-
function parseOnFailure(raw, field) {
|
|
413
|
-
if (raw === undefined)
|
|
414
|
-
return undefined;
|
|
415
|
-
if (!isRecord(raw)) {
|
|
416
|
-
throw new PlaybookParseError(`${field} must be an object`, field, raw);
|
|
417
|
-
}
|
|
418
|
-
const inject_into = typeof raw.inject_into === 'string' ? raw.inject_into : undefined;
|
|
419
|
-
let max_iterations;
|
|
420
|
-
if (raw.max_iterations !== undefined) {
|
|
421
|
-
if (typeof raw.max_iterations !== 'number' || !Number.isInteger(raw.max_iterations)) {
|
|
422
|
-
throw new PlaybookParseError(`${field}.max_iterations must be an integer`, `${field}.max_iterations`, raw.max_iterations);
|
|
423
|
-
}
|
|
424
|
-
max_iterations = raw.max_iterations;
|
|
425
|
-
}
|
|
426
|
-
let escalate;
|
|
427
|
-
if (raw.escalate !== undefined) {
|
|
428
|
-
if (typeof raw.escalate !== 'boolean') {
|
|
429
|
-
throw new PlaybookParseError(`${field}.escalate must be boolean`, `${field}.escalate`, raw.escalate);
|
|
430
|
-
}
|
|
431
|
-
escalate = raw.escalate;
|
|
432
|
-
}
|
|
433
|
-
return { inject_into, max_iterations, escalate };
|
|
434
|
-
}
|
|
435
|
-
function parseStringArray(raw, field) {
|
|
436
|
-
if (raw === undefined)
|
|
437
|
-
return undefined;
|
|
438
|
-
if (!Array.isArray(raw)) {
|
|
439
|
-
throw new PlaybookParseError(`${field} must be an array of strings`, field, raw);
|
|
440
|
-
}
|
|
441
|
-
return raw.map((v, i) => {
|
|
442
|
-
if (typeof v !== 'string') {
|
|
443
|
-
throw new PlaybookParseError(`${field}[${i}] must be a string`, `${field}[${i}]`, v);
|
|
444
|
-
}
|
|
445
|
-
return v;
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
/**
|
|
449
|
-
* Detect cycles across the combined edge-set (explicit `edges[]` plus
|
|
450
|
-
* `depends[]` back-references). Uses 3-color DFS.
|
|
451
|
-
*
|
|
452
|
-
* @returns `true` if any cycle exists.
|
|
453
|
-
*/
|
|
454
|
-
function hasCycle(nodes, edges) {
|
|
455
|
-
const adj = new Map();
|
|
456
|
-
for (const n of nodes)
|
|
457
|
-
adj.set(n.id, new Set());
|
|
458
|
-
for (const e of edges)
|
|
459
|
-
adj.get(e.from)?.add(e.to);
|
|
460
|
-
// depends[] is a reverse dependency: dep → node. Add as incoming edge for DAG purposes.
|
|
461
|
-
for (const n of nodes) {
|
|
462
|
-
if (!n.depends)
|
|
463
|
-
continue;
|
|
464
|
-
for (const dep of n.depends)
|
|
465
|
-
adj.get(dep)?.add(n.id);
|
|
466
|
-
}
|
|
467
|
-
const WHITE = 0;
|
|
468
|
-
const GRAY = 1;
|
|
469
|
-
const BLACK = 2;
|
|
470
|
-
const color = new Map();
|
|
471
|
-
for (const n of nodes)
|
|
472
|
-
color.set(n.id, WHITE);
|
|
473
|
-
function visit(id) {
|
|
474
|
-
color.set(id, GRAY);
|
|
475
|
-
for (const next of adj.get(id) ?? []) {
|
|
476
|
-
const c = color.get(next) ?? WHITE;
|
|
477
|
-
if (c === GRAY)
|
|
478
|
-
return true;
|
|
479
|
-
if (c === WHITE && visit(next))
|
|
480
|
-
return true;
|
|
481
|
-
}
|
|
482
|
-
color.set(id, BLACK);
|
|
483
|
-
return false;
|
|
484
|
-
}
|
|
485
|
-
for (const n of nodes) {
|
|
486
|
-
if (color.get(n.id) === WHITE && visit(n.id))
|
|
487
|
-
return true;
|
|
488
|
-
}
|
|
489
|
-
return false;
|
|
490
|
-
}
|
|
491
|
-
/**
|
|
492
|
-
* Type guard for YAML maps. `yaml.load` returns `unknown`, so we narrow here
|
|
493
|
-
* rather than using broad casts.
|
|
494
|
-
*/
|
|
495
|
-
function isRecord(v) {
|
|
496
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
497
|
-
}
|
|
498
|
-
/** Format arbitrary input for error messages without leaking huge structures. */
|
|
499
|
-
function formatValue(v) {
|
|
500
|
-
if (v === null)
|
|
501
|
-
return 'null';
|
|
502
|
-
if (v === undefined)
|
|
503
|
-
return 'undefined';
|
|
504
|
-
if (typeof v === 'string')
|
|
505
|
-
return JSON.stringify(v);
|
|
506
|
-
if (typeof v === 'number' || typeof v === 'boolean')
|
|
507
|
-
return String(v);
|
|
508
|
-
return typeof v;
|
|
509
|
-
}
|
package/dist/policy.d.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HITL auto-policy — evaluates whether a deterministic command requires
|
|
3
|
-
* human approval before execution. Conservative defaults per OpenProse standard.
|
|
4
|
-
*
|
|
5
|
-
* `require-human` rules are evaluated FIRST and cannot be bypassed by
|
|
6
|
-
* `auto-approve` rules even when callers append custom rules to the list.
|
|
7
|
-
* The default decision for an unmatched command is `require-human` so the
|
|
8
|
-
* runtime fails closed when confronted with unknown surface area.
|
|
9
|
-
*
|
|
10
|
-
* @task T889 / T908 / W4-9
|
|
11
|
-
*/
|
|
12
|
-
/**
|
|
13
|
-
* One policy rule in the auto-approval evaluation order. Rules are matched by
|
|
14
|
-
* applying `pattern.test(command)` against the fully-resolved command string.
|
|
15
|
-
*/
|
|
16
|
-
export interface PolicyRule {
|
|
17
|
-
pattern: RegExp;
|
|
18
|
-
action: 'auto-approve' | 'require-human';
|
|
19
|
-
reason: string;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Result of {@link evaluatePolicy}. `matchedPattern` carries the source of
|
|
23
|
-
* the regex that produced the decision so operators can audit the trail;
|
|
24
|
-
* it is absent for the terminal `default` fallthrough.
|
|
25
|
-
*/
|
|
26
|
-
export interface EvaluatePolicyResult {
|
|
27
|
-
action: 'auto-approve' | 'require-human';
|
|
28
|
-
reason: string;
|
|
29
|
-
matchedPattern?: string;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Conservative default policy. `require-human` rules come first for ordering
|
|
33
|
-
* clarity, but evaluation order in {@link evaluatePolicy} enforces the
|
|
34
|
-
* priority regardless of array position.
|
|
35
|
-
*/
|
|
36
|
-
export declare const DEFAULT_POLICY_RULES: readonly PolicyRule[];
|
|
37
|
-
/**
|
|
38
|
-
* Evaluates a command against the supplied policy rules and returns the
|
|
39
|
-
* resolved approval decision.
|
|
40
|
-
*
|
|
41
|
-
* Priority:
|
|
42
|
-
* 1. Every `require-human` rule across the list is tested first.
|
|
43
|
-
* 2. `auto-approve` rules are tested only if no block fired.
|
|
44
|
-
* 3. Fallback is `{ action: 'require-human', reason: 'default' }` so
|
|
45
|
-
* unknown commands never auto-execute.
|
|
46
|
-
*
|
|
47
|
-
* Callers MAY pass a custom rule list, but they CANNOT relax default blocks —
|
|
48
|
-
* any rule elsewhere in the list that matches with `require-human` wins over
|
|
49
|
-
* any auto-approve match, regardless of order.
|
|
50
|
-
*
|
|
51
|
-
* @param command The fully-resolved command string (executable plus arguments).
|
|
52
|
-
* @param rules The ordered rule list. Defaults to {@link DEFAULT_POLICY_RULES}.
|
|
53
|
-
* @returns The approval decision including the matched reason.
|
|
54
|
-
*/
|
|
55
|
-
export declare function evaluatePolicy(command: string, rules?: readonly PolicyRule[]): EvaluatePolicyResult;
|