@cleocode/playbooks 2026.6.12 → 2026.6.14

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/dist/index.d.ts CHANGED
@@ -27,5 +27,5 @@ export { approveGate, type CreateApprovalGateInput, createApprovalGate, E_APPROV
27
27
  export { type MigratePlaybookFileResult, migratePlaybook, migratePlaybookFile, type NodeComplianceEntry, type PlaybookComplianceReport, validatePlaybookCompliance, } from './migrate-e4.js';
28
28
  export { type ParsePlaybookResult, PlaybookParseError, parsePlaybook, } from './parser.js';
29
29
  export { DEFAULT_POLICY_RULES, type EvaluatePolicyResult, evaluatePolicy, type PolicyRule, } from './policy.js';
30
- export { type AgentDispatcher, type AgentDispatchInput, type AgentDispatchResult, type DeterministicRunInput, type DeterministicRunner, type DeterministicRunResult, E_PLAYBOOK_RESUME_BLOCKED, E_PLAYBOOK_RUNTIME_INVALID, type ExecutePlaybookOptions, type ExecutePlaybookResult, executePlaybook, type PlaybookTerminalStatus, type ResumePlaybookOptions, resumePlaybook, validateDecompositionTaskTree, validateIvtrEvidenceOutput, } from './runtime.js';
30
+ export { type AgentDispatcher, type AgentDispatchInput, type AgentDispatchResult, type DeterministicRunInput, type DeterministicRunner, type DeterministicRunResult, E_PLAYBOOK_RESUME_BLOCKED, E_PLAYBOOK_RUNTIME_INVALID, type ExecutePlaybookOptions, type ExecutePlaybookResult, executePlaybook, type PlaybookTerminalStatus, type ResumePlaybookOptions, resolveStepBindings, resumePlaybook, } from './runtime.js';
31
31
  export { type CreatePlaybookApprovalInput, type CreatePlaybookRunInput, createPlaybookApproval, createPlaybookRun, deletePlaybookRun, getPlaybookApprovalByToken, getPlaybookRun, type ListPlaybookRunsOptions, listPlaybookApprovals, listPlaybookRuns, updatePlaybookApproval, updatePlaybookRun, } from './state.js';
package/dist/index.js CHANGED
@@ -31,7 +31,14 @@ export { PlaybookParseError, parsePlaybook, } from './parser.js';
31
31
  // W4-9: HITL auto-policy evaluator
32
32
  export { DEFAULT_POLICY_RULES, evaluatePolicy, } from './policy.js';
33
33
  // W4-10 / T930: playbook runtime state machine + HITL resume
34
- // T11499 E7-CLOSE-LOOPS AC3: schema validators exported for testing + external use
35
- export { E_PLAYBOOK_RESUME_BLOCKED, E_PLAYBOOK_RUNTIME_INVALID, executePlaybook, resumePlaybook, validateDecompositionTaskTree, validateIvtrEvidenceOutput, } from './runtime.js';
34
+ // T11762 ST-2: the bespoke `validateDecompositionTaskTree` /
35
+ // `validateIvtrEvidenceOutput` validators (and their barrel re-exports) were
36
+ // deleted — their rules are now the `task_tree` / `evidence` Zod schemas in the
37
+ // ensures-schema registry (`@cleocode/contracts` data + `@cleocode/core`
38
+ // accessors). No cross-package consumer imported them, so dropping the
39
+ // re-exports is safe.
40
+ export { E_PLAYBOOK_RESUME_BLOCKED, E_PLAYBOOK_RUNTIME_INVALID, executePlaybook,
41
+ // T1944 (M4): per-step `node.inputs` → dispatch-boundary binding resolver.
42
+ resolveStepBindings, resumePlaybook, } from './runtime.js';
36
43
  // W4-8: state layer CRUD for playbook_runs + playbook_approvals
37
44
  export { createPlaybookApproval, createPlaybookRun, deletePlaybookRun, getPlaybookApprovalByToken, getPlaybookRun, listPlaybookApprovals, listPlaybookRuns, updatePlaybookApproval, updatePlaybookRun, } from './state.js';
package/dist/parser.js CHANGED
@@ -230,6 +230,17 @@ function parseAgenticNode(raw, base, index) {
230
230
  inputs = acc;
231
231
  }
232
232
  const context_files = parseStringArray(raw.context_files, `nodes[${index}].context_files`);
233
+ // T11759 (M4): per-stage LLM pin. Each is an optional non-empty string — the
234
+ // dispatcher resolves them through the E9 chokepoint at dispatch time. Absent
235
+ // fields leave resolution unchanged (additive).
236
+ const profile = parseOptionalNonEmptyString(raw.profile, `nodes[${index}].profile`);
237
+ const model = parseOptionalNonEmptyString(raw.model, `nodes[${index}].model`);
238
+ const provider = parseOptionalNonEmptyString(raw.provider, `nodes[${index}].provider`);
239
+ // T1947 (M4): per-step skill/tool allowlists. Each is an optional string[] —
240
+ // enforced at spawn time by composeSpawnPayload as an INTERSECTION against the
241
+ // Tier-0/1/2 baseline (restrict-only, never expand). Absent → no restriction.
242
+ const allowed_skills = parseStringArray(raw.allowed_skills, `nodes[${index}].allowed_skills`);
243
+ const allowed_tools = parseStringArray(raw.allowed_tools, `nodes[${index}].allowed_tools`);
233
244
  return {
234
245
  ...base,
235
246
  type: 'agentic',
@@ -238,6 +249,11 @@ function parseAgenticNode(raw, base, index) {
238
249
  role,
239
250
  inputs,
240
251
  ...(context_files !== undefined ? { context_files } : {}),
252
+ ...(profile !== undefined ? { profile } : {}),
253
+ ...(model !== undefined ? { model } : {}),
254
+ ...(provider !== undefined ? { provider } : {}),
255
+ ...(allowed_skills !== undefined ? { allowed_skills } : {}),
256
+ ...(allowed_tools !== undefined ? { allowed_tools } : {}),
241
257
  };
242
258
  }
243
259
  function parseDeterministicNode(raw, base, index) {
@@ -326,7 +342,87 @@ function parseEdge(raw, index, ids) {
326
342
  const ensures = parseStringArray(raw.contract.ensures, `edges[${index}].contract.ensures`);
327
343
  contract = { requires, ensures };
328
344
  }
329
- return { from: raw.from, to: raw.to, contract };
345
+ const when = parseEdgeCondition(raw.when, `edges[${index}].when`);
346
+ const edge = { from: raw.from, to: raw.to, contract };
347
+ if (when !== undefined)
348
+ edge.when = when;
349
+ return edge;
350
+ }
351
+ /**
352
+ * Recognised comparison operators on a {@link PlaybookEdgeCondition}. Exactly
353
+ * one MUST be present so a guarded edge is unambiguous (T11806).
354
+ */
355
+ const EDGE_CONDITION_OPERATORS = ['equals', 'notEquals', 'exists', 'in', 'truthy'];
356
+ /**
357
+ * Parse the optional `when` branch guard on an edge into a validated
358
+ * {@link PlaybookEdgeCondition}. Returns `undefined` for an absent guard
359
+ * (the edge is unconditional).
360
+ *
361
+ * Validation:
362
+ * - `field` MUST be a non-empty string.
363
+ * - Exactly ONE comparison operator (`equals` | `notEquals` | `exists` | `in`
364
+ * | `truthy`) MUST be supplied — zero is ambiguous, more than one is
365
+ * contradictory, both rejected at parse time so the runtime never has to
366
+ * disambiguate.
367
+ * - `exists` / `truthy` MUST be boolean; `in` MUST be a non-empty array.
368
+ *
369
+ * @param raw - Raw YAML value for `edge.when`.
370
+ * @param field - Field path for diagnostics (e.g. `"edges[2].when"`).
371
+ * @returns The validated condition, or `undefined` when absent.
372
+ * @throws {PlaybookParseError} On any structural violation.
373
+ *
374
+ * @task T11806 — cantbook runtime branching
375
+ */
376
+ function parseEdgeCondition(raw, field) {
377
+ if (raw === undefined)
378
+ return undefined;
379
+ if (!isRecord(raw)) {
380
+ throw new PlaybookParseError(`${field} must be an object`, field, raw);
381
+ }
382
+ if (typeof raw.field !== 'string' || raw.field.length === 0) {
383
+ throw new PlaybookParseError(`${field}.field must be a non-empty string`, `${field}.field`, raw.field);
384
+ }
385
+ const present = EDGE_CONDITION_OPERATORS.filter((op) => Object.hasOwn(raw, op));
386
+ if (present.length === 0) {
387
+ throw new PlaybookParseError(`${field} must declare exactly one of ${EDGE_CONDITION_OPERATORS.join(' | ')}`, field, raw);
388
+ }
389
+ if (present.length > 1) {
390
+ throw new PlaybookParseError(`${field} declares multiple operators (${present.join(', ')}); exactly one is allowed`, field, raw);
391
+ }
392
+ const condition = { field: raw.field };
393
+ const [op] = present;
394
+ switch (op) {
395
+ case 'equals':
396
+ condition.equals = raw.equals;
397
+ break;
398
+ case 'notEquals':
399
+ condition.notEquals = raw.notEquals;
400
+ break;
401
+ case 'in':
402
+ if (!Array.isArray(raw.in) || raw.in.length === 0) {
403
+ throw new PlaybookParseError(`${field}.in must be a non-empty array`, `${field}.in`, raw.in);
404
+ }
405
+ condition.in = [...raw.in];
406
+ break;
407
+ case 'exists':
408
+ if (typeof raw.exists !== 'boolean') {
409
+ throw new PlaybookParseError(`${field}.exists must be a boolean`, `${field}.exists`, raw.exists);
410
+ }
411
+ condition.exists = raw.exists;
412
+ break;
413
+ case 'truthy':
414
+ if (typeof raw.truthy !== 'boolean') {
415
+ throw new PlaybookParseError(`${field}.truthy must be a boolean`, `${field}.truthy`, raw.truthy);
416
+ }
417
+ condition.truthy = raw.truthy;
418
+ break;
419
+ default: {
420
+ // Exhaustiveness guard — EDGE_CONDITION_OPERATORS is the SSoT.
421
+ const exhaustive = op;
422
+ throw new PlaybookParseError(`${field} unknown operator ${String(exhaustive)}`, field, raw);
423
+ }
424
+ }
425
+ return condition;
330
426
  }
331
427
  function parseInputs(raw) {
332
428
  if (raw === undefined)
@@ -449,6 +545,23 @@ function parseStringArray(raw, field) {
449
545
  return v;
450
546
  });
451
547
  }
