@cleocode/playbooks 2026.4.128 → 2026.4.129

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
@@ -24,6 +24,7 @@
24
24
  */
25
25
  export declare const PLAYBOOKS_PACKAGE_VERSION: string;
26
26
  export { approveGate, type CreateApprovalGateInput, createApprovalGate, E_APPROVAL_ALREADY_DECIDED, E_APPROVAL_NOT_FOUND, generateResumeToken, getPendingApprovals, getPlaybookSecret, rejectGate, } from './approval.js';
27
+ export { type MigratePlaybookFileResult, migratePlaybook, migratePlaybookFile, type NodeComplianceEntry, type PlaybookComplianceReport, validatePlaybookCompliance, } from './migrate-e4.js';
27
28
  export { type ParsePlaybookResult, PlaybookParseError, parsePlaybook, } from './parser.js';
28
29
  export { DEFAULT_POLICY_RULES, type EvaluatePolicyResult, evaluatePolicy, type PolicyRule, } from './policy.js';
29
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, } from './runtime.js';
package/dist/index.js CHANGED
@@ -22,8 +22,10 @@
22
22
  * Consumers can use this to assert dependency alignment at runtime
23
23
  * (e.g. ensuring the `@cleocode/playbooks` runtime matches CLEO core).
24
24
  */
25
- export const PLAYBOOKS_PACKAGE_VERSION = '2026.4.85';
25
+ export const PLAYBOOKS_PACKAGE_VERSION = '2026.4.129';
26
26
  export { approveGate, createApprovalGate, E_APPROVAL_ALREADY_DECIDED, E_APPROVAL_NOT_FOUND, generateResumeToken, getPendingApprovals, getPlaybookSecret, rejectGate, } from './approval.js';
27
+ // PSYCHE E4 migration tool (T1261) — STRICT cutover validator + migrator
28
+ export { migratePlaybook, migratePlaybookFile, validatePlaybookCompliance, } from './migrate-e4.js';
27
29
  // W4-7: .cantbook YAML parser → PlaybookDefinition
28
30
  export { PlaybookParseError, parsePlaybook, } from './parser.js';
29
31
  // W4-9: HITL auto-policy evaluator
