@nanobpm/nano-workforce 0.64.0 → 0.66.0

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.
@@ -22,6 +22,7 @@ import { fileURLToPath } from "node:url";
22
22
  import type { EngineJob } from "@nanobpm/urban/runtime";
23
23
  import { bootTestApp, type TestApp } from "@nanobpm/urban-testkit";
24
24
  import { admitGithubState, installAdmitGithub } from "./support/github-admit.ts";
25
+ import { pollFeatureEscalations } from "../app/service.ts";
25
26
 
26
27
  const APP_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
27
28
 
@@ -56,6 +57,8 @@ interface FeatureRow {
56
57
  status: string;
57
58
  pr_key: string | null;
58
59
  delivery_label: string | null;
60
+ escalation_question: string | null;
61
+ escalation_user_task_key: string | null;
59
62
  }
60
63
  interface PrRow {
61
64
  pr_key: string;
@@ -253,4 +256,59 @@ describe("single-issue feature run (#172 — feature.bpmn)", () => {
253
256
  },
254
257
  );
255
258
  });
259
+
260
+ test("escalate → the poller surfaces it on the read model, and the operator answer resolves it (issue #210)", async () => {
261
+ let calls = 0;
262
+ await withApp(
263
+ {
264
+ "senior:feature": () => {
265
+ calls += 1;
266
+ return calls === 1
267
+ ? { status: "escalated", question: "Which API should I use?", summary: "parked for a human" }
268
+ : { status: "opened", pr: "owner/repo#210", summary: "resumed and opened" };
269
+ },
270
+ },
271
+ { baseBranch: "epic/e2e" },
272
+ async ({ app, featureKey }) => {
273
+ // The `record-feature-escalation` service task runs on the escalated arm (before the user task),
274
+ // so the row already carries the flipped status + the agent's question when the run parks — the
275
+ // read model the pages read is no longer blind to the native user-task wait (the #210 bug).
276
+ const parked = await featureRow(app, featureKey);
277
+ assert.equal(parked.status, "escalated", "the escalated status is surfaced on the read model");
278
+ assert.equal(parked.escalation_question, "Which API should I use?", "the agent's question is surfaced");
279
+
280
+ // The poller fills in the completable user-task key (which the service task can't know — the
281
+ // task doesn't exist yet when it runs) so the UI can drive an attributed answer.
282
+ await pollFeatureEscalations(app.db, app.engine);
283
+ const escalated = await featureRow(app, featureKey);
284
+ assert.ok(escalated.escalation_user_task_key, "the poller denormalised the completable user-task key");
285
+ assert.equal(escalated.status, "escalated", "the run stays escalated while parked");
286
+
287
+ // Answer through the app's OWN operation (the nwf UI's answer affordance) — the attributed
288
+ // completer resumes the SAME implement task a human would from the task inbox.
289
+ const answered = await app.api?.call("answerFeatureEscalation", {
290
+ body: { userTaskKey: escalated.escalation_user_task_key, resolution: "answer", answer: "use v2" },
291
+ });
292
+ assert.equal(answered?.status, 200, "the operator answer completed the escalation task");
293
+ await app.settle();
294
+
295
+ const flows = takenFlows(app);
296
+ assert.ok(
297
+ flows.includes("w_gw_answer->implement-task"),
298
+ `the answer re-dispatched the same implement task (flows: ${flows.join(", ")})`,
299
+ );
300
+ assert.equal(calls, 2, "the implementation agent was re-dispatched exactly once after the answer");
301
+
302
+ // The run opened its PR; the escalation pointer + question were cleared once resolved.
303
+ const settled = await featureRow(app, featureKey);
304
+ assert.equal(settled.status, "opened", "the resumed run opened its PR");
305
+ assert.equal(settled.escalation_user_task_key, null, "the escalation pointer was cleared once resolved");
306
+ assert.equal(settled.escalation_question, null, "the surfaced question was cleared once resolved");
307
+
308
+ // A further poll pass is an idempotent no-op — a terminal run is not a candidate.
309
+ await pollFeatureEscalations(app.db, app.engine);
310
+ assert.equal((await featureRow(app, featureKey)).status, "opened");
311
+ },
312
+ );
313
+ });
256
314
  });
package/nano.app.json CHANGED
@@ -65,7 +65,8 @@
65
65
  "keyField": "process_key",
66
66
  "statusField": "status",
67
67
  "activeStatuses": [
68
- "running"
68
+ "running",
69
+ "escalated"
69
70
  ],