548
+ /**
549
+ * Parse an optional scalar string field that, when present, MUST be a non-empty
550
+ * string (T11759 — the per-stage LLM pins `profile` / `model` / `provider`).
551
+ *
552
+ * @param raw - Raw YAML value.
553
+ * @param field - Field path for diagnostics.
554
+ * @returns The trimmed-as-authored string, or `undefined` when absent.
555
+ * @throws {PlaybookParseError} When present but not a non-empty string.
556
+ */
557
+ function parseOptionalNonEmptyString(raw, field) {
558
+ if (raw === undefined)
559
+ return undefined;
560
+ if (typeof raw !== 'string' || raw.length === 0) {
561
+ throw new PlaybookParseError(`${field} must be a non-empty string`, field, raw);
562
+ }
563
+ return raw;
564
+ }
452
565
  /**
453
566
  * Detect cycles across the combined edge-set (explicit `edges[]` plus
454
567
  * `depends[]` back-references). Uses 3-color DFS.
package/dist/runtime.d.ts CHANGED
@@ -35,7 +35,7 @@
35
35
  * @task T1261 PSYCHE E4 — contract enforcement + context boundary
36
36
  */
37
37
  import type { DatabaseSync } from 'node:sqlite';
38
- import type { PlaybookDefinition } from '@cleocode/contracts';
38
+ import type { PlaybookAgenticNode, PlaybookDefinition } from '@cleocode/contracts';
39
39
  /**
40
40
  * Input payload handed to {@link AgentDispatcher.dispatch} on every
41
41
  * `agentic` node execution. All fields are read-only from the dispatcher's
@@ -52,8 +52,69 @@ export interface AgentDispatchInput {
52
52
  taskId: string;
53
53
  /** Snapshot of the accumulated bindings at dispatch time. */