@@ -0,0 +1,103 @@
1
+ /**
2
+ * PSYCHE E4 STRICT cutover migration tool for `.cantbook` files.
3
+ *
4
+ * Per Council mandate (T1261 ADR-055 STRICT cutover): all `.cantbook` files
5
+ * MUST comply with the E4 DSL contract before the v2026.4.129 ship. This
6
+ * module provides:
7
+ *
8
+ * 1. {@link validatePlaybookCompliance} — pure validator, returns compliance
9
+ * report without modifying files.
10
+ * 2. {@link migratePlaybook} — enriches a `.cantbook` source string with
11
+ * minimal E4-compatible scaffolding (adds skeleton error_handlers if
12
+ * absent, adds requires/ensures stubs to nodes without them).
13
+ * 3. {@link migratePlaybookFile} — reads, migrates, and optionally writes a
14
+ * `.cantbook` file in place. Dry-run mode returns the migrated source
15
+ * without writing.
16
+ *
17
+ * STRICT cutover policy (no opt-in flag):
18
+ * - All existing `.cantbook` files MUST be validated/migrated at E4 ship.
19
+ * - The starter playbooks (rcasd, ivtr, release) ship with full E4 DSL and
20
+ * pass validation without modification.
21
+ * - User-authored playbooks MUST run this tool before using the E4 runtime.
22
+ *
23
+ * @task T1261 PSYCHE E4 — STRICT cutover migration
24
+ */
25
+ /** Per-node compliance entry. */
26
+ export interface NodeComplianceEntry {
27
+ /** Node id. */
28
+ id: string;
29
+ /** Node type. */
30
+ type: string;
31
+ /** Whether the node has a `requires` block. */
32
+ hasRequires: boolean;
33
+ /** Whether the node has an `ensures` block. */
34
+ hasEnsures: boolean;
35
+ }
36
+ /**
37
+ * Result of {@link validatePlaybookCompliance}. Reports which E4 DSL
38
+ * features are present and which are missing.
39
+ */
40
+ export interface PlaybookComplianceReport {
41
+ /** Whether the file parses successfully (pre-condition). */
42
+ parses: boolean;
43
+ /** Parse error message when `parses === false`. */
44
+ parseError?: string;
45
+ /** Whether the file has at least one `error_handlers` entry. */
46
+ hasErrorHandlers: boolean;
47
+ /** Number of agentic nodes missing `requires` (first predecessor context). */
48
+ nodesMissingRequires: number;
49
+ /** Number of nodes missing `ensures` (output guarantee). */
50
+ nodesMissingEnsures: number;
51
+ /** Per-node breakdown. */
52
+ nodes: NodeComplianceEntry[];
53
+ /** `true` when all E4 requirements are satisfied. */
54
+ compliant: boolean;
55
+ }
56
+ /**
57
+ * Validate a `.cantbook` source string for E4 DSL compliance without
58
+ * modifying it.
59
+ *
60
+ * @param source - Raw `.cantbook` YAML text.
61
+ * @returns A compliance report. Callers should check `compliant` before
62
+ * running the playbook.
63
+ */
64
+ export declare function validatePlaybookCompliance(source: string): PlaybookComplianceReport;
65
+ /**
66
+ * Apply PSYCHE E4 STRICT cutover scaffolding to a `.cantbook` source string.
67
+ *
68
+ * Conservative strategy: adds skeleton DSL where absent; never removes or
69
+ * modifies existing DSL. Callers should review the migrated output before
70
+ * writing to disk.
71
+ *
72
+ * Migrations applied:
73
+ * - Adds a default `error_handlers` block if absent (iteration_cap_exceeded
74
+ * → hitl_escalate + contract_violation → inject_hint).
75
+ * - Adds a stub `requires: {}` to work nodes (non-approval) that lack it.
76
+ * - Adds a stub `ensures: {}` to work nodes that lack it.
77
+ *
78
+ * @param source - Raw `.cantbook` YAML text.
79
+ * @returns The migrated YAML source string.
80
+ * @throws {PlaybookParseError} when the source cannot be parsed.
81
+ */
82
+ export declare function migratePlaybook(source: string): string;
83
+ /** Result of {@link migratePlaybookFile}. */
84
+ export interface MigratePlaybookFileResult {
85
+ /** Path that was processed. */
86
+ filePath: string;
87
+ /** Pre-migration compliance report. */
88
+ before: PlaybookComplianceReport;
89
+ /** Post-migration compliance report (same as `before` in dry-run mode). */
90
+ after: PlaybookComplianceReport;
91
+ /** Whether the file was written (false in dry-run mode). */
92
+ written: boolean;
93
+ /** Migrated YAML source (always set, even in dry-run mode). */
94
+ migratedSource: string;
95
+ }
96
+ /**
97
+ * Read, validate, migrate, and optionally write a `.cantbook` file.
98
+ *
99
+ * @param filePath - Absolute path to the `.cantbook` file.
100
+ * @param dryRun - When `true`, return the migrated source without writing.
101
+ * @returns Migration result including before/after compliance reports.
102
+ */
103
+ export declare function migratePlaybookFile(filePath: string, dryRun?: boolean): MigratePlaybookFileResult;
@@ -0,0 +1,163 @@
1
+ /**
2
+ * PSYCHE E4 STRICT cutover migration tool for `.cantbook` files.
3
+ *
4
+ * Per Council mandate (T1261 ADR-055 STRICT cutover): all `.cantbook` files
5
+ * MUST comply with the E4 DSL contract before the v2026.4.129 ship. This
6
+ * module provides:
7
+ *
8
+ * 1. {@link validatePlaybookCompliance} — pure validator, returns compliance
9
+ * report without modifying files.
10
+ * 2. {@link migratePlaybook} — enriches a `.cantbook` source string with
11
+ * minimal E4-compatible scaffolding (adds skeleton error_handlers if
12
+ * absent, adds requires/ensures stubs to nodes without them).
13
+ * 3. {@link migratePlaybookFile} — reads, migrates, and optionally writes a
14
+ * `.cantbook` file in place. Dry-run mode returns the migrated source
15
+ * without writing.
16
+ *
17
+ * STRICT cutover policy (no opt-in flag):
18
+ * - All existing `.cantbook` files MUST be validated/migrated at E4 ship.
19
+ * - The starter playbooks (rcasd, ivtr, release) ship with full E4 DSL and
20
+ * pass validation without modification.
21
+ * - User-authored playbooks MUST run this tool before using the E4 runtime.
22
+ *
23
+ * @task T1261 PSYCHE E4 — STRICT cutover migration
24
+ */
25
+ import { readFileSync, writeFileSync } from 'node:fs';
26
+ import { dump as yamlDump, load as yamlLoad } from 'js-yaml';
27
+ import { PlaybookParseError, parsePlaybook } from './parser.js';
28
+ /**
29
+ * Validate a `.cantbook` source string for E4 DSL compliance without
30
+ * modifying it.
31
+ *
32
+ * @param source - Raw `.cantbook` YAML text.
33
+ * @returns A compliance report. Callers should check `compliant` before
34
+ * running the playbook.
35
+ */
36
+ export function validatePlaybookCompliance(source) {
37
+ let parsed;
38
+ try {
39
+ parsed = parsePlaybook(source);
40
+ }
41
+ catch (err) {
42
+ return {
43
+ parses: false,
44
+ parseError: err instanceof PlaybookParseError ? err.message : String(err),
45
+ hasErrorHandlers: false,
46
+ nodesMissingRequires: 0,
47
+ nodesMissingEnsures: 0,
48
+ nodes: [],
49
+ compliant: false,
50
+ };
51
+ }
52
+ const { definition } = parsed;
53
+ const hasErrorHandlers = (definition.error_handlers?.length ?? 0) > 0;
54
+ const nodeEntries = definition.nodes.map((n) => ({
55
+ id: n.id,
56
+ type: n.type,
57
+ hasRequires: n.requires !== undefined,
58
+ hasEnsures: n.ensures !== undefined,
59
+ }));
60
+ // Agentic + deterministic nodes should have requires/ensures; approval
61
+ // nodes are exempt (they are gate-only, not data-producing).
62
+ const workNodes = nodeEntries.filter((e) => e.type !== 'approval');
63
+ const nodesMissingRequires = workNodes.filter((e) => !e.hasRequires).length;
64
+ const nodesMissingEnsures = workNodes.filter((e) => !e.hasEnsures).length;
65
+ const compliant = hasErrorHandlers && nodesMissingRequires === 0 && nodesMissingEnsures === 0;
66
+ return {
67
+ parses: true,
68
+ hasErrorHandlers,
69
+ nodesMissingRequires,
70
+ nodesMissingEnsures,
71
+ nodes: nodeEntries,
72
+ compliant,
73
+ };
74
+ }
75
+ // ---------------------------------------------------------------------------
76
+ // Migration
77
+ // ---------------------------------------------------------------------------
78
+ /**
79
+ * Apply PSYCHE E4 STRICT cutover scaffolding to a `.cantbook` source string.
80
+ *
81
+ * Conservative strategy: adds skeleton DSL where absent; never removes or
82
+ * modifies existing DSL. Callers should review the migrated output before
83
+ * writing to disk.
84
+ *
85
+ * Migrations applied:
86
+ * - Adds a default `error_handlers` block if absent (iteration_cap_exceeded
87
+ * → hitl_escalate + contract_violation → inject_hint).
88
+ * - Adds a stub `requires: {}` to work nodes (non-approval) that lack it.
89
+ * - Adds a stub `ensures: {}` to work nodes that lack it.
90
+ *
91
+ * @param source - Raw `.cantbook` YAML text.
92
+ * @returns The migrated YAML source string.
93
+ * @throws {PlaybookParseError} when the source cannot be parsed.
94
+ */
95
+ export function migratePlaybook(source) {
96
+ // Parse first to validate structural correctness.
97
+ parsePlaybook(source);
98
+ // Work on the raw YAML object so we preserve ordering and comments where
99
+ // possible (js-yaml dump always re-serialises, but it's the safest
100
+ // approach for a migration tool).
101
+ const raw = yamlLoad(source);
102
+ // 1. Ensure error_handlers exists.
103
+ if (!Array.isArray(raw.error_handlers) || raw.error_handlers.length === 0) {
104
+ raw.error_handlers = [
105
+ {
106
+ on: 'iteration_cap_exceeded',
107
+ action: 'hitl_escalate',
108
+ message: 'Stage exhausted retries — escalate to human for direction.',
109
+ },
110
+ {
111
+ on: 'contract_violation',
112
+ action: 'inject_hint',
113
+ message: 'Contract violated at stage boundary — check requires/ensures fields.',
114
+ },
115
+ ];
116
+ }
117
+ // 2. Add requires/ensures stubs to work nodes.
118
+ if (Array.isArray(raw.nodes)) {
119
+ raw.nodes = raw.nodes.map((node) => {
120
+ if (node.type === 'approval')
121
+ return node; // approval nodes exempt
122
+ const patched = { ...node };
123
+ if (!patched.requires)
124
+ patched.requires = {};
125
+ if (!patched.ensures)
126
+ patched.ensures = {};
127
+ return patched;
128
+ });
129
+ }
130
+ return yamlDump(raw, { lineWidth: 100, noRefs: true });
131
+ }
132
+ /**
133
+ * Read, validate, migrate, and optionally write a `.cantbook` file.
134
+ *
135
+ * @param filePath - Absolute path to the `.cantbook` file.
136
+ * @param dryRun - When `true`, return the migrated source without writing.
137
+ * @returns Migration result including before/after compliance reports.
138
+ */
139
+ export function migratePlaybookFile(filePath, dryRun = false) {
140
+ const source = readFileSync(filePath, 'utf8');
141
+ const before = validatePlaybookCompliance(source);
142
+ if (!before.parses) {
143
+ return {
144
+ filePath,
145
+ before,
146
+ after: before,
147
+ written: false,
148
+ migratedSource: source,
149
+ };
150
+ }
151
+ const migratedSource = migratePlaybook(source);
152
+ const after = validatePlaybookCompliance(migratedSource);
153
+ if (!dryRun && !before.compliant) {
154
+ writeFileSync(filePath, migratedSource, 'utf8');
155
+ }
156
+ return {
157
+ filePath,
158
+ before,
159
+ after,
160
+ written: !dryRun && !before.compliant,
161
+ migratedSource,
162
+ };
163
+ }
package/dist/parser.d.ts CHANGED
@@ -17,12 +17,14 @@
17
17
  * - every edge.from + edge.to MUST reference a known node id