70
71
  "onTerminated": {
71
72
  "set": {
@@ -132,6 +133,10 @@
132
133
  "taskType": "pr.record-feature",
133
134
  "handler": "workers/record-feature/worker.ts"
134
135
  },
136
+ {
137
+ "taskType": "pr.record-feature-escalation",
138
+ "handler": "workers/record-feature-escalation/worker.ts"
139
+ },
135
140
  {
136
141
  "taskType": "pr.record-blocked-ack",
137
142
  "handler": "workers/record-blocked-ack/worker.ts"
package/openapi.yaml CHANGED
@@ -925,6 +925,58 @@ paths:
925
925
  application/json:
926
926
  schema:
927
927
  $ref: "#/components/schemas/MessageResult"
928
+ /actions/answer-escalation:
929
+ post:
930
+ operationId: answerFeatureEscalation
931
+ summary: "Answer a native feature-run escalation (issue #210). Completes the parked
932
+ `feature-escalation` user task with the operator's typed form variables (resolution=answer/abandon
933
+ + guidance), driving the canonical attributed completer (completeUserTaskAttributed) — the same
934
+ resume path the task inbox uses, recording who answered. The poller then reconciles the run off
935
+ `escalated`. This is the nwf UI's answer affordance for a feature run parked on a human decision."
936
+ requestBody:
937
+ required: true
938
+ content:
939
+ application/json:
940
+ schema:
941
+ type: object
942
+ additionalProperties: false
943
+ required:
944
+ - userTaskKey
945
+ - resolution
946
+ properties:
947
+ userTaskKey:
948
+ type: string
949
+ minLength: 1
950
+ description: The parked `feature-escalation` user-task key (feature_runs.escalation_user_task_key).
951
+ resolution:
952
+ type: string
953
+ enum: [answer, abandon]
954
+ description: answer ⇒ re-dispatch the implement task with the guidance; abandon ⇒ give up on the task.
955
+ answer:
956
+ type: string
957
+ description: Guidance for the implementation agent. Required (non-blank) when resolution is answer.
958
+ operator:
959
+ type: string
960
+ description: Optional operator handle recorded in the attribution ledger; defaults to "operator".
961
+ responses:
962
+ "200":
963
+ description: The escalation user task was completed and the process resumed.
964
+ content:
965
+ application/json:
966
+ schema:
967
+ $ref: "#/components/schemas/MessageResult"
968
+ "400":
969
+ description: A required field was missing/invalid, or the target is not an escalation task.
970
+ content:
971
+ application/json:
972
+ schema:
973
+ $ref: "#/components/schemas/MessageResult"
974
+ "404":
975
+ description: No open escalation user task matches the userTaskKey.
976
+ content:
977
+ application/json:
978
+ schema:
979
+ $ref: "#/components/schemas/MessageResult"
928
980
  /hooks/agent-complete:
929
981
  post:
930
982
  operationId: agentCompleteEscalation
@@ -0,0 +1,68 @@
1
+ // POST /app/api/actions/answer-escalation → operationId `answerFeatureEscalation` (issue #210).
2
+ // The nwf UI's answer affordance for a native feature-run escalation: an operator answers the parked
3
+ // `feature-escalation` user task (resolution=answer/abandon + optional guidance) directly from the
4
+ // Feature / Overview pages, instead of only from the generic task inbox.
5
+ //
6
+ // It routes through the ONE canonical attributed completer (`completeEscalationAsHuman` →
7
+ // `completeUserTaskAttributed`), so the completion uses the exact same typed `.form` variables and
8
+ // engine resume path a human drives from the task inbox — no parallel completion — while recording
9
+ // WHO answered in the `task_completions` ledger. The poller then reconciles the run off `escalated`
10
+ // (pollFeatureEscalations) once the task is gone.
11
+ //
12
+ // The runtime validates the body against openapi.yaml (`userTaskKey` + `resolution` required); this
13
+ // delegate narrows the validated shape, enforces the answer/abandon contract, and builds the typed
14
+ // completion variables the `feature-escalation` form + the `w_gw_answer` gateway expect.
15
+
16
+ import { completeEscalationAsHuman } from "../app/agentCompletion.ts";
17
+ import { featureRuns } from "../app/feature.ts";
18
+ import { defineOperation } from "../nano-generated/operations.ts";
19
+
20
+ const str = (v: unknown): string => (typeof v === "string" ? v.trim() : "");
21
+
22
+ export default defineOperation("answerFeatureEscalation", async ({ body }, app) => {
23
+ if (!body || typeof body !== "object") {
24
+ app.log.warn("answer-escalation rejected: missing request body");
25
+ return { status: 400, body: { ok: false, error: "userTaskKey and resolution are required" } };
26
+ }
27
+
28
+ const userTaskKey = str(body.userTaskKey);
29
+ if (!userTaskKey) return { status: 400, body: { ok: false, error: "userTaskKey is required" } };
30
+
31
+ const resolution = str(body.resolution);
32
+ if (resolution !== "answer" && resolution !== "abandon") {
33
+ return { status: 400, body: { ok: false, error: "resolution must be 'answer' or 'abandon'" } };
34
+ }
35
+
36
+ // The `feature-escalation` form completes with `{ resolution, answer }`; the `w_gw_answer` gateway
37
+ // re-dispatches implement-task with `answer` when resolution=answer, else routes to record-feature.
38
+ // Guidance is required for an answer (an empty answer re-dispatches the agent with nothing new).
39
+ const answer = str(body.answer);
40
+ if (resolution === "answer" && !answer) {
41
+ return { status: 400, body: { ok: false, error: "answer (guidance) is required when resolution is 'answer'" } };
42
+ }
43
+ const variables: Record<string, unknown> = resolution === "answer" ? { resolution, answer } : { resolution };
44
+
45
+ // The completing operator, for the attribution ledger. Optional — the UI has no per-operator auth,
46
+ // so default to a generic handle rather than blocking the answer.
47
+ const operatorId = str(body.operator) || "operator";
48
+
49
+ const r = await completeEscalationAsHuman(app.data, app.engine, { userTaskKey, operatorId, variables });
50
+ if (r.ok) {
51
+ // Reconcile this operation's OWN action immediately: clear the denormalised escalation pointer +
52
+ // question so the pages stop offering an answer affordance for a task that is now completed. Leave
53
+ // `status` to the poller (escalated → running on the answer loop) / record-feature (terminal on
54
+ // abandon), so we never overwrite a status the resumed run has already advanced to.
55
+ for (const run of await featureRuns(app.data).find({ escalation_user_task_key: userTaskKey })) {
56
+ await featureRuns(app.data).update(run.feature_key, {
57
+ escalation_question: null,
58
+ escalation_user_task_key: null,
59
+ updated_at: new Date().toISOString(),
60
+ });
61
+ }
62
+ app.log.info("operator answered feature escalation", { userTaskKey, resolution, elementId: r.elementId });
63
+ return { status: 200, body: { ok: true, completionId: r.completionId, elementId: r.elementId } };
64
+ }
65
+ const status = r.reason === "no open escalation task" ? 404 : 400;
66
+ app.log.warn("answer-escalation: not completed", { userTaskKey, reason: r.reason });
67
+ return { status, body: { ok: false, error: r.reason } };
68
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.64.0",
3
+ "version": "0.66.0",
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",
@@ -60,16 +60,17 @@
60
60
  "source": "app",
61
61
  "table": "feature_runs",
62
62
  "orderBy": { "field": "updated_at", "dir": "desc" },
63
- "filter": [{ "field": "status", "in": ["running", "awaiting_operator"] }]
63
+ "filter": [{ "field": "status", "in": ["running", "escalated", "awaiting_operator"] }]
64
64
  },
65
65
  "tabs": [
66
- { "label": "Active", "filter": [{ "field": "status", "in": ["running", "awaiting_operator"] }] },
66
+ { "label": "Active", "filter": [{ "field": "status", "in": ["running", "escalated", "awaiting_operator"] }] },
67
67
  { "label": "History", "filter": [{ "field": "status", "in": ["opened", "converging", "merged", "converged", "blocked", "skipped", "failed", "abandoned"] }] },
68
68
  { "label": "All", "filter": [] }
69
69
  ],
70
70
  "columns": [
71
71
  { "field": "feature_key", "header": "Feature", "linkField": "issue_url" },
72
72
  { "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
73
+ { "field": "escalation_question", "header": "Escalation" },
73
74
  { "field": "base_branch", "header": "Base branch" },
74
75
  { "field": "pr_key", "header": "PR", "link": { "kind": "page", "page": "home", "keyField": "pr_key" } },
75
76
  { "field": "delivery_label", "header": "Delivery" },
@@ -77,7 +78,42 @@
77
78
  { "field": "auto_merge", "header": "Auto-merge" },
78
79
  { "field": "outcome", "header": "Outcome" },
79
80
  { "field": "updated_at", "header": "Updated" }
80
- ]
81
+ ],
82
+ "rowActions": [
83
+ {
84
+ "label": "Abandon",
85
+ "confirm": "Abandon this escalated task? The run gives up on it (no PR).",
86
+ "showWhenField": "escalation_user_task_key",
87
+ "action": {
88
+ "path": "/app/api/actions/answer-escalation",
89
+ "body": { "userTaskKey": "{{row.escalation_user_task_key}}", "resolution": "abandon" }
90
+ }
91
+ }
92
+ ],
93
+ "detail": {
94
+ "fields": [
95
+ { "field": "escalation_question", "label": "Escalation question" },
96
+ { "field": "outcome", "label": "Outcome" },
97
+ { "field": "delivery_label", "label": "Delivery" }
98
+ ],
99
+ "form": {
100
+ "showWhenField": "escalation_user_task_key",
101
+ "title": "Answer escalation",
102
+ "promptField": "escalation_question",
103
+ "inputKey": "answer",
104
+ "inputLabel": "Guidance for the implementation agent",
105
+ "submitLabel": "Answer & re-dispatch",
106
+ "action": {
107
+ "path": "/app/api/actions/answer-escalation",
108
+ "successLabel": "Answered — the agent will resume",
109
+ "body": {
110
+ "userTaskKey": "{{row.escalation_user_task_key}}",
111
+ "resolution": "answer",
112
+ "answer": "{{form.answer}}"
113
+ }
114
+ }
115
+ }
116
+ }
81
117
  }
82
118
  }