54
54
  context: Record<string, unknown>;
55
+ /**
56
+ * Per-step resolved bindings for this node (T1944 · M4 cantbook done-gate).
57
+ *
58
+ * Additive field. Built by {@link resolveStepBindings} from
59
+ * {@link PlaybookAgenticNode.inputs} — each declared input is a mustache
60
+ * `{{path}}` template resolved against the accumulated run `context` (via the
61
+ * canonical T1238 {@link DefaultVariableResolver}), then layered ON TOP of
62
+ * that context so a step input SHADOWS a same-named context key.
63
+ *
64
+ * The runtime always populates this when dispatching an agentic node (it is a
65
+ * shallow copy of `context` when the node declares no `inputs`). It is typed
66
+ * optional only to keep the change strictly additive for any out-of-tree
67
+ * dispatcher that constructs an {@link AgentDispatchInput} directly.
68
+ *
69
+ * Resolver precedence (highest → lowest):
70
+ * `step bindings ⊳ playbook bindings (run context) ⊳ session ⊳ project-context ⊳ env ⊳ default`.
71
+ */
72
+ bindings?: Record<string, unknown>;
55
73
  /** 1-based iteration counter for this specific node (retry-aware). */
56
74
  iteration: number;
75
+ /**
76
+ * Name of the running playbook (`.cantbook`) — the first segment of the
77
+ * cantbook node identity `cantbook:<playbook>#<nodeId>` (T11759 · M4).
78
+ *
79
+ * Additive. The runtime always populates it for agentic nodes; the dispatcher
80
+ * uses it (with {@link nodeId}) to resolve a declared per-stage LLM profile
81
+ * through the E9 chokepoint. Optional only so an out-of-tree dispatcher that
82
+ * constructs an {@link AgentDispatchInput} directly remains source-compatible.
83
+ */
84
+ playbookName?: string;
85
+ /**
86
+ * The node's declared per-stage LLM pin (T11759 · M4) — lifted verbatim from
87
+ * {@link PlaybookAgenticNode.profile} / `.model` / `.provider`.
88
+ *
89
+ * Additive + all-optional. When any field is set the dispatcher resolves the
90
+ * stage's LLM through `resolveLLMForSystem` keyed by the cantbook node
91
+ * identity, honoring the profile pin; when all are unset resolution is
92
+ * unchanged. The runtime NEVER acts on these itself — it only forwards them.
93
+ */
94
+ profile?: string;
95
+ /** Inline model-id pin forwarded from {@link PlaybookAgenticNode.model} (T11759). */
96
+ model?: string;
97
+ /** Inline provider pin forwarded from {@link PlaybookAgenticNode.provider} (T11759). */
98
+ provider?: string;
99
+ /**
100
+ * Per-step skill allowlist lifted verbatim from
101
+ * {@link PlaybookAgenticNode.allowed_skills} (T1947 · M4).
102
+ *
103
+ * Additive + optional. When present the dispatcher passes it to
104
+ * `composeSpawnPayload`, which intersects it with the resolved agent's skill
105
+ * baseline (Tier-0/1/2) — the allowlist can only RESTRICT, never expand. When
106
+ * absent the spawn carries its full baseline skill set. The runtime NEVER acts
107
+ * on this itself — it only forwards it.
108
+ */
109
+ allowedSkills?: string[];
110
+ /**
111
+ * Per-step tool allowlist lifted verbatim from
112
+ * {@link PlaybookAgenticNode.allowed_tools} (T1947 · M4).
113
+ *
114
+ * Same restrict-only intersection semantics as {@link allowedSkills}, applied
115
+ * to the spawn's tool baseline.
116
+ */
117
+ allowedTools?: string[];
57
118
  }
