@nanobpm/nano-workforce 0.201.2 → 0.201.4

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.
@@ -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
+ }