83
119
  ]
@@ -119,18 +119,53 @@
119
119
  "source": "app",
120
120
  "table": "feature_runs",
121
121
  "orderBy": { "field": "updated_at", "dir": "desc" },
122
- "filter": [{ "field": "status", "in": ["running", "awaiting_operator"] }]
122
+ "filter": [{ "field": "status", "in": ["running", "escalated", "awaiting_operator"] }]
123
123
  },
124
124
  "columns": [
125
125
  { "field": "feature_key", "header": "Feature", "link": { "kind": "page", "page": "feature", "keyField": "feature_key" } },
126
126
  { "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
127
+ { "field": "escalation_question", "header": "Escalation" },
127
128
  { "field": "base_branch", "header": "Base branch" },
128
129
  { "field": "pr_key", "header": "PR", "link": { "kind": "page", "page": "home", "keyField": "pr_key" } },
129
130
  { "field": "converge", "header": "Converge" },
130
131
  { "field": "auto_merge", "header": "Auto-merge" },
131
132
  { "field": "outcome", "header": "Outcome" },
132
133
  { "field": "updated_at", "header": "Updated" }
133
- ]
134
+ ],
135
+ "rowActions": [
136
+ {
137
+ "label": "Abandon",
138
+ "confirm": "Abandon this escalated task? The run gives up on it (no PR).",
139
+ "showWhenField": "escalation_user_task_key",
140
+ "action": {
141
+ "path": "/app/api/actions/answer-escalation",
142
+ "body": { "userTaskKey": "{{row.escalation_user_task_key}}", "resolution": "abandon" }
143
+ }
144
+ }
145
+ ],
146
+ "detail": {
147
+ "fields": [
148
+ { "field": "escalation_question", "label": "Escalation question" },
149
+ { "field": "outcome", "label": "Outcome" }
150
+ ],
151
+ "form": {
152
+ "showWhenField": "escalation_user_task_key",
153
+ "title": "Answer escalation",
154
+ "promptField": "escalation_question",
155
+ "inputKey": "answer",
156
+ "inputLabel": "Guidance for the implementation agent",
157
+ "submitLabel": "Answer & re-dispatch",
158
+ "action": {
159
+ "path": "/app/api/actions/answer-escalation",
160
+ "successLabel": "Answered — the agent will resume",
161
+ "body": {
162
+ "userTaskKey": "{{row.escalation_user_task_key}}",
163
+ "resolution": "answer",
164
+ "answer": "{{form.answer}}"
165
+ }
166
+ }
167
+ }
168
+ }
134
169
  }
