@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
|
@@ -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
|
@@ -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>",
|
|
@@ -89,13 +89,15 @@ const DEFAULT_VARS: Record<string, unknown> = {
|
|
|
89
89
|
mergeableRepollInterval: "PT2M",
|
|
90
90
|
mergeStallRounds: 0,
|
|
91
91
|
mergeStallMax: 3,
|
|
92
|
-
abandonBrief:
|
|
92
|
+
abandonBrief: "\n\n---\nAbort if cancelled: curl …",
|
|
93
93
|
failingChecksList: null,
|
|
94
94
|
status: null,
|
|
95
95
|
mergeState: null,
|
|
96
96
|
mergeStatus: null,
|
|
97
97
|
ciBlockedReconciled: null,
|
|
98
98
|
failingChecks: 0,
|
|
99
|
+
round: 2,
|
|
100
|
+
abandonUrl: "http://host/app/api/hooks/abandon?token=tok-seeded",
|
|
99
101
|
};
|
|
100
102
|
|
|
101
103
|
/**
|
|
@@ -303,6 +305,86 @@ test("a CONFLICTING PR whose poller never signalled reaches the rebase arm via t
|
|
|
303
305
|
.hasVariable("rebaseRound", 1);
|
|
304
306
|
});
|
|
305
307
|
|
|
308
|
+
test("a rebase agent echoing engine-owned timeout state cannot clobber it (#887)", async () => {
|
|
309
|
+
// Same guard class as fix-ci: the rebase agent must not reshape the seeded timers that later loop
|
|
310
|
+
// passes re-arm, nor forge `ciBlockedReconciled`. Drive to the rebase arm via the timeout path with
|
|
311
|
+
// the agent echoing a forged marker and tiny timeouts; the restore must pin the timers.
|
|
312
|
+
const engine = await boot({
|
|
313
|
+
responses: {
|
|
314
|
+
"pr.merge-stall-probe": { mergeState: "conflict", failingChecks: 0, failingChecksList: "" },
|
|
315
|
+
"senior:rebase": {
|
|
316
|
+
status: "rebased",
|
|
317
|
+
ciBlockedReconciled: true,
|
|
318
|
+
agentSlaTimeout: "PT1S",
|
|
319
|
+
landedWaitTimeout: "PT1S",
|
|
320
|
+
mergeableWaitTimeout: "PT1S",
|
|
321
|
+
mergeableRepollInterval: "PT1S",
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
await engine.publishMessage({ name: "deps-cleared", correlationKey: "pr-1" });
|
|
326
|
+
await engine.advanceTime(MERGEABLE_WAIT_MS + 1);
|
|
327
|
+
assertThatInstance(engine, byProcessId("merge-loop"))
|
|
328
|
+
.isActive()
|
|
329
|
+
.hasCompletedElements("rebase")
|
|
330
|
+
.hasVariable("agentSlaTimeout", "PT30M")
|
|
331
|
+
.hasVariable("landedWaitTimeout", "PT30M")
|
|
332
|
+
.hasVariable("mergeableWaitTimeout", "PT30M")
|
|
333
|
+
.hasVariable("mergeableRepollInterval", "PT2M");
|
|
334
|
+
// The forged lifecycle marker was pinned out too (the rebase path never reconciles).
|
|
335
|
+
assert(instanceVars(engine).ciBlockedReconciled !== true, "a rebase agent must not forge ciBlockedReconciled:true");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("a rebase agent echoing round/abandonUrl cannot corrupt the converged round or the abandon token (#887)", async () => {
|
|
339
|
+
// Same guard class as fix-ci: `round`/`abandonUrl` are seeded once and consumed by `pr.merge`
|
|
340
|
+
// after a successful rebase re-arms the poller. The restore must pin both against an agent echo.
|
|
341
|
+
const engine = await boot({
|
|
342
|
+
responses: {
|
|
343
|
+
"pr.merge-stall-probe": { mergeState: "conflict", failingChecks: 0, failingChecksList: "" },
|
|
344
|
+
"senior:rebase": { status: "rebased", round: 99, abandonUrl: "http://evil.test/abandon?token=forged" },
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
await engine.publishMessage({ name: "deps-cleared", correlationKey: "pr-1" });
|
|
348
|
+
await engine.advanceTime(MERGEABLE_WAIT_MS + 1);
|
|
349
|
+
assertThatInstance(engine, byProcessId("merge-loop"))
|
|
350
|
+
.isActive()
|
|
351
|
+
.hasCompletedElements("rebase")
|
|
352
|
+
.hasVariable("round", 2)
|
|
353
|
+
.hasVariable("abandonUrl", "http://host/app/api/hooks/abandon?token=tok-seeded");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("a rebase agent echoing abandonBrief cannot corrupt the seeded abort brief (#887 round 5)", async () => {
|
|
357
|
+
// `abandonBrief` feeds the fix-ci/rebase `appendPrompt` input on the NEXT re-arm; a non-string
|
|
358
|
+
// echo would make that later prompt mapping carry a non-string brief. The restore must pin it.
|
|
359
|
+
const engine = await boot({
|
|
360
|
+
responses: {
|
|
361
|
+
"pr.merge-stall-probe": { mergeState: "conflict", failingChecks: 0, failingChecksList: "" },
|
|
362
|
+
"senior:rebase": { status: "rebased", abandonBrief: 42 },
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
await engine.publishMessage({ name: "deps-cleared", correlationKey: "pr-1" });
|
|
366
|
+
await engine.advanceTime(MERGEABLE_WAIT_MS + 1);
|
|
367
|
+
assertThatInstance(engine, byProcessId("merge-loop"))
|
|
368
|
+
.isActive()
|
|
369
|
+
.hasCompletedElements("rebase")
|
|
370
|
+
.hasVariable("abandonBrief", "\n\n---\nAbort if cancelled: curl …");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("a fix-ci agent echoing abandonBrief cannot corrupt the seeded abort brief (#887 round 5)", async () => {
|
|
374
|
+
const engine = await boot({
|
|
375
|
+
responses: {
|
|
376
|
+
"pr.merge-stall-probe": { mergeState: "blocked", failingChecks: 1, failingChecksList: "build" },
|
|
377
|
+
"senior:fix-ci": { status: "fixed", abandonBrief: 42 },
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
await engine.publishMessage({ name: "deps-cleared", correlationKey: "pr-1" });
|
|
381
|
+
await engine.advanceTime(MERGEABLE_WAIT_MS + 1);
|
|
382
|
+
assertThatInstance(engine, byProcessId("merge-loop"))
|
|
383
|
+
.isActive()
|
|
384
|
+
.hasCompletedElements("merge-stall-probe", "fix-ci")
|
|
385
|
+
.hasVariable("abandonBrief", "\n\n---\nAbort if cancelled: curl …");
|
|
386
|
+
});
|
|
387
|
+
|
|
306
388
|
test("a blocked PR probed on timeout reaches the CI-fix arm via the timeout path (#636)", async () => {
|
|
307
389
|
const engine = await boot({
|
|
308
390
|
responses: {
|
|
@@ -529,6 +611,67 @@ test("a blocked CI verdict with nothing pushed reconciles once before escalating
|
|
|
529
611
|
assert(!completedElementIds(engine).has("merge-esc-attempt"), "the first block episode must reconcile, not page a human");
|
|
530
612
|
});
|
|
531
613
|
|
|
614
|
+
test("a fix-ci agent echoing ciBlockedReconciled:true cannot skip the one-shot ground-truth reconcile (#887)", async () => {
|
|
615
|
+
// `ciBlockedReconciled` is an engine-owned lifecycle marker (arm-merge seeds it null; ci-reconcile
|
|
616
|
+
// sets it true). gw-ci-blocked reconciles exactly once per episode while it is != true. If the agent's
|
|
617
|
+
// completion could echo `ciBlockedReconciled:true`, a `{status:"blocked", pushed:false}` verdict would
|
|
618
|
+
// skip the mandatory first reconcile and page a human immediately. The snapshot/restore guard pins it,
|
|
619
|
+
// so the engine still reconciles once.
|
|
620
|
+
const engine = await boot({
|
|
621
|
+
responses: { "senior:fix-ci": { status: "blocked", pushed: false, ciBlockedReconciled: true } },
|
|
622
|
+
});
|
|
623
|
+
await driveToCiFix(engine);
|
|
624
|
+
assertThatInstance(engine, byProcessId("merge-loop"))
|
|
625
|
+
.isActive()
|
|
626
|
+
.hasActiveElement("wait-mergeable")
|
|
627
|
+
.hasCompletedElements("ci-reconcile") // the reconcile STILL runs — the agent's echo was pinned out
|
|
628
|
+
.hasVariable("ciBlockedReconciled", true);
|
|
629
|
+
assert(!completedElementIds(engine).has("merge-esc-attempt"), "an agent-forged reconciled flag must not skip the first reconcile");
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
test("a fix-ci agent echoing seeded timeouts cannot clobber the re-armed merge/SLA timers (#887)", async () => {
|
|
633
|
+
// The seeded timeout/interval config (agentSlaTimeout, landedWaitTimeout, mergeableWaitTimeout,
|
|
634
|
+
// mergeableRepollInterval) drives timers re-armed on every loop pass. A `reattempt` re-arms the
|
|
635
|
+
// merge wait, so a clobbered timeout would silently reshape the next wait. The guard restores them.
|
|
636
|
+
const engine = await boot({
|
|
637
|
+
responses: {
|
|
638
|
+
"senior:fix-ci": {
|
|
639
|
+
status: "reattempt",
|
|
640
|
+
agentSlaTimeout: "PT1S",
|
|
641
|
+
landedWaitTimeout: "PT1S",
|
|
642
|
+
mergeableWaitTimeout: "PT1S",
|
|
643
|
+
mergeableRepollInterval: "PT1S",
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
});
|
|
647
|
+
await driveToCiFix(engine);
|
|
648
|
+
assertThatInstance(engine, byProcessId("merge-loop"))
|
|
649
|
+
.isActive()
|
|
650
|
+
.hasActiveElement("wait-mergeable")
|
|
651
|
+
.hasVariable("agentSlaTimeout", "PT30M")
|
|
652
|
+
.hasVariable("landedWaitTimeout", "PT30M")
|
|
653
|
+
.hasVariable("mergeableWaitTimeout", "PT30M")
|
|
654
|
+
.hasVariable("mergeableRepollInterval", "PT2M");
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test("a fix-ci agent echoing round/abandonUrl cannot corrupt the converged round or the abandon token (#887)", async () => {
|
|
658
|
+
// `round` (the converged round recorded by ensurePr) and `abandonUrl` (the cooperative-abandon
|
|
659
|
+
// capability URL) are seeded once and consumed directly by `pr.merge` (attempt-merge). A fix-ci
|
|
660
|
+
// completion echoing either would corrupt the recorded round or replace the abandon token. The
|
|
661
|
+
// snapshot/restore guard pins both.
|
|
662
|
+
const engine = await boot({
|
|
663
|
+
responses: {
|
|
664
|
+
"senior:fix-ci": { status: "reattempt", round: 99, abandonUrl: "http://evil.test/abandon?token=forged" },
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
await driveToCiFix(engine);
|
|
668
|
+
assertThatInstance(engine, byProcessId("merge-loop"))
|
|
669
|
+
.isActive()
|
|
670
|
+
.hasActiveElement("wait-mergeable")
|
|
671
|
+
.hasVariable("round", 2)
|
|
672
|
+
.hasVariable("abandonUrl", "http://host/app/api/hooks/abandon?token=tok-seeded");
|
|
673
|
+
});
|
|
674
|
+
|
|
532
675
|
test("a blocked CI verdict that already pushed escalates with a could-not-fix question", async () => {
|
|
533
676
|
const engine = await boot({ responses: { "senior:fix-ci": { status: "blocked", pushed: true } } });
|
|
534
677
|
await driveToCiFix(engine);
|
|
@@ -602,6 +745,70 @@ test("a no-verdict rebase result reconciles from ground truth, not escalation (#
|
|
|
602
745
|
assert(!completedElementIds(engine).has("merge-esc-attempt"), "a missing rebase status must not escalate");
|
|
603
746
|
});
|
|
604
747
|
|
|
748
|
+
// ---------------------------------------------------------------------------
|
|
749
|
+
// #887 — an external-agent completion must not clobber engine-owned counters
|
|
750
|
+
// ---------------------------------------------------------------------------
|
|
751
|
+
// The engine merges EVERY variable an external agent returns into process scope unfiltered, so a
|
|
752
|
+
// `senior:fix-ci`/`senior:rebase` agent echoing a loop counter (`ciFixRound`, `rebaseRound`, …) with
|
|
753
|
+
// the wrong type/value would corrupt it — and because the counter feeds the task's own `+ 1`
|
|
754
|
+
// increment and the `ciFixRound < ciFixMax` / `rebaseRound < rebaseMax` guards, a string echo wedges
|
|
755
|
+
// the sub-process on a "comparison/arithmetic not defined for string and number" incident. The fix
|
|
756
|
+
// snapshots each engine-owned counter into a task-local input before the agent runs and restores it
|
|
757
|
+
// on the task output (driving the increment from the snapshot), so the agent can never alter it.
|
|
758
|
+
|
|
759
|
+
test("a fix-ci agent echoing ciFixRound:\"9\" cannot clobber the counter or wedge the increment (#887)", async () => {
|
|
760
|
+
const engine = await boot({ responses: { "senior:fix-ci": { status: "fixed", ciFixRound: "9", ciFixMax: "1" } } });
|
|
761
|
+
await driveToCiFix(engine);
|
|
762
|
+
assertEquals((await engine.searchIncidents()).length, 0, "the agent echo must not raise an incident");
|
|
763
|
+
assertThatInstance(engine, byProcessId("merge-loop"))
|
|
764
|
+
.isActive()
|
|
765
|
+
.hasCompletedElements("fix-ci")
|
|
766
|
+
.hasVariable("ciFixRound", 1) // the engine increment (0 → 1), NOT the agent's "9"
|
|
767
|
+
.hasVariable("ciFixMax", 3); // the budget ceiling is engine-owned, unclobbered
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
test("a rebase agent echoing rebaseRound:\"9\" cannot clobber the counter or wedge the increment (#887)", async () => {
|
|
771
|
+
const engine = await boot({ responses: { "senior:rebase": { status: "rebased", rebaseRound: "9", rebaseMax: "1" } } });
|
|
772
|
+
await driveToRebase(engine);
|
|
773
|
+
assertEquals((await engine.searchIncidents()).length, 0, "the agent echo must not raise an incident");
|
|
774
|
+
assertThatInstance(engine, byProcessId("merge-loop"))
|
|
775
|
+
.isActive()
|
|
776
|
+
.hasCompletedElements("rebase")
|
|
777
|
+
.hasVariable("rebaseRound", 1) // the engine increment (0 → 1), NOT the agent's "9"
|
|
778
|
+
.hasVariable("rebaseMax", 3); // the budget ceiling is engine-owned, unclobbered
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
test("a fix-ci agent echoing PR identity inputs cannot clobber them (#887)", async () => {
|
|
782
|
+
// `repo`/`prNumber`/`prUrl` are the agent's declared *inputs* (FixCiIn), not its outputs — but the
|
|
783
|
+
// engine merges a completion UNFILTERED, so an input-echo would replace the process identity that
|
|
784
|
+
// `pr.merge` reads for GitHub operations after re-arming. The snapshot/restore guard must pin them.
|
|
785
|
+
const engine = await boot({
|
|
786
|
+
responses: { "senior:fix-ci": { status: "fixed", repo: "evil/repo", prNumber: 999, prUrl: "https://evil.test/pr/999" } },
|
|
787
|
+
});
|
|
788
|
+
await driveToCiFix(engine);
|
|
789
|
+
assertEquals((await engine.searchIncidents()).length, 0, "the agent echo must not raise an incident");
|
|
790
|
+
assertThatInstance(engine, byProcessId("merge-loop"))
|
|
791
|
+
.isActive()
|
|
792
|
+
.hasCompletedElements("fix-ci")
|
|
793
|
+
.hasVariable("repo", "acme/app")
|
|
794
|
+
.hasVariable("prNumber", 1)
|
|
795
|
+
.hasVariable("prUrl", "https://example.test/pr/1");
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
test("a rebase agent echoing PR identity inputs cannot clobber them (#887)", async () => {
|
|
799
|
+
const engine = await boot({
|
|
800
|
+
responses: { "senior:rebase": { status: "rebased", repo: "evil/repo", prNumber: 999, prUrl: "https://evil.test/pr/999" } },
|
|
801
|
+
});
|
|
802
|
+
await driveToRebase(engine);
|
|
803
|
+
assertEquals((await engine.searchIncidents()).length, 0, "the agent echo must not raise an incident");
|
|
804
|
+
assertThatInstance(engine, byProcessId("merge-loop"))
|
|
805
|
+
.isActive()
|
|
806
|
+
.hasCompletedElements("rebase")
|
|
807
|
+
.hasVariable("repo", "acme/app")
|
|
808
|
+
.hasVariable("prNumber", 1)
|
|
809
|
+
.hasVariable("prUrl", "https://example.test/pr/1");
|
|
810
|
+
});
|
|
811
|
+
|
|
605
812
|
test("rebase budget exhaustion escalates as not-mergeable without running the agent", async () => {
|
|
606
813
|
const engine = await boot({ responses: { "senior:rebase": { status: "rebased" } }, vars: { rebaseMax: 0 } });
|
|
607
814
|
await driveToRebase(engine);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.201.
|
|
3
|
+
"version": "0.201.5",
|
|
4
4
|
"description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "main.ts",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"typecheck": "tsc --noEmit",
|
|
38
38
|
"pretypecheck": "urban gen",
|
|
39
39
|
"check:prompts": "node --experimental-strip-types scripts/check-agent-prompts.ts",
|
|
40
|
+
"check:agent-definitions": "node --experimental-strip-types scripts/check-agent-definitions.ts",
|
|
40
41
|
"check:migrations": "node --experimental-strip-types scripts/check-migrations.ts",
|
|
41
42
|
"heal:migrations": "node --experimental-strip-types scripts/heal-migration-ledger.ts",
|
|
42
43
|
"check:contracts": "node --experimental-strip-types scripts/check-contracts.ts",
|
|
@@ -147,9 +147,45 @@
|
|
|
147
147
|
</zeebe:properties>
|
|
148
148
|
<zeebe:ioMapping>
|
|
149
149
|
<zeebe:input source="=(if (abandonBrief = null) then "" else abandonBrief)" target="appendPrompt" />
|
|
150
|
+
<zeebe:input source="=round" target="guardRound" />
|
|
151
|
+
<zeebe:input source="=maxRounds" target="guardMaxRounds" />
|
|
152
|
+
<zeebe:input source="=advPass" target="guardAdvPass" />
|
|
153
|
+
<zeebe:input source="=advMax" target="guardAdvMax" />
|
|
154
|
+
<zeebe:input source="=ackRetryRound" target="guardAckRetryRound" />
|
|
155
|
+
<zeebe:input source="=ackRetryMax" target="guardAckRetryMax" />
|
|
156
|
+
<zeebe:input source="=huskRetries" target="guardHuskRetries" />
|
|
157
|
+
<zeebe:input source="=prKey" target="guardPrKey" />
|
|
158
|
+
<zeebe:input source="=repo" target="guardRepo" />
|
|
159
|
+
<zeebe:input source="=prNumber" target="guardPrNumber" />
|
|
160
|
+
<zeebe:input source="=prUrl" target="guardPrUrl" />
|
|
161
|
+
<zeebe:input source="=reviewWaitTimeout" target="guardReviewWaitTimeout" />
|
|
162
|
+
<zeebe:input source="=humanApproval" target="guardHumanApproval" />
|
|
163
|
+
<zeebe:input source="=convergeOnly" target="guardConvergeOnly" />
|
|
164
|
+
<zeebe:input source="=reviewStale" target="guardReviewStale" />
|
|
165
|
+
<zeebe:input source="=roundEntryHead" target="guardRoundEntryHead" />
|
|
166
|
+
<zeebe:input source="=abandonUrl" target="guardAbandonUrl" />
|
|
167
|
+
<zeebe:input source="=abandonBrief" target="guardAbandonBrief" />
|
|
150
168
|
<zeebe:output source="=null" target="answer" />
|
|
151
169
|
<zeebe:output source="=false" target="scopePending" />
|
|
152
170
|
<zeebe:output source="=null" target="adversarialFindings" />
|
|
171
|
+
<zeebe:output source="=guardRound" target="round" />
|
|
172
|
+
<zeebe:output source="=guardMaxRounds" target="maxRounds" />
|
|
173
|
+
<zeebe:output source="=guardAdvPass" target="advPass" />
|
|
174
|
+
<zeebe:output source="=guardAdvMax" target="advMax" />
|
|
175
|
+
<zeebe:output source="=guardAckRetryRound" target="ackRetryRound" />
|
|
176
|
+
<zeebe:output source="=guardAckRetryMax" target="ackRetryMax" />
|
|
177
|
+
<zeebe:output source="=guardHuskRetries" target="huskRetries" />
|
|
178
|
+
<zeebe:output source="=guardPrKey" target="prKey" />
|
|
179
|
+
<zeebe:output source="=guardRepo" target="repo" />
|
|
180
|
+
<zeebe:output source="=guardPrNumber" target="prNumber" />
|
|
181
|
+
<zeebe:output source="=guardPrUrl" target="prUrl" />
|
|
182
|
+
<zeebe:output source="=guardReviewWaitTimeout" target="reviewWaitTimeout" />
|
|
183
|
+
<zeebe:output source="=(guardHumanApproval = true) or (humanApproval = true)" target="humanApproval" />
|
|
184
|
+
<zeebe:output source="=guardConvergeOnly" target="convergeOnly" />
|
|
185
|
+
<zeebe:output source="=guardReviewStale" target="reviewStale" />
|
|
186
|
+
<zeebe:output source="=guardRoundEntryHead" target="roundEntryHead" />
|
|
187
|
+
<zeebe:output source="=guardAbandonUrl" target="abandonUrl" />
|
|
188
|
+
<zeebe:output source="=guardAbandonBrief" target="abandonBrief" />
|
|
153
189
|
</zeebe:ioMapping>
|
|
154
190
|
</bpmn:extensionElements>
|
|
155
191
|
<bpmn:incoming>f_capture</bpmn:incoming>
|
|
@@ -205,11 +241,46 @@
|
|
|
205
241
|
</zeebe:properties>
|
|
206
242
|
<zeebe:ioMapping>
|
|
207
243
|
<zeebe:input source="=(if (abandonBrief = null) then "" else abandonBrief)" target="appendPrompt" />
|
|
244
|
+
<zeebe:input source="=round" target="guardRound" />
|
|
245
|
+
<zeebe:input source="=maxRounds" target="guardMaxRounds" />
|
|
246
|
+
<zeebe:input source="=advPass" target="guardAdvPass" />
|
|
247
|
+
<zeebe:input source="=advMax" target="guardAdvMax" />
|
|
248
|
+
<zeebe:input source="=ackRetryRound" target="guardAckRetryRound" />
|
|
249
|
+
<zeebe:input source="=ackRetryMax" target="guardAckRetryMax" />
|
|
250
|
+
<zeebe:input source="=huskRetries" target="guardHuskRetries" />
|
|
251
|
+
<zeebe:input source="=prKey" target="guardPrKey" />
|
|
252
|
+
<zeebe:input source="=repo" target="guardRepo" />
|
|
253
|
+
<zeebe:input source="=prNumber" target="guardPrNumber" />
|
|
254
|
+
<zeebe:input source="=prUrl" target="guardPrUrl" />
|
|
255
|
+
<zeebe:input source="=reviewWaitTimeout" target="guardReviewWaitTimeout" />
|
|
256
|
+
<zeebe:input source="=humanApproval" target="guardHumanApproval" />
|
|
257
|
+
<zeebe:input source="=convergeOnly" target="guardConvergeOnly" />
|
|
258
|
+
<zeebe:input source="=reviewStale" target="guardReviewStale" />
|
|
259
|
+
<zeebe:input source="=roundEntryHead" target="guardRoundEntryHead" />
|
|
260
|
+
<zeebe:input source="=abandonUrl" target="guardAbandonUrl" />
|
|
261
|
+
<zeebe:input source="=abandonBrief" target="guardAbandonBrief" />
|
|
208
262
|
<zeebe:output source="=adversarialFindings" target="adversarialFindings" />
|
|
209
|
-
<zeebe:output source="=(if
|
|
263
|
+
<zeebe:output source="=(if guardAdvPass = null then 0 else guardAdvPass) + 1" target="advPass" />
|
|
210
264
|
<zeebe:output source="=(if adversarialFindings != null and trim(adversarialFindings) != "" then null else status)" target="status" />
|
|
211
265
|
<zeebe:output source="=(if adversarialFindings != null and trim(adversarialFindings) != "" then null else summary)" target="summary" />
|
|
212
266
|
<zeebe:output source="=(if adversarialFindings != null and trim(adversarialFindings) != "" then null else question)" target="question" />
|
|
267
|
+
<zeebe:output source="=guardRound" target="round" />
|
|
268
|
+
<zeebe:output source="=guardMaxRounds" target="maxRounds" />
|
|
269
|
+
<zeebe:output source="=guardAdvMax" target="advMax" />
|
|
270
|
+
<zeebe:output source="=guardAckRetryRound" target="ackRetryRound" />
|
|
271
|
+
<zeebe:output source="=guardAckRetryMax" target="ackRetryMax" />
|
|
272
|
+
<zeebe:output source="=guardHuskRetries" target="huskRetries" />
|
|
273
|
+
<zeebe:output source="=guardPrKey" target="prKey" />
|
|
274
|
+
<zeebe:output source="=guardRepo" target="repo" />
|
|
275
|
+
<zeebe:output source="=guardPrNumber" target="prNumber" />
|
|
276
|
+
<zeebe:output source="=guardPrUrl" target="prUrl" />
|
|
277
|
+
<zeebe:output source="=guardReviewWaitTimeout" target="reviewWaitTimeout" />
|
|
278
|
+
<zeebe:output source="=(guardHumanApproval = true) or (humanApproval = true)" target="humanApproval" />
|
|
279
|
+
<zeebe:output source="=guardConvergeOnly" target="convergeOnly" />
|
|
280
|
+
<zeebe:output source="=guardReviewStale" target="reviewStale" />
|
|
281
|
+
<zeebe:output source="=guardRoundEntryHead" target="roundEntryHead" />
|
|
282
|
+
<zeebe:output source="=guardAbandonUrl" target="abandonUrl" />
|
|
283
|
+
<zeebe:output source="=guardAbandonBrief" target="abandonBrief" />
|
|
213
284
|
</zeebe:ioMapping>
|
|
214
285
|
</bpmn:extensionElements>
|
|
215
286
|
<bpmn:incoming>f_advDue</bpmn:incoming>
|
|
@@ -478,7 +549,43 @@
|
|
|
478
549
|
<zeebe:property name="io.nanobpm.dataEnvelope.out" value="PrScopeClassifyOut" />
|
|
479
550
|
</zeebe:properties>
|
|
480
551
|
<zeebe:ioMapping>
|
|
552
|
+
<zeebe:input source="=round" target="guardRound" />
|
|
553
|
+
<zeebe:input source="=maxRounds" target="guardMaxRounds" />
|
|
554
|
+
<zeebe:input source="=advPass" target="guardAdvPass" />
|
|
555
|
+
<zeebe:input source="=advMax" target="guardAdvMax" />
|
|
556
|
+
<zeebe:input source="=ackRetryRound" target="guardAckRetryRound" />
|
|
557
|
+
<zeebe:input source="=ackRetryMax" target="guardAckRetryMax" />
|
|
558
|
+
<zeebe:input source="=huskRetries" target="guardHuskRetries" />
|
|
559
|
+
<zeebe:input source="=prKey" target="guardPrKey" />
|
|
560
|
+
<zeebe:input source="=repo" target="guardRepo" />
|
|
561
|
+
<zeebe:input source="=prNumber" target="guardPrNumber" />
|
|
562
|
+
<zeebe:input source="=prUrl" target="guardPrUrl" />
|
|
563
|
+
<zeebe:input source="=reviewWaitTimeout" target="guardReviewWaitTimeout" />
|
|
564
|
+
<zeebe:input source="=humanApproval" target="guardHumanApproval" />
|
|
565
|
+
<zeebe:input source="=convergeOnly" target="guardConvergeOnly" />
|
|
566
|
+
<zeebe:input source="=reviewStale" target="guardReviewStale" />
|
|
567
|
+
<zeebe:input source="=roundEntryHead" target="guardRoundEntryHead" />
|
|
568
|
+
<zeebe:input source="=abandonUrl" target="guardAbandonUrl" />
|
|
569
|
+
<zeebe:input source="=abandonBrief" target="guardAbandonBrief" />
|
|
481
570
|
<zeebe:output source="=null" target="scopeAnswer" />
|
|
571
|
+
<zeebe:output source="=guardRound" target="round" />
|
|
572
|
+
<zeebe:output source="=guardMaxRounds" target="maxRounds" />
|
|
573
|
+
<zeebe:output source="=guardAdvPass" target="advPass" />
|
|
574
|
+
<zeebe:output source="=guardAdvMax" target="advMax" />
|
|
575
|
+
<zeebe:output source="=guardAckRetryRound" target="ackRetryRound" />
|
|
576
|
+
<zeebe:output source="=guardAckRetryMax" target="ackRetryMax" />
|
|
577
|
+
<zeebe:output source="=guardHuskRetries" target="huskRetries" />
|
|
578
|
+
<zeebe:output source="=guardPrKey" target="prKey" />
|
|
579
|
+
<zeebe:output source="=guardRepo" target="repo" />
|
|
580
|
+
<zeebe:output source="=guardPrNumber" target="prNumber" />
|
|
581
|
+
<zeebe:output source="=guardPrUrl" target="prUrl" />
|
|
582
|
+
<zeebe:output source="=guardReviewWaitTimeout" target="reviewWaitTimeout" />
|
|
583
|
+
<zeebe:output source="=(guardHumanApproval = true) or (humanApproval = true)" target="humanApproval" />
|
|
584
|
+
<zeebe:output source="=guardConvergeOnly" target="convergeOnly" />
|
|
585
|
+
<zeebe:output source="=guardReviewStale" target="reviewStale" />
|
|
586
|
+
<zeebe:output source="=guardRoundEntryHead" target="roundEntryHead" />
|
|
587
|
+
<zeebe:output source="=guardAbandonUrl" target="abandonUrl" />
|
|
588
|
+
<zeebe:output source="=guardAbandonBrief" target="abandonBrief" />
|
|
482
589
|
</zeebe:ioMapping>
|
|
483
590
|
</bpmn:extensionElements>
|
|
484
591
|
<bpmn:incoming>f_convergeOk</bpmn:incoming>
|