@cleocode/playbooks 2026.4.128 → 2026.4.130

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,9 +26,16 @@
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
  */
37
+ import { appendFileSync, mkdirSync } from 'node:fs';
38
+ import { dirname, join } from 'node:path';
32
39
  import { createApprovalGate, getPlaybookSecret } from './approval.js';
33
40
  import { createPlaybookApproval, createPlaybookRun, getPlaybookApprovalByToken, getPlaybookRun, updatePlaybookRun, } from './state.js';
34
41
  /**
@@ -257,6 +264,78 @@ function executeApprovalNode(node, runId, context, db, secret) {
257
264
  // ---------------------------------------------------------------------------
258
265
  // Main execution loop
259
266
  // ---------------------------------------------------------------------------
267
+ // ---------------------------------------------------------------------------
268
+ // Contract enforcement helpers (T1261 PSYCHE E4)
269
+ // ---------------------------------------------------------------------------
270
+ /**
271
+ * Check that all required fields from a predecessor node are present in the
272
+ * current execution context.
273
+ *
274
+ * @param fields - List of keys that must be in `context`.
275
+ * @param from - Optional predecessor node id for error message context.
276
+ * @param context - Current accumulated run context.
277
+ * @returns `null` if all fields are present; a human-readable violation
278
+ * description otherwise.
279
+ */
280
+ function checkRequires(fields, from, context) {
281
+ for (const key of fields) {
282
+ if (!(key in context)) {
283
+ const source = from !== undefined ? ` (from node '${from}')` : '';
284
+ return `requires.fields['${key}']${source} not present in context`;
285
+ }
286
+ }
287
+ return null;
288
+ }
289
+ /**
290
+ * Resolve the first outgoing edge from `fromId` to `toId` in the playbook edge
291
+ * list. Returns `undefined` when no explicit edge exists.
292
+ */
293
+ function resolveEdge(fromId, toId, edges) {
294
+ return edges.find((e) => e.from === fromId && e.to === toId);
295
+ }
296
+ /**
297
+ * Best-effort contract violation audit write. Non-fatal: errors are swallowed
298
+ * so a broken filesystem never blocks playbook execution.
299
+ *
300
+ * Writes to `<projectRoot>/.cleo/audit/contract-violations.jsonl` following
301
+ * the ADR-039 pattern.
302
+ */
303
+ function auditContractViolation(projectRoot, runId, nodeId, field, key, playbookName) {
304
+ if (!projectRoot)
305
+ return;
306
+ try {
307
+ // Write directly via node:fs to avoid importing @cleocode/core from
308
+ // @cleocode/playbooks (avoids circular TS project reference issues).
309
+ // Follows the same ADR-039 append-only NDJSON pattern as audit.ts.
310
+ const filePath = join(projectRoot, '.cleo', 'audit', 'contract-violations.jsonl');
311
+ mkdirSync(dirname(filePath), { recursive: true });
312
+ const entry = JSON.stringify({
313
+ timestamp: new Date().toISOString(),
314
+ runId,
315
+ nodeId,
316
+ field,
317
+ key,
318
+ message: `contract_violation: ${field}['${key}'] check failed on node '${nodeId}'`,
319
+ playbookName,
320
+ });
321
+ appendFileSync(filePath, `${entry}\n`, { encoding: 'utf-8' });
322
+ }
323
+ catch {
324
+ // non-fatal
325
+ }
326
+ }
327
+ /**
328
+ * Map a `contract_violation` trigger to the registered error handler action.
329
+ *
330
+ * @returns `'inject_hint'` | `'hitl_escalate'` | `'abort'` based on the
331
+ * first matching handler, or `null` when no handler is registered.
332
+ */
333
+ function handleContractErrorHandler(playbook, trigger, _message) {
334
+ if (!playbook.error_handlers)
335
+ return null;
336
+ const handler = playbook.error_handlers.find((h) => h.on === trigger);
337
+ return handler?.action ?? null;
338
+ }
260
339
  /**
261
340
  * Determine the effective iteration cap for a node. Falls back to the
262
341
  * runtime default (3) when `on_failure.max_iterations` is unset. The parser
@@ -282,7 +361,7 @@ function iterationCapFor(node, runtimeDefault) {
282
361
  * @internal
283
362
  */
284
363
  async function runFromNode(args) {
285
- const { db, run, startNodeId, nodeIndex, edgeIndex, context, iterationCounts, dispatcher, deterministicRunner, approvalSecret, maxIterationsDefault, now, } = args;
364
+ const { db, playbook, run, startNodeId, nodeIndex, edgeIndex, context, iterationCounts, dispatcher, deterministicRunner, approvalSecret, maxIterationsDefault, now, } = args;
286
365
  let currentId = startNodeId;
287
366
  let lastError;
288
367
  let failedNodeId;
@@ -298,6 +377,31 @@ async function runFromNode(args) {
298
377
  currentNode: node.id,
299
378
  iterationCounts: { ...iterationCounts },
300
379
  });
