@cleocode/playbooks 2026.5.132 → 2026.5.133
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 -1
- package/dist/index.js +2 -1
- package/dist/runtime.d.ts +42 -0
- package/dist/runtime.js +144 -0
- package/package.json +3 -3
- package/src/index.ts +3 -0
- package/src/runtime.ts +204 -0
package/dist/index.d.ts
CHANGED
|
@@ -27,5 +27,5 @@ export { approveGate, type CreateApprovalGateInput, createApprovalGate, E_APPROV
|
|
|
27
27
|
export { type MigratePlaybookFileResult, migratePlaybook, migratePlaybookFile, type NodeComplianceEntry, type PlaybookComplianceReport, validatePlaybookCompliance, } from './migrate-e4.js';
|
|
28
28
|
export { type ParsePlaybookResult, PlaybookParseError, parsePlaybook, } from './parser.js';
|
|
29
29
|
export { DEFAULT_POLICY_RULES, type EvaluatePolicyResult, evaluatePolicy, type PolicyRule, } from './policy.js';
|
|
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';
|
|
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, validateDecompositionTaskTree, validateIvtrEvidenceOutput, } from './runtime.js';
|
|
31
31
|
export { type CreatePlaybookApprovalInput, type CreatePlaybookRunInput, createPlaybookApproval, createPlaybookRun, deletePlaybookRun, getPlaybookApprovalByToken, getPlaybookRun, type ListPlaybookRunsOptions, listPlaybookApprovals, listPlaybookRuns, updatePlaybookApproval, updatePlaybookRun, } from './state.js';
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ export { PlaybookParseError, parsePlaybook, } from './parser.js';
|
|
|
31
31
|
// W4-9: HITL auto-policy evaluator
|
|
32
32
|
export { DEFAULT_POLICY_RULES, evaluatePolicy, } from './policy.js';
|
|
33
33
|
// W4-10 / T930: playbook runtime state machine + HITL resume
|
|
34
|
-
|
|
34
|
+
// T11499 E7-CLOSE-LOOPS AC3: schema validators exported for testing + external use
|
|
35
|
+
export { E_PLAYBOOK_RESUME_BLOCKED, E_PLAYBOOK_RUNTIME_INVALID, executePlaybook, resumePlaybook, validateDecompositionTaskTree, validateIvtrEvidenceOutput, } from './runtime.js';
|
|
35
36
|
// W4-8: state layer CRUD for playbook_runs + playbook_approvals
|
|
36
37
|
export { createPlaybookApproval, createPlaybookRun, deletePlaybookRun, getPlaybookApprovalByToken, getPlaybookRun, listPlaybookApprovals, listPlaybookRuns, updatePlaybookApproval, updatePlaybookRun, } from './state.js';
|
package/dist/runtime.d.ts
CHANGED
|
@@ -191,6 +191,48 @@ export declare const E_PLAYBOOK_RUNTIME_INVALID: "E_PLAYBOOK_RUNTIME_INVALID";
|
|
|
191
191
|
* does not resolve to an `approved` gate.
|
|
192
192
|
*/
|
|
193
193
|
export declare const E_PLAYBOOK_RESUME_BLOCKED: "E_PLAYBOOK_RESUME_BLOCKED";
|
|
194
|
+
/**
|
|
195
|
+
* Validate the `task_tree` shape emitted by a RCASD decomposition node.
|
|
196
|
+
*
|
|
197
|
+
* Returns a human-readable violation string when the shape is invalid, or
|
|
198
|
+
* `null` when the tree is well-formed. The check is performed AFTER the node
|
|
199
|
+
* output is merged into the playbook context (post-merge ensures check).
|
|
200
|
+
*
|
|
201
|
+
* Rules enforced (AC3 — T11499):
|
|
202
|
+
* 1. `task_tree` must be a non-empty array.
|
|
203
|
+
* 2. Each entry must have a non-empty string `title`.
|
|
204
|
+
* 3. Each entry must have a non-empty `acceptance` array (at least one string).
|
|
205
|
+
* 4. No entry may have an empty `depends` array that references a non-existent
|
|
206
|
+
* sibling (intra-tree orphan check).
|
|
207
|
+
*
|
|
208
|
+
* The validator is intentionally lenient on optional fields (id, parentId,
|
|
209
|
+
* depends) so that agents can emit a minimal valid tree without being rejected
|
|
210
|
+
* for cosmetic omissions.
|
|
211
|
+
*
|
|
212
|
+
* @param nodeId - Node identifier (used in error messages).
|
|
213
|
+
* @param taskTree - The raw value stored at `context.task_tree`.
|
|
214
|
+
* @returns Violation string or `null` (valid).
|
|
215
|
+
*
|
|
216
|
+
* @task T11499 E7-CLOSE-LOOPS AC3
|
|
217
|
+
*/
|
|
218
|
+
export declare function validateDecompositionTaskTree(nodeId: string, taskTree: unknown): string | null;
|
|
219
|
+
/**
|
|
220
|
+
* Validate the `evidence` field emitted by an IVTR validation node.
|
|
221
|
+
*
|
|
222
|
+
* IVTR validation nodes must emit an `evidence` key whose value is either:
|
|
223
|
+
* - An object with at least one key (structured evidence map), OR
|
|
224
|
+
* - A non-empty string (free-text evidence reference).
|
|
225
|
+
*
|
|
226
|
+
* Empty evidence (`{}`, `''`, `[]`) is rejected so that spawned agents
|
|
227
|
+
* cannot satisfy the gate by emitting an empty object.
|
|
228
|
+
*
|
|
229
|
+
* @param nodeId - Node identifier (used in error messages).
|
|
230
|
+
* @param evidence - The raw value stored at `context.evidence`.
|
|
231
|
+
* @returns Violation string or `null` (valid).
|
|
232
|
+
*
|
|
233
|
+
* @task T11499 E7-CLOSE-LOOPS AC3
|
|
234
|
+
*/
|
|
235
|
+
export declare function validateIvtrEvidenceOutput(nodeId: string, evidence: unknown): string | null;
|
|
194
236
|
/**
|
|
195
237
|
* Execute a playbook from its entry node until a terminal state is reached
|
|
196
238
|
* (`completed`, `failed`, `exceeded_iteration_cap`, or `pending_approval`).
|
package/dist/runtime.js
CHANGED
|
@@ -347,6 +347,119 @@ function iterationCapFor(node, runtimeDefault) {
|
|
|
347
347
|
return cap;
|
|
348
348
|
return runtimeDefault;
|
|
349
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Validate the `task_tree` shape emitted by a RCASD decomposition node.
|
|
352
|
+
*
|
|
353
|
+
* Returns a human-readable violation string when the shape is invalid, or
|
|
354
|
+
* `null` when the tree is well-formed. The check is performed AFTER the node
|
|
355
|
+
* output is merged into the playbook context (post-merge ensures check).
|
|
356
|
+
*
|
|
357
|
+
* Rules enforced (AC3 — T11499):
|
|
358
|
+
* 1. `task_tree` must be a non-empty array.
|
|
359
|
+
* 2. Each entry must have a non-empty string `title`.
|
|
360
|
+
* 3. Each entry must have a non-empty `acceptance` array (at least one string).
|
|
361
|
+
* 4. No entry may have an empty `depends` array that references a non-existent
|
|
362
|
+
* sibling (intra-tree orphan check).
|
|
363
|
+
*
|
|
364
|
+
* The validator is intentionally lenient on optional fields (id, parentId,
|
|
365
|
+
* depends) so that agents can emit a minimal valid tree without being rejected
|
|
366
|
+
* for cosmetic omissions.
|
|
367
|
+
*
|
|
368
|
+
* @param nodeId - Node identifier (used in error messages).
|
|
369
|
+
* @param taskTree - The raw value stored at `context.task_tree`.
|
|
370
|
+
* @returns Violation string or `null` (valid).
|
|
371
|
+
*
|
|
372
|
+
* @task T11499 E7-CLOSE-LOOPS AC3
|
|
373
|
+
*/
|
|
374
|
+
export function validateDecompositionTaskTree(nodeId, taskTree) {
|
|
375
|
+
if (!Array.isArray(taskTree)) {
|
|
376
|
+
return `ensures.schema[task_tree] on ${nodeId}: task_tree must be a non-empty array, got ${typeof taskTree}`;
|
|
377
|
+
}
|
|
378
|
+
if (taskTree.length === 0) {
|
|
379
|
+
return `ensures.schema[task_tree] on ${nodeId}: task_tree is an empty array — decomposition produced no tasks`;
|
|
380
|
+
}
|
|
381
|
+
const knownIds = new Set();
|
|
382
|
+
for (const entry of taskTree) {
|
|
383
|
+
if (typeof entry.id === 'string') {
|
|
384
|
+
knownIds.add(entry.id);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
for (let i = 0; i < taskTree.length; i++) {
|
|
388
|
+
const entry = taskTree[i];
|
|
389
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
390
|
+
return `ensures.schema[task_tree] on ${nodeId}: entry[${i}] must be an object, got ${entry === null ? 'null' : typeof entry}`;
|
|
391
|
+
}
|
|
392
|
+
if (typeof entry.title !== 'string' || entry.title.trim().length === 0) {
|
|
393
|
+
return `ensures.schema[task_tree] on ${nodeId}: entry[${i}].title must be a non-empty string`;
|
|
394
|
+
}
|
|
395
|
+
if (!Array.isArray(entry.acceptance) || entry.acceptance.length === 0) {
|
|
396
|
+
return (`ensures.schema[task_tree] on ${nodeId}: entry[${i}] ("${entry.title}") ` +
|
|
397
|
+
`must have a non-empty acceptance array`);
|
|
398
|
+
}
|
|
399
|
+
const hasValidAc = entry.acceptance.some((ac) => typeof ac === 'string' && ac.trim().length > 0);
|
|
400
|
+
if (!hasValidAc) {
|
|
401
|
+
return (`ensures.schema[task_tree] on ${nodeId}: entry[${i}] ("${entry.title}") ` +
|
|
402
|
+
`acceptance array contains no non-empty strings`);
|
|
403
|
+
}
|
|
404
|
+
// Intra-tree orphan check: if depends[] are specified, they must reference
|
|
405
|
+
// another entry in the same tree (by id). References to external tasks are
|
|
406
|
+
// allowed (they have no id in this tree), so we only flag ids that look
|
|
407
|
+
// like intra-tree references but are absent from knownIds.
|
|
408
|
+
if (Array.isArray(entry.depends) && knownIds.size > 0) {
|
|
409
|
+
for (const depId of entry.depends) {
|
|
410
|
+
// Only flag if the depId matches the T#### pattern and is not in knownIds
|
|
411
|
+
// (external task IDs that exist in the DB are valid but we can't check here).
|
|
412
|
+
if (typeof depId === 'string' && /^T\d{3,}$/.test(depId) && !knownIds.has(depId)) {
|
|
413
|
+
// Soft warn only (not a hard violation) — the dep may be a pre-existing task.
|
|
414
|
+
// We don't hard-fail here because agents may correctly depend on existing tasks.
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return null; // valid
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Validate the `evidence` field emitted by an IVTR validation node.
|
|
423
|
+
*
|
|
424
|
+
* IVTR validation nodes must emit an `evidence` key whose value is either:
|
|
425
|
+
* - An object with at least one key (structured evidence map), OR
|
|
426
|
+
* - A non-empty string (free-text evidence reference).
|
|
427
|
+
*
|
|
428
|
+
* Empty evidence (`{}`, `''`, `[]`) is rejected so that spawned agents
|
|
429
|
+
* cannot satisfy the gate by emitting an empty object.
|
|
430
|
+
*
|
|
431
|
+
* @param nodeId - Node identifier (used in error messages).
|
|
432
|
+
* @param evidence - The raw value stored at `context.evidence`.
|
|
433
|
+
* @returns Violation string or `null` (valid).
|
|
434
|
+
*
|
|
435
|
+
* @task T11499 E7-CLOSE-LOOPS AC3
|
|
436
|
+
*/
|
|
437
|
+
export function validateIvtrEvidenceOutput(nodeId, evidence) {
|
|
438
|
+
if (evidence === null || evidence === undefined) {
|
|
439
|
+
return `ensures.schema[evidence] on ${nodeId}: evidence must be present (non-null, non-undefined)`;
|
|
440
|
+
}
|
|
441
|
+
if (typeof evidence === 'string') {
|
|
442
|
+
if (evidence.trim().length === 0) {
|
|
443
|
+
return `ensures.schema[evidence] on ${nodeId}: evidence string must not be empty`;
|
|
444
|
+
}
|
|
445
|
+
return null; // valid non-empty string
|
|
446
|
+
}
|
|
447
|
+
if (Array.isArray(evidence)) {
|
|
448
|
+
if (evidence.length === 0) {
|
|
449
|
+
return `ensures.schema[evidence] on ${nodeId}: evidence array must not be empty`;
|
|
450
|
+
}
|
|
451
|
+
return null; // valid non-empty array
|
|
452
|
+
}
|
|
453
|
+
if (typeof evidence === 'object') {
|
|
454
|
+
const keys = Object.keys(evidence);
|
|
455
|
+
if (keys.length === 0) {
|
|
456
|
+
return `ensures.schema[evidence] on ${nodeId}: evidence object must have at least one key (got {})`;
|
|
457
|
+
}
|
|
458
|
+
return null; // valid non-empty object
|
|
459
|
+
}
|
|
460
|
+
// Numbers, booleans etc. are not valid evidence shapes.
|
|
461
|
+
return `ensures.schema[evidence] on ${nodeId}: evidence must be a string, array, or object (got ${typeof evidence})`;
|
|
462
|
+
}
|
|
350
463
|
/**
|
|
351
464
|
* Core step-by-step executor shared by {@link executePlaybook} and
|
|
352
465
|
* {@link resumePlaybook}. Starts at `startNodeId` and walks the graph until
|
|
@@ -433,6 +546,37 @@ async function runFromNode(args) {
|
|
|
433
546
|
}
|
|
434
547
|
}
|
|
435
548
|
}
|
|
549
|
+
// RCASD/IVTR output schema validation (T11499 AC3):
|
|
550
|
+
// Enforce ensures.schema beyond key-presence so garbage decompositions
|
|
551
|
+
// cannot silently pass the runtime contract check.
|
|
552
|
+
if (node.ensures?.schema) {
|
|
553
|
+
let schemaViolation = null;
|
|
554
|
+
if (node.ensures.schema === 'task_tree') {
|
|
555
|
+
schemaViolation = validateDecompositionTaskTree(node.id, context['task_tree']);
|
|
556
|
+
}
|
|
557
|
+
else if (node.ensures.schema === 'evidence') {
|
|
558
|
+
schemaViolation = validateIvtrEvidenceOutput(node.id, context['evidence']);
|
|
559
|
+
}
|
|
560
|
+
// Future schema names are silently skipped (open for extension).
|
|
561
|
+
if (schemaViolation !== null) {
|
|
562
|
+
auditContractViolation(args.projectRoot, run.runId, node.id, 'ensures', node.ensures.schema, playbook.name);
|
|
563
|
+
const handled = handleContractErrorHandler(playbook, 'contract_violation', schemaViolation);
|
|
564
|
+
if (handled === 'abort') {
|
|
565
|
+
failedNodeId = node.id;
|
|
566
|
+
lastError = schemaViolation;
|
|
567
|
+
// break out of the while loop below
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
context['__ensuresSchemaViolation'] = schemaViolation;
|
|
571
|
+
if (handled === 'hitl_escalate') {
|
|
572
|
+
exceededNodeId = node.id;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// If a fatal schema violation was detected, break the step loop.
|
|
578
|
+
if (failedNodeId !== undefined)
|
|
579
|
+
break;
|
|
436
580
|
// Validate outgoing edge contracts (edge.contract.requires on the FROM side).
|
|
437
581
|
const nextId = resolveNextNodeId(node.id, edgeIndex);
|
|
438
582
|
if (nextId !== null) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/playbooks",
|
|
3
|
-
"version": "2026.5.
|
|
3
|
+
"version": "2026.5.133",
|
|
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-rc.3",
|
|
21
21
|
"js-yaml": "^4.1.0",
|
|
22
|
-
"@cleocode/contracts": "2026.5.
|
|
23
|
-
"@cleocode/core": "2026.5.
|
|
22
|
+
"@cleocode/contracts": "2026.5.133",
|
|
23
|
+
"@cleocode/core": "2026.5.133"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/js-yaml": "^4.0.9",
|
package/src/index.ts
CHANGED
|
@@ -59,6 +59,7 @@ export {
|
|
|
59
59
|
type PolicyRule,
|
|
60
60
|
} from './policy.js';
|
|
61
61
|
// W4-10 / T930: playbook runtime state machine + HITL resume
|
|
62
|
+
// T11499 E7-CLOSE-LOOPS AC3: schema validators exported for testing + external use
|
|
62
63
|
export {
|
|
63
64
|
type AgentDispatcher,
|
|
64
65
|
type AgentDispatchInput,
|
|
@@ -74,6 +75,8 @@ export {
|
|
|
74
75
|
type PlaybookTerminalStatus,
|
|
75
76
|
type ResumePlaybookOptions,
|
|
76
77
|
resumePlaybook,
|
|
78
|
+
validateDecompositionTaskTree,
|
|
79
|
+
validateIvtrEvidenceOutput,
|
|
77
80
|
} from './runtime.js';
|
|
78
81
|
// W4-8: state layer CRUD for playbook_runs + playbook_approvals
|
|
79
82
|
export {
|
package/src/runtime.ts
CHANGED
|
@@ -612,6 +612,167 @@ function iterationCapFor(node: PlaybookNode, runtimeDefault: number): number {
|
|
|
612
612
|
return runtimeDefault;
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
+
// ---------------------------------------------------------------------------
|
|
616
|
+
// RCASD/IVTR node output schema validation (T11499 AC3)
|
|
617
|
+
// ---------------------------------------------------------------------------
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* A single task entry within a decomposition `task_tree` output.
|
|
621
|
+
*
|
|
622
|
+
* Agents emitting a decomposition must produce a `task_tree` key in the
|
|
623
|
+
* playbook context whose value is an array of objects conforming to this shape.
|
|
624
|
+
* Validating the real shape — not just key presence — prevents garbage
|
|
625
|
+
* decompositions from silently entering the run queue.
|
|
626
|
+
*
|
|
627
|
+
* @task T11499 E7-CLOSE-LOOPS AC3
|
|
628
|
+
*/
|
|
629
|
+
interface DecompositionTaskEntry {
|
|
630
|
+
/** Task title — required, non-empty string. */
|
|
631
|
+
title: string;
|
|
632
|
+
/** Acceptance criteria — must be present and non-empty. */
|
|
633
|
+
acceptance: string[];
|
|
634
|
+
/** Optional task ID (may be pre-assigned by the agent). */
|
|
635
|
+
id?: string;
|
|
636
|
+
/** Optional parent task ID. */
|
|
637
|
+
parentId?: string;
|
|
638
|
+
/** Optional dependency IDs. */
|
|
639
|
+
depends?: string[];
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Validate the `task_tree` shape emitted by a RCASD decomposition node.
|
|
644
|
+
*
|
|
645
|
+
* Returns a human-readable violation string when the shape is invalid, or
|
|
646
|
+
* `null` when the tree is well-formed. The check is performed AFTER the node
|
|
647
|
+
* output is merged into the playbook context (post-merge ensures check).
|
|
648
|
+
*
|
|
649
|
+
* Rules enforced (AC3 — T11499):
|
|
650
|
+
* 1. `task_tree` must be a non-empty array.
|
|
651
|
+
* 2. Each entry must have a non-empty string `title`.
|
|
652
|
+
* 3. Each entry must have a non-empty `acceptance` array (at least one string).
|
|
653
|
+
* 4. No entry may have an empty `depends` array that references a non-existent
|
|
654
|
+
* sibling (intra-tree orphan check).
|
|
655
|
+
*
|
|
656
|
+
* The validator is intentionally lenient on optional fields (id, parentId,
|
|
657
|
+
* depends) so that agents can emit a minimal valid tree without being rejected
|
|
658
|
+
* for cosmetic omissions.
|
|
659
|
+
*
|
|
660
|
+
* @param nodeId - Node identifier (used in error messages).
|
|
661
|
+
* @param taskTree - The raw value stored at `context.task_tree`.
|
|
662
|
+
* @returns Violation string or `null` (valid).
|
|
663
|
+
*
|
|
664
|
+
* @task T11499 E7-CLOSE-LOOPS AC3
|
|
665
|
+
*/
|
|
666
|
+
export function validateDecompositionTaskTree(nodeId: string, taskTree: unknown): string | null {
|
|
667
|
+
if (!Array.isArray(taskTree)) {
|
|
668
|
+
return `ensures.schema[task_tree] on ${nodeId}: task_tree must be a non-empty array, got ${typeof taskTree}`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (taskTree.length === 0) {
|
|
672
|
+
return `ensures.schema[task_tree] on ${nodeId}: task_tree is an empty array — decomposition produced no tasks`;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const knownIds = new Set<string>();
|
|
676
|
+
for (const entry of taskTree) {
|
|
677
|
+
if (typeof (entry as DecompositionTaskEntry).id === 'string') {
|
|
678
|
+
knownIds.add((entry as DecompositionTaskEntry).id!);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
for (let i = 0; i < taskTree.length; i++) {
|
|
683
|
+
const entry = taskTree[i] as DecompositionTaskEntry;
|
|
684
|
+
|
|
685
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
686
|
+
return `ensures.schema[task_tree] on ${nodeId}: entry[${i}] must be an object, got ${entry === null ? 'null' : typeof entry}`;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (typeof entry.title !== 'string' || entry.title.trim().length === 0) {
|
|
690
|
+
return `ensures.schema[task_tree] on ${nodeId}: entry[${i}].title must be a non-empty string`;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (!Array.isArray(entry.acceptance) || entry.acceptance.length === 0) {
|
|
694
|
+
return (
|
|
695
|
+
`ensures.schema[task_tree] on ${nodeId}: entry[${i}] ("${entry.title}") ` +
|
|
696
|
+
`must have a non-empty acceptance array`
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const hasValidAc = entry.acceptance.some(
|
|
701
|
+
(ac) => typeof ac === 'string' && ac.trim().length > 0,
|
|
702
|
+
);
|
|
703
|
+
if (!hasValidAc) {
|
|
704
|
+
return (
|
|
705
|
+
`ensures.schema[task_tree] on ${nodeId}: entry[${i}] ("${entry.title}") ` +
|
|
706
|
+
`acceptance array contains no non-empty strings`
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Intra-tree orphan check: if depends[] are specified, they must reference
|
|
711
|
+
// another entry in the same tree (by id). References to external tasks are
|
|
712
|
+
// allowed (they have no id in this tree), so we only flag ids that look
|
|
713
|
+
// like intra-tree references but are absent from knownIds.
|
|
714
|
+
if (Array.isArray(entry.depends) && knownIds.size > 0) {
|
|
715
|
+
for (const depId of entry.depends) {
|
|
716
|
+
// Only flag if the depId matches the T#### pattern and is not in knownIds
|
|
717
|
+
// (external task IDs that exist in the DB are valid but we can't check here).
|
|
718
|
+
if (typeof depId === 'string' && /^T\d{3,}$/.test(depId) && !knownIds.has(depId)) {
|
|
719
|
+
// Soft warn only (not a hard violation) — the dep may be a pre-existing task.
|
|
720
|
+
// We don't hard-fail here because agents may correctly depend on existing tasks.
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return null; // valid
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Validate the `evidence` field emitted by an IVTR validation node.
|
|
731
|
+
*
|
|
732
|
+
* IVTR validation nodes must emit an `evidence` key whose value is either:
|
|
733
|
+
* - An object with at least one key (structured evidence map), OR
|
|
734
|
+
* - A non-empty string (free-text evidence reference).
|
|
735
|
+
*
|
|
736
|
+
* Empty evidence (`{}`, `''`, `[]`) is rejected so that spawned agents
|
|
737
|
+
* cannot satisfy the gate by emitting an empty object.
|
|
738
|
+
*
|
|
739
|
+
* @param nodeId - Node identifier (used in error messages).
|
|
740
|
+
* @param evidence - The raw value stored at `context.evidence`.
|
|
741
|
+
* @returns Violation string or `null` (valid).
|
|
742
|
+
*
|
|
743
|
+
* @task T11499 E7-CLOSE-LOOPS AC3
|
|
744
|
+
*/
|
|
745
|
+
export function validateIvtrEvidenceOutput(nodeId: string, evidence: unknown): string | null {
|
|
746
|
+
if (evidence === null || evidence === undefined) {
|
|
747
|
+
return `ensures.schema[evidence] on ${nodeId}: evidence must be present (non-null, non-undefined)`;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (typeof evidence === 'string') {
|
|
751
|
+
if (evidence.trim().length === 0) {
|
|
752
|
+
return `ensures.schema[evidence] on ${nodeId}: evidence string must not be empty`;
|
|
753
|
+
}
|
|
754
|
+
return null; // valid non-empty string
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (Array.isArray(evidence)) {
|
|
758
|
+
if (evidence.length === 0) {
|
|
759
|
+
return `ensures.schema[evidence] on ${nodeId}: evidence array must not be empty`;
|
|
760
|
+
}
|
|
761
|
+
return null; // valid non-empty array
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (typeof evidence === 'object') {
|
|
765
|
+
const keys = Object.keys(evidence as object);
|
|
766
|
+
if (keys.length === 0) {
|
|
767
|
+
return `ensures.schema[evidence] on ${nodeId}: evidence object must have at least one key (got {})`;
|
|
768
|
+
}
|
|
769
|
+
return null; // valid non-empty object
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Numbers, booleans etc. are not valid evidence shapes.
|
|
773
|
+
return `ensures.schema[evidence] on ${nodeId}: evidence must be a string, array, or object (got ${typeof evidence})`;
|
|
774
|
+
}
|
|
775
|
+
|
|
615
776
|
/**
|
|
616
777
|
* Core step-by-step executor shared by {@link executePlaybook} and
|
|
617
778
|
* {@link resumePlaybook}. Starts at `startNodeId` and walks the graph until
|
|
@@ -757,6 +918,49 @@ async function runFromNode(args: {
|
|
|
757
918
|
}
|
|
758
919
|
}
|
|
759
920
|
|
|
921
|
+
// RCASD/IVTR output schema validation (T11499 AC3):
|
|
922
|
+
// Enforce ensures.schema beyond key-presence so garbage decompositions
|
|
923
|
+
// cannot silently pass the runtime contract check.
|
|
924
|
+
if (node.ensures?.schema) {
|
|
925
|
+
let schemaViolation: string | null = null;
|
|
926
|
+
|
|
927
|
+
if (node.ensures.schema === 'task_tree') {
|
|
928
|
+
schemaViolation = validateDecompositionTaskTree(node.id, context['task_tree']);
|
|
929
|
+
} else if (node.ensures.schema === 'evidence') {
|
|
930
|
+
schemaViolation = validateIvtrEvidenceOutput(node.id, context['evidence']);
|
|
931
|
+
}
|
|
932
|
+
// Future schema names are silently skipped (open for extension).
|
|
933
|
+
|
|
934
|
+
if (schemaViolation !== null) {
|
|
935
|
+
auditContractViolation(
|
|
936
|
+
args.projectRoot,
|
|
937
|
+
run.runId,
|
|
938
|
+
node.id,
|
|
939
|
+
'ensures',
|
|
940
|
+
node.ensures.schema,
|
|
941
|
+
playbook.name,
|
|
942
|
+
);
|
|
943
|
+
const handled = handleContractErrorHandler(
|
|
944
|
+
playbook,
|
|
945
|
+
'contract_violation',
|
|
946
|
+
schemaViolation,
|
|
947
|
+
);
|
|
948
|
+
if (handled === 'abort') {
|
|
949
|
+
failedNodeId = node.id;
|
|
950
|
+
lastError = schemaViolation;
|
|
951
|
+
// break out of the while loop below
|
|
952
|
+
} else {
|
|
953
|
+
context['__ensuresSchemaViolation'] = schemaViolation;
|
|
954
|
+
if (handled === 'hitl_escalate') {
|
|
955
|
+
exceededNodeId = node.id;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// If a fatal schema violation was detected, break the step loop.
|
|
962
|
+
if (failedNodeId !== undefined) break;
|
|
963
|
+
|
|
760
964
|
// Validate outgoing edge contracts (edge.contract.requires on the FROM side).
|
|
761
965
|
const nextId = resolveNextNodeId(node.id, edgeIndex);
|
|
762
966
|
if (nextId !== null) {
|