58
119
  /**
59
120
  * Terminal output returned by {@link AgentDispatcher.dispatch}. The runtime
@@ -192,47 +253,44 @@ export declare const E_PLAYBOOK_RUNTIME_INVALID: "E_PLAYBOOK_RUNTIME_INVALID";
192
253
  */
193
254
  export declare const E_PLAYBOOK_RESUME_BLOCKED: "E_PLAYBOOK_RESUME_BLOCKED";
194
255
  /**
195
- * Validate the `task_tree` shape emitted by a RCASD decomposition node.
196
- *
197
- * Returns a human-readable violation string when the shape is invalid, or
198
- * `null` when the tree is well-formed. The check is performed AFTER the node
199
- * output is merged into the playbook context (post-merge ensures check).
256
+ * Resolve a {@link PlaybookAgenticNode}'s declared `inputs` into the per-step
257
+ * binding map handed to the dispatcher (T1944 · M4 cantbook done-gate).
200
258
  *
201
- * Rules enforced (AC3 — T11499):
202
- * 1. `task_tree` must be a non-empty array.
203
- * 2. Each entry must have a non-empty string `title`.
204
- * 3. Each entry must have a non-empty `acceptance` array (at least one string).
205
- * 4. No entry may have an empty `depends` array that references a non-existent
206
- * sibling (intra-tree orphan check).
259
+ * Each `inputs` value is a mustache `{{path}}` template. Templates are resolved
260
+ * against the accumulated run `context` using the canonical T1238
261
+ * {@link DefaultVariableResolver} (no second substitution engine — DRY) and the
262
+ * resolved key/value pairs are then layered ON TOP of the run context. The
263
+ * result is the dispatch-time binding map.
207
264
  *
208
- * The validator is intentionally lenient on optional fields (id, parentId,
209
- * depends) so that agents can emit a minimal valid tree without being rejected
210
- * for cosmetic omissions.
265
+ * ## Canonical resolver precedence (highest → lowest)
211
266
  *
212
- * @param nodeId - Node identifier (used in error messages).
213
- * @param taskTree - The raw value stored at `context.task_tree`.
214
- * @returns Violation string or `null` (valid).
215
- *
216
- * @task T11499 E7-CLOSE-LOOPS AC3
217
- */
218
- export declare function validateDecompositionTaskTree(nodeId: string, taskTree: unknown): string | null;
219
- /**
220
- * Validate the `evidence` field emitted by an IVTR validation node.
267
+ * 1. **step bindings** — the resolved `node.inputs` themselves. They are merged
268
+ * LAST over the run context, so a step input always SHADOWS a same-named
269
+ * context key (AC3 / AC6).
270
+ * 2. **playbook bindings** — the accumulated run `context` (seeded from the
271
+ * playbook's declared inputs + every prior node's merged output).
272
+ * 3. **session / project-context / env / default** — supplied to the underlying
273
+ * {@link DefaultVariableResolver} via the {@link SubstitutionContext} below.
221
274
  *
222
- * IVTR validation nodes must emit an `evidence` key whose value is either:
223
- * - An object with at least one key (structured evidence map), OR
224
- * - A non-empty string (free-text evidence reference).
275
+ * The run `context` is exposed to the resolver through BOTH the `projectContext`
276
+ * tier (which supports dot-notation, so `{{inputs.epicId}}` and `{{context.foo}}`
277
+ * resolve into nested objects) and `process.env` (the `env` tier). The
278
+ * resolver runs in lenient mode.
225
279
  *
226
- * Empty evidence (`{}`, `''`, `[]`) is rejected so that spawned agents
227
- * cannot satisfy the gate by emitting an empty object.
280
+ * ## Missing-key rule
228
281
  *
229
- * @param nodeId - Node identifier (used in error messages).
230
- * @param evidence - The raw value stored at `context.evidence`.
231
- * @returns Violation string or `null` (valid).
282
+ * Lenient. When a `{{path}}` template references a value that resolves through
283
+ * NO tier, the literal placeholder text is left intact in the bound value (e.g.
284
+ * `{{missing}}` stays `"{{missing}}"`). This mirrors the canonical T1238
285
+ * lenient posture — a missing input never aborts the dispatch; the agent
286
+ * receives the literal placeholder and can fail loudly downstream if required.
232
287
  *
233
- * @task T11499 E7-CLOSE-LOOPS AC3
288
+ * @param node - The agentic node whose `inputs` are resolved.
289
+ * @param context - The accumulated run bindings at dispatch time.
290
+ * @returns A new binding map = `{ ...context, ...resolvedStepInputs }`. When the
291
+ * node declares no `inputs`, this is a shallow copy of `context`.
234
292
  */
235
- export declare function validateIvtrEvidenceOutput(nodeId: string, evidence: unknown): string | null;
293
+ export declare function resolveStepBindings(node: PlaybookAgenticNode, context: Record<string, unknown>): Record<string, unknown>;
236
294
  /**
237
295
  * Execute a playbook from its entry node until a terminal state is reached
238
296
  * (`completed`, `failed`, `exceeded_iteration_cap`, or `pending_approval`).