135
170
  }
136
171
  ]
@@ -9,6 +9,10 @@
9
9
  <nano:extend name="pr" type="string" optional="true" />
10
10
  <nano:extend name="summary" type="string" optional="true" />
11
11
  </nano:shape>
12
+ <nano:shape id="RecordFeatureEscalationIn" name="Record feature escalation — input">
13
+ <nano:extend name="featureKey" type="string" />
14
+ <nano:extend name="question" type="string" optional="true" />
15
+ </nano:shape>
12
16
  <nano:shape id="ConvergeFeatureIn" name="Converge feature PR — input">
13
17
  <nano:extend name="featureKey" type="string" />
14
18
  <nano:extend name="prKey" type="string" optional="true" />
@@ -54,13 +58,23 @@
54
58
  <bpmn:outgoing>w_escalate</bpmn:outgoing>
55
59
  <bpmn:outgoing>w_done</bpmn:outgoing>
56
60
  </bpmn:exclusiveGateway>
61
+ <bpmn:serviceTask id="record-feature-escalation" name="Record escalation">
62
+ <bpmn:extensionElements>
63
+ <zeebe:taskDefinition type="pr.record-feature-escalation" />
64
+ <zeebe:properties>
65
+ <zeebe:property name="io.nanobpm.dataEnvelope.in" value="RecordFeatureEscalationIn" />
66
+ </zeebe:properties>
67
+ </bpmn:extensionElements>
68
+ <bpmn:incoming>w_escalate</bpmn:incoming>
69
+ <bpmn:outgoing>w_toEscalationTask</bpmn:outgoing>
70
+ </bpmn:serviceTask>
57
71
  <bpmn:userTask id="feature-escalation" name="Task escalation (human)">
