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