@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.
- package/.github/workflows/ci.yml +11 -0
- package/AGENTS.md +3 -1
- package/CHANGELOG.md +12 -0
- package/SPEC.md +19 -5
- 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 +4 -4
- package/app/deliveryGraphCompiler.test.ts +81 -0
- package/app/deliveryGraphCompiler.ts +22 -1
- package/app/deliveryRunner.test.ts +14 -11
- package/app/feature.test.ts +5 -3
- package/app/plan.test.ts +5 -2
- package/app/repoEnvelope.test.ts +139 -0
- package/app/repoEnvelope.ts +24 -15
- package/app/service.test.ts +21 -16
- package/e2e/feature-run.e2e.ts +6 -4
- package/e2e/plan-fanout.e2e.ts +6 -4
- package/package.json +2 -1
- package/scripts/check-agent-definitions.test.ts +126 -0
- package/scripts/check-agent-definitions.ts +191 -0
package/.github/workflows/ci.yml
CHANGED
|
@@ -58,6 +58,17 @@ jobs:
|
|
|
58
58
|
- name: Check agent prompt templates
|
|
59
59
|
run: npm run check:prompts
|
|
60
60
|
|
|
61
|
+
# Agent-definition gate (issue #884): every `senior:*` agent service task the app deploys —
|
|
62
|
+
# hand-authored in resources/processes/*.bpmn OR produced by the delivery-graph compiler — must
|
|
63
|
+
# carry `<zeebe:agentDefinition agentType="external"/>`, or the engine refuses to mint an
|
|
64
|
+
# AgentInstance for it and the run persists no durable agentic transcript (the 269 failed
|
|
65
|
+
# creates on omarchy). Compiled agent cells are deliberately prompt-LESS (the resolved #884
|
|
66
|
+
# "Decide", Option (a)): the compiler emits the marker on EVERY agent cell unconditionally, and
|
|
67
|
+
# the gate keys agent-ness on the `<rank>:<task>` colon-form type (the type-keyed scanner), so a
|
|
68
|
+
# compiled cell that drops the marker fails here, not silently at runtime.
|
|
69
|
+
- name: Check agent task definitions (external marker)
|
|
70
|
+
run: npm run check:agent-definitions
|
|
71
|
+
|
|
61
72
|
# Merge-safety gate: migrations are applied in numeric-prefix order, but each fan-out branch
|
|
62
73
|
# picks "the next" prefix independently, so parallel siblings silently collide on one slot
|
|
63
74
|
# (the historical 004/005/006/007 pairs; recurred on epic #142). This fails the build on any
|
package/AGENTS.md
CHANGED
|
@@ -393,13 +393,15 @@ npm run typecheck # tsc --noEmit (Node)
|
|
|
393
393
|
npm run check # urban check (manifest validation)
|
|
394
394
|
npm run layout:check # BPMN diagram freshness (no drift)
|
|
395
395
|
npm run check:prompts # agent-prompt linkedResource resolution
|
|
396
|
+
npm run check:agent-definitions # every senior:* agent task (authored corpus + compiled delivery-graph output) carries the agentDefinition marker
|
|
396
397
|
npm run check:migrations # migration prefixes + immutability (no rename/delete/edit of a merged migration)
|
|
397
398
|
npm run check:contracts # contract registry (no synonyms / undeclared env keys)
|
|
398
399
|
npm test # unit tests (node --test)
|
|
399
400
|
```
|
|
400
401
|
|
|
401
402
|
CI (`.github/workflows/ci.yml`) gates lint, typecheck, `urban check`, `layout:check`,
|
|
402
|
-
the prompt check, the
|
|
403
|
+
the prompt check, the agent-definition check, the migration-prefix check, the
|
|
404
|
+
contract-registry check, and the Node test suite. Run `npm run layout <file.bpmn>` after
|
|
403
405
|
any BPMN flow change and commit the regenerated diagram — the `layout:check`
|
|
404
406
|
gate fails the build otherwise.
|
|
405
407
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [0.201.4](https://github.com/nanobpm/nano-workforce/compare/v0.201.3...v0.201.4) (2026-10-11)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* **delivery-graph:** emit zeebe:agentDefinition marker on every compiled agent cell (prompt-less, Option (a)) ([#886](https://github.com/nanobpm/nano-workforce/issues/886)) ([1e4fa4f](https://github.com/nanobpm/nano-workforce/commit/1e4fa4f46aad542b2e6fc175a3af0b46d0980f50)), closes [#464](https://github.com/nanobpm/nano-workforce/issues/464) [#745](https://github.com/nanobpm/nano-workforce/issues/745) [#884](https://github.com/nanobpm/nano-workforce/issues/884) [#884](https://github.com/nanobpm/nano-workforce/issues/884) [#776](https://github.com/nanobpm/nano-workforce/issues/776) [#884](https://github.com/nanobpm/nano-workforce/issues/884) [#884](https://github.com/nanobpm/nano-workforce/issues/884) [#884](https://github.com/nanobpm/nano-workforce/issues/884)
|
|
6
|
+
|
|
7
|
+
## [0.201.3](https://github.com/nanobpm/nano-workforce/compare/v0.201.2...v0.201.3) (2026-10-10)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **repo-envelope:** emit branch.create as a top-level sibling of repository ([#885](https://github.com/nanobpm/nano-workforce/issues/885)) ([abb7b1c](https://github.com/nanobpm/nano-workforce/commit/abb7b1cc8439229724668fa85ffac22afd95c470)), closes [nanobpm/nano-bpm#1344](https://github.com/nanobpm/nano-bpm/issues/1344) [#227](https://github.com/nanobpm/nano-workforce/issues/227) [#882](https://github.com/nanobpm/nano-workforce/issues/882)
|
|
12
|
+
|
|
1
13
|
## [0.201.2](https://github.com/nanobpm/nano-workforce/compare/v0.201.1...v0.201.2) (2026-10-08)
|
|
2
14
|
|
|
3
15
|
### Bug Fixes
|
package/SPEC.md
CHANGED
|
@@ -646,11 +646,25 @@ the app, which deploys on boot), and the next agent job of that type picks it up
|
|
|
646
646
|
> marker is CI-enforced over the deployed `resources/` process models
|
|
647
647
|
> (`agentTaskTypesMissingExternalMarker`,
|
|
648
648
|
> `agent-marker.test.ts`), so no prompt-bearing agent task in those models relies on
|
|
649
|
-
> prompt-link-only discovery.
|
|
650
|
-
> deployed at run time by `runDeliveryGraph`, not authored under `resources/` — is
|
|
651
|
-
>
|
|
652
|
-
>
|
|
653
|
-
>
|
|
649
|
+
> prompt-link-only discovery. The delivery-graph compiler's GENERATED agent BPMN —
|
|
650
|
+
> deployed at run time by `runDeliveryGraph`, not authored under `resources/` — is the
|
|
651
|
+
> second producer of agent service tasks, and it is covered too (issue #884): the
|
|
652
|
+
> compiler emits the marker on **every** `agent` cell it compiles. Per the resolved
|
|
653
|
+
> "Decide" half of #884 (Option (a), PR #886 review) a compiled cell carries **no**
|
|
654
|
+
> base-prompt link — the hand-authored process prompts (`prompts/feature.md`, …) are
|
|
655
|
+
> process-specific contracts that require process variables (`variables.task`/
|
|
656
|
+
> `variables.issue`) and a `feat/<task.id>` branch the generic compiled cell never
|
|
657
|
+
> supplies (it seeds only `{jobType, appendPrompt, timeout}` and the harness provisions
|
|
658
|
+
> `feat/<node.id>`, #776), so linking one would hand the cell instructions referencing
|
|
659
|
+
> missing inputs and a conflicting branch. The cell is deliberately prompt-less, steered
|
|
660
|
+
> by its per-node `appendPrompt` alone; the marker alone fixes the #884
|
|
661
|
+
> `createAgentInstance` failure. `npm run check:agent-definitions`
|
|
662
|
+
> (CI gate `check:agent-definitions`, `scripts/check-agent-definitions.ts`) compiles a
|
|
663
|
+
> representative graph — including a prompt-less probe cell — and scans the
|
|
664
|
+
> output with both the prompt-keyed scanner and the prompt-link-independent
|
|
665
|
+
> `externalAgentTaskTypesMissingMarker` (a task is an agent by its `<rank>:<task>`
|
|
666
|
+
> colon-form type alone), so a compiler regression that drops the marker fails the
|
|
667
|
+
> build rather than silently at runtime. To **exclude** a task from `--auto` — one that must be served only by a
|
|
654
668
|
> worker that explicitly subscribes (`--job-type <type>` / a profile capability) — add
|
|
655
669
|
> the inert opt-out property inside its `extensionElements`, nested in the
|
|
656
670
|
> `<zeebe:properties>` wrapper the models and engine expect (as
|
|
@@ -9,7 +9,8 @@ import { test } from "node:test";
|
|
|
9
9
|
import { dirname, join, relative } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { assert, assertEquals } from "#test-assert";
|
|
12
|
-
import {
|
|
12
|
+
import { compileDeliveryGraph } from "../../deliveryGraphCompiler.ts";
|
|
13
|
+
import { agentTaskTypesMissingExternalMarker, missingExternalMarkerAgentTaskTypes, promptBearingTaskTypes } from "./job-types.ts";
|
|
13
14
|
|
|
14
15
|
const PROCESSES_DIR = join(dirname(fileURLToPath(import.meta.url)), "../../../resources/processes");
|
|
15
16
|
|
|
@@ -72,3 +73,31 @@ test("DEFECT-CLASS GUARD: every deployed prompt-bearing agent task carries the e
|
|
|
72
73
|
// Sanity: the models really do declare agent tasks (guard is not vacuously green).
|
|
73
74
|
assert(anyAgentTasks, "the deployed models declare prompt-bearing agent tasks");
|
|
74
75
|
});
|
|
76
|
+
|
|
77
|
+
test("DEFECT-CLASS GUARD (compiler output, issue #884): every COMPILED delivery-graph agent cell carries the marker", async () => {
|
|
78
|
+
// The hand-authored corpus above is only HALF the class — the delivery-graph COMPILER
|
|
79
|
+
// (`app/deliveryGraphCompiler.ts`) is the other producer of agent service tasks, and it emitted
|
|
80
|
+
// cells with NO `agentDefinition` until #884: every delivery-graph agent run failed AgentInstance
|
|
81
|
+
// creation (`… has no agentDefinition`) and persisted no durable transcript (269 failed creates on
|
|
82
|
+
// omarchy). Per the resolved "Decide" half of #884 (Option (a), PR #886 review) a compiled cell is
|
|
83
|
+
// prompt-LESS (no base-prompt link), so the prompt-keyed scanner cannot see it — it is the
|
|
84
|
+
// type-keyed half of the union scanner (`missingExternalMarkerAgentTaskTypes`, keyed by the
|
|
85
|
+
// `<rank>:<task>` colon-form type) that sees these cells. Apply the SAME union to the compiler's
|
|
86
|
+
// OUTPUT so a compiler regression that drops the marker fails here — not silently at runtime.
|
|
87
|
+
const r = await compileDeliveryGraph({
|
|
88
|
+
nodes: [
|
|
89
|
+
{ id: "impl", kind: "agent", agent: { jobType: "senior:feature", prompt: "implement #1" }, emits: [{ name: "pr", type: "pr" }] },
|
|
90
|
+
{ id: "review", kind: "agent", agent: { jobType: "senior:pr-review" } },
|
|
91
|
+
],
|
|
92
|
+
edges: [{ from: "impl", to: "review" }],
|
|
93
|
+
});
|
|
94
|
+
assert(r.ok, `expected the graph to compile, got ${JSON.stringify(r)}`);
|
|
95
|
+
// The compiled agent cells are prompt-LESS (Option (a)), so the prompt-keyed scan sees none — the
|
|
96
|
+
// union's type-keyed half is what covers them.
|
|
97
|
+
assertEquals(promptBearingTaskTypes(r.bpmn), []);
|
|
98
|
+
const missing = missingExternalMarkerAgentTaskTypes(r.bpmn);
|
|
99
|
+
assert(
|
|
100
|
+
missing.length === 0,
|
|
101
|
+
`compiled delivery-graph agent cell(s) missing <zeebe:agentDefinition agentType="external" /> — ${missing.join(", ")}`,
|
|
102
|
+
);
|
|
103
|
+
});
|
|
@@ -9,7 +9,7 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
import { assert, assertEquals } from "#test-assert";
|
|
10
10
|
import type { Capability } from "@nanobpm/agentic/protocol";
|
|
11
11
|
import { crewResolver } from "./crew-vocab.ts";
|
|
12
|
-
import { jobTypeToRoutingToken, promptBearingTaskTypes } from "./job-types.ts";
|
|
12
|
+
import { externalAgentTaskTypesMissingMarker, jobTypeToRoutingToken, missingExternalMarkerAgentTaskTypes, promptBearingTaskTypes } from "./job-types.ts";
|
|
13
13
|
|
|
14
14
|
const PROCESSES_DIR = join(dirname(fileURLToPath(import.meta.url)), "../../../resources/processes");
|
|
15
15
|
|
|
@@ -105,3 +105,135 @@ test("DEFECT-CLASS GUARD: every deployed prompt-bearing agent job type resolves
|
|
|
105
105
|
);
|
|
106
106
|
}
|
|
107
107
|
});
|
|
108
|
+
|
|
109
|
+
test("externalAgentTaskTypesMissingMarker flags an UNMAPPED (prompt-less) agent task with no marker", () => {
|
|
110
|
+
// The prompt-keyed scanner (`agentTaskTypesMissingExternalMarker`) cannot see this block — it has no
|
|
111
|
+
// prompt link. The marker scan must key on the colon-form job type ALONE (issue #886 review): a
|
|
112
|
+
// compiled delivery-graph cell for a bespoke `senior:<task>` (no deployed base prompt, deliberately
|
|
113
|
+
// prompt-less) must still carry the marker or it runs transcript-less.
|
|
114
|
+
const xml = `
|
|
115
|
+
<bpmn:serviceTask id="agent">
|
|
116
|
+
<bpmn:extensionElements>
|
|
117
|
+
<zeebe:taskDefinition type="senior:bespoke" />
|
|
118
|
+
</bpmn:extensionElements>
|
|
119
|
+
</bpmn:serviceTask>`;
|
|
120
|
+
assertEquals(externalAgentTaskTypesMissingMarker(xml), ["senior:bespoke"]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("externalAgentTaskTypesMissingMarker passes a marked prompt-less agent task and ignores a plain host task", () => {
|
|
124
|
+
const xml = `
|
|
125
|
+
<bpmn:serviceTask id="host">
|
|
126
|
+
<bpmn:extensionElements>
|
|
127
|
+
<zeebe:taskDefinition type="pr.finalize" />
|
|
128
|
+
</bpmn:extensionElements>
|
|
129
|
+
</bpmn:serviceTask>
|
|
130
|
+
<bpmn:serviceTask id="agent">
|
|
131
|
+
<bpmn:extensionElements>
|
|
132
|
+
<zeebe:taskDefinition type="senior:bespoke" />
|
|
133
|
+
<zeebe:agentDefinition agentType="external" />
|
|
134
|
+
</bpmn:extensionElements>
|
|
135
|
+
</bpmn:serviceTask>`;
|
|
136
|
+
assertEquals(externalAgentTaskTypesMissingMarker(xml), []);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("externalAgentTaskTypesMissingMarker flags a marked-outside-extensionElements task (placement contract)", () => {
|
|
140
|
+
// A marker sitting OUTSIDE `<bpmn:extensionElements>` is ignored by the engine, so it must not
|
|
141
|
+
// "cover" the task here either (the same placement scope the prompt-keyed scanner enforces).
|
|
142
|
+
const xml = `
|
|
143
|
+
<bpmn:serviceTask id="agent">
|
|
144
|
+
<zeebe:agentDefinition agentType="external" />
|
|
145
|
+
<bpmn:extensionElements>
|
|
146
|
+
<zeebe:taskDefinition type="senior:bespoke" />
|
|
147
|
+
</bpmn:extensionElements>
|
|
148
|
+
</bpmn:serviceTask>`;
|
|
149
|
+
assertEquals(externalAgentTaskTypesMissingMarker(xml), ["senior:bespoke"]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("externalAgentTaskTypesMissingMarker ignores malformed/non-agent types (no false positives)", () => {
|
|
153
|
+
// Only a `<rank>:<task>` colon-form type (the `jobTypeToRoutingToken` grammar) keys a block as an
|
|
154
|
+
// agent task: a dot-form host job, a typeless block, and a malformed colon form are all out of
|
|
155
|
+
// scope — the scanner must not flag them as unmarked agents.
|
|
156
|
+
const xml = `
|
|
157
|
+
<bpmn:serviceTask id="host1">
|
|
158
|
+
<bpmn:extensionElements>
|
|
159
|
+
<zeebe:taskDefinition type="pr.finalize" />
|
|
160
|
+
</bpmn:extensionElements>
|
|
161
|
+
</bpmn:serviceTask>
|
|
162
|
+
<bpmn:serviceTask id="host2">
|
|
163
|
+
<bpmn:extensionElements>
|
|
164
|
+
<zeebe:taskDefinition type="senior:feature:extra" />
|
|
165
|
+
</bpmn:extensionElements>
|
|
166
|
+
</bpmn:serviceTask>
|
|
167
|
+
<bpmn:serviceTask id="host3">
|
|
168
|
+
<bpmn:extensionElements>
|
|
169
|
+
<zeebe:taskDefinition type="" />
|
|
170
|
+
</bpmn:extensionElements>
|
|
171
|
+
</bpmn:serviceTask>
|
|
172
|
+
<bpmn:serviceTask id="host4">
|
|
173
|
+
<bpmn:extensionElements>
|
|
174
|
+
</bpmn:extensionElements>
|
|
175
|
+
</bpmn:serviceTask>`;
|
|
176
|
+
assertEquals(externalAgentTaskTypesMissingMarker(xml), []);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("missingExternalMarkerAgentTaskTypes unions BOTH scanners — catches a prompt-bearing AND a prompt-less unmarked agent task", () => {
|
|
180
|
+
// The single "every agent task carries the external marker" invariant (PR #886 review): a
|
|
181
|
+
// prompt-BEARING senior:* task that loses its marker is caught only by the prompt-keyed scanner,
|
|
182
|
+
// while a prompt-LESS senior:* task that loses its marker is caught only by the type-keyed scanner.
|
|
183
|
+
// Scanning by just one leaves the other's blind spot open, so the union must flag BOTH in one pass.
|
|
184
|
+
const xml = `
|
|
185
|
+
<bpmn:serviceTask id="prompted">
|
|
186
|
+
<bpmn:extensionElements>
|
|
187
|
+
<zeebe:taskDefinition type="senior:feature" />
|
|
188
|
+
<zeebe:linkedResources>
|
|
189
|
+
<zeebe:linkedResource resourceId="prompts/feature.md" linkName="prompt" />
|
|
190
|
+
</zeebe:linkedResources>
|
|
191
|
+
</bpmn:extensionElements>
|
|
192
|
+
</bpmn:serviceTask>
|
|
193
|
+
<bpmn:serviceTask id="bespoke">
|
|
194
|
+
<bpmn:extensionElements>
|
|
195
|
+
<zeebe:taskDefinition type="senior:bespoke" />
|
|
196
|
+
</bpmn:extensionElements>
|
|
197
|
+
</bpmn:serviceTask>`;
|
|
198
|
+
assertEquals(missingExternalMarkerAgentTaskTypes(xml), ["senior:feature", "senior:bespoke"]);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("missingExternalMarkerAgentTaskTypes passes when every agent task is marked and ignores host tasks", () => {
|
|
202
|
+
const xml = `
|
|
203
|
+
<bpmn:serviceTask id="host">
|
|
204
|
+
<bpmn:extensionElements>
|
|
205
|
+
<zeebe:taskDefinition type="pr.finalize" />
|
|
206
|
+
</bpmn:extensionElements>
|
|
207
|
+
</bpmn:serviceTask>
|
|
208
|
+
<bpmn:serviceTask id="prompted">
|
|
209
|
+
<bpmn:extensionElements>
|
|
210
|
+
<zeebe:taskDefinition type="senior:feature" />
|
|
211
|
+
<zeebe:agentDefinition agentType="external" />
|
|
212
|
+
<zeebe:linkedResources>
|
|
213
|
+
<zeebe:linkedResource resourceId="prompts/feature.md" linkName="prompt" />
|
|
214
|
+
</zeebe:linkedResources>
|
|
215
|
+
</bpmn:extensionElements>
|
|
216
|
+
</bpmn:serviceTask>
|
|
217
|
+
<bpmn:serviceTask id="bespoke">
|
|
218
|
+
<bpmn:extensionElements>
|
|
219
|
+
<zeebe:taskDefinition type="senior:bespoke" />
|
|
220
|
+
<zeebe:agentDefinition agentType="external" />
|
|
221
|
+
</bpmn:extensionElements>
|
|
222
|
+
</bpmn:serviceTask>`;
|
|
223
|
+
assertEquals(missingExternalMarkerAgentTaskTypes(xml), []);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("missingExternalMarkerAgentTaskTypes de-duplicates a type caught by both scanners", () => {
|
|
227
|
+
// A prompt-bearing task missing its marker is seen by BOTH the prompt-keyed and the type-keyed
|
|
228
|
+
// scanner; the union must report it ONCE, not twice.
|
|
229
|
+
const xml = `
|
|
230
|
+
<bpmn:serviceTask id="prompted">
|
|
231
|
+
<bpmn:extensionElements>
|
|
232
|
+
<zeebe:taskDefinition type="senior:feature" />
|
|
233
|
+
<zeebe:linkedResources>
|
|
234
|
+
<zeebe:linkedResource resourceId="prompts/feature.md" linkName="prompt" />
|
|
235
|
+
</zeebe:linkedResources>
|
|
236
|
+
</bpmn:extensionElements>
|
|
237
|
+
</bpmn:serviceTask>`;
|
|
238
|
+
assertEquals(missingExternalMarkerAgentTaskTypes(xml), ["senior:feature"]);
|
|
239
|
+
});
|
|
@@ -140,6 +140,63 @@ export function agentTaskTypesMissingExternalMarker(xml: string): string[] {
|
|
|
140
140
|
return missing;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Scan one BPMN document for the job types of agent service tasks — keyed by their `<rank>:<task>`
|
|
145
|
+
* colon-form `<zeebe:taskDefinition type>` ALONE, NOT by a prompt link — that are MISSING the
|
|
146
|
+
* engine-native AgentTask marker `<zeebe:agentDefinition agentType="external" />` (issue #884,
|
|
147
|
+
* PR #886 review). The prompt-keyed scanner (`agentTaskTypesMissingExternalMarker`) is blind to a
|
|
148
|
+
* DELIBERATELY prompt-less agent task — and EVERY compiled delivery-graph cell is prompt-less (the
|
|
149
|
+
* resolved #884 "Decide", Option (a): the cell carries the marker alone and is steered by its
|
|
150
|
+
* per-node `appendPrompt`, never a linked base prompt) — so a marker regression on exactly those
|
|
151
|
+
* cells would pass that scan vacuously. This scan keys a service task as an AGENT by the same grammar
|
|
152
|
+
* the crew bridge uses (`jobTypeToRoutingToken`: a single-colon `<rank>:<task>` whose rank is a bare
|
|
153
|
+
* routing segment), which needs no prompt link, so EVERY agent task — prompt-bearing or not — is
|
|
154
|
+
* inspected for the marker independently of its prompt status. A dot-form host job (`pr.finalize`),
|
|
155
|
+
* a typeless block, and a malformed colon form (`senior:feature:extra`) are not agent job types and
|
|
156
|
+
* are ignored (no false positives). The marker check is scoped to the block's
|
|
157
|
+
* `<bpmn:extensionElements>` (the engine-honoured PLACEMENT scope, via `extensionElementsOf`) — a
|
|
158
|
+
* marker sitting outside `extensionElements` is ignored by the engine, so it must not "cover" a
|
|
159
|
+
* task here either. Returns the offending task types in first-occurrence order (empty when every
|
|
160
|
+
* agent task is marked).
|
|
161
|
+
*/
|
|
162
|
+
export function externalAgentTaskTypesMissingMarker(xml: string): string[] {
|
|
163
|
+
const seen = new Set<string>();
|
|
164
|
+
const missing: string[] = [];
|
|
165
|
+
for (const [block] of xml.matchAll(SERVICE_TASK)) {
|
|
166
|
+
const type = block.match(TASK_DEFINITION_TYPE)?.[1];
|
|
167
|
+
if (type === undefined || jobTypeToRoutingToken(type) === undefined) continue;
|
|
168
|
+
if (EXTERNAL_AGENT_MARKER.test(extensionElementsOf(block))) continue;
|
|
169
|
+
if (seen.has(type)) continue;
|
|
170
|
+
seen.add(type);
|
|
171
|
+
missing.push(type);
|
|
172
|
+
}
|
|
173
|
+
return missing;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The SINGLE "every agent service task carries the engine-native `<zeebe:agentDefinition
|
|
178
|
+
* agentType="external" />` marker" invariant (issue #745/#884, PR #886 review), as the UNION of both
|
|
179
|
+
* marker scanners. The two scanners each have a blind spot: `agentTaskTypesMissingExternalMarker` keys
|
|
180
|
+
* a task as an agent by its PROMPT link, so it cannot see a deliberately prompt-less agent cell;
|
|
181
|
+
* `externalAgentTaskTypesMissingMarker` keys it by its `<rank>:<task>` colon-form type, so it covers
|
|
182
|
+
* the prompt-less case but is the newer path. Scanning a corpus with only one leaves the other's blind
|
|
183
|
+
* spot open — a hand-authored (or compiled) `senior:*` task that loses BOTH its prompt link and its
|
|
184
|
+
* marker would pass a prompt-keyed-only gate. Every producer of agent tasks (the hand-authored corpus
|
|
185
|
+
* AND the delivery-graph compiler) is checked against THIS union, so neither half can regress through
|
|
186
|
+
* the other's blind spot. Returns the distinct offending task types in first-occurrence order (the
|
|
187
|
+
* prompt-keyed matches first, then any type-keyed-only ones), empty when every agent task is marked.
|
|
188
|
+
*/
|
|
189
|
+
export function missingExternalMarkerAgentTaskTypes(xml: string): string[] {
|
|
190
|
+
const seen = new Set<string>();
|
|
191
|
+
const missing: string[] = [];
|
|
192
|
+
for (const type of [...agentTaskTypesMissingExternalMarker(xml), ...externalAgentTaskTypesMissingMarker(xml)]) {
|
|
193
|
+
if (seen.has(type)) continue;
|
|
194
|
+
seen.add(type);
|
|
195
|
+
missing.push(type);
|
|
196
|
+
}
|
|
197
|
+
return missing;
|
|
198
|
+
}
|
|
199
|
+
|
|
143
200
|
/**
|
|
144
201
|
* Scan one BPMN document for the job types of agent service tasks that OPT OUT of the harness
|
|
145
202
|
* `--auto` reconciliation (issue #779) — those carrying `<zeebe:property
|
package/app/contracts.ts
CHANGED
|
@@ -448,9 +448,9 @@ export const WIRE_CONTRACTS = {
|
|
|
448
448
|
name: "io.nanobpm.agentTask.repository",
|
|
449
449
|
owner: "app/repoEnvelope.ts",
|
|
450
450
|
semantics:
|
|
451
|
-
"Repo-provisioning envelope the app emits as a `createInstance` process variable (`repoEnvelopeVars`, app/repoEnvelope.ts) and the c8ctl worker harness consumes to provision an isolated clone — instead of the agent inheriting the worker's launch dir (issue #684). `ref` is the branch checked out: the PR HEAD branch on the PR-based paths (review-round / fix-ci / rebase), or — on the PRE-PR implementation path (feature.bpmn / plan-fanout's `implement-cell`, issue #684; the delivery-graph runner's agent cells, issue #686) — the BASE branch, off which the harness cuts a new feature branch named by the optional `branch.create`. On the PRE-PR path `branch.create` is the deterministic `feat/<task.id>` (emitted for a single-task feature run AND — per issue #776 — the deterministic `feat/<node.id>` for each single-instance delivery-graph agent cell, injected per-cell by `agentNodeRepoEnvelope`/`app/deliveryRunner.ts` so a forgetful agent can never be left committing on the base branch and stranding its run on a non-ff push; the epic plan-fanout seed still omits it because its MI children each cut a per-child `feat/<task.id>` the app can't name at compile time, so those agents branch themselves). On the PR-based paths `branch.create` is set to the PR HEAD branch itself (equal to `ref`, jwulf/c8ctl-plugin-nano#231): without it the harness's base-branch guard treats the checkout as base-like and cuts a throwaway `nano/agent-work/<base>-<run>` fallback, stranding the round's commits OFF the PR; with `ref == create == head` the harness does a no-op `checkout -B <head>` and commits/pushes to the PR head branch. So `branch.create` is present on both the pre-PR and PR-based paths (only a blank/omitted value drops the key). Beyond `{provider,url,ref}`, it carries clone-shaping fields for large monorepos (issue #287): `singleBranch:true` + `filter:\"blob:none\"` (a branch-scoped, blobless partial clone — trees fetched up-front, blobs lazily, no `--depth 1` so the merge-base/3-dot diff stays valid) and an optional `baseRef` (the PR base branch, emitted only when resolvable, so the harness fetches its tip and keeps `origin/<base>` reachable) and a `cloneTimeoutMs` (from `NANO_PR_CLONE_TIMEOUT_MS`, default 600000 = 10 min) that raises the harness's 120s default so a large monorepo's blobless single-branch clone provisions instead of dying at 120s (issue #694). World-restore (issue #324, ADR 0062 Slice 4/5): an optional `sha` — the last durable push-checkpoint — is emitted so a REPLACEMENT activation on a fresh worktree reconstructs the tree to the EXACT pushed SHA (inverting the round's `git push` into `git fetch && git checkout <sha>`), omitted when the PR has no checkpoint yet. The field is named `sha` because that is the field the c8ctl harness's `provisionRepo` reads to drive the checkout — an earlier `commitSha` key was a silent no-op (issue #695). Alongside the `repository` slice the envelope carries a sibling `task.allowPr: true` (issue #770): c8ctl-plugin-nano (≥1.60.2) only resolves the git credential (GITHUB_TOKEN, or the `gh` default) for repo provisioning behind that flag, so every repo-backed envelope sets it or the clone dies with `unable to get password from user`; the repoless path emits no envelope and so no `task`. Gated on c8ctl provisioner support (jwulf/c8ctl-plugin-nano#91, branch-cut guard jwulf/c8ctl-plugin-nano#231).",
|
|
451
|
+
"Repo-provisioning envelope the app emits as a `createInstance` process variable (`repoEnvelopeVars`, app/repoEnvelope.ts) and the c8ctl worker harness consumes to provision an isolated clone — instead of the agent inheriting the worker's launch dir (issue #684). `ref` is the branch checked out: the PR HEAD branch on the PR-based paths (review-round / fix-ci / rebase), or — on the PRE-PR implementation path (feature.bpmn / plan-fanout's `implement-cell`, issue #684; the delivery-graph runner's agent cells, issue #686) — the BASE branch, off which the harness cuts a new feature branch named by the optional `branch.create`. On the PRE-PR path `branch.create` is the deterministic `feat/<task.id>` (emitted for a single-task feature run AND — per issue #776 — the deterministic `feat/<node.id>` for each single-instance delivery-graph agent cell, injected per-cell by `agentNodeRepoEnvelope`/`app/deliveryRunner.ts` so a forgetful agent can never be left committing on the base branch and stranding its run on a non-ff push; the epic plan-fanout seed still omits it because its MI children each cut a per-child `feat/<task.id>` the app can't name at compile time, so those agents branch themselves). On the PR-based paths `branch.create` is set to the PR HEAD branch itself (equal to `ref`, jwulf/c8ctl-plugin-nano#231): without it the harness's base-branch guard treats the checkout as base-like and cuts a throwaway `nano/agent-work/<base>-<run>` fallback, stranding the round's commits OFF the PR; with `ref == create == head` the harness does a no-op `checkout -B <head>` and commits/pushes to the PR head branch. So `branch.create` is present on both the pre-PR and PR-based paths (only a blank/omitted value drops the key). Beyond `{provider,url,ref}`, it carries clone-shaping fields for large monorepos (issue #287): `singleBranch:true` + `filter:\"blob:none\"` (a branch-scoped, blobless partial clone — trees fetched up-front, blobs lazily, no `--depth 1` so the merge-base/3-dot diff stays valid) and an optional `baseRef` (the PR base branch, emitted only when resolvable, so the harness fetches its tip and keeps `origin/<base>` reachable) and a `cloneTimeoutMs` (from `NANO_PR_CLONE_TIMEOUT_MS`, default 600000 = 10 min) that raises the harness's 120s default so a large monorepo's blobless single-branch clone provisions instead of dying at 120s (issue #694). World-restore (issue #324, ADR 0062 Slice 4/5): an optional `sha` — the last durable push-checkpoint — is emitted so a REPLACEMENT activation on a fresh worktree reconstructs the tree to the EXACT pushed SHA (inverting the round's `git push` into `git fetch && git checkout <sha>`), omitted when the PR has no checkpoint yet. The field is named `sha` because that is the field the c8ctl harness's `provisionRepo` reads to drive the checkout — an earlier `commitSha` key was a silent no-op (issue #695). Alongside the `repository` slice the envelope carries a sibling `task.allowPr: true` (issue #770): c8ctl-plugin-nano (≥1.60.2) only resolves the git credential (GITHUB_TOKEN, or the `gh` default) for repo provisioning behind that flag, so every repo-backed envelope sets it or the clone dies with `unable to get password from user`; the repoless path emits no envelope and so no `task`. Gated on c8ctl provisioner support (jwulf/c8ctl-plugin-nano#91, branch-cut guard jwulf/c8ctl-plugin-nano#231). PLACEMENT (issue #882): `branch` is emitted as a TOP-LEVEL sibling of `repository` (and of `task`) under the namespace — the harness `normalizeTaskEnvelope` reads `branch` from the envelope ROOT (`isPlainObject(raw.branch)`), never from `repository.branch`, so the producer must not nest it there (nesting was the #227 two-sides-of-one-contract drift that left `branch.create` silently unread — nanobpm/nano-bpm#1344). The round-trip contract test `app/repoEnvelope.test.ts` pins producer and consumer together.",
|
|
452
452
|
shape:
|
|
453
|
-
'io.nanobpm.agentTask: { repository: { provider: "github", url: string, ref?: string, singleBranch: true, filter: "blob:none", cloneTimeoutMs: number, baseRef?: string, sha?: string, branch?: { create: string }
|
|
453
|
+
'io.nanobpm.agentTask: { repository: { provider: "github", url: string, ref?: string, singleBranch: true, filter: "blob:none", cloneTimeoutMs: number, baseRef?: string, sha?: string }, branch?: { create: string }, task: { allowPr: true } } — `branch` is a TOP-LEVEL sibling of `repository` (issue #882): the harness `normalizeTaskEnvelope` reads it from the envelope root (`isPlainObject(raw.branch)`), NOT from `repository.branch`, so the flattened header is `io.nanobpm.agentTask.branch.create` (never `…repository.branch.create`).',
|
|
454
454
|
},
|
|
455
455
|
"epicSet.submit": {
|
|
456
456
|
category: "wire",
|
|
@@ -538,9 +538,9 @@ export const WIRE_CONTRACTS = {
|
|
|
538
538
|
"agentTask.agentDefinition": {
|
|
539
539
|
category: "wire",
|
|
540
540
|
name: "agentTask.agentDefinition",
|
|
541
|
-
owner: "resources/processes/*.bpmn",
|
|
541
|
+
owner: "resources/processes/*.bpmn + app/deliveryGraphCompiler.ts",
|
|
542
542
|
semantics:
|
|
543
|
-
"The engine-native AgentTask marker (issue #745, umbrella #746 — Camunda 8.10 parity). Every `senior:*` agent service task carries `<zeebe:agentDefinition agentType=\"external\" />` INSIDE its `<bpmn:extensionElements>`, COEXISTING with the existing `<zeebe:taskDefinition type=\"senior:*\"/>` dispatch verb (the verb stays, per #464). The marker makes the element eligible for engine-native AgentInstance minting by the worker harness (jwulf/c8ctl-plugin-nano#194): the harness mints Create/Update/Complete AgentInstance/AgentHistory records against the pinned engine (`@nanobpm/engine-wasm` 0.8.6, broker REST, SDK) while the element still emits its NORMAL `senior:*` job. `agentType=\"external\"` means the agent runs OUTSIDE the engine (a remote fleet worker), not an engine-embedded model call. This is the PRODUCER half; the durable AgentInstance/AgentHistory it mints is read back by the Cockpit historical view via `searchAgentInstanceHistory` (the CONSUMER half — see the `agentTask.historyRead` contract; the read path landed on `@nanobpm/urban`'s EngineClient in urban 0.93 / nanobpm/nano-ide#563). It is authored in the hand-written BPMN semantic model, NOT the generated `<bpmndi:…>` DI, and survives `npm run layout` untouched. Add the marker to a NEW `senior:*` agent task — never a second/synonym marker element.",
|
|
543
|
+
"The engine-native AgentTask marker (issue #745, umbrella #746 — Camunda 8.10 parity). Every `senior:*` agent service task carries `<zeebe:agentDefinition agentType=\"external\" />` INSIDE its `<bpmn:extensionElements>`, COEXISTING with the existing `<zeebe:taskDefinition type=\"senior:*\"/>` dispatch verb (the verb stays, per #464). The marker makes the element eligible for engine-native AgentInstance minting by the worker harness (jwulf/c8ctl-plugin-nano#194): the harness mints Create/Update/Complete AgentInstance/AgentHistory records against the pinned engine (`@nanobpm/engine-wasm` 0.8.6, broker REST, SDK) while the element still emits its NORMAL `senior:*` job. `agentType=\"external\"` means the agent runs OUTSIDE the engine (a remote fleet worker), not an engine-embedded model call. This is the PRODUCER half; the durable AgentInstance/AgentHistory it mints is read back by the Cockpit historical view via `searchAgentInstanceHistory` (the CONSUMER half — see the `agentTask.historyRead` contract; the read path landed on `@nanobpm/urban`'s EngineClient in urban 0.93 / nanobpm/nano-ide#563). It is authored in the hand-written BPMN semantic model, NOT the generated `<bpmndi:…>` DI, and survives `npm run layout` untouched. The class is EVERY agent service task the app generates (issue #884): the hand-authored corpus AND every delivery-graph-compiler `agent` cell. The compiler emits the marker on EVERY cell and — per the resolved \"Decide\" half of #884 (Option (a), PR #886 review) — NO base-prompt link: the hand-authored process prompts are process-specific contracts (they require process variables and a `feat/<task.id>` branch the generic compiled cell never supplies), so a compiled cell is deliberately prompt-LESS and steered by its per-node `appendPrompt` alone. `npm run check:agent-definitions` (scripts/check-agent-definitions.ts, in CI) gates BOTH producers: it scans the compiled output with the prompt-keyed `agentTaskTypesMissingExternalMarker` AND the prompt-link-independent `externalAgentTaskTypesMissingMarker` (an agent task is keyed by its `<rank>:<task>` colon-form type alone, so a marker regression on a prompt-less cell cannot hide). Add the marker to a NEW `senior:*` agent task — never a second/synonym marker element.",
|
|
544
544
|
shape: '<zeebe:agentDefinition agentType="external" /> (sibling of <zeebe:taskDefinition> in a senior:* service task\'s extensionElements)',
|
|
545
545
|
},
|
|
546
546
|
"agentTask.autoSubscribe": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { test } from "node:test";
|
|
15
15
|
import { assert, assertEquals } from "#test-assert";
|
|
16
16
|
import type { DeliveryGraph } from "../nano-generated/api-io.d.ts";
|
|
17
|
+
import { missingExternalMarkerAgentTaskTypes } from "./agentic/vocab/job-types.ts";
|
|
17
18
|
import { compileDeliveryGraph, digestInvisibleRawValues, graphCarriesRedactedSecrets, nodeDisplay, redactFreeText, resumeValidVar } from "./deliveryGraphCompiler.ts";
|
|
18
19
|
import { isDeliveryEscalationTwin } from "./deliveryHuman.ts";
|
|
19
20
|
|
|
@@ -158,6 +159,86 @@ test("trust bound: every node inlines an embedded subProcess delegating to an al
|
|
|
158
159
|
assert(/<bpmn:userTask id="delivery-human-task__n\d+__esc"/.test(r.bpmn), "a bounded node inlines an escalation user task");
|
|
159
160
|
});
|
|
160
161
|
|
|
162
|
+
test("#884 agentDefinition: every compiled agent cell carries the engine-native AgentTask marker (durable transcript)", async () => {
|
|
163
|
+
// Issue #884: a compiled `agent` cell's service task carried NO
|
|
164
|
+
// `<zeebe:agentDefinition agentType="external"/>`, so the engine refused to attach an AgentInstance
|
|
165
|
+
// to it (`createAgentInstance failed — … has no agentDefinition`) and every delivery-graph agent run
|
|
166
|
+
// persisted NO durable agentic transcript. The hand-authored processes declare the marker on every
|
|
167
|
+
// `senior:*` task (issue #745); the compiler must emit it on every agent cell it generates — the
|
|
168
|
+
// defect CLASS is "every agent service task the app generates", so this guards the compiler output
|
|
169
|
+
// through the SAME canonical union scanner (`missingExternalMarkerAgentTaskTypes`, the one the
|
|
170
|
+
// repo-wide gate in `check-agent-definitions.ts` uses) rather than a second, driftable regex.
|
|
171
|
+
const r = await compileOk(RELEASE_RUNBOOK);
|
|
172
|
+
// RELEASE_RUNBOOK has exactly one agent node (`open-b` → `senior:feature`); its compiled service
|
|
173
|
+
// task is `<el>_task`. A compiled agent cell is prompt-LESS (the resolved #884 "Decide", Option (a)),
|
|
174
|
+
// so it is the type-keyed half of the union scanner (keyed by the `<rank>:<task>` colon-form type)
|
|
175
|
+
// that sees it — and the marker must be present on THAT cell, not merely somewhere in the document
|
|
176
|
+
// (a stray marker on a DIFFERENT element would not make THIS cell eligible for AgentInstance minting).
|
|
177
|
+
const taskBlock = [...r.bpmn.matchAll(/<bpmn:serviceTask id="n\d+_task"[\s\S]*?<\/bpmn:serviceTask>/g)]
|
|
178
|
+
.map((m) => m[0])
|
|
179
|
+
.find((b) => b.includes('<zeebe:taskDefinition type="senior:feature" />')) ?? "";
|
|
180
|
+
assert(taskBlock !== "", "the agent node compiles to an inner senior:feature service task");
|
|
181
|
+
const ext = /<bpmn:extensionElements>[\s\S]*?<\/bpmn:extensionElements>/.exec(taskBlock)?.[0] ?? "";
|
|
182
|
+
assert(
|
|
183
|
+
/<zeebe:agentDefinition agentType="external" \/>/.test(ext),
|
|
184
|
+
`the compiled agent cell carries <zeebe:agentDefinition agentType="external" /> inside its extensionElements, got:\n${taskBlock}`,
|
|
185
|
+
);
|
|
186
|
+
// The marker is a SIBLING of the dispatch verb, which stays (per #464): both must be present.
|
|
187
|
+
assert(/<zeebe:taskDefinition type="senior:feature" \/>/.test(ext), "the dispatch verb stays alongside the marker");
|
|
188
|
+
// And the CANONICAL union scanner — the one the repo-wide guard applies — finds NO unmarked agent
|
|
189
|
+
// task in the whole compiled document (it keys an agent by its colon-form type, which every compiled
|
|
190
|
+
// agent cell carries; a connector/wait/host task is not colon-form and is correctly ignored).
|
|
191
|
+
assertEquals(missingExternalMarkerAgentTaskTypes(r.bpmn), []);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("#884 agentDefinition: EVERY agent cell in a multi-agent graph is marked (not just the first)", async () => {
|
|
195
|
+
// A marker emitted once (or only on a specific shape) would leave sibling agent cells transcript-less
|
|
196
|
+
// — the 269 failed creates on omarchy came from several processes. Compile a graph whose agent nodes
|
|
197
|
+
// differ (distinct job types, one with a repo spec + emits, one bare) and assert EVERY compiled
|
|
198
|
+
// `<el>_task` service task carries the marker inside its own extensionElements.
|
|
199
|
+
const r = await compileOk({
|
|
200
|
+
nodes: [
|
|
201
|
+
{ id: "impl", kind: "agent", agent: { jobType: "senior:feature", repository: "nanobpm/nano-workforce", prompt: "implement #1" }, emits: [{ name: "pr", type: "pr" }] },
|
|
202
|
+
{ id: "review", kind: "agent", agent: { jobType: "senior:pr-review" } },
|
|
203
|
+
{ id: "ship", kind: "agent", agent: { jobType: "senior:trial-merge", prompt: "merge it" } },
|
|
204
|
+
],
|
|
205
|
+
edges: [
|
|
206
|
+
{ from: "impl", to: "review" },
|
|
207
|
+
{ from: "review", to: "ship" },
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
const taskBlocks = [...r.bpmn.matchAll(/<bpmn:serviceTask id="(n\d+_task)"[\s\S]*?<\/bpmn:serviceTask>/g)];
|
|
211
|
+
assertEquals(taskBlocks.length, 3, "all three agent nodes compile to an inner service task");
|
|
212
|
+
for (const [block, id] of taskBlocks) {
|
|
213
|
+
const ext = /<bpmn:extensionElements>[\s\S]*?<\/bpmn:extensionElements>/.exec(block)?.[0] ?? "";
|
|
214
|
+
assert(
|
|
215
|
+
/<zeebe:agentDefinition agentType="external" \/>/.test(ext),
|
|
216
|
+
`agent cell ${id} is missing <zeebe:agentDefinition agentType="external" /> in its extensionElements`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("#884 prompt-less: a compiled agent cell carries NO base-prompt link (Option (a))", async () => {
|
|
222
|
+
// The resolved "Decide" half of #884 (Option (a), PR #886 review): a compiled agent cell is
|
|
223
|
+
// deliberately prompt-LESS. The hand-authored process prompts (`prompts/feature.md`, …) are
|
|
224
|
+
// process-specific contracts — they require process variables (`variables.task`/`variables.issue`)
|
|
225
|
+
// and a `feat/<task.id>` branch the generic compiled cell never supplies (it seeds only
|
|
226
|
+
// `{jobType, appendPrompt, timeout}` and the harness provisions `feat/<node.id>`, #776) — so linking
|
|
227
|
+
// one hands the cell instructions referencing missing inputs and a conflicting branch. The cell is
|
|
228
|
+
// steered by its per-node `appendPrompt` alone; the marker alone fixes the createAgentInstance
|
|
229
|
+
// failure. Pin that NO compiled agent cell emits a prompt `linkedResource` — for a mapped-looking
|
|
230
|
+
// type, a bespoke type, AND a prototype-chain jobType (which must never resolve to a bogus link).
|
|
231
|
+
for (const jobType of ["senior:feature", "senior:pr-review", "senior:no-such-prompt", "constructor", "__proto__", "toString"]) {
|
|
232
|
+
const r = await compileOk({ nodes: [{ id: "p", kind: "agent", agent: { jobType } }], edges: [] });
|
|
233
|
+
assert(
|
|
234
|
+
!r.bpmn.includes('linkName="prompt"'),
|
|
235
|
+
`compiled agent cell ${JSON.stringify(jobType)} must link NO base prompt (prompt-less, Option (a)), got:\n${
|
|
236
|
+
r.bpmn.split("\n").filter((l) => l.includes("linkedResource")).join("\n")
|
|
237
|
+
}`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
161
242
|
test("late-binding: a fact-qualified edge threads a boundFacts input into the consumer subProcess", async () => {
|
|
162
243
|
const r = await compileOk(RELEASE_RUNBOOK);
|
|
163
244
|
// `publish.resolvedArtifact -> consume`: the connector subProcess receives the human's emitted fact as
|
|
@@ -1931,7 +1931,7 @@ function innerBodyLines(w: NodeWiring, requiredEmits: ReadonlySet<string>, displ
|
|
|
1931
1931
|
// be a leak here), but the `descriptor` is embedded by `serviceBodyLines` into the operator-visible
|
|
1932
1932
|
// timeout / producer-contract escalation FEEL `prompt` — so it takes the SAME display redaction
|
|
1933
1933
|
// `nodeDisplay` applies, never the raw value (issue #778 review — thread deliveryGraphCompiler.ts:1606).
|
|
1934
|
-
return serviceBodyLines(el, node.id, attr("type", node.agent.jobType), [], redactConnectorValue(node.agent.jobType), contractGate, agentRepoSpecHeaderLines(node), displayName, "agent", nodeEmits);
|
|
1934
|
+
return serviceBodyLines(el, node.id, attr("type", node.agent.jobType), [], redactConnectorValue(node.agent.jobType), contractGate, agentRepoSpecHeaderLines(node), displayName, "agent", nodeEmits, undefined);
|
|
1935
1935
|
}
|
|
1936
1936
|
case "connector": {
|
|
1937
1937
|
// TRIM the target before building the escalation descriptor — `nodeDisplay` and the connector
|
|
@@ -2113,6 +2113,27 @@ function serviceBodyLines(
|
|
|
2113
2113
|
const taskExt = [
|
|
2114
2114
|
" <bpmn:extensionElements>",
|
|
2115
2115
|
` <zeebe:taskDefinition ${taskDefAttr} />`,
|
|
2116
|
+
// Issue #884: an `agent` cell MUST carry the engine-native AgentTask marker
|
|
2117
|
+
// (`<zeebe:agentDefinition agentType="external"/>`, the `agentTask.agentDefinition` contract —
|
|
2118
|
+
// issue #745) INSIDE its extensionElements, as a sibling of the dispatch verb. Without it the
|
|
2119
|
+
// engine refuses to attach an AgentInstance to the element (`createAgentInstance failed — … has
|
|
2120
|
+
// no agentDefinition`), so every delivery-graph agent run persisted NO durable agentic
|
|
2121
|
+
// transcript (269 failed creates on omarchy). The hand-authored `senior:*` tasks all declare it
|
|
2122
|
+
// (guarded by `agent-marker.test.ts`); the compiler emits it on EVERY agent cell it generates —
|
|
2123
|
+
// the defect class is "every agent service task the app generates". A `connector` delegates to a
|
|
2124
|
+
// HOST worker (`pr.delivery-connector`), not an external agent, so it is NOT marked.
|
|
2125
|
+
...(kind === "agent" ? [' <zeebe:agentDefinition agentType="external" />'] : []),
|
|
2126
|
+
// Issue #884 (the "Decide" half — resolved Option (a), PR #886 review): an agent cell carries ONLY
|
|
2127
|
+
// the marker above and NO base-prompt `linkedResource`. The hand-authored process prompts
|
|
2128
|
+
// (`prompts/feature.md`, `prompts/review-round.md`, …) are PROCESS-specific contracts, not generic
|
|
2129
|
+
// cell contracts — they require process variables (`variables.task`/`variables.issue`) and a branch
|
|
2130
|
+
// (`feat/<task.id>`) the generic compiled cell never supplies (it seeds only
|
|
2131
|
+
// `{jobType, appendPrompt, timeout}` and the harness provisions `feat/<node.id>`, #776). Linking one
|
|
2132
|
+
// would hand every compiled cell instructions referencing missing inputs and a conflicting branch.
|
|
2133
|
+
// The cell is therefore deliberately prompt-LESS: the per-node `appendPrompt` FEEL (runner-seeded
|
|
2134
|
+
// from `node.agent.prompt` + the emit/producer contracts) is its sole steering channel, matching the
|
|
2135
|
+
// historically-intentional design (`deliveryRunner.ts` — "the delivery agent node carries no
|
|
2136
|
+
// base-prompt resource"). The marker alone is what fixes the #884 `createAgentInstance` failure.
|
|
2116
2137
|
...(taskProps.length > 0 ? [" <zeebe:properties>", ...taskProps, " </zeebe:properties>"] : []),
|
|
2117
2138
|
...taskHeaders,
|
|
2118
2139
|
" <zeebe:ioMapping>",
|
|
@@ -533,7 +533,7 @@ test("prepareDeliveryGraph injects the repository envelope PER agent cell from t
|
|
|
533
533
|
assert(headers.some((h) => h.includes('task.allowPr" value="true"')), `expected a task.allowPr header, got ${JSON.stringify(headers)}`);
|
|
534
534
|
// The deterministic per-node isolation branch (issue #776): the harness cuts `feat/<node.id>` itself,
|
|
535
535
|
// so the agent can never be left committing on the base branch (a non-ff push that strands the run).
|
|
536
|
-
assert(headers.some((h) => h.includes('
|
|
536
|
+
assert(headers.some((h) => h.includes('io.nanobpm.agentTask.branch.create" value="feat/open-b"')), `expected a per-node branch.create header, got ${JSON.stringify(headers)}`);
|
|
537
537
|
// No `__repoSpec` marker survives injection — it is the compiler's digest-stable anchor only.
|
|
538
538
|
assert(!p.bpmn.includes("__repoSpec"), "the __repoSpec marker is fully replaced");
|
|
539
539
|
// No run-root `io.nanobpm.agentTask` variable — the envelope rides headers now, not a run variable.
|
|
@@ -590,7 +590,7 @@ test("a declared repository WITHOUT a base branch omits ref/baseRef — the harn
|
|
|
590
590
|
assert(!headers.some((h) => h.includes("repository.baseRef")), "no baseRef either");
|
|
591
591
|
// Even without a known base, the deterministic isolation branch is still emitted (issue #776): the
|
|
592
592
|
// harness cuts `feat/<node.id>` off the cloned default tip, so the agent never commits on the default.
|
|
593
|
-
assert(headers.some((h) => h.includes('
|
|
593
|
+
assert(headers.some((h) => h.includes('io.nanobpm.agentTask.branch.create" value="feat/a"')), `expected branch.create even without a base, got ${JSON.stringify(headers)}`);
|
|
594
594
|
});
|
|
595
595
|
|
|
596
596
|
test("each agent cell gets its OWN deterministic feat/<node.id> branch.create — never a shared branch (#776)", async () => {
|
|
@@ -603,7 +603,7 @@ test("each agent cell gets its OWN deterministic feat/<node.id> branch.create
|
|
|
603
603
|
edges: [{ from: "impl-a", to: "impl-b" }],
|
|
604
604
|
};
|
|
605
605
|
const p = await prepareOk(graph, {});
|
|
606
|
-
const branches = agentHeaders(p.bpmn).filter((h) => h.includes("
|
|
606
|
+
const branches = agentHeaders(p.bpmn).filter((h) => h.includes("io.nanobpm.agentTask.branch.create"));
|
|
607
607
|
// Two distinct cells → two distinct per-node branches, so no two agents can collide on one branch.
|
|
608
608
|
assert(branches.some((h) => h.includes('value="feat/impl-a"')), `expected feat/impl-a, got ${JSON.stringify(branches)}`);
|
|
609
609
|
assert(branches.some((h) => h.includes('value="feat/impl-b"')), `expected feat/impl-b, got ${JSON.stringify(branches)}`);
|
|
@@ -612,7 +612,7 @@ test("each agent cell gets its OWN deterministic feat/<node.id> branch.create
|
|
|
612
612
|
|
|
613
613
|
test("an explicit repoless run emits NO branch.create — no envelope at all (#776/#729)", async () => {
|
|
614
614
|
const p = await prepareOk(GRAPH, { repoless: true });
|
|
615
|
-
assert(!p.bpmn.includes("
|
|
615
|
+
assert(!p.bpmn.includes("io.nanobpm.agentTask.branch.create"), "a repoless run carries no isolation branch either");
|
|
616
616
|
});
|
|
617
617
|
|
|
618
618
|
test("a node id that derives an invalid git ref degrades to NO branch.create — never emits an unusable feat/... (#776)", async () => {
|
|
@@ -630,7 +630,7 @@ test("a node id that derives an invalid git ref degrades to NO branch.create —
|
|
|
630
630
|
edges: [{ from: "a..b", to: "a.lock" }, { from: "a.lock", to: "ok" }],
|
|
631
631
|
};
|
|
632
632
|
const p = await prepareOk(graph, {});
|
|
633
|
-
const branches = agentHeaders(p.bpmn).filter((h) => h.includes("
|
|
633
|
+
const branches = agentHeaders(p.bpmn).filter((h) => h.includes("io.nanobpm.agentTask.branch.create"));
|
|
634
634
|
// Only the valid-id cell keeps a branch.create; the two invalid-ref ids emit none (their agents still
|
|
635
635
|
// provision an isolated clone, they just cut their own branch inside it — the pre-#776 fallback).
|
|
636
636
|
assertEquals(branches.length, 1, `only the valid id keeps a branch.create, got ${JSON.stringify(branches)}`);
|
|
@@ -638,16 +638,19 @@ test("a node id that derives an invalid git ref degrades to NO branch.create —
|
|
|
638
638
|
});
|
|
639
639
|
|
|
640
640
|
test("agentNodeRepoEnvelope emits branch.create off the base when known, off the default when not, and omits it when blank (#776)", () => {
|
|
641
|
-
|
|
642
|
-
|
|
641
|
+
// `branch` is a TOP-LEVEL sibling of `repository` (issue #882) — the harness reads it from the
|
|
642
|
+
// envelope root, NOT from `repository.branch`.
|
|
643
|
+
const withBase = (agentNodeRepoEnvelope("acme/one", "main", "feat/n1") as any)["io.nanobpm.agentTask"];
|
|
644
|
+
assertEquals(withBase.repository.ref, "main", "known base → checked-out ref");
|
|
643
645
|
assertEquals(withBase.branch.create, "feat/n1", "the harness cuts feat/<node.id> off the base");
|
|
644
|
-
|
|
645
|
-
|
|
646
|
+
assertEquals("branch" in withBase.repository, false, "branch must NOT be nested under repository");
|
|
647
|
+
const noBase = (agentNodeRepoEnvelope("acme/one", null, "feat/n2") as any)["io.nanobpm.agentTask"];
|
|
648
|
+
assertEquals("ref" in noBase.repository, false, "no base → clone the default branch");
|
|
646
649
|
assertEquals(noBase.branch.create, "feat/n2", "branch.create is still emitted off the default tip");
|
|
647
650
|
// A whitespace-tainted branch is trimmed; a blank/absent one omits the `branch` key (pre-#776 behaviour).
|
|
648
|
-
assertEquals((agentNodeRepoEnvelope("acme/one", "main", " feat/n3 ") as any)["io.nanobpm.agentTask"].
|
|
651
|
+
assertEquals((agentNodeRepoEnvelope("acme/one", "main", " feat/n3 ") as any)["io.nanobpm.agentTask"].branch.create, "feat/n3");
|
|
649
652
|
for (const blank of [null, undefined, "", " "]) {
|
|
650
|
-
const r = (agentNodeRepoEnvelope("acme/one", "main", blank as any) as any)["io.nanobpm.agentTask"]
|
|
653
|
+
const r = (agentNodeRepoEnvelope("acme/one", "main", blank as any) as any)["io.nanobpm.agentTask"];
|
|
651
654
|
assertEquals("branch" in r, false, `expected no branch key for ${JSON.stringify(blank)}`);
|
|
652
655
|
}
|
|
653
656
|
});
|
package/app/feature.test.ts
CHANGED
|
@@ -196,11 +196,13 @@ test("startFeature: seeds the pre-PR repository envelope so the harness provisio
|
|
|
196
196
|
// shared launch dir; with it, the harness clones a throwaway workspace. The implementation path is
|
|
197
197
|
// PRE-PR, so it checks out the BASE branch (`ref`) and the harness cuts the deterministic
|
|
198
198
|
// `feat/<task.id>` feature branch off it (`branch.create`).
|
|
199
|
-
const
|
|
199
|
+
const ns = (captured.variables as Record<string, any>)["io.nanobpm.agentTask"];
|
|
200
|
+
const repo = ns.repository;
|
|
200
201
|
assertEquals(repo.url, "https://github.com/owner/repo.git");
|
|
201
202
|
assertEquals(repo.ref, "epic/x");
|
|
202
|
-
|
|
203
|
-
assertEquals(
|
|
203
|
+
// `branch` is a TOP-LEVEL sibling of `repository` (issue #882) — the harness reads it from the root.
|
|
204
|
+
assertEquals(ns.branch.create, "feat/issue-42");
|
|
205
|
+
assertEquals(ns.branch.create, `feat/${featureTaskId(PARSED.number)}`);
|
|
204
206
|
// The blobless/single-branch monorepo shaping rides along, exactly like the PR-based envelope.
|
|
205
207
|
assertEquals(repo.singleBranch, true);
|
|
206
208
|
assertEquals(repo.filter, "blob:none");
|