58
72
  <bpmn:extensionElements>
59
73
  <zeebe:formDefinition formId="feature-escalation" />
60
74
  <zeebe:userTask />
61
- <zeebe:assignmentDefinition candidateGroups="operators" assignee="=escalationAssignee" />
75
+ <zeebe:assignmentDefinition candidateGroups="operators" assignee="=if (is defined(escalationAssignee) and escalationAssignee != null and trim(string(escalationAssignee)) != &#34;&#34;) then escalationAssignee else null" />
62
76
  </bpmn:extensionElements>
63
- <bpmn:incoming>w_escalate</bpmn:incoming>
77
+ <bpmn:incoming>w_toEscalationTask</bpmn:incoming>
64
78
  <bpmn:outgoing>w_toAnswerGw</bpmn:outgoing>
65
79
  </bpmn:userTask>
66
80
  <bpmn:boundaryEvent id="be_feature_sla" name="SLA elapsed" attachedToRef="feature-escalation">
@@ -134,9 +148,10 @@
134
148
  <bpmn:sequenceFlow id="f_toEnsureBase" sourceRef="Start" targetRef="ensure-base-branch" />
135
149
  <bpmn:sequenceFlow id="f_toImplement" sourceRef="ensure-base-branch" targetRef="implement-task" />
136
150
  <bpmn:sequenceFlow id="w_toGw" sourceRef="implement-task" targetRef="w_gw" />
137
- <bpmn:sequenceFlow id="w_escalate" name="escalated" sourceRef="w_gw" targetRef="feature-escalation">
151
+ <bpmn:sequenceFlow id="w_escalate" name="escalated" sourceRef="w_gw" targetRef="record-feature-escalation">
138
152
  <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=status = "escalated" and question != null and trim(question) != ""</bpmn:conditionExpression>
139
153
  </bpmn:sequenceFlow>
154
+ <bpmn:sequenceFlow id="w_toEscalationTask" sourceRef="record-feature-escalation" targetRef="feature-escalation" />
140
155
  <bpmn:sequenceFlow id="w_done" name="done" sourceRef="w_gw" targetRef="record-feature" />
141
156
  <bpmn:sequenceFlow id="w_toAnswerGw" sourceRef="feature-escalation" targetRef="w_gw_answer" />
142
157
  <bpmn:sequenceFlow id="w_answerLoop" name="answer" sourceRef="w_gw_answer" targetRef="implement-task">
@@ -177,49 +192,52 @@
177
192
  <dc:Bounds x="604" y="76" width="74" height="14" />
178
193
  </bpmndi:BPMNLabel>
179
194
  </bpmndi:BPMNShape>
180
- <bpmndi:BPMNShape id="BPMNShape_feature-escalation" bpmnElement="feature-escalation">
195
+ <bpmndi:BPMNShape id="BPMNShape_record-feature-escalation" bpmnElement="record-feature-escalation">
181
196
  <dc:Bounds x="766" y="240" width="100" height="80" />
182
197
  </bpmndi:BPMNShape>
