@nanobpm/nano-workforce 0.201.4 → 0.202.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.
@@ -89,13 +89,15 @@ const DEFAULT_VARS: Record<string, unknown> = {
89
89
  mergeableRepollInterval: "PT2M",
90
90
  mergeStallRounds: 0,
91
91
  mergeStallMax: 3,
92
- abandonBrief: null,
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);
@@ -124,6 +124,40 @@ when a worker ages out past the liveness TTL. On (re)mount the presence family
124
124
  reconciles the store against live connections, so a worker that vanished while
125
125
  the app was down does not linger as a ghost row after a restart.
126
126
 
127
+ ### Job ownership: authoritative reconnect resync
128
+
129
+ A worker's **jobs** are tracked by the explicit job-ownership claim registry
130
+ (`app/agentic/claim-registry.ts`): a supervisor sends a `claim` frame when a
131
+ worker picks up a job and a `release` when it finishes. Each frame names its
132
+ owning `instance` explicitly, so one per-host supervisor connection can carry
133
+ many workers' ownership.
134
+
135
+ On a WS reconnect a supervisor re-`claim`s every active jobKey. Those re-claims
136
+ are **additive** on their own — a claim that is not re-asserted is never dropped
137
+ — so a `release` lost during a hub outage would leave the job shown as *working*
138
+ until the worker exits entirely, forcing the supervisor to buffer every RELEASE
139
+ without limit across the outage.
140
+
141
+ To close that, a supervisor may end its resync with an **authoritative
142
+ snapshot**: a `claim` frame carrying the full set of jobKeys the instance still
143
+ owns (`{ instance, jobKeys: [...] }` — the empty **array** is allowed, meaning
144
+ "this instance now owns nothing", but every **element** must be a non-empty
145
+ string). The hub validates the frame as the snapshot-or-per-job
146
+ union (a malformed snapshot is dropped before it touches the registry — the
147
+ *presence* of `jobKeys`, not its validity, selects the snapshot variant, so a
148
+ present-but-malformed `jobKeys` — a non-array, a non-string element, or an
149
+ empty-string element — is rejected rather than laundered through the
150
+ per-job path; an empty-string element can't masquerade as an "owns nothing"
151
+ snapshot and clear real claims) and then
152
+ **replaces** that instance's claims with the snapshot — dropping any it did not
153
+ re-assert. The supervisor's
154
+ view wins, because ownership is advisory, short-lived visibility it can always
155
+ restate (it never gates a BPMN flow, ADR 0056). A jobKey that has moved to
156
+ another instance is never blanked by the old instance's resync, and a supervisor
157
+ that sends no snapshot keeps the old additive behaviour. This authoritative
158
+ contract lets a hub client (nano-supervisor#63) **bound** its RELEASE buffer
159
+ during an outage.
160
+
127
161
  ## What you will NOT find here (and where it lives)
128
162
 
129
163
  - **Demand×supply matrix, missing-agent-type reds, diversity-SLO lights** →
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.201.4",
3
+ "version": "0.202.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",
@@ -147,9 +147,45 @@
147
147
  </zeebe:properties>
148
148
  <zeebe:ioMapping>
149
149
  <zeebe:input source="=(if (abandonBrief = null) then &#34;&#34; 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 &#34;&#34; 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 advPass = null then 0 else advPass) + 1" target="advPass" />
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) != &#34;&#34; then null else status)" target="status" />
211
265
  <zeebe:output source="=(if adversarialFindings != null and trim(adversarialFindings) != &#34;&#34; then null else summary)" target="summary" />
212
266
  <zeebe:output source="=(if adversarialFindings != null and trim(adversarialFindings) != &#34;&#34; 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>
@@ -362,7 +362,46 @@
362
362
  </zeebe:properties>
363
363
  <zeebe:ioMapping>
364
364
  <zeebe:input source="=(if (failingChecksList = null or failingChecksList = &#34;&#34;) then &#34;&#34; else &#34;&#10;&#10;Failing checks to make green:&#10;&#34; + failingChecksList) + (if (abandonBrief = null) then &#34;&#34; else abandonBrief)" target="appendPrompt" />