380
+ // Contract enforcement (T1261 E4): validate node.requires BEFORE dispatch.
381
+ // On violation, trigger contract_violation error handler or fail the node.
382
+ if (node.requires?.fields) {
383
+ const violation = checkRequires(node.requires.fields, node.requires.from, context);
384
+ if (violation !== null) {
385
+ const contractFailure = `contract_violation: ${violation}`;
386
+ auditContractViolation(args.projectRoot, run.runId, node.id, 'requires', violation, playbook.name);
387
+ const handled = handleContractErrorHandler(playbook, 'contract_violation', contractFailure);
388
+ if (handled === 'abort') {
389
+ failedNodeId = node.id;
390
+ lastError = contractFailure;
391
+ break;
392
+ }
393
+ if (handled !== null) {
394
+ context['__lastError'] = contractFailure;
395
+ context['__lastFailedNode'] = node.id;
396
+ context['__contractViolation'] = violation;
397
+ if (handled === 'hitl_escalate') {
398
+ exceededNodeId = node.id;
399
+ break;
400
+ }
401
+ // inject_hint: continue to dispatch with the hint in context
402
+ }
403
+ }
404
+ }
301
405
  let outcome;
302
406
  if (node.type === 'agentic') {
303
407
  outcome = await executeAgenticNode(node, run.runId, context, attempt, dispatcher);
@@ -318,7 +422,40 @@ async function runFromNode(args) {
318
422
  // Merge outputs into context and persist.
319
423
  Object.assign(context, outcome.output);
320
424
  updatePlaybookRun(db, run.runId, { bindings: { ...context } });
321
- currentId = resolveNextNodeId(node.id, edgeIndex);
425
+ // Contract enforcement (T1261 E4): validate node.ensures AFTER merge.
426
+ if (node.ensures?.outputFiles) {
427
+ for (const key of node.ensures.outputFiles) {
428
+ if (!(key in context)) {
429
+ const violation = `ensures.outputFiles[${key}] not present in context after ${node.id}`;
430
+ auditContractViolation(args.projectRoot, run.runId, node.id, 'ensures', key, playbook.name);
431
+ handleContractErrorHandler(playbook, 'contract_violation', violation);
432
+ context['__ensuresViolation'] = violation;
433
+ }
434
+ }
435
+ }
436
+ // Validate outgoing edge contracts (edge.contract.requires on the FROM side).
437
+ const nextId = resolveNextNodeId(node.id, edgeIndex);
438
+ if (nextId !== null) {
439
+ const edge = resolveEdge(node.id, nextId, playbook.edges);
440
+ if (edge?.contract?.requires) {
441
+ for (const key of edge.contract.requires) {
442
+ if (!(key in context)) {
443
+ const violation = `edge.contract.requires[${key}] missing when crossing ${node.id} → ${nextId}`;
444
+ auditContractViolation(args.projectRoot, run.runId, node.id, 'requires', key, playbook.name);
445
+ const handled = handleContractErrorHandler(playbook, 'contract_violation', violation);
446
+ if (handled === 'abort') {
447
+ failedNodeId = node.id;
448
+ lastError = violation;
449
+ break;
450
+ }
451
+ context['__contractViolation'] = violation;
452
+ }
453
+ }
454
+ if (failedNodeId !== undefined)
455
+ break;
456
+ }
457
+ }
458
+ currentId = nextId;
322
459
  continue;
323
460
  }
324
461
  if (outcome.kind === 'awaiting_approval') {
@@ -488,6 +625,7 @@ export async function executePlaybook(options) {
488
625
  approvalSecret,
489
626
  maxIterationsDefault,
490
627
  now,
628
+ projectRoot: options.projectRoot,
491
629
  };
492
630
  return runFromNode(runArgs);
493
631
  }
@@ -596,6 +734,7 @@ export async function resumePlaybook(options) {
596
734
  approvalSecret,
597
735
  maxIterationsDefault,
598
736
  now,
737
+ projectRoot: options.projectRoot,
599
738
  };
600
739
  return runFromNode(runArgs);
601
740
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/playbooks",
3
- "version": "2026.4.128",
3
+ "version": "2026.4.130",
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/core": "2026.4.130",
23
+ "@cleocode/contracts": "2026.4.130"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/js-yaml": "^4.0.9",