198
+ <bpmndi:BPMNShape id="BPMNShape_feature-escalation" bpmnElement="feature-escalation">
199
+ <dc:Bounds x="966" y="240" width="100" height="80" />
200
+ </bpmndi:BPMNShape>
183
201
  <bpmndi:BPMNShape id="BPMNShape_w_gw_answer" bpmnElement="w_gw_answer" isMarkerVisible="true">
184
- <dc:Bounds x="966" y="255" width="50" height="50" />
202
+ <dc:Bounds x="1166" y="255" width="50" height="50" />
185
203
  <bpmndi:BPMNLabel>
186
- <dc:Bounds x="963" y="236" width="56" height="14" />
204
+ <dc:Bounds x="1163" y="236" width="56" height="14" />
187
205
  </bpmndi:BPMNLabel>
188
206
  </bpmndi:BPMNShape>
189
207
  <bpmndi:BPMNShape id="BPMNShape_record-feature" bpmnElement="record-feature">
190
- <dc:Bounds x="1116" y="80" width="100" height="80" />
208
+ <dc:Bounds x="1316" y="80" width="100" height="80" />
191
209
  </bpmndi:BPMNShape>
192
210
  <bpmndi:BPMNShape id="BPMNShape_gw-blocked" bpmnElement="gw-blocked" isMarkerVisible="true">
193
- <dc:Bounds x="1316" y="95" width="50" height="50" />
211
+ <dc:Bounds x="1516" y="95" width="50" height="50" />
194
212
  <bpmndi:BPMNLabel>
195
- <dc:Bounds x="1311" y="76" width="60" height="14" />
213
+ <dc:Bounds x="1511" y="76" width="60" height="14" />
196
214
  </bpmndi:BPMNLabel>
197
215
  </bpmndi:BPMNShape>
198
216
  <bpmndi:BPMNShape id="BPMNShape_feature-blocked" bpmnElement="feature-blocked">
199
- <dc:Bounds x="1466" y="400" width="100" height="80" />
217
+ <dc:Bounds x="1666" y="400" width="100" height="80" />
200
218
  </bpmndi:BPMNShape>
201
219
  <bpmndi:BPMNShape id="BPMNShape_record-blocked-ack" bpmnElement="record-blocked-ack">
202
- <dc:Bounds x="1666" y="400" width="100" height="80" />
220
+ <dc:Bounds x="1866" y="400" width="100" height="80" />
203
221
  </bpmndi:BPMNShape>
204
222
  <bpmndi:BPMNShape id="BPMNShape_gw-converge" bpmnElement="gw-converge" isMarkerVisible="true">
205
- <dc:Bounds x="1491" y="95" width="50" height="50" />
223
+ <dc:Bounds x="1691" y="95" width="50" height="50" />
206
224
  <bpmndi:BPMNLabel>
207
- <dc:Bounds x="1483" y="76" width="67" height="14" />
225
+ <dc:Bounds x="1683" y="76" width="67" height="14" />
208
226
  </bpmndi:BPMNLabel>
209
227
  </bpmndi:BPMNShape>
210
228
  <bpmndi:BPMNShape id="BPMNShape_converge" bpmnElement="converge">
211
- <dc:Bounds x="1666" y="240" width="100" height="80" />
229
+ <dc:Bounds x="1866" y="240" width="100" height="80" />
212
230
  </bpmndi:BPMNShape>
213
231
  <bpmndi:BPMNShape id="BPMNShape_End" bpmnElement="End">
214
- <dc:Bounds x="1866" y="102" width="36" height="36" />
232
+ <dc:Bounds x="2066" y="102" width="36" height="36" />
215
233
  <bpmndi:BPMNLabel>
216
- <dc:Bounds x="1867" y="83" width="34" height="14" />
234
+ <dc:Bounds x="2067" y="83" width="34" height="14" />
217
235
  </bpmndi:BPMNLabel>
218
236
  </bpmndi:BPMNShape>
219
237
  <bpmndi:BPMNShape id="BPMNShape_be_feature_sla" bpmnElement="be_feature_sla">
220
- <dc:Bounds x="798" y="302" width="36" height="36" />
238
+ <dc:Bounds x="998" y="302" width="36" height="36" />
221
239
  <bpmndi:BPMNLabel>
222
- <dc:Bounds x="774" y="383" width="84" height="14" />
240
+ <dc:Bounds x="974" y="383" width="84" height="14" />
223
241
  </bpmndi:BPMNLabel>
224
242
  </bpmndi:BPMNShape>