365
- <zeebe:output source="=ciFixRound + 1" target="ciFixRound" />
365
+ <zeebe:input source="=ciFixRound" target="guardCiFixRound" />
366
+ <zeebe:input source="=ciFixMax" target="guardCiFixMax" />
367
+ <zeebe:input source="=rebaseRound" target="guardRebaseRound" />
368
+ <zeebe:input source="=rebaseMax" target="guardRebaseMax" />
369
+ <zeebe:input source="=mergeRetryRound" target="guardMergeRetryRound" />
370
+ <zeebe:input source="=mergeRetryMax" target="guardMergeRetryMax" />
371
+ <zeebe:input source="=mergeStallRounds" target="guardMergeStallRounds" />
372
+ <zeebe:input source="=mergeStallMax" target="guardMergeStallMax" />
373
+ <zeebe:input source="=prKey" target="guardPrKey" />
374
+ <zeebe:input source="=repo" target="guardRepo" />
375
+ <zeebe:input source="=prNumber" target="guardPrNumber" />
376
+ <zeebe:input source="=prUrl" target="guardPrUrl" />
377
+ <zeebe:input source="=ciBlockedReconciled" target="guardCiBlockedReconciled" />
378
+ <zeebe:input source="=agentSlaTimeout" target="guardAgentSlaTimeout" />
379
+ <zeebe:input source="=landedWaitTimeout" target="guardLandedWaitTimeout" />
380
+ <zeebe:input source="=mergeableWaitTimeout" target="guardMergeableWaitTimeout" />
381
+ <zeebe:input source="=mergeableRepollInterval" target="guardMergeableRepollInterval" />
382
+ <zeebe:input source="=round" target="guardRound" />
383
+ <zeebe:input source="=abandonUrl" target="guardAbandonUrl" />
384
+ <zeebe:input source="=abandonBrief" target="guardAbandonBrief" />
385
+ <zeebe:output source="=guardCiFixRound + 1" target="ciFixRound" />
386
+ <zeebe:output source="=guardCiFixMax" target="ciFixMax" />
387
+ <zeebe:output source="=guardRebaseRound" target="rebaseRound" />
388
+ <zeebe:output source="=guardRebaseMax" target="rebaseMax" />
389
+ <zeebe:output source="=guardMergeRetryRound" target="mergeRetryRound" />
390
+ <zeebe:output source="=guardMergeRetryMax" target="mergeRetryMax" />
391
+ <zeebe:output source="=guardMergeStallRounds" target="mergeStallRounds" />
392
+ <zeebe:output source="=guardMergeStallMax" target="mergeStallMax" />
393
+ <zeebe:output source="=guardPrKey" target="prKey" />
394
+ <zeebe:output source="=guardRepo" target="repo" />
395
+ <zeebe:output source="=guardPrNumber" target="prNumber" />
396
+ <zeebe:output source="=guardPrUrl" target="prUrl" />
397
+ <zeebe:output source="=guardCiBlockedReconciled" target="ciBlockedReconciled" />
398
+ <zeebe:output source="=guardAgentSlaTimeout" target="agentSlaTimeout" />
399
+ <zeebe:output source="=guardLandedWaitTimeout" target="landedWaitTimeout" />
400
+ <zeebe:output source="=guardMergeableWaitTimeout" target="mergeableWaitTimeout" />
401
+ <zeebe:output source="=guardMergeableRepollInterval" target="mergeableRepollInterval" />
402
+ <zeebe:output source="=guardRound" target="round" />
403
+ <zeebe:output source="=guardAbandonUrl" target="abandonUrl" />
404
+ <zeebe:output source="=guardAbandonBrief" target="abandonBrief" />
366
405
  </zeebe:ioMapping>
367
406
  </bpmn:extensionElements>
368
407
  <bpmn:incoming>f_ci_s0</bpmn:incoming>
@@ -524,7 +563,46 @@
524
563
  </zeebe:properties>