18
18
  * - nodes form a DAG when combined with edges (no cycles)
19
19
  * - agentic nodes MUST have skill OR agent (at least one)
20
+ * - agentic nodes MAY have context_files (thin-agent boundary, T1261 E4)
20
21
  * - deterministic nodes MUST have command + args
21
22
  * - approval nodes MUST have prompt
22
23
  * - depends[] entries MUST be valid node ids
23
24
  * - iteration_cap (max_iterations) MUST be 0..10 (hard limit)
24
25
  *
25
26
  * @task T889 / T904 / W4-7
27
+ * @task T1261 PSYCHE E4 — context_files thin-agent boundary
26
28
  */
27
29
  import type { PlaybookDefinition } from '@cleocode/contracts';
28
30
  /**
package/dist/parser.js CHANGED
@@ -17,12 +17,14 @@
17
17
  * - every edge.from + edge.to MUST reference a known node id
18
18
  * - nodes form a DAG when combined with edges (no cycles)
19
19
  * - agentic nodes MUST have skill OR agent (at least one)
20
+ * - agentic nodes MAY have context_files (thin-agent boundary, T1261 E4)
20
21
  * - deterministic nodes MUST have command + args
21
22
  * - approval nodes MUST have prompt
22
23
  * - depends[] entries MUST be valid node ids
23
24
  * - iteration_cap (max_iterations) MUST be 0..10 (hard limit)
24
25
  *
25
26
  * @task T889 / T904 / W4-7
27
+ * @task T1261 PSYCHE E4 — context_files thin-agent boundary
26
28
  */