225
243
  <bpmndi:BPMNEdge id="BPMNEdge_f_toEnsureBase" bpmnElement="f_toEnsureBase">
@@ -236,35 +254,35 @@
236
254
  </bpmndi:BPMNEdge>
237
255
  <bpmndi:BPMNEdge id="BPMNEdge_w_done" bpmnElement="w_done">
238
256
  <di:waypoint x="666" y="120" />
239
- <di:waypoint x="1116" y="120" />
257
+ <di:waypoint x="1316" y="120" />
240
258
  <bpmndi:BPMNLabel>
241
- <dc:Bounds x="875" y="98" width="32" height="14" />
259
+ <dc:Bounds x="975" y="98" width="32" height="14" />
242
260
  </bpmndi:BPMNLabel>
243
261
  </bpmndi:BPMNEdge>
244
262
  <bpmndi:BPMNEdge id="BPMNEdge_f_toBlockedGw" bpmnElement="f_toBlockedGw">
245
- <di:waypoint x="1216" y="120" />
246
- <di:waypoint x="1316" y="120" />
263
+ <di:waypoint x="1416" y="120" />
264
+ <di:waypoint x="1516" y="120" />
247
265
  </bpmndi:BPMNEdge>
248
266
  <bpmndi:BPMNEdge id="BPMNEdge_f_notBlocked" bpmnElement="f_notBlocked">
249
- <di:waypoint x="1366" y="120" />
250
- <di:waypoint x="1491" y="120" />
267
+ <di:waypoint x="1566" y="120" />
268
+ <di:waypoint x="1691" y="120" />
251
269
  <bpmndi:BPMNLabel>
252
- <dc:Bounds x="1390" y="98" width="78" height="14" />
270
+ <dc:Bounds x="1590" y="98" width="78" height="14" />
253
271
  </bpmndi:BPMNLabel>
254
272
  </bpmndi:BPMNEdge>
255
273
  <bpmndi:BPMNEdge id="BPMNEdge_f_noConverge" bpmnElement="f_noConverge">
256
- <di:waypoint x="1541" y="120" />
257
- <di:waypoint x="1866" y="120" />
274
+ <di:waypoint x="1741" y="120" />
275
+ <di:waypoint x="2066" y="120" />
258
276
  <bpmndi:BPMNLabel>
259
- <dc:Bounds x="1668" y="98" width="71" height="14" />
277
+ <dc:Bounds x="1868" y="98" width="71" height="14" />
260
278
  </bpmndi:BPMNLabel>
261
279
  </bpmndi:BPMNEdge>
262
280
  <bpmndi:BPMNEdge id="BPMNEdge_w_abandon" bpmnElement="w_abandon">
263
- <di:waypoint x="1016" y="280" />
264
- <di:waypoint x="1166" y="280" />
265
- <di:waypoint x="1166" y="160" />
281
+ <di:waypoint x="1216" y="280" />
282
+ <di:waypoint x="1366" y="280" />
283
+ <di:waypoint x="1366" y="160" />
266
284
  <bpmndi:BPMNLabel>
267
- <dc:Bounds x="1065" y="258" width="53" height="14" />
285
+ <dc:Bounds x="1265" y="258" width="53" height="14" />
268
286
  </bpmndi:BPMNLabel>
269
287
  </bpmndi:BPMNEdge>
270
288
  <bpmndi:BPMNEdge id="BPMNEdge_w_escalate" bpmnElement="w_escalate">
@@ -276,55 +294,59 @@
276
294
  </bpmndi:BPMNLabel>
277
295
  </bpmndi:BPMNEdge>
278
296
  <bpmndi:BPMNEdge id="BPMNEdge_f_blocked" bpmnElement="f_blocked">
279
- <di:waypoint x="1341" y="145" />
280
- <di:waypoint x="1341" y="440" />
281
- <di:waypoint x="1466" y="440" />
297
+ <di:waypoint x="1541" y="145" />
298
+ <di:waypoint x="1541" y="440" />
299
+ <di:waypoint x="1666" y="440" />
282
300
  <bpmndi:BPMNLabel>
283
- <dc:Bounds x="1346" y="286" width="53" height="14" />
301
+ <dc:Bounds x="1546" y="286" width="53" height="14" />
284
302
  </bpmndi:BPMNLabel>
285
303
  </bpmndi:BPMNEdge>
286
304
  <bpmndi:BPMNEdge id="BPMNEdge_f_converge" bpmnElement="f_converge">