525
564
  <zeebe:ioMapping>
526
565
  <zeebe:input source="=(if (abandonBrief = null) then &#34;&#34; else abandonBrief)" target="appendPrompt" />
527
- <zeebe:output source="=rebaseRound + 1" target="rebaseRound" />
566
+ <zeebe:input source="=ciFixRound" target="guardCiFixRound" />
567
+ <zeebe:input source="=ciFixMax" target="guardCiFixMax" />
568
+ <zeebe:input source="=rebaseRound" target="guardRebaseRound" />
569
+ <zeebe:input source="=rebaseMax" target="guardRebaseMax" />
570
+ <zeebe:input source="=mergeRetryRound" target="guardMergeRetryRound" />
571
+ <zeebe:input source="=mergeRetryMax" target="guardMergeRetryMax" />
572
+ <zeebe:input source="=mergeStallRounds" target="guardMergeStallRounds" />
573
+ <zeebe:input source="=mergeStallMax" target="guardMergeStallMax" />
574
+ <zeebe:input source="=prKey" target="guardPrKey" />
575
+ <zeebe:input source="=repo" target="guardRepo" />
576
+ <zeebe:input source="=prNumber" target="guardPrNumber" />
577
+ <zeebe:input source="=prUrl" target="guardPrUrl" />
578
+ <zeebe:input source="=ciBlockedReconciled" target="guardCiBlockedReconciled" />
579
+ <zeebe:input source="=agentSlaTimeout" target="guardAgentSlaTimeout" />
580
+ <zeebe:input source="=landedWaitTimeout" target="guardLandedWaitTimeout" />
581
+ <zeebe:input source="=mergeableWaitTimeout" target="guardMergeableWaitTimeout" />
582
+ <zeebe:input source="=mergeableRepollInterval" target="guardMergeableRepollInterval" />
583
+ <zeebe:input source="=round" target="guardRound" />
584
+ <zeebe:input source="=abandonUrl" target="guardAbandonUrl" />
585
+ <zeebe:input source="=abandonBrief" target="guardAbandonBrief" />
586
+ <zeebe:output source="=guardRebaseRound + 1" target="rebaseRound" />
587
+ <zeebe:output source="=guardCiFixRound" target="ciFixRound" />
588
+ <zeebe:output source="=guardCiFixMax" target="ciFixMax" />
589
+ <zeebe:output source="=guardRebaseMax" target="rebaseMax" />
590
+ <zeebe:output source="=guardMergeRetryRound" target="mergeRetryRound" />
591
+ <zeebe:output source="=guardMergeRetryMax" target="mergeRetryMax" />
592
+ <zeebe:output source="=guardMergeStallRounds" target="mergeStallRounds" />
593
+ <zeebe:output source="=guardMergeStallMax" target="mergeStallMax" />
594
+ <zeebe:output source="=guardPrKey" target="prKey" />
595
+ <zeebe:output source="=guardRepo" target="repo" />
596
+ <zeebe:output source="=guardPrNumber" target="prNumber" />
597
+ <zeebe:output source="=guardPrUrl" target="prUrl" />
598
+ <zeebe:output source="=guardCiBlockedReconciled" target="ciBlockedReconciled" />
599
+ <zeebe:output source="=guardAgentSlaTimeout" target="agentSlaTimeout" />
600
+ <zeebe:output source="=guardLandedWaitTimeout" target="landedWaitTimeout" />
601
+ <zeebe:output source="=guardMergeableWaitTimeout" target="mergeableWaitTimeout" />
602
+ <zeebe:output source="=guardMergeableRepollInterval" target="mergeableRepollInterval" />
603
+ <zeebe:output source="=guardRound" target="round" />
604
+ <zeebe:output source="=guardAbandonUrl" target="abandonUrl" />
605
+ <zeebe:output source="=guardAbandonBrief" target="abandonBrief" />
528
606
  </zeebe:ioMapping>
529
607
  </bpmn:extensionElements>
530
608
  <bpmn:incoming>f_reb_s0</bpmn:incoming>