@nanobpm/nano-workforce 0.131.0 → 0.131.1
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/CHANGELOG.md +6 -0
- package/app/deliveryGraphCompiler.test.ts +47 -0
- package/app/deliveryGraphCompiler.ts +66 -8
- package/app/deliveryRunner.test.ts +35 -1
- package/app/deliveryRunner.ts +13 -3
- package/package.json +1 -1
- package/resources/forms/delivery-human-generic.form +24 -0
- package/resources/processes/delivery-human.bpmn +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.131.1](https://github.com/nanobpm/nano-workforce/compare/v0.131.0...v0.131.1) (2026-08-24)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* **delivery-graph:** seed human-task prompt/context so its form is not contextless ([#502](https://github.com/nanobpm/nano-workforce/issues/502)) ([d2991ea](https://github.com/nanobpm/nano-workforce/commit/d2991ea0ec54e36711c27f58b8901ca85b7179e6)), closes [#499](https://github.com/nanobpm/nano-workforce/issues/499) [#499](https://github.com/nanobpm/nano-workforce/issues/499)
|
|
6
|
+
|
|
1
7
|
## [0.131.0](https://github.com/nanobpm/nano-workforce/compare/v0.130.0...v0.131.0) (2026-08-23)
|
|
2
8
|
|
|
3
9
|
### Features
|
|
@@ -29,6 +29,17 @@ async function compileFail(graph: unknown) {
|
|
|
29
29
|
return r.errors;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** The sub-process element id of the PLANNED human user task in a compiled graph — i.e. the
|
|
33
|
+
* delivery-human-task element that is NOT a bounded node's __esc timeout twin. Returns "" if none. */
|
|
34
|
+
function humanTaskSubEl(bpmn: string): string {
|
|
35
|
+
const parts = bpmn.split('<bpmn:userTask id="delivery-human-task__');
|
|
36
|
+
for (let k = 1; k < parts.length; k++) {
|
|
37
|
+
const id = parts[k].slice(0, parts[k].indexOf('"'));
|
|
38
|
+
if (!id.endsWith("__esc")) return id;
|
|
39
|
+
}
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
// The ADR's motivating case: an agent merges PR #B, a `pr` wait node watches it merge and emits
|
|
33
44
|
// `mergedSha`, a human does the manual OTP publish emitting `resolvedArtifact`, and a connector
|
|
34
45
|
// consumes the published artifact.
|
|
@@ -114,6 +125,42 @@ test("late-binding: a fact-qualified edge threads a boundFacts input into the co
|
|
|
114
125
|
assert(boundInput, `boundFacts is a single-quoted FEEL list literal, got: ${r.bpmn.match(/source='[^']*' target="boundFacts"/)?.[0] ?? r.bpmn.match(/source="[^"]*" target="boundFacts"/)?.[0]}`);
|
|
115
126
|
});
|
|
116
127
|
|
|
128
|
+
test("#499 human context: the human user-task seeds prompt/nodeId/emit context so its generic form is not contextless", async () => {
|
|
129
|
+
const r = await compileOk(RELEASE_RUNBOOK);
|
|
130
|
+
// The human node's subProcess ioMapping must thread the authored prompt + node identity + emit
|
|
131
|
+
// context from `nodeInputs.<el>` onto the user task (the form reads them). A dropped prompt input is
|
|
132
|
+
// exactly the contextless-form bug (#499). Locate the PLANNED human task (not an `__esc` twin).
|
|
133
|
+
const subEl = humanTaskSubEl(r.bpmn);
|
|
134
|
+
assert(subEl !== "", "the graph inlines a planned human user task");
|
|
135
|
+
assert(r.bpmn.includes(`source="=nodeInputs.${subEl}.prompt" target="prompt"`), "the human task seeds its authored prompt");
|
|
136
|
+
assert(r.bpmn.includes(`source="=nodeInputs.${subEl}.nodeId" target="nodeId"`), "the human task seeds its node identity");
|
|
137
|
+
// The emit label/mode are DERIVED in FEEL from the seeded emits list (single source of truth) — a
|
|
138
|
+
// single-quoted attribute so the literal quotes survive the engine deploy path.
|
|
139
|
+
assert(
|
|
140
|
+
r.bpmn.includes(`source='=if count(nodeInputs.${subEl}.emits) = 0 then "none" else "typed"' target="emitMode"`),
|
|
141
|
+
"emitMode is derived from the emits count",
|
|
142
|
+
);
|
|
143
|
+
assert(
|
|
144
|
+
r.bpmn.includes(`for _e in nodeInputs.${subEl}.emits return _e.name`) && r.bpmn.includes('target="emitLabel"'),
|
|
145
|
+
"emitLabel is derived from the emits list",
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("#499 escalation context: an agent-node timeout escalation seeds a context line naming the node, its job type, and the elapsed SLA", async () => {
|
|
150
|
+
const r = await compileOk(RELEASE_RUNBOOK);
|
|
151
|
+
// The `__esc` timeout-escalation user task previously carried NO ioMapping — a blank form that never
|
|
152
|
+
// said a timeout occurred, on which node. It must now seed a `prompt` context line from a compile-time
|
|
153
|
+
// literal (node id + job type) concatenated with the runtime elapsed SLA (`nodeTimeout`).
|
|
154
|
+
const start = r.bpmn.indexOf('<bpmn:userTask id="delivery-human-task__n1__esc"');
|
|
155
|
+
assert(start !== -1, "a bounded node inlines an escalation user task");
|
|
156
|
+
const escBlock = r.bpmn.slice(start, r.bpmn.indexOf("</bpmn:userTask>", start));
|
|
157
|
+
assert(escBlock.includes('target="prompt"'), "the escalation task seeds a prompt context line");
|
|
158
|
+
assert(escBlock.includes("Node open-b (senior:feature) exceeded its SLA ("), "the context names the node and its job type");
|
|
159
|
+
assert(escBlock.includes("string(nodeTimeout)"), "the context reports the elapsed SLA at runtime");
|
|
160
|
+
assert(escBlock.includes('="none"') && escBlock.includes('target="emitMode"'), "the escalation labels its emit field N/A so the generic form hides the inert value input");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
|
|
117
164
|
test("rejects unknown kind (by construction) with a path-qualified error, nothing compiled", async () => {
|
|
118
165
|
const errors = await compileFail({
|
|
119
166
|
nodes: [{ id: "x", kind: "deploy", deploy: { target: "prod" } }],
|
|
@@ -823,6 +823,16 @@ function ioMappingLines(w: NodeWiring, boundInputs: readonly BoundInput[]): stri
|
|
|
823
823
|
case "human":
|
|
824
824
|
inputs.push({ source: cfg("escalationSlaTimeout"), target: "escalationSlaTimeout" });
|
|
825
825
|
inputs.push({ source: cfg("escalationAssignee"), target: "escalationAssignee" });
|
|
826
|
+
// Seed the authored instruction + node identity + emit context so the generic human form renders
|
|
827
|
+
// "now do X", names the parked node, and labels/hides its emit field (issue #499). `emits` is the
|
|
828
|
+
// single source of truth; the emit label/mode are derived from it in FEEL here (no duplicate seed).
|
|
829
|
+
inputs.push({ source: cfg("prompt"), target: "prompt" });
|
|
830
|
+
inputs.push({ source: cfg("nodeId"), target: "nodeId" });
|
|
831
|
+
inputs.push({ source: `=if count(${cfg("emits").slice(1)}) = 0 then "none" else "typed"`, target: "emitMode" });
|
|
832
|
+
inputs.push({
|
|
833
|
+
source: `=string join(for _e in ${cfg("emits").slice(1)} return _e.name + " (" + _e.type + ")", ", ")`,
|
|
834
|
+
target: "emitLabel",
|
|
835
|
+
});
|
|
826
836
|
break;
|
|
827
837
|
case "connector":
|
|
828
838
|
inputs.push({ source: cfg("target"), target: "target" });
|
|
@@ -868,9 +878,9 @@ function innerBodyLines(w: NodeWiring): string[] {
|
|
|
868
878
|
const node = w.node;
|
|
869
879
|
switch (node.kind) {
|
|
870
880
|
case "agent":
|
|
871
|
-
return serviceBodyLines(el, node.id, attr("type", node.agent.jobType), []);
|
|
881
|
+
return serviceBodyLines(el, node.id, attr("type", node.agent.jobType), [], node.agent.jobType);
|
|
872
882
|
case "connector":
|
|
873
|
-
return serviceBodyLines(el, node.id, `type="${DELEGATE_TASK_TYPE.connector}"`, []);
|
|
883
|
+
return serviceBodyLines(el, node.id, `type="${DELEGATE_TASK_TYPE.connector}"`, [], `connector → ${node.connector.target}`);
|
|
874
884
|
case "wait":
|
|
875
885
|
return waitBodyLines(el, node.id);
|
|
876
886
|
case "human":
|
|
@@ -882,8 +892,15 @@ function innerBodyLines(w: NodeWiring): string[] {
|
|
|
882
892
|
|
|
883
893
|
/** `agent`/`connector` body: `start → serviceTask → end`, with a bounded `=nodeTimeout` boundary that
|
|
884
894
|
* escalates the stalled node onto a human-completable user task. `taskDefAttr` is the pre-rendered
|
|
885
|
-
* `type="…"` attribute; `taskProps` are optional `<zeebe:property>` envelope lines
|
|
886
|
-
|
|
895
|
+
* `type="…"` attribute; `taskProps` are optional `<zeebe:property>` envelope lines; `descriptor`
|
|
896
|
+
* names the stalled work (job type / connector target) for the escalation task's context line (#499). */
|
|
897
|
+
function serviceBodyLines(
|
|
898
|
+
el: string,
|
|
899
|
+
nodeId: string,
|
|
900
|
+
taskDefAttr: string,
|
|
901
|
+
taskProps: readonly string[],
|
|
902
|
+
descriptor: string,
|
|
903
|
+
): string[] {
|
|
887
904
|
const esc = escalationTaskElement(el);
|
|
888
905
|
const taskExt =
|
|
889
906
|
taskProps.length > 0
|
|
@@ -911,7 +928,18 @@ function serviceBodyLines(el: string, nodeId: string, taskDefAttr: string, taskP
|
|
|
911
928
|
` <bpmn:outgoing>${el}_i2</bpmn:outgoing>`,
|
|
912
929
|
` <bpmn:timerEventDefinition id="${el}_ted"><bpmn:timeDuration xsi:type="bpmn:tFormalExpression">=nodeTimeout</bpmn:timeDuration></bpmn:timerEventDefinition>`,
|
|
913
930
|
" </bpmn:boundaryEvent>",
|
|
914
|
-
...escalationTaskLines(
|
|
931
|
+
...escalationTaskLines(
|
|
932
|
+
esc,
|
|
933
|
+
nodeId,
|
|
934
|
+
[`${el}_i2`],
|
|
935
|
+
`${el}_i3`,
|
|
936
|
+
escalationContextFeel(
|
|
937
|
+
nodeId,
|
|
938
|
+
descriptor,
|
|
939
|
+
"nodeTimeout",
|
|
940
|
+
"; in-flight work may already exist — check for a draft PR or partial state before retrying or reassigning.",
|
|
941
|
+
),
|
|
942
|
+
),
|
|
915
943
|
` <bpmn:endEvent id="${el}_end"><bpmn:incoming>${el}_i1</bpmn:incoming><bpmn:incoming>${el}_i3</bpmn:incoming></bpmn:endEvent>`,
|
|
916
944
|
flow(`${el}_i0`, `${el}_start`, `${el}_task`),
|
|
917
945
|
flow(`${el}_i1`, `${el}_task`, `${el}_end`),
|
|
@@ -991,7 +1019,13 @@ function waitBodyLines(el: string, nodeId: string): string[] {
|
|
|
991
1019
|
` <bpmn:outgoing>${el}_i7</bpmn:outgoing>`,
|
|
992
1020
|
` <bpmn:outgoing>${el}_i4</bpmn:outgoing>`,
|
|
993
1021
|
" </bpmn:exclusiveGateway>",
|
|
994
|
-
...escalationTaskLines(
|
|
1022
|
+
...escalationTaskLines(
|
|
1023
|
+
esc,
|
|
1024
|
+
nodeId,
|
|
1025
|
+
[`${el}_i4`],
|
|
1026
|
+
`${el}_i5`,
|
|
1027
|
+
escalationContextFeel(nodeId, "readiness gate", "probeTimeout", " before its ReadinessProbe went green — decide how to proceed."),
|
|
1028
|
+
),
|
|
995
1029
|
` <bpmn:endEvent id="${el}_end"><bpmn:incoming>${el}_i1</bpmn:incoming><bpmn:incoming>${el}_i5</bpmn:incoming><bpmn:incoming>${el}_i7</bpmn:incoming></bpmn:endEvent>`,
|
|
996
1030
|
flow(`${el}_i0`, `${el}_start`, `${el}_probeLoop`),
|
|
997
1031
|
flow(`${el}_i1`, `${el}_probeLoop`, `${el}_end`),
|
|
@@ -1047,14 +1081,29 @@ function humanBodyLines(el: string, nodeId: string): string[] {
|
|
|
1047
1081
|
}
|
|
1048
1082
|
|
|
1049
1083
|
/** A bounded node's escalation user task — a human-completable stop (`isDeliveryHumanElement`
|
|
1050
|
-
* convention) that a human OR an agent (ADR 0046) answers to unstick a stalled node.
|
|
1051
|
-
|
|
1084
|
+
* convention) that a human OR an agent (ADR 0046) answers to unstick a stalled node. `contextFeel` is
|
|
1085
|
+
* a FEEL expression yielding the context line seeded onto the generic form's read-only prompt field
|
|
1086
|
+
* (issue #499) — e.g. "Node n1 (senior:feature) exceeded its SLA (PT30M); …" — so the operator can see
|
|
1087
|
+
* WHICH node timed out and that in-flight work may already exist, instead of a blank form. The emit
|
|
1088
|
+
* field is labelled "none" so the generic form hides its (inert on an escalation) typed-value input. */
|
|
1089
|
+
function escalationTaskLines(
|
|
1090
|
+
esc: string,
|
|
1091
|
+
nodeId: string,
|
|
1092
|
+
incoming: readonly string[],
|
|
1093
|
+
outgoing: string,
|
|
1094
|
+
contextFeel: string,
|
|
1095
|
+
): string[] {
|
|
1052
1096
|
return [
|
|
1053
1097
|
` <bpmn:userTask id="${esc}" name="Escalate: ${escapeXml(nodeId)}">`,
|
|
1054
1098
|
" <bpmn:extensionElements>",
|
|
1055
1099
|
` <zeebe:formDefinition formId="${GENERIC_HUMAN_FORM}" />`,
|
|
1056
1100
|
" <zeebe:userTask />",
|
|
1057
1101
|
' <zeebe:assignmentDefinition candidateGroups="operators" />',
|
|
1102
|
+
" <zeebe:ioMapping>",
|
|
1103
|
+
` <zeebe:input ${attr("source", contextFeel)} target="prompt" />`,
|
|
1104
|
+
` <zeebe:input ${attr("source", `=${feelStr(nodeId)}`)} target="nodeId" />`,
|
|
1105
|
+
` <zeebe:input ${attr("source", '="none"')} target="emitMode" />`,
|
|
1106
|
+
" </zeebe:ioMapping>",
|
|
1058
1107
|
" </bpmn:extensionElements>",
|
|
1059
1108
|
...incoming.map((id) => ` <bpmn:incoming>${id}</bpmn:incoming>`),
|
|
1060
1109
|
` <bpmn:outgoing>${outgoing}</bpmn:outgoing>`,
|
|
@@ -1062,6 +1111,15 @@ function escalationTaskLines(esc: string, nodeId: string, incoming: readonly str
|
|
|
1062
1111
|
];
|
|
1063
1112
|
}
|
|
1064
1113
|
|
|
1114
|
+
/** Build the FEEL context line seeded onto an escalation task's read-only prompt field (issue #499).
|
|
1115
|
+
* The node id + descriptor (job type / connector target / "readiness gate") are baked as compile-time
|
|
1116
|
+
* literals; the elapsed SLA is read from the node body's runtime `timeoutVar` (`nodeTimeout` for a
|
|
1117
|
+
* bounded service node, `probeTimeout` for a `wait` gate). `tail` closes the sentence per kind. */
|
|
1118
|
+
function escalationContextFeel(nodeId: string, descriptor: string, timeoutVar: string, tail: string): string {
|
|
1119
|
+
const head = feelStr(`Node ${nodeId} (${descriptor}) exceeded its SLA (`);
|
|
1120
|
+
return `=${head} + string(${timeoutVar}) + ${feelStr(`)${tail}`)}`;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1065
1123
|
/** A plain `<bpmn:sequenceFlow>` (6-space indented). */
|
|
1066
1124
|
function flow(id: string, source: string, target: string): string {
|
|
1067
1125
|
return ` <bpmn:sequenceFlow id="${id}" sourceRef="${source}" targetRef="${target}" />`;
|
|
@@ -79,12 +79,46 @@ test("nodeInputs seeds the exact per-kind fields each node's subProcess ioMappin
|
|
|
79
79
|
assert(wait?.probe && typeof wait.probe === "object", "the wait node carries its ReadinessProbe descriptor");
|
|
80
80
|
|
|
81
81
|
const human = byField((v) => "escalationSlaTimeout" in v);
|
|
82
|
-
assertEquals(human, {
|
|
82
|
+
assertEquals(human, {
|
|
83
|
+
escalationSlaTimeout: "PT2H",
|
|
84
|
+
escalationAssignee: "alice",
|
|
85
|
+
// #499: the human node seeds its authored prompt, node identity, and declared emits so the
|
|
86
|
+
// generic user-task form renders "now do X", names the parked node, and labels its emit field.
|
|
87
|
+
prompt: "run the manual OTP publish",
|
|
88
|
+
nodeId: "publish",
|
|
89
|
+
emits: [{ name: "resolvedArtifact", type: "artifact" }],
|
|
90
|
+
});
|
|
83
91
|
|
|
84
92
|
const connector = byField((v) => v.target === "npm:install");
|
|
85
93
|
assertEquals(connector, { target: "npm:install", dedupeKey: "consume-1", payload: null, timeout: "PT10M" });
|
|
86
94
|
});
|
|
87
95
|
|
|
96
|
+
test("the human node seeds prompt/nodeId/emits; a click-done (no-emit, no-prompt) node seeds empty defaults", async () => {
|
|
97
|
+
// #499: the compiled human user-task's form reads `prompt`/`nodeId`/`emits` from `nodeInputs.<el>`;
|
|
98
|
+
// a discarded prompt is the contextless-form bug. Pin both an emit-declaring node and the degenerate
|
|
99
|
+
// click-done node (no `human` config, no `emits`) so the seed never regresses to null/undefined.
|
|
100
|
+
const graph: DeliveryGraph = {
|
|
101
|
+
nodes: [
|
|
102
|
+
{ id: "publish", kind: "human", human: { prompt: "run the manual OTP publish" }, emits: [{ name: "resolvedArtifact", type: "artifact" }] },
|
|
103
|
+
{ id: "ack", kind: "human" },
|
|
104
|
+
],
|
|
105
|
+
edges: [{ from: "publish.resolvedArtifact", to: "ack" }],
|
|
106
|
+
};
|
|
107
|
+
const p = await prepareOk(graph);
|
|
108
|
+
const humans = Object.values(p.nodeInputs).filter((v) => "escalationSlaTimeout" in v) as Array<Record<string, unknown>>;
|
|
109
|
+
const publish = humans.find((v) => v.nodeId === "publish");
|
|
110
|
+
const ack = humans.find((v) => v.nodeId === "ack");
|
|
111
|
+
|
|
112
|
+
assertEquals(publish?.prompt, "run the manual OTP publish");
|
|
113
|
+
assertEquals(publish?.emits, [{ name: "resolvedArtifact", type: "artifact" }]);
|
|
114
|
+
|
|
115
|
+
// The click-done node carries a defined-but-empty prompt and an empty emits list (never undefined),
|
|
116
|
+
// so the form seeds a blank instruction and hides its emit field rather than seeding null.
|
|
117
|
+
assertEquals(ack?.prompt, "");
|
|
118
|
+
assertEquals(ack?.emits, []);
|
|
119
|
+
assertEquals(ack?.nodeId, "ack");
|
|
120
|
+
});
|
|
121
|
+
|
|
88
122
|
test("wait gateKeys default to a fresh per-run token so concurrent runs of one graph never cross-correlate", async () => {
|
|
89
123
|
const gateKeyOf = (p: Awaited<ReturnType<typeof prepareOk>>) =>
|
|
90
124
|
(Object.values(p.nodeInputs).find((v) => "gateKey" in v) as { gateKey?: string } | undefined)?.gateKey;
|
package/app/deliveryRunner.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { createHash, randomUUID } from "node:crypto";
|
|
17
17
|
import type { EngineClient } from "@nanobpm/urban";
|
|
18
|
-
import type { DeliveryGraph, DeliveryNode } from "../nano-generated/api-io.d.ts";
|
|
18
|
+
import type { DeliveryFact, DeliveryGraph, DeliveryNode } from "../nano-generated/api-io.d.ts";
|
|
19
19
|
import { assertNever, compileDeliveryGraph, DELIVERY_GRAPH_PROCESS_ID } from "./deliveryGraphCompiler.ts";
|
|
20
20
|
import { DEFAULT_EVERY_MS, msToIsoDuration, parseProbe, readinessPollEvery } from "./readiness.ts";
|
|
21
21
|
|
|
@@ -65,7 +65,7 @@ const DEFAULTS: Required<Omit<DeliveryRunTimeouts, "escalationAssignee">> = {
|
|
|
65
65
|
type NodeInput =
|
|
66
66
|
| { jobType: string; appendPrompt: string; timeout: string }
|
|
67
67
|
| { gateKey: string; probe: unknown; probeTimeout: string; probePollEvery: string }
|
|
68
|
-
| { escalationSlaTimeout: string; escalationAssignee: string | null }
|
|
68
|
+
| { escalationSlaTimeout: string; escalationAssignee: string | null; prompt: string; nodeId: string; emits: DeliveryFact[] }
|
|
69
69
|
| { target: string; dedupeKey: string | null; payload: Record<string, unknown> | null; timeout: string };
|
|
70
70
|
|
|
71
71
|
/** The result of compiling + preparing a graph for deployment: the content-addressed process id, the
|
|
@@ -185,7 +185,17 @@ function buildNodeInput(
|
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
187
|
case "human":
|
|
188
|
-
return {
|
|
188
|
+
return {
|
|
189
|
+
escalationSlaTimeout: ctx.escalationSlaTimeout,
|
|
190
|
+
escalationAssignee: ctx.escalationAssignee,
|
|
191
|
+
// Seed the authored instruction, node identity, and declared emits so the human user-task's
|
|
192
|
+
// form can render its "now do X" prompt, name the parked node, and label/hide its emit field
|
|
193
|
+
// (issue #499 — the generic form otherwise renders contextless). `emits` stays the single
|
|
194
|
+
// source of truth: the compiled ioMapping derives the emit label/mode from it in FEEL.
|
|
195
|
+
prompt: node.human?.prompt ?? "",
|
|
196
|
+
nodeId: node.id ?? ctx.element,
|
|
197
|
+
emits: Array.isArray(node.emits) ? node.emits.map((f) => ({ ...f })) : [],
|
|
198
|
+
};
|
|
189
199
|
case "connector":
|
|
190
200
|
return {
|
|
191
201
|
target: node.connector.target,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.131.
|
|
3
|
+
"version": "0.131.1",
|
|
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",
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
"schemaVersion": 18,
|
|
4
4
|
"type": "default",
|
|
5
5
|
"components": [
|
|
6
|
+
{
|
|
7
|
+
"type": "text",
|
|
8
|
+
"text": "### Delivery graph — node `{{nodeId}}`",
|
|
9
|
+
"conditional": {
|
|
10
|
+
"hide": "=(nodeId = null) or (nodeId = \"\")"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
6
13
|
{
|
|
7
14
|
"type": "textarea",
|
|
8
15
|
"key": "prompt",
|
|
@@ -10,15 +17,32 @@
|
|
|
10
17
|
"description": "The scheduled human step this delivery graph is waiting on.",
|
|
11
18
|
"readonly": true
|
|
12
19
|
},
|
|
20
|
+
{
|
|
21
|
+
"type": "text",
|
|
22
|
+
"text": "**Emits:** {{emitLabel}} — enter its typed value below (validated against the node's declared fact).",
|
|
23
|
+
"conditional": {
|
|
24
|
+
"hide": "=emitMode != \"typed\""
|
|
25
|
+
}
|
|
26
|
+
},
|
|
13
27
|
{
|
|
14
28
|
"type": "textfield",
|
|
15
29
|
"key": "value",
|
|
16
30
|
"label": "Emitted value",
|
|
17
31
|
"description": "The typed value this step hands forward to its downstream dependents. Validated against the node's declared emitted fact.",
|
|
32
|
+
"conditional": {
|
|
33
|
+
"hide": "=emitMode != \"typed\""
|
|
34
|
+
},
|
|
18
35
|
"validate": {
|
|
19
36
|
"required": true
|
|
20
37
|
}
|
|
21
38
|
},
|
|
39
|
+
{
|
|
40
|
+
"type": "text",
|
|
41
|
+
"text": "_This step emits no typed fact (N/A) — just complete it to unblock its dependents._",
|
|
42
|
+
"conditional": {
|
|
43
|
+
"hide": "=emitMode = \"typed\""
|
|
44
|
+
}
|
|
45
|
+
},
|
|
22
46
|
{
|
|
23
47
|
"type": "textarea",
|
|
24
48
|
"key": "note",
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
<zeebe:userTask />
|
|
11
11
|
<zeebe:assignmentDefinition candidateGroups="operators" assignee="=if (is defined(escalationAssignee) and escalationAssignee != null and trim(string(escalationAssignee)) != "") then escalationAssignee else null" />
|
|
12
12
|
<zeebe:ioMapping>
|
|
13
|
+
<zeebe:input source="=if (is defined(prompt)) then prompt else null" target="prompt" />
|
|
14
|
+
<zeebe:input source="=if (is defined(nodeId)) then nodeId else null" target="nodeId" />
|
|
15
|
+
<zeebe:input source="=if (is defined(emits) and count(emits) != 0) then "typed" else "none"" target="emitMode" />
|
|
16
|
+
<zeebe:input source="=if (is defined(emits)) then string join(for _e in emits return _e.name + " (" + _e.type + ")", ", ") else """ target="emitLabel" />
|
|
13
17
|
<zeebe:output source="="completed"" target="humanOutcome" />
|
|
14
18
|
<zeebe:output source="=if (is defined(value)) then value else null" target="humanEmitValue" />
|
|
15
19
|
<zeebe:output source="=if (is defined(resolvedArtifact)) then resolvedArtifact else null" target="humanEmitArtifact" />
|