@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/src/runtime.ts
CHANGED
|
@@ -26,16 +26,24 @@
|
|
|
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
|
|
|
38
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
39
|
+
import { dirname, join } from 'node:path';
|
|
33
40
|
import type { DatabaseSync } from 'node:sqlite';
|
|
34
41
|
import type {
|
|
35
42
|
PlaybookAgenticNode,
|
|
36
43
|
PlaybookApprovalNode,
|
|
37
44
|
PlaybookDefinition,
|
|
38
45
|
PlaybookDeterministicNode,
|
|
46
|
+
PlaybookEdge,
|
|
39
47
|
PlaybookNode,
|
|
40
48
|
PlaybookRun,
|
|
41
49
|
PlaybookRunStatus,
|
|
@@ -163,13 +171,14 @@ export interface ExecutePlaybookOptions {
|
|
|
163
171
|
sessionId?: string;
|
|
164
172
|
/** Injectable clock for deterministic tests (defaults to `() => new Date()`). */
|
|
165
173
|
now?: () => Date;
|
|
174
|
+
/**
|
|
175
|
+
* Project root for contract-violation audit writes (T1261 PSYCHE E4).
|
|
176
|
+
* When absent, contract violations are still enforced but not appended to
|
|
177
|
+
* `.cleo/audit/contract-violations.jsonl`.
|
|
178
|
+
*/
|
|
179
|
+
projectRoot?: string;
|
|
166
180
|
}
|
|
167
181
|
|
|
168
|
-
/**
|
|
169
|
-
* Options accepted by {@link resumePlaybook}. The runtime validates that the
|
|
170
|
-
* supplied approval token resolves to an `approved` {@link PlaybookApproval}
|
|
171
|
-
* row before continuing execution.
|
|
172
|
-
*/
|
|
173
182
|
export interface ResumePlaybookOptions {
|
|
174
183
|
db: DatabaseSync;
|
|
175
184
|
playbook: PlaybookDefinition;
|
|
@@ -180,8 +189,13 @@ export interface ResumePlaybookOptions {
|
|
|
180
189
|
approvalSecret?: string;
|
|
181
190
|
maxIterationsDefault?: number;
|
|
182
191
|
now?: () => Date;
|
|
192
|
+
/**
|
|
193
|
+
* Project root for contract-violation audit writes (T1261 PSYCHE E4).
|
|
194
|
+
*/
|
|
195
|
+
projectRoot?: string;
|
|
183
196
|
}
|
|
184
197
|
|
|
198
|
+
/**
|
|
185
199
|
/**
|
|
186
200
|
* Terminal status values reported by the runtime.
|
|
187
201
|
*/
|
|
@@ -494,6 +508,99 @@ function executeApprovalNode(
|
|
|
494
508
|
// Main execution loop
|
|
495
509
|
// ---------------------------------------------------------------------------
|
|
496
510
|
|
|
511
|
+
// ---------------------------------------------------------------------------
|
|
512
|
+
// Contract enforcement helpers (T1261 PSYCHE E4)
|
|
513
|
+
// ---------------------------------------------------------------------------
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Check that all required fields from a predecessor node are present in the
|
|
517
|
+
* current execution context.
|
|
518
|
+
*
|
|
519
|
+
* @param fields - List of keys that must be in `context`.
|
|
520
|
+
* @param from - Optional predecessor node id for error message context.
|
|
521
|
+
* @param context - Current accumulated run context.
|
|
522
|
+
* @returns `null` if all fields are present; a human-readable violation
|
|
523
|
+
* description otherwise.
|
|
524
|
+
*/
|
|
525
|
+
function checkRequires(
|
|
526
|
+
fields: readonly string[],
|
|
527
|
+
from: string | undefined,
|
|
528
|
+
context: Record<string, unknown>,
|
|
529
|
+
): string | null {
|
|
530
|
+
for (const key of fields) {
|
|
531
|
+
if (!(key in context)) {
|
|
532
|
+
const source = from !== undefined ? ` (from node '${from}')` : '';
|
|
533
|
+
return `requires.fields['${key}']${source} not present in context`;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Resolve the first outgoing edge from `fromId` to `toId` in the playbook edge
|
|
541
|
+
* list. Returns `undefined` when no explicit edge exists.
|
|
542
|
+
*/
|
|
543
|
+
function resolveEdge(
|
|
544
|
+
fromId: string,
|
|
545
|
+
toId: string,
|
|
546
|
+
edges: readonly PlaybookEdge[],
|
|
547
|
+
): PlaybookEdge | undefined {
|
|
548
|
+
return edges.find((e) => e.from === fromId && e.to === toId);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Best-effort contract violation audit write. Non-fatal: errors are swallowed
|
|
553
|
+
* so a broken filesystem never blocks playbook execution.
|
|
554
|
+
*
|
|
555
|
+
* Writes to `<projectRoot>/.cleo/audit/contract-violations.jsonl` following
|
|
556
|
+
* the ADR-039 pattern.
|
|
557
|
+
*/
|
|
558
|
+
function auditContractViolation(
|
|
559
|
+
projectRoot: string | undefined,
|
|
560
|
+
runId: string,
|
|
561
|
+
nodeId: string,
|
|
562
|
+
field: 'requires' | 'ensures',
|
|
563
|
+
key: string,
|
|
564
|
+
playbookName: string,
|
|
565
|
+
): void {
|
|
566
|
+
if (!projectRoot) return;
|
|
567
|
+
try {
|
|
568
|
+
// Write directly via node:fs to avoid importing @cleocode/core from
|
|
569
|
+
// @cleocode/playbooks (avoids circular TS project reference issues).
|
|
570
|
+
// Follows the same ADR-039 append-only NDJSON pattern as audit.ts.
|
|
571
|
+
const filePath = join(projectRoot, '.cleo', 'audit', 'contract-violations.jsonl');
|
|
572
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
573
|
+
const entry = JSON.stringify({
|
|
574
|
+
timestamp: new Date().toISOString(),
|
|
575
|
+
runId,
|
|
576
|
+
nodeId,
|
|
577
|
+
field,
|
|
578
|
+
key,
|
|
579
|
+
message: `contract_violation: ${field}['${key}'] check failed on node '${nodeId}'`,
|
|
580
|
+
playbookName,
|
|
581
|
+
});
|
|
582
|
+
appendFileSync(filePath, `${entry}\n`, { encoding: 'utf-8' });
|
|
583
|
+
} catch {
|
|
584
|
+
// non-fatal
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Map a `contract_violation` trigger to the registered error handler action.
|
|
590
|
+
*
|
|
591
|
+
* @returns `'inject_hint'` | `'hitl_escalate'` | `'abort'` based on the
|
|
592
|
+
* first matching handler, or `null` when no handler is registered.
|
|
593
|
+
*/
|
|
594
|
+
function handleContractErrorHandler(
|
|
595
|
+
playbook: PlaybookDefinition,
|
|
596
|
+
trigger: 'contract_violation',
|
|
597
|
+
_message: string,
|
|
598
|
+
): 'inject_hint' | 'hitl_escalate' | 'abort' | null {
|
|
599
|
+
if (!playbook.error_handlers) return null;
|
|
600
|
+
const handler = playbook.error_handlers.find((h) => h.on === trigger);
|
|
601
|
+
return handler?.action ?? null;
|
|
602
|
+
}
|
|
603
|
+
|
|
497
604
|
/**
|
|
498
605
|
* Determine the effective iteration cap for a node. Falls back to the
|
|
499
606
|
* runtime default (3) when `on_failure.max_iterations` is unset. The parser
|
|
@@ -532,9 +639,12 @@ async function runFromNode(args: {
|
|
|
532
639
|
approvalSecret: string;
|
|
533
640
|
maxIterationsDefault: number;
|
|
534
641
|
now: () => Date;
|
|
642
|
+
/** Project root for contract-violations.jsonl audit writes (T1261 E4). */
|
|
643
|
+
projectRoot?: string;
|
|
535
644
|
}): Promise<ExecutePlaybookResult> {
|
|
536
645
|
const {
|
|
537
646
|
db,
|
|
647
|
+
playbook,
|
|
538
648
|
run,
|
|
539
649
|
startNodeId,
|
|
540
650
|
nodeIndex,
|
|
@@ -567,6 +677,39 @@ async function runFromNode(args: {
|
|
|
567
677
|
iterationCounts: { ...iterationCounts },
|
|
568
678
|
});
|
|
569
679
|
|
|
680
|
+
// Contract enforcement (T1261 E4): validate node.requires BEFORE dispatch.
|
|
681
|
+
// On violation, trigger contract_violation error handler or fail the node.
|
|
682
|
+
if (node.requires?.fields) {
|
|
683
|
+
const violation = checkRequires(node.requires.fields, node.requires.from, context);
|
|
684
|
+
if (violation !== null) {
|
|
685
|
+
const contractFailure = `contract_violation: ${violation}`;
|
|
686
|
+
auditContractViolation(
|
|
687
|
+
args.projectRoot,
|
|
688
|
+
run.runId,
|
|
689
|
+
node.id,
|
|
690
|
+
'requires',
|
|
691
|
+
violation,
|
|
692
|
+
playbook.name,
|
|
693
|
+
);
|
|
694
|
+
const handled = handleContractErrorHandler(playbook, 'contract_violation', contractFailure);
|
|
695
|
+
if (handled === 'abort') {
|
|
696
|
+
failedNodeId = node.id;
|
|
697
|
+
lastError = contractFailure;
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
if (handled !== null) {
|
|
701
|
+
context['__lastError'] = contractFailure;
|
|
702
|
+
context['__lastFailedNode'] = node.id;
|
|
703
|
+
context['__contractViolation'] = violation;
|
|
704
|
+
if (handled === 'hitl_escalate') {
|
|
705
|
+
exceededNodeId = node.id;
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
// inject_hint: continue to dispatch with the hint in context
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
570
713
|
let outcome: NodeOutcome;
|
|
571
714
|
if (node.type === 'agentic') {
|
|
572
715
|
outcome = await executeAgenticNode(node, run.runId, context, attempt, dispatcher);
|
|
@@ -594,7 +737,56 @@ async function runFromNode(args: {
|
|
|
594
737
|
// Merge outputs into context and persist.
|
|
595
738
|
Object.assign(context, outcome.output);
|
|
596
739
|
updatePlaybookRun(db, run.runId, { bindings: { ...context } });
|
|
597
|
-
|
|
740
|
+
|
|
741
|
+
// Contract enforcement (T1261 E4): validate node.ensures AFTER merge.
|
|
742
|
+
if (node.ensures?.outputFiles) {
|
|
743
|
+
for (const key of node.ensures.outputFiles) {
|
|
744
|
+
if (!(key in context)) {
|
|
745
|
+
const violation = `ensures.outputFiles[${key}] not present in context after ${node.id}`;
|
|
746
|
+
auditContractViolation(
|
|
747
|
+
args.projectRoot,
|
|
748
|
+
run.runId,
|
|
749
|
+
node.id,
|
|
750
|
+
'ensures',
|
|
751
|
+
key,
|
|
752
|
+
playbook.name,
|
|
753
|
+
);
|
|
754
|
+
handleContractErrorHandler(playbook, 'contract_violation', violation);
|
|
755
|
+
context['__ensuresViolation'] = violation;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Validate outgoing edge contracts (edge.contract.requires on the FROM side).
|
|
761
|
+
const nextId = resolveNextNodeId(node.id, edgeIndex);
|
|
762
|
+
if (nextId !== null) {
|
|
763
|
+
const edge = resolveEdge(node.id, nextId, playbook.edges);
|
|
764
|
+
if (edge?.contract?.requires) {
|
|
765
|
+
for (const key of edge.contract.requires) {
|
|
766
|
+
if (!(key in context)) {
|
|
767
|
+
const violation = `edge.contract.requires[${key}] missing when crossing ${node.id} → ${nextId}`;
|
|
768
|
+
auditContractViolation(
|
|
769
|
+
args.projectRoot,
|
|
770
|
+
run.runId,
|
|
771
|
+
node.id,
|
|
772
|
+
'requires',
|
|
773
|
+
key,
|
|
774
|
+
playbook.name,
|
|
775
|
+
);
|
|
776
|
+
const handled = handleContractErrorHandler(playbook, 'contract_violation', violation);
|
|
777
|
+
if (handled === 'abort') {
|
|
778
|
+
failedNodeId = node.id;
|
|
779
|
+
lastError = violation;
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
context['__contractViolation'] = violation;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (failedNodeId !== undefined) break;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
currentId = nextId;
|
|
598
790
|
continue;
|
|
599
791
|
}
|
|
600
792
|
|
|
@@ -777,6 +969,7 @@ export async function executePlaybook(
|
|
|
777
969
|
approvalSecret,
|
|
778
970
|
maxIterationsDefault,
|
|
779
971
|
now,
|
|
972
|
+
projectRoot: options.projectRoot,
|
|
780
973
|
};
|
|
781
974
|
return runFromNode(runArgs);
|
|
782
975
|
}
|
|
@@ -907,6 +1100,7 @@ export async function resumePlaybook(
|
|
|
907
1100
|
approvalSecret,
|
|
908
1101
|
maxIterationsDefault,
|
|
909
1102
|
now,
|
|
1103
|
+
projectRoot: options.projectRoot,
|
|
910
1104
|
};
|
|
911
1105
|
return runFromNode(runArgs);
|
|
912
1106
|
}
|