287
- <di:waypoint x="1516" y="145" />
288
- <di:waypoint x="1516" y="280" />
289
- <di:waypoint x="1666" y="280" />
305
+ <di:waypoint x="1716" y="145" />
306
+ <di:waypoint x="1716" y="280" />
307
+ <di:waypoint x="1866" y="280" />
290
308
  <bpmndi:BPMNLabel>
291
- <dc:Bounds x="1521" y="206" width="60" height="14" />
309
+ <dc:Bounds x="1721" y="206" width="60" height="14" />
292
310
  </bpmndi:BPMNLabel>
293
311
  </bpmndi:BPMNEdge>
294
- <bpmndi:BPMNEdge id="BPMNEdge_w_toAnswerGw" bpmnElement="w_toAnswerGw">
312
+ <bpmndi:BPMNEdge id="BPMNEdge_w_toEscalationTask" bpmnElement="w_toEscalationTask">
295
313
  <di:waypoint x="866" y="280" />
296
314
  <di:waypoint x="966" y="280" />
297
315
  </bpmndi:BPMNEdge>
316
+ <bpmndi:BPMNEdge id="BPMNEdge_w_toAnswerGw" bpmnElement="w_toAnswerGw">
317
+ <di:waypoint x="1066" y="280" />
318
+ <di:waypoint x="1166" y="280" />
319
+ </bpmndi:BPMNEdge>
298
320
  <bpmndi:BPMNEdge id="BPMNEdge_f_toBlockedAck" bpmnElement="f_toBlockedAck">
299
- <di:waypoint x="1566" y="440" />
300
- <di:waypoint x="1666" y="440" />
321
+ <di:waypoint x="1766" y="440" />
322
+ <di:waypoint x="1866" y="440" />
301
323
  </bpmndi:BPMNEdge>
302
324
  <bpmndi:BPMNEdge id="BPMNEdge_f_blockedEnd" bpmnElement="f_blockedEnd">
303
- <di:waypoint x="1766" y="440" />
304
- <di:waypoint x="1884" y="440" />
305
- <di:waypoint x="1884" y="138" />
325
+ <di:waypoint x="1966" y="440" />
326
+ <di:waypoint x="2084" y="440" />
327
+ <di:waypoint x="2084" y="138" />
306
328
  </bpmndi:BPMNEdge>
307
329
  <bpmndi:BPMNEdge id="BPMNEdge_f_toEndConverged" bpmnElement="f_toEndConverged">
308
- <di:waypoint x="1766" y="280" />
309
- <di:waypoint x="1884" y="280" />
310
- <di:waypoint x="1884" y="138" />
330
+ <di:waypoint x="1966" y="280" />
331
+ <di:waypoint x="2084" y="280" />
332
+ <di:waypoint x="2084" y="138" />
311
333
  </bpmndi:BPMNEdge>
312
334
  <bpmndi:BPMNEdge id="BPMNEdge_w_sla" bpmnElement="w_sla">
313
- <di:waypoint x="816" y="338" />
314
- <di:waypoint x="816" y="358" />
315
- <di:waypoint x="1166" y="358" />
316
- <di:waypoint x="1166" y="160" />
335
+ <di:waypoint x="1016" y="338" />
336
+ <di:waypoint x="1016" y="358" />
337
+ <di:waypoint x="1366" y="358" />
338
+ <di:waypoint x="1366" y="160" />
317
339
  <bpmndi:BPMNLabel>
318
- <dc:Bounds x="1007" y="325" width="88" height="28" />
340
+ <dc:Bounds x="1207" y="325" width="88" height="28" />
319
341
  </bpmndi:BPMNLabel>
320
342
  </bpmndi:BPMNEdge>
321
343
  <bpmndi:BPMNEdge id="BPMNEdge_w_answerLoop" bpmnElement="w_answerLoop">
322
- <di:waypoint x="991" y="305" />
323
- <di:waypoint x="991" y="360" />
344
+ <di:waypoint x="1191" y="305" />
345
+ <di:waypoint x="1191" y="360" />
324
346
  <di:waypoint x="466" y="360" />
325
347
  <di:waypoint x="466" y="160" />
326
348
  <bpmndi:BPMNLabel>
327
- <dc:Bounds x="704" y="338" width="49" height="14" />
349
+ <dc:Bounds x="804" y="338" width="49" height="14" />
328
350
  </bpmndi:BPMNLabel>
329
351
  </bpmndi:BPMNEdge>
330
352
  </bpmndi:BPMNPlane>