27
29
  import { createHash } from 'node:crypto';
28
30
  import { load as yamlLoad } from 'js-yaml';
@@ -227,6 +229,7 @@ function parseAgenticNode(raw, base, index) {
227
229
  }
228
230
  inputs = acc;
229
231
  }
232
+ const context_files = parseStringArray(raw.context_files, `nodes[${index}].context_files`);
230
233
  return {
231
234
  ...base,
232
235
  type: 'agentic',
@@ -234,6 +237,7 @@ function parseAgenticNode(raw, base, index) {
234
237
  agent,
235
238
  role,
236
239
  inputs,
240
+ ...(context_files !== undefined ? { context_files } : {}),
237
241
  };
238
242
  }
239
243
  function parseDeterministicNode(raw, base, index) {
package/dist/runtime.d.ts CHANGED
@@ -26,8 +26,13 @@
26
26
  * written with the HMAC-signed resume token, and the returned
27
27
  * {@link ExecutePlaybookResult.approvalToken} is what the human reviewer
28
28
  * must present via `resumePlaybook` to continue.
29
+ * 5. Contract enforcement (T1261 PSYCHE E4) — requires/ensures DSL validated
30
+ * at every node boundary. Violations are appended to
31
+ * `.cleo/audit/contract-violations.jsonl` and trigger the `contract_violation`
32
+ * error_handler (inject_hint | hitl_escalate | abort).
29
33
  *
30
34
  * @task T930 — Playbook Runtime State Machine
35
+ * @task T1261 PSYCHE E4 — contract enforcement + context boundary
31
36
  */
32
37
  import type { DatabaseSync } from 'node:sqlite';
33
38
  import type { PlaybookDefinition } from '@cleocode/contracts';
@@ -131,12 +136,13 @@ export interface ExecutePlaybookOptions {
131
136
  sessionId?: string;
132
137
  /** Injectable clock for deterministic tests (defaults to `() => new Date()`). */
133
138
  now?: () => Date;
139
+ /**
140
+ * Project root for contract-violation audit writes (T1261 PSYCHE E4).
141
+ * When absent, contract violations are still enforced but not appended to
142
+ * `.cleo/audit/contract-violations.jsonl`.
143
+ */
144
+ projectRoot?: string;
134
145
  }
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
146
  export interface ResumePlaybookOptions {
141
147
  db: DatabaseSync;
142
148
  playbook: PlaybookDefinition;
@@ -147,7 +153,12 @@ export interface ResumePlaybookOptions {
147
153
  approvalSecret?: string;
148
154
  maxIterationsDefault?: number;
149
155
  now?: () => Date;
156
+ /**
157
+ * Project root for contract-violation audit writes (T1261 PSYCHE E4).
158
+ */
159
+ projectRoot?: string;
150
160
  }
161
+ /**
151
162
  /**
152
163
  * Terminal status values reported by the runtime.
153
164
  */
package/dist/runtime.js CHANGED
@@ -26,8 +26,13 @@
26
26
  * written with the HMAC-signed resume token, and the returned
27
27
  * {@link ExecutePlaybookResult.approvalToken} is what the human reviewer
28
28
  * must present via `resumePlaybook` to continue.
29
+ * 5. Contract enforcement (T1261 PSYCHE E4) — requires/ensures DSL validated
30
+ * at every node boundary. Violations are appended to
31
+ * `.cleo/audit/contract-violations.jsonl` and trigger the `contract_violation`
32
+ * error_handler (inject_hint | hitl_escalate | abort).
29
33
  *
30
34
  * @task T930 — Playbook Runtime State Machine
35
+ * @task T1261 PSYCHE E4 — contract enforcement + context boundary
31
36
  */
32
37
  import { createApprovalGate, getPlaybookSecret } from './approval.js';
33
38
  import { createPlaybookApproval, createPlaybookRun, getPlaybookApprovalByToken, getPlaybookRun, updatePlaybookRun, } from './state.js';
@@ -257,6 +262,75 @@ function executeApprovalNode(node, runId, context, db, secret) {
257
262
  // ---------------------------------------------------------------------------
258
263
  // Main execution loop
259
264
  // ---------------------------------------------------------------------------
265
+ // ---------------------------------------------------------------------------
266
+ // Contract enforcement helpers (T1261 PSYCHE E4)
267
+ // ---------------------------------------------------------------------------
268
+ /**
269
+ * Check that all required fields from a predecessor node are present in the
270
+ * current execution context.
271
+ *
272
+ * @param fields - List of keys that must be in `context`.
273
+ * @param from - Optional predecessor node id for error message context.
274
+ * @param context - Current accumulated run context.
275
+ * @returns `null` if all fields are present; a human-readable violation
276
+ * description otherwise.
277
+ */
278
+ function checkRequires(fields, from, context) {
279
+ for (const key of fields) {
280
+ if (!(key in context)) {
281
+ const source = from !== undefined ? ` (from node '${from}')` : '';
282
+ return `requires.fields['${key}']${source} not present in context`;
283
+ }
284
+ }
285
+ return null;
286
+ }
287
+ /**
288
+ * Resolve the first outgoing edge from `fromId` to `toId` in the playbook edge
289
+ * list. Returns `undefined` when no explicit edge exists.
290
+ */
291
+ function resolveEdge(fromId, toId, edges) {
292
+ return edges.find((e) => e.from === fromId && e.to === toId);
293
+ }
294
+ /**
295
+ * Best-effort contract violation audit write. Non-fatal: errors are swallowed
296
+ * so a broken filesystem never blocks playbook execution.
297
+ *
298
+ * Writes to `<projectRoot>/.cleo/audit/contract-violations.jsonl` following
299
+ * the ADR-039 pattern.
300
+ */
301
+ function auditContractViolation(projectRoot, runId, nodeId, field, key, playbookName) {
302
+ if (!projectRoot)
303
+ return;
304
+ try {
305
+ // Lazy-import to avoid a hard dep on @cleocode/core from @cleocode/playbooks.
306
+ // The audit is best-effort; we swallow any dynamic import failure.
307
+ void import('@cleocode/core').then(({ appendContractViolation }) => {
308
+ appendContractViolation(projectRoot, {
309
+ runId,
310
+ nodeId,
311
+ field,
312
+ key,
313
+ message: `contract_violation: ${field}['${key}'] check failed on node '${nodeId}'`,
314
+ playbookName,
315
+ });
316
+ });
317
+ }
318
+ catch {
319
+ // non-fatal
320
+ }
321
+ }
322
+ /**
323
+ * Map a `contract_violation` trigger to the registered error handler action.
324
+ *
325
+ * @returns `'inject_hint'` | `'hitl_escalate'` | `'abort'` based on the
326
+ * first matching handler, or `null` when no handler is registered.
327
+ */
328
+ function handleContractErrorHandler(playbook, trigger, _message) {
329
+ if (!playbook.error_handlers)
330
+ return null;
331
+ const handler = playbook.error_handlers.find((h) => h.on === trigger);
332
+ return handler?.action ?? null;
333
+ }
260
334
  /**
261
335
  * Determine the effective iteration cap for a node. Falls back to the
262
336
  * runtime default (3) when `on_failure.max_iterations` is unset. The parser
@@ -282,7 +356,7 @@ function iterationCapFor(node, runtimeDefault) {
282
356
  * @internal
283
357
  */
284
358
  async function runFromNode(args) {
285
- const { db, run, startNodeId, nodeIndex, edgeIndex, context, iterationCounts, dispatcher, deterministicRunner, approvalSecret, maxIterationsDefault, now, } = args;
359
+ const { db, playbook, run, startNodeId, nodeIndex, edgeIndex, context, iterationCounts, dispatcher, deterministicRunner, approvalSecret, maxIterationsDefault, now, } = args;
286
360
  let currentId = startNodeId;
287
361
  let lastError;
288
362
  let failedNodeId;
@@ -298,6 +372,31 @@ async function runFromNode(args) {
298
372
  currentNode: node.id,
299
373
  iterationCounts: { ...iterationCounts },
300
374
  });
375
+ // Contract enforcement (T1261 E4): validate node.requires BEFORE dispatch.
376
+ // On violation, trigger contract_violation error handler or fail the node.
377
+ if (node.requires?.fields) {
378
+ const violation = checkRequires(node.requires.fields, node.requires.from, context);
379
+ if (violation !== null) {
380
+ const contractFailure = `contract_violation: ${violation}`;
381
+ auditContractViolation(args.projectRoot, run.runId, node.id, 'requires', violation, playbook.name);
382
+ const handled = handleContractErrorHandler(playbook, 'contract_violation', contractFailure);
383
+ if (handled === 'abort') {
384
+ failedNodeId = node.id;
385
+ lastError = contractFailure;
386
+ break;
387
+ }
388
+ if (handled !== null) {
389
+ context['__lastError'] = contractFailure;
390
+ context['__lastFailedNode'] = node.id;
391
+ context['__contractViolation'] = violation;
392
+ if (handled === 'hitl_escalate') {
393
+ exceededNodeId = node.id;
394
+ break;
395
+ }
396
+ // inject_hint: continue to dispatch with the hint in context
397
+ }
398
+ }
399
+ }
301
400
  let outcome;
302
401
  if (node.type === 'agentic') {
303
402
  outcome = await executeAgenticNode(node, run.runId, context, attempt, dispatcher);
@@ -318,7 +417,40 @@ async function runFromNode(args) {
318
417
  // Merge outputs into context and persist.
319
418
  Object.assign(context, outcome.output);
320
419
  updatePlaybookRun(db, run.runId, { bindings: { ...context } });
321
- currentId = resolveNextNodeId(node.id, edgeIndex);
420
+ // Contract enforcement (T1261 E4): validate node.ensures AFTER merge.
421
+ if (node.ensures?.outputFiles) {
422
+ for (const key of node.ensures.outputFiles) {
423
+ if (!(key in context)) {
424
+ const violation = `ensures.outputFiles[${key}] not present in context after ${node.id}`;
425
+ auditContractViolation(args.projectRoot, run.runId, node.id, 'ensures', key, playbook.name);
426
+ handleContractErrorHandler(playbook, 'contract_violation', violation);
427
+ context['__ensuresViolation'] = violation;
428
+ }
429
+ }
430
+ }
431
+ // Validate outgoing edge contracts (edge.contract.requires on the FROM side).
432
+ const nextId = resolveNextNodeId(node.id, edgeIndex);
433
+ if (nextId !== null) {
434
+ const edge = resolveEdge(node.id, nextId, playbook.edges);
435
+ if (edge?.contract?.requires) {
436
+ for (const key of edge.contract.requires) {
437
+ if (!(key in context)) {
438
+ const violation = `edge.contract.requires[${key}] missing when crossing ${node.id} → ${nextId}`;
439
+ auditContractViolation(args.projectRoot, run.runId, node.id, 'requires', key, playbook.name);
440
+ const handled = handleContractErrorHandler(playbook, 'contract_violation', violation);
441
+ if (handled === 'abort') {
442
+ failedNodeId = node.id;
443
+ lastError = violation;
444
+ break;
445
+ }
446
+ context['__contractViolation'] = violation;
447
+ }
448
+ }
449
+ if (failedNodeId !== undefined)
450
+ break;
451
+ }
452
+ }
453
+ currentId = nextId;
322
454
  continue;
323
455
  }
324
456
  if (outcome.kind === 'awaiting_approval') {
@@ -488,6 +620,7 @@ export async function executePlaybook(options) {
488
620
  approvalSecret,
489
621
  maxIterationsDefault,
490
622
  now,
623
+ projectRoot: options.projectRoot,
491
624
  };
492
625
  return runFromNode(runArgs);
493
626
  }
@@ -596,6 +729,7 @@ export async function resumePlaybook(options) {
596
729
  approvalSecret,
597
730
  maxIterationsDefault,
598
731
  now,
732
+ projectRoot: options.projectRoot,
599
733
  };
600
734
  return runFromNode(runArgs);
601
735
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/playbooks",
3
- "version": "2026.4.128",
3
+ "version": "2026.4.129",
4
4
  "description": "Playbook DSL + runtime for CLEO — T889 Orchestration Coherence v3",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,8 +19,8 @@
19
19
  "dependencies": {
20
20
  "drizzle-orm": "1.0.0-beta.22-ec7b61d",
21
21
  "js-yaml": "^4.1.0",
22
- "@cleocode/contracts": "2026.4.128",
23
- "@cleocode/core": "2026.4.128"
22
+ "@cleocode/contracts": "2026.4.129",
23
+ "@cleocode/core": "2026.4.129"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/js-yaml": "^4.0.9",