@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 +1 -0
- package/dist/index.js +3 -1
- package/dist/migrate-e4.d.ts +103 -0
- package/dist/migrate-e4.js +163 -0
- package/dist/parser.d.ts +2 -0
- package/dist/parser.js +4 -0
- package/dist/runtime.d.ts +16 -5
- package/dist/runtime.js +141 -2
- package/package.json +3 -3
- package/src/__tests__/migrate-e4.test.ts +251 -0
- package/src/__tests__/starter.e2e.test.ts +49 -1
- package/src/index.ts +10 -1
- package/src/migrate-e4.ts +232 -0
- package/src/parser.ts +5 -0
- package/src/runtime.ts +200 -6
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.
|
|
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
|
-
|
|
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.
|
|
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/
|
|
23
|
-
"@cleocode/
|
|
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",
|