@nanobpm/nano-workforce 0.201.3 → 0.201.5
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/.github/workflows/ci.yml +11 -0
- package/AGENTS.md +3 -1
- package/CHANGELOG.md +12 -0
- package/SPEC.md +123 -5
- package/app/agentOutputEnvelope.test.ts +464 -0
- package/app/agentic/vocab/agent-marker.test.ts +30 -1
- package/app/agentic/vocab/job-types.test.ts +133 -1
- package/app/agentic/vocab/job-types.ts +57 -0
- package/app/contracts.ts +2 -2
- package/app/deliveryGraphCompiler.test.ts +81 -0
- package/app/deliveryGraphCompiler.ts +22 -1
- package/app/mergeLoopBehaviour.test.ts +208 -1
- package/package.json +2 -1
- package/resources/processes/convergence-loop.bpmn +108 -1
- package/resources/processes/merge-loop.bpmn +80 -2
- package/scripts/check-agent-definitions.test.ts +126 -0
- package/scripts/check-agent-definitions.ts +191 -0
|
@@ -362,7 +362,46 @@
|
|
|
362
362
|
</zeebe:properties>
|
|
363
363
|
<zeebe:ioMapping>
|
|
364
364
|
<zeebe:input source="=(if (failingChecksList = null or failingChecksList = "") then "" else " Failing checks to make green: " + failingChecksList) + (if (abandonBrief = null) then "" else abandonBrief)" target="appendPrompt" />
|
|
365
|
-
<zeebe:
|
|
365
|
+
<zeebe:input source="=ciFixRound" target="guardCiFixRound" />
|
|
366
|
+
<zeebe:input source="=ciFixMax" target="guardCiFixMax" />
|
|
367
|
+
<zeebe:input source="=rebaseRound" target="guardRebaseRound" />
|
|
368
|
+
<zeebe:input source="=rebaseMax" target="guardRebaseMax" />
|
|
369
|
+
<zeebe:input source="=mergeRetryRound" target="guardMergeRetryRound" />
|
|
370
|
+
<zeebe:input source="=mergeRetryMax" target="guardMergeRetryMax" />
|
|
371
|
+
<zeebe:input source="=mergeStallRounds" target="guardMergeStallRounds" />
|
|
372
|
+
<zeebe:input source="=mergeStallMax" target="guardMergeStallMax" />
|
|
373
|
+
<zeebe:input source="=prKey" target="guardPrKey" />
|
|
374
|
+
<zeebe:input source="=repo" target="guardRepo" />
|
|
375
|
+
<zeebe:input source="=prNumber" target="guardPrNumber" />
|
|
376
|
+
<zeebe:input source="=prUrl" target="guardPrUrl" />
|
|
377
|
+
<zeebe:input source="=ciBlockedReconciled" target="guardCiBlockedReconciled" />
|
|
378
|
+
<zeebe:input source="=agentSlaTimeout" target="guardAgentSlaTimeout" />
|
|
379
|
+
<zeebe:input source="=landedWaitTimeout" target="guardLandedWaitTimeout" />
|
|
380
|
+
<zeebe:input source="=mergeableWaitTimeout" target="guardMergeableWaitTimeout" />
|
|
381
|
+
<zeebe:input source="=mergeableRepollInterval" target="guardMergeableRepollInterval" />
|
|
382
|
+
<zeebe:input source="=round" target="guardRound" />
|
|
383
|
+
<zeebe:input source="=abandonUrl" target="guardAbandonUrl" />
|
|
384
|
+
<zeebe:input source="=abandonBrief" target="guardAbandonBrief" />
|
|
385
|
+
<zeebe:output source="=guardCiFixRound + 1" target="ciFixRound" />
|
|
386
|
+
<zeebe:output source="=guardCiFixMax" target="ciFixMax" />
|
|
387
|
+
<zeebe:output source="=guardRebaseRound" target="rebaseRound" />
|
|
388
|
+
<zeebe:output source="=guardRebaseMax" target="rebaseMax" />
|
|
389
|
+
<zeebe:output source="=guardMergeRetryRound" target="mergeRetryRound" />
|
|
390
|
+
<zeebe:output source="=guardMergeRetryMax" target="mergeRetryMax" />
|
|
391
|
+
<zeebe:output source="=guardMergeStallRounds" target="mergeStallRounds" />
|
|
392
|
+
<zeebe:output source="=guardMergeStallMax" target="mergeStallMax" />
|
|
393
|
+
<zeebe:output source="=guardPrKey" target="prKey" />
|
|
394
|
+
<zeebe:output source="=guardRepo" target="repo" />
|
|
395
|
+
<zeebe:output source="=guardPrNumber" target="prNumber" />
|
|
396
|
+
<zeebe:output source="=guardPrUrl" target="prUrl" />
|
|
397
|
+
<zeebe:output source="=guardCiBlockedReconciled" target="ciBlockedReconciled" />
|
|
398
|
+
<zeebe:output source="=guardAgentSlaTimeout" target="agentSlaTimeout" />
|
|
399
|
+
<zeebe:output source="=guardLandedWaitTimeout" target="landedWaitTimeout" />
|
|
400
|
+
<zeebe:output source="=guardMergeableWaitTimeout" target="mergeableWaitTimeout" />
|
|
401
|
+
<zeebe:output source="=guardMergeableRepollInterval" target="mergeableRepollInterval" />
|
|
402
|
+
<zeebe:output source="=guardRound" target="round" />
|
|
403
|
+
<zeebe:output source="=guardAbandonUrl" target="abandonUrl" />
|
|
404
|
+
<zeebe:output source="=guardAbandonBrief" target="abandonBrief" />
|
|
366
405
|
</zeebe:ioMapping>
|
|
367
406
|
</bpmn:extensionElements>
|
|
368
407
|
<bpmn:incoming>f_ci_s0</bpmn:incoming>
|
|
@@ -524,7 +563,46 @@
|
|
|
524
563
|
</zeebe:properties>
|
|
525
564
|
<zeebe:ioMapping>
|
|
526
565
|
<zeebe:input source="=(if (abandonBrief = null) then "" else abandonBrief)" target="appendPrompt" />
|
|
527
|
-
<zeebe:
|
|
566
|
+
<zeebe:input source="=ciFixRound" target="guardCiFixRound" />
|
|
567
|
+
<zeebe:input source="=ciFixMax" target="guardCiFixMax" />
|
|
568
|
+
<zeebe:input source="=rebaseRound" target="guardRebaseRound" />
|
|
569
|
+
<zeebe:input source="=rebaseMax" target="guardRebaseMax" />
|
|
570
|
+
<zeebe:input source="=mergeRetryRound" target="guardMergeRetryRound" />
|
|
571
|
+
<zeebe:input source="=mergeRetryMax" target="guardMergeRetryMax" />
|
|
572
|
+
<zeebe:input source="=mergeStallRounds" target="guardMergeStallRounds" />
|
|
573
|
+
<zeebe:input source="=mergeStallMax" target="guardMergeStallMax" />
|
|
574
|
+
<zeebe:input source="=prKey" target="guardPrKey" />
|
|
575
|
+
<zeebe:input source="=repo" target="guardRepo" />
|
|
576
|
+
<zeebe:input source="=prNumber" target="guardPrNumber" />
|
|
577
|
+
<zeebe:input source="=prUrl" target="guardPrUrl" />
|
|
578
|
+
<zeebe:input source="=ciBlockedReconciled" target="guardCiBlockedReconciled" />
|
|
579
|
+
<zeebe:input source="=agentSlaTimeout" target="guardAgentSlaTimeout" />
|
|
580
|
+
<zeebe:input source="=landedWaitTimeout" target="guardLandedWaitTimeout" />
|
|
581
|
+
<zeebe:input source="=mergeableWaitTimeout" target="guardMergeableWaitTimeout" />
|
|
582
|
+
<zeebe:input source="=mergeableRepollInterval" target="guardMergeableRepollInterval" />
|
|
583
|
+
<zeebe:input source="=round" target="guardRound" />
|
|
584
|
+
<zeebe:input source="=abandonUrl" target="guardAbandonUrl" />
|
|
585
|
+
<zeebe:input source="=abandonBrief" target="guardAbandonBrief" />
|
|
586
|
+
<zeebe:output source="=guardRebaseRound + 1" target="rebaseRound" />
|
|
587
|
+
<zeebe:output source="=guardCiFixRound" target="ciFixRound" />
|
|
588
|
+
<zeebe:output source="=guardCiFixMax" target="ciFixMax" />
|
|
589
|
+
<zeebe:output source="=guardRebaseMax" target="rebaseMax" />
|
|
590
|
+
<zeebe:output source="=guardMergeRetryRound" target="mergeRetryRound" />
|
|
591
|
+
<zeebe:output source="=guardMergeRetryMax" target="mergeRetryMax" />
|
|
592
|
+
<zeebe:output source="=guardMergeStallRounds" target="mergeStallRounds" />
|
|
593
|
+
<zeebe:output source="=guardMergeStallMax" target="mergeStallMax" />
|
|
594
|
+
<zeebe:output source="=guardPrKey" target="prKey" />
|
|
595
|
+
<zeebe:output source="=guardRepo" target="repo" />
|
|
596
|
+
<zeebe:output source="=guardPrNumber" target="prNumber" />
|
|
597
|
+
<zeebe:output source="=guardPrUrl" target="prUrl" />
|
|
598
|
+
<zeebe:output source="=guardCiBlockedReconciled" target="ciBlockedReconciled" />
|
|
599
|
+
<zeebe:output source="=guardAgentSlaTimeout" target="agentSlaTimeout" />
|
|
600
|
+
<zeebe:output source="=guardLandedWaitTimeout" target="landedWaitTimeout" />
|
|
601
|
+
<zeebe:output source="=guardMergeableWaitTimeout" target="mergeableWaitTimeout" />
|
|
602
|
+
<zeebe:output source="=guardMergeableRepollInterval" target="mergeableRepollInterval" />
|
|
603
|
+
<zeebe:output source="=guardRound" target="round" />
|
|
604
|
+
<zeebe:output source="=guardAbandonUrl" target="abandonUrl" />
|
|
605
|
+
<zeebe:output source="=guardAbandonBrief" target="abandonBrief" />
|
|
528
606
|
</zeebe:ioMapping>
|
|
529
607
|
</bpmn:extensionElements>
|
|
530
608
|
<bpmn:incoming>f_reb_s0</bpmn:incoming>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Red/green coverage for the agent-definition repo-wide guard (scripts/check-agent-definitions.ts,
|
|
2
|
+
// issue #884). The gate fails the build when any `senior:*` agent service task — hand-authored in
|
|
3
|
+
// `resources/processes/*.bpmn` OR produced by the delivery-graph compiler — lacks the engine-native
|
|
4
|
+
// `<zeebe:agentDefinition agentType="external" />` marker (the omission that left every delivery-graph
|
|
5
|
+
// agent run without a durable transcript). These cases assert the scanner wiring flags an unmarked
|
|
6
|
+
// prompt-bearing task and that the gate's compiled-output half sees a real compiled agent cell.
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { test } from "node:test";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { assert, assertEquals } from "#test-assert";
|
|
12
|
+
import {
|
|
13
|
+
agentTaskTypesMissingExternalMarker,
|
|
14
|
+
externalAgentTaskTypesMissingMarker,
|
|
15
|
+
} from "../app/agentic/vocab/job-types.ts";
|
|
16
|
+
import { checkAuthoredProcesses, checkCompiledOutput } from "./check-agent-definitions.ts";
|
|
17
|
+
|
|
18
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
+
|
|
20
|
+
test("check-agent-definitions: the canonical scanner flags an unmarked prompt-bearing senior:* task", () => {
|
|
21
|
+
// The gate's red path rests entirely on the canonical scanner — pin that wiring here so the gate
|
|
22
|
+
// can never rot into a vacuous green (a scanner that stopped flagging would pass every corpus).
|
|
23
|
+
const xml = `
|
|
24
|
+
<bpmn:serviceTask id="agent">
|
|
25
|
+
<bpmn:extensionElements>
|
|
26
|
+
<zeebe:taskDefinition type="senior:feature" />
|
|
27
|
+
<zeebe:linkedResources>
|
|
28
|
+
<zeebe:linkedResource resourceId="prompts/feature.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
29
|
+
</zeebe:linkedResources>
|
|
30
|
+
</bpmn:extensionElements>
|
|
31
|
+
</bpmn:serviceTask>`;
|
|
32
|
+
assertEquals(agentTaskTypesMissingExternalMarker(xml), ["senior:feature"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("check-agent-definitions: the hand-authored corpus passes (every senior:* task is marked)", () => {
|
|
36
|
+
const r = checkAuthoredProcesses();
|
|
37
|
+
assert(r.ok, `expected the authored corpus to pass, got ${JSON.stringify(r.errors)}`);
|
|
38
|
+
// Sanity: the corpus really does declare agent tasks (the guard is not vacuously green).
|
|
39
|
+
assert(r.agentTypes.length > 0, "the authored corpus declares prompt-bearing senior:* agent tasks");
|
|
40
|
+
assert(r.agentTypes.includes("senior:feature"), "senior:feature is among the scanned agent types");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("check-agent-definitions: the compiled-output half sees a real compiled agent cell and finds it marked", async () => {
|
|
44
|
+
const r = await checkCompiledOutput();
|
|
45
|
+
assert(r.ok, `expected the compiled output to pass, got ${JSON.stringify(r.errors)}`);
|
|
46
|
+
// Every compiled agent cell is prompt-LESS (Option (a)), so it is the type-keyed half of the union
|
|
47
|
+
// scanner (keyed by the `<rank>:<task>` colon-form type) that sees them. The probe's agent types are
|
|
48
|
+
// derived by that same grammar — assert the cells really compiled (the scan is not vacuously green).
|
|
49
|
+
assert(r.agentTypes.length > 0, "the compiled probe declares senior:* agent cells");
|
|
50
|
+
assert(r.agentTypes.includes("senior:feature"), "the compiled senior:feature cell is scanned");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("check-agent-definitions: the compiled probe includes a prompt-LESS agent cell, and it is marked", async () => {
|
|
54
|
+
// Every compiled agent cell is deliberately prompt-less (the resolved #884 "Decide", Option (a)), so
|
|
55
|
+
// the prompt-keyed scanner never sees it — the gate must probe one and assert its marker with the
|
|
56
|
+
// type-keyed scan, or a compiler regression that drops the marker on exactly those cells reports ok
|
|
57
|
+
// (the false-negative path from the PR #886 review).
|
|
58
|
+
const r = await checkCompiledOutput();
|
|
59
|
+
assert(r.ok, `expected the compiled output to pass, got ${JSON.stringify(r.errors)}`);
|
|
60
|
+
assert(
|
|
61
|
+
r.agentTypes.includes("senior:bespoke-promptless"),
|
|
62
|
+
`the compiled probe includes a prompt-less agent cell, got [${r.agentTypes.join(", ")}]`,
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("check-agent-definitions: the type-keyed scanner flags a prompt-less compiled cell that lost its marker", () => {
|
|
67
|
+
// Pin the false-negative shape the review cited: a prompt-less agent task with NO marker. The
|
|
68
|
+
// prompt-keyed scan reports nothing (it cannot see the block); the type-keyed scan must flag it.
|
|
69
|
+
const xml = `
|
|
70
|
+
<bpmn:serviceTask id="n0_task">
|
|
71
|
+
<bpmn:extensionElements>
|
|
72
|
+
<zeebe:taskDefinition type="senior:bespoke-promptless" />
|
|
73
|
+
</bpmn:extensionElements>
|
|
74
|
+
</bpmn:serviceTask>`;
|
|
75
|
+
assertEquals(agentTaskTypesMissingExternalMarker(xml), []);
|
|
76
|
+
assertEquals(externalAgentTaskTypesMissingMarker(xml), ["senior:bespoke-promptless"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("check-agent-definitions: the authored half unions BOTH scanners — a prompt-less + marker-less senior:* task fails", () => {
|
|
80
|
+
// The authored-process half must hold the SAME "every senior:* task carries the marker" invariant
|
|
81
|
+
// the compiled half does (PR #886 review). A hand-authored senior:* task that loses BOTH its prompt
|
|
82
|
+
// link and its marker is invisible to the prompt-keyed scanner, so a prompt-keyed-only authored gate
|
|
83
|
+
// reports ok — the false-negative the compiled half already closes. Inject a doc with exactly that
|
|
84
|
+
// shape and assert the gate flags it (via the type-keyed half of the union).
|
|
85
|
+
const bad = `
|
|
86
|
+
<bpmn:serviceTask id="agent">
|
|
87
|
+
<bpmn:extensionElements>
|
|
88
|
+
<zeebe:taskDefinition type="senior:bespoke-promptless" />
|
|
89
|
+
</bpmn:extensionElements>
|
|
90
|
+
</bpmn:serviceTask>`;
|
|
91
|
+
const r = checkAuthoredProcesses([{ file: "bad.bpmn", xml: bad }]);
|
|
92
|
+
assert(!r.ok, "a prompt-less + marker-less senior:* authored task must fail the gate");
|
|
93
|
+
assert(
|
|
94
|
+
r.errors.some((e) => e.includes("bad.bpmn") && e.includes("senior:bespoke-promptless")),
|
|
95
|
+
`the error names the unmarked prompt-less task, got ${JSON.stringify(r.errors)}`,
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("check-agent-definitions: the authored half still flags a prompt-BEARING task that lost its marker", () => {
|
|
100
|
+
const bad = `
|
|
101
|
+
<bpmn:serviceTask id="agent">
|
|
102
|
+
<bpmn:extensionElements>
|
|
103
|
+
<zeebe:taskDefinition type="senior:feature" />
|
|
104
|
+
<zeebe:linkedResources>
|
|
105
|
+
<zeebe:linkedResource resourceId="prompts/feature.md" linkName="prompt" />
|
|
106
|
+
</zeebe:linkedResources>
|
|
107
|
+
</bpmn:extensionElements>
|
|
108
|
+
</bpmn:serviceTask>`;
|
|
109
|
+
const r = checkAuthoredProcesses([{ file: "bad.bpmn", xml: bad }]);
|
|
110
|
+
assert(!r.ok, "a prompt-bearing senior:* authored task without a marker must fail the gate");
|
|
111
|
+
assert(
|
|
112
|
+
r.errors.some((e) => e.includes("bad.bpmn") && e.includes("senior:feature")),
|
|
113
|
+
`the error names the unmarked prompt-bearing task, got ${JSON.stringify(r.errors)}`,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("check-agent-definitions: AGENTS.md documents the check:agent-definitions CI gate", () => {
|
|
118
|
+
// Advisory (PR #886 review): adding the `check:agent-definitions` CI command left the contributor
|
|
119
|
+
// guide's "Runtime & CI gates" list stale — AGENTS.md tells contributors how to match CI locally
|
|
120
|
+
// but did not name the new gate. Pin the doc so the gate cannot silently drop out of the guide.
|
|
121
|
+
const agents = readFileSync(join(ROOT, "AGENTS.md"), "utf8");
|
|
122
|
+
assert(
|
|
123
|
+
agents.includes("check:agent-definitions"),
|
|
124
|
+
"AGENTS.md must document the check:agent-definitions gate in its Runtime & CI gates list",
|
|
125
|
+
);
|
|
126
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// check-agent-definitions — repo-wide CI gate (issue #884) for the engine-native AgentTask marker:
|
|
2
|
+
// EVERY `senior:*` agent service task the app deploys must carry
|
|
3
|
+
// `<zeebe:agentDefinition agentType="external" />` INSIDE its `<bpmn:extensionElements>`, as a sibling
|
|
4
|
+
// of its `<zeebe:taskDefinition type="senior:*"/>` dispatch verb. Without the marker the engine refuses
|
|
5
|
+
// to attach an AgentInstance to the element (`createAgentInstance failed — … has no agentDefinition`),
|
|
6
|
+
// so the agent run persists NO durable agentic transcript — the exact #884 failure (269 failed creates
|
|
7
|
+
// on omarchy across three compiled delivery-graph processes).
|
|
8
|
+
//
|
|
9
|
+
// The defect CLASS is "every agent service task the app generates", and there are TWO producers.
|
|
10
|
+
// Both are checked against the SAME union invariant `missingExternalMarkerAgentTaskTypes`
|
|
11
|
+
// (`app/agentic/vocab/job-types.ts`) — the union of the two marker scanners, each of which alone has
|
|
12
|
+
// a blind spot (PR #886 review):
|
|
13
|
+
//
|
|
14
|
+
// • the prompt-keyed `agentTaskTypesMissingExternalMarker` keys a task as an AGENT by its
|
|
15
|
+
// `<zeebe:linkedResource … linkName="prompt">` (the base-prompt link a fleet agent task carries),
|
|
16
|
+
// so it cannot see a DELIBERATELY prompt-less agent cell; and
|
|
17
|
+
// • the type-keyed `externalAgentTaskTypesMissingMarker` keys it by its `<rank>:<task>` colon-form
|
|
18
|
+
// type ALONE, covering the prompt-less case.
|
|
19
|
+
//
|
|
20
|
+
// Scanning a corpus with only ONE leaves the other's blind spot open — a `senior:*` task that loses
|
|
21
|
+
// BOTH its prompt link and its marker would pass a prompt-keyed-only gate. The union closes that for
|
|
22
|
+
// every producer:
|
|
23
|
+
//
|
|
24
|
+
// 1. The HAND-AUTHORED corpus — `resources/processes/**.bpmn`. `checkAuthoredProcesses` runs the
|
|
25
|
+
// union over every deployed model. This is the SAME invariant the `agent-marker.test.ts` unit
|
|
26
|
+
// guard rests on — kept here as a standalone, fast CI gate (alongside `check:prompts`) so the
|
|
27
|
+
// corpus invariant is visible as its own check, not only inside the test suite.
|
|
28
|
+
//
|
|
29
|
+
// 2. The delivery-graph COMPILER (`app/deliveryGraphCompiler.ts`) — the producer #884 caught. Its
|
|
30
|
+
// compiled cells carried NO marker AND no prompt link, so they were invisible to the
|
|
31
|
+
// prompt-link-keyed scanner. The compiler now emits the marker on EVERY `agent` cell — and, per
|
|
32
|
+
// the resolved "Decide" half of #884 (Option (a), PR #886 review), NO base-prompt link: the
|
|
33
|
+
// hand-authored process prompts are process-specific contracts (they require process variables
|
|
34
|
+
// and a `feat/<task.id>` branch the generic compiled cell never supplies), so a compiled cell is
|
|
35
|
+
// deliberately prompt-LESS and steered by its per-node `appendPrompt` alone. This gate compiles a
|
|
36
|
+
// representative graph — including a prompt-less probe cell — and runs the SAME union over the
|
|
37
|
+
// OUTPUT, so a marker regression on a prompt-less cell cannot hide behind the prompt-keyed scan's
|
|
38
|
+
// blind spot.
|
|
39
|
+
//
|
|
40
|
+
// In the hand-authored corpus a `senior:*` task is identified by its prompt link OR its colon-form
|
|
41
|
+
// type; a plain host job (`pr.finalize`, dot-form, no prompt link) is correctly out of scope. The
|
|
42
|
+
// scanners are the ONE canonical implementations — this gate DERIVES all its halves from them rather
|
|
43
|
+
// than re-implementing a second, driftable regex.
|
|
44
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
45
|
+
import { dirname, join, relative } from "node:path";
|
|
46
|
+
import { fileURLToPath } from "node:url";
|
|
47
|
+
import {
|
|
48
|
+
missingExternalMarkerAgentTaskTypes,
|
|
49
|
+
promptBearingTaskTypes,
|
|
50
|
+
} from "../app/agentic/vocab/job-types.ts";
|
|
51
|
+
import { compileDeliveryGraph } from "../app/deliveryGraphCompiler.ts";
|
|
52
|
+
|
|
53
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
54
|
+
const PROCESSES_DIR = join(ROOT, "resources/processes");
|
|
55
|
+
|
|
56
|
+
// The agent job type the compiled-output probe uses for a cell with NO base-prompt link (every
|
|
57
|
+
// compiled agent cell is prompt-less per the resolved #884 "Decide" — Option (a)). It is colon-form
|
|
58
|
+
// (`<rank>:<task>`) so the type-keyed marker scanner keys it as an agent — the exact prompt-less shape
|
|
59
|
+
// the prompt-keyed scanner cannot see (PR #886 review).
|
|
60
|
+
const PROMPTLESS_PROBE_JOB_TYPE = "senior:bespoke-promptless";
|
|
61
|
+
|
|
62
|
+
// urban deploys `resources/` recursively (every file at any depth), so a process model added under a
|
|
63
|
+
// subdirectory would still deploy — walk recursively here too, or the guard would miss it and let an
|
|
64
|
+
// unmarked agent task slip through. Mirrors `agent-marker.test.ts`'s `bpmnFiles()`.
|
|
65
|
+
function bpmnFiles(): string[] {
|
|
66
|
+
const walk = (dir: string): string[] =>
|
|
67
|
+
readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
68
|
+
const full = join(dir, entry.name);
|
|
69
|
+
if (entry.isDirectory()) return walk(full);
|
|
70
|
+
return entry.name.endsWith(".bpmn") ? [relative(PROCESSES_DIR, full)] : [];
|
|
71
|
+
});
|
|
72
|
+
return walk(PROCESSES_DIR).sort();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface AgentDefinitionCheckResult {
|
|
76
|
+
ok: boolean;
|
|
77
|
+
errors: string[];
|
|
78
|
+
/** prompt-bearing agent job types found across the hand-authored corpus (for the CLI summary). */
|
|
79
|
+
agentTypes: string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** One hand-authored BPMN document to scan: its path (relative to `resources/processes`) and XML. */
|
|
83
|
+
export interface AuthoredDoc {
|
|
84
|
+
file: string;
|
|
85
|
+
xml: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Read the hand-authored deployed corpus (`resources/processes/**.bpmn`) off disk. */
|
|
89
|
+
function authoredDocs(): AuthoredDoc[] {
|
|
90
|
+
return bpmnFiles().map((file) => ({ file, xml: readFileSync(join(PROCESSES_DIR, file), "utf8") }));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Scan the HAND-AUTHORED deployed corpus (`resources/processes/**.bpmn`). `docs` is injectable so the
|
|
95
|
+
* red-path test can feed a drifting document without writing to the corpus; it defaults to the real
|
|
96
|
+
* on-disk corpus. Every agent task — prompt-bearing OR deliberately prompt-less — must carry the
|
|
97
|
+
* external marker, so this holds the SAME union invariant as the compiled half (PR #886 review).
|
|
98
|
+
*/
|
|
99
|
+
export function checkAuthoredProcesses(docs: readonly AuthoredDoc[] = authoredDocs()): AgentDefinitionCheckResult {
|
|
100
|
+
const errors: string[] = [];
|
|
101
|
+
const agentTypes = new Set<string>();
|
|
102
|
+
for (const { file, xml } of docs) {
|
|
103
|
+
for (const t of promptBearingTaskTypes(xml)) agentTypes.add(t);
|
|
104
|
+
const missing = missingExternalMarkerAgentTaskTypes(xml);
|
|
105
|
+
if (missing.length > 0) {
|
|
106
|
+
errors.push(
|
|
107
|
+
`resources/processes/${file}: agent task(s) missing <zeebe:agentDefinition agentType="external" /> — ` +
|
|
108
|
+
`${missing.join(", ")} (every senior:* agent task must carry the marker so the engine mints a ` +
|
|
109
|
+
`durable AgentInstance/transcript for it, issue #745/#884)`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (agentTypes.size === 0) {
|
|
114
|
+
errors.push("no prompt-bearing agent task found in resources/processes — the guard would be vacuously green");
|
|
115
|
+
}
|
|
116
|
+
return { ok: errors.length === 0, errors, agentTypes: [...agentTypes].sort() };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Scan the delivery-graph COMPILER's output — the second producer of agent service tasks (#884). */
|
|
120
|
+
export async function checkCompiledOutput(): Promise<AgentDefinitionCheckResult> {
|
|
121
|
+
const errors: string[] = [];
|
|
122
|
+
const r = await compileDeliveryGraph({
|
|
123
|
+
nodes: [
|
|
124
|
+
{ id: "impl", kind: "agent", agent: { jobType: "senior:feature", prompt: "implement #1" }, emits: [{ name: "pr", type: "pr" }] },
|
|
125
|
+
{ id: "review", kind: "agent", agent: { jobType: "senior:pr-review" } },
|
|
126
|
+
// A prompt-LESS agent cell (every compiled agent cell is prompt-less per the resolved #884
|
|
127
|
+
// "Decide" — Option (a)). The prompt-keyed scanner never sees it, so probe the shape to keep the
|
|
128
|
+
// type-keyed marker scan below covering it (PR #886 review).
|
|
129
|
+
{ id: "bespoke", kind: "agent", agent: { jobType: PROMPTLESS_PROBE_JOB_TYPE } },
|
|
130
|
+
],
|
|
131
|
+
edges: [
|
|
132
|
+
{ from: "impl", to: "review" },
|
|
133
|
+
{ from: "review", to: "bespoke" },
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
if (!r.ok) {
|
|
137
|
+
return { ok: false, errors: [`delivery-graph compiler rejected the probe graph: ${JSON.stringify(r.errors)}`], agentTypes: [] };
|
|
138
|
+
}
|
|
139
|
+
// Inspect EVERY generated agent service task for the marker via the single union invariant
|
|
140
|
+
// (`missingExternalMarkerAgentTaskTypes`). Every compiled agent cell is prompt-LESS (Option (a)), so
|
|
141
|
+
// it is the type-keyed half of the union (keyed by the `<rank>:<task>` colon-form type alone) that
|
|
142
|
+
// sees these cells — a compiler regression that drops the marker on a prompt-less cell cannot hide
|
|
143
|
+
// behind the prompt-keyed scan's blind spot (PR #886 review).
|
|
144
|
+
const missing = missingExternalMarkerAgentTaskTypes(r.bpmn);
|
|
145
|
+
if (missing.length > 0) {
|
|
146
|
+
errors.push(
|
|
147
|
+
`compiled delivery-graph agent cell(s) missing <zeebe:agentDefinition agentType="external" /> — ` +
|
|
148
|
+
`${missing.join(", ")} (the compiler must emit the marker on EVERY agent cell, including a ` +
|
|
149
|
+
"prompt-less one, issue #884)",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
// Sanity: the prompt-less probe cell really compiled into the output (the type-keyed scan is not
|
|
153
|
+
// vacuously green over a graph that lost its prompt-less cell).
|
|
154
|
+
if (!r.bpmn.includes(`type="${PROMPTLESS_PROBE_JOB_TYPE}"`)) {
|
|
155
|
+
errors.push(
|
|
156
|
+
`the compiled probe graph lost its prompt-less agent cell ${PROMPTLESS_PROBE_JOB_TYPE} — ` +
|
|
157
|
+
"the type-keyed marker scan would be vacuously green over it",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
// The agent job types this probe compiled (for the CLI summary) — derived by the SAME type-keyed
|
|
161
|
+
// grammar the marker scanner uses, since the cells are prompt-less (a prompt-keyed list is empty).
|
|
162
|
+
const agentTypes = [
|
|
163
|
+
...new Set(
|
|
164
|
+
[...r.bpmn.matchAll(/<(?:\w+:)?taskDefinition\b[^>]*\btype="([^"]*)"/g)]
|
|
165
|
+
.map((m) => m[1])
|
|
166
|
+
.filter((t) => t.startsWith("senior:")),
|
|
167
|
+
),
|
|
168
|
+
];
|
|
169
|
+
return { ok: errors.length === 0, errors, agentTypes: agentTypes.sort() };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function main(): Promise<void> {
|
|
173
|
+
const authored = checkAuthoredProcesses();
|
|
174
|
+
const compiled = await checkCompiledOutput();
|
|
175
|
+
const errors = [...authored.errors, ...compiled.errors];
|
|
176
|
+
if (errors.length > 0) {
|
|
177
|
+
console.error(`✖ agent-definition check failed (${errors.length} problem(s)):`);
|
|
178
|
+
for (const e of errors) console.error(` - ${e}`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
console.log(
|
|
182
|
+
`✔ every senior:* agent service task carries <zeebe:agentDefinition agentType="external" /> ` +
|
|
183
|
+
`(authored: ${authored.agentTypes.length} type(s) [${authored.agentTypes.join(", ")}]; ` +
|
|
184
|
+
`compiled: ${compiled.agentTypes.length} type(s) [${compiled.agentTypes.join(", ")}])`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// CLI: check the repo rooted at the package root. Exit non-zero (fail CI) on any problem.
|
|
189
|
+
if (import.meta.main) {
|
|
190
|
+
await main();
|
|
191
|
+
}
|