@nanobpm/nano-workforce 0.187.6 → 0.188.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.
@@ -65,6 +65,7 @@
65
65
  <nano:shape id="PrConvergeGateOut" name="Converge gate — result">
66
66
  <nano:extend name="convergeBlocked" type="boolean" />
67
67
  <nano:extend name="convergeBlockReason" type="string" optional="true" />
68
+ <nano:extend name="convergeAckOnly" type="boolean" optional="true" />
68
69
  <nano:extend name="reviewStale" type="boolean" optional="true" />
69
70
  </nano:shape>
70
71
  <nano:shape id="EscalationIn" name="Record escalation — input">
@@ -145,6 +146,7 @@
145
146
  <bpmn:incoming>f_reviewLoop</bpmn:incoming>
146
147
  <bpmn:incoming>f_answerLoop</bpmn:incoming>
147
148
  <bpmn:incoming>f_huskRetry</bpmn:incoming>
149
+ <bpmn:incoming>f_ackRetry</bpmn:incoming>
148
150
  <bpmn:outgoing>f_capture</bpmn:outgoing>
149
151
  </bpmn:serviceTask>
150
152
  <bpmn:exclusiveGateway id="gw-status" name="status?" default="f_addressed">
@@ -331,6 +333,9 @@
331
333
  <zeebe:property name="io.nanobpm.dataEnvelope.in" value="PrConvergeGateIn" />
332
334
  <zeebe:property name="io.nanobpm.dataEnvelope.out" value="PrConvergeGateOut" />
333
335
  </zeebe:properties>
336
+ <zeebe:ioMapping>
337
+ <zeebe:output source="=if convergeBlocked = true and convergeAckOnly = true then ackRetryRound + 1 else ackRetryRound" target="ackRetryRound" />
338
+ </zeebe:ioMapping>
334
339
  </bpmn:extensionElements>
335
340
  <bpmn:incoming>f_converged</bpmn:incoming>
336
341
  <bpmn:outgoing>f_toConvergeGate</bpmn:outgoing>
@@ -341,6 +346,11 @@
341
346
  <bpmn:outgoing>f_convergeStale</bpmn:outgoing>
342
347
  <bpmn:outgoing>f_convergeOk</bpmn:outgoing>
343
348
  </bpmn:exclusiveGateway>
349
+ <bpmn:exclusiveGateway id="gw-ack-retry" name="auto-ack within budget?" default="f_ackEscalate">
350
+ <bpmn:incoming>f_convergeBlocked</bpmn:incoming>
351
+ <bpmn:outgoing>f_ackRetry</bpmn:outgoing>
352
+ <bpmn:outgoing>f_ackEscalate</bpmn:outgoing>
353
+ </bpmn:exclusiveGateway>
344
354
  <bpmn:serviceTask id="persist-escalation-blockedcomments" name="Escalate: unaddressed comments">
345
355
  <bpmn:extensionElements>
346
356
  <zeebe:taskDefinition type="pr.persist-escalation" />
@@ -354,7 +364,7 @@
354
364
  <zeebe:input source="=convergeBlockReason" target="question" />
355
365
  </zeebe:ioMapping>
356
366
  </bpmn:extensionElements>
357
- <bpmn:incoming>f_convergeBlocked</bpmn:incoming>
367
+ <bpmn:incoming>f_ackEscalate</bpmn:incoming>
358
368
  <bpmn:outgoing>f_blockedGate</bpmn:outgoing>
359
369
  </bpmn:serviceTask>
360
370
  <bpmn:serviceTask id="classify-scope" name="Scope classifier (agent)">
@@ -407,9 +417,13 @@
407
417
  <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=status = "converged"</bpmn:conditionExpression>
408
418
  </bpmn:sequenceFlow>
409
419
  <bpmn:sequenceFlow id="f_toConvergeGate" sourceRef="check-converge" targetRef="gw-converge-gate" />
410
- <bpmn:sequenceFlow id="f_convergeBlocked" name="unaddressed comments" sourceRef="gw-converge-gate" targetRef="persist-escalation-blockedcomments">
420
+ <bpmn:sequenceFlow id="f_convergeBlocked" name="unaddressed comments" sourceRef="gw-converge-gate" targetRef="gw-ack-retry">
411
421
  <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=convergeBlocked = true</bpmn:conditionExpression>
412
422
  </bpmn:sequenceFlow>
423
+ <bpmn:sequenceFlow id="f_ackRetry" name="ack-only within budget" sourceRef="gw-ack-retry" targetRef="capture-head">
424
+ <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=convergeAckOnly = true and ackRetryRound &lt;= ackRetryMax</bpmn:conditionExpression>
425
+ </bpmn:sequenceFlow>
426
+ <bpmn:sequenceFlow id="f_ackEscalate" name="threads / budget exhausted" sourceRef="gw-ack-retry" targetRef="persist-escalation-blockedcomments" />
413
427
  <bpmn:sequenceFlow id="f_convergeStale" name="review stale" sourceRef="gw-converge-gate" targetRef="persist-round">
414
428
  <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=reviewStale = true</bpmn:conditionExpression>
415
429
  </bpmn:sequenceFlow>
@@ -556,8 +570,14 @@
556
570
  <dc:Bounds x="954" y="62" width="74" height="28" />
557
571
  </bpmndi:BPMNLabel>
558
572
  </bpmndi:BPMNShape>
573
+ <bpmndi:BPMNShape id="BPMNShape_gw-ack-retry" bpmnElement="gw-ack-retry" isMarkerVisible="true">
574
+ <dc:Bounds x="1141" y="415" width="50" height="50" />
575
+ <bpmndi:BPMNLabel>
576
+ <dc:Bounds x="1136" y="368" width="60" height="42" />
577
+ </bpmndi:BPMNLabel>
578
+ </bpmndi:BPMNShape>
559
579
  <bpmndi:BPMNShape id="BPMNShape_persist-escalation-blockedcomments" bpmnElement="persist-escalation-blockedcomments">
560
- <dc:Bounds x="1116" y="400" width="100" height="80" />
580
+ <dc:Bounds x="1316" y="400" width="100" height="80" />
561
581
  </bpmndi:BPMNShape>
562
582
  <bpmndi:BPMNShape id="BPMNShape_classify-scope" bpmnElement="classify-scope">
563
583
  <dc:Bounds x="1116" y="80" width="100" height="80" />
@@ -622,6 +642,18 @@
622
642
  <di:waypoint x="1616" y="120" />
623
643
  <di:waypoint x="1723" y="120" />
624
644
  </bpmndi:BPMNEdge>
645
+ <bpmndi:BPMNEdge id="BPMNEdge_f_ackEscalate" bpmnElement="f_ackEscalate">
646
+ <di:waypoint x="1191" y="440" />
647
+ <di:waypoint x="1316" y="440" />
648
+ <bpmndi:BPMNLabel>
649
+ <dc:Bounds x="1220" y="393" width="67" height="42" />
650
+ </bpmndi:BPMNLabel>
651
+ </bpmndi:BPMNEdge>
652
+ <bpmndi:BPMNEdge id="BPMNEdge_f_blockedGate" bpmnElement="f_blockedGate">
653
+ <di:waypoint x="1416" y="440" />
654
+ <di:waypoint x="2427" y="440" />
655
+ <di:waypoint x="2427" y="305" />
656
+ </bpmndi:BPMNEdge>
625
657
  <bpmndi:BPMNEdge id="BPMNEdge_f_guardOk" bpmnElement="f_guardOk">
626
658
  <di:waypoint x="1766" y="600" />
627
659
  <di:waypoint x="1891" y="600" />
@@ -672,15 +704,15 @@
672
704
  <di:waypoint x="1166" y="660" />
673
705
  <di:waypoint x="1166" y="640" />
674
706
  <bpmndi:BPMNLabel>
675
- <dc:Bounds x="2159" y="613" width="74" height="42" />
707
+ <dc:Bounds x="2219" y="613" width="74" height="42" />
676
708
  </bpmndi:BPMNLabel>
677
709
  </bpmndi:BPMNEdge>
678
710
  <bpmndi:BPMNEdge id="BPMNEdge_f_convergeBlocked" bpmnElement="f_convergeBlocked">
679
711
  <di:waypoint x="991" y="145" />
680
712
  <di:waypoint x="991" y="440" />
681
- <di:waypoint x="1116" y="440" />
713
+ <di:waypoint x="1141" y="440" />
682
714
  <bpmndi:BPMNLabel>
683
- <dc:Bounds x="1013" y="407" width="81" height="28" />
715
+ <dc:Bounds x="1026" y="407" width="81" height="28" />
684
716
  </bpmndi:BPMNLabel>
685
717
  </bpmndi:BPMNEdge>
686
718
  <bpmndi:BPMNEdge id="BPMNEdge_f_convergeStale" bpmnElement="f_convergeStale">
@@ -750,22 +782,16 @@
750
782
  <di:waypoint x="1616" y="280" />
751
783
  <di:waypoint x="2402" y="280" />
752
784
  </bpmndi:BPMNEdge>
753
- <bpmndi:BPMNEdge id="BPMNEdge_f_blockedGate" bpmnElement="f_blockedGate">
754
- <di:waypoint x="1216" y="440" />
755
- <di:waypoint x="1636" y="440" />
756
- <di:waypoint x="1636" y="305" />
757
- <di:waypoint x="2382" y="305" />
758
- <di:waypoint x="2382" y="280" />
759
- <di:waypoint x="2402" y="280" />
760
- </bpmndi:BPMNEdge>
761
785
  <bpmndi:BPMNEdge id="BPMNEdge_f_reviewStalled" bpmnElement="f_reviewStalled">
762
786
  <di:waypoint x="2102" y="760" />
763
787
  <di:waypoint x="2202" y="760" />
764
788
  </bpmndi:BPMNEdge>
765
789
  <bpmndi:BPMNEdge id="BPMNEdge_f_stalledGate" bpmnElement="f_stalledGate">
766
- <di:waypoint x="2252" y="720" />
767
- <di:waypoint x="2252" y="280" />
768
- <di:waypoint x="2402" y="280" />
790
+ <di:waypoint x="2302" y="760" />
791
+ <di:waypoint x="2472" y="760" />
792
+ <di:waypoint x="2472" y="305" />
793
+ <di:waypoint x="2472" y="280" />
794
+ <di:waypoint x="2452" y="280" />
769
795
  </bpmndi:BPMNEdge>
770
796
  <bpmndi:BPMNEdge id="BPMNEdge_f_escGate" bpmnElement="f_escGate">
771
797
  <di:waypoint x="866" y="1240" />
@@ -780,16 +806,24 @@
780
806
  </bpmndi:BPMNEdge>
781
807
  <bpmndi:BPMNEdge id="BPMNEdge_f_maxGate" bpmnElement="f_maxGate">
782
808
  <di:waypoint x="1966" y="920" />
783
- <di:waypoint x="2322" y="920" />
784
- <di:waypoint x="2322" y="305" />
785
- <di:waypoint x="2382" y="305" />
786
- <di:waypoint x="2382" y="280" />
787
- <di:waypoint x="2402" y="280" />
809
+ <di:waypoint x="2472" y="920" />
810
+ <di:waypoint x="2472" y="305" />
811
+ <di:waypoint x="2472" y="280" />
812
+ <di:waypoint x="2452" y="280" />
788
813
  </bpmndi:BPMNEdge>
789
814
  <bpmndi:BPMNEdge id="BPMNEdge_f_answerRecord" bpmnElement="f_answerRecord">
790
815
  <di:waypoint x="2652" y="280" />
791
816
  <di:waypoint x="2752" y="280" />
792
817
  </bpmndi:BPMNEdge>
818
+ <bpmndi:BPMNEdge id="BPMNEdge_f_ackRetry" bpmnElement="f_ackRetry">
819
+ <di:waypoint x="1166" y="465" />
820
+ <di:waypoint x="1166" y="485" />
821
+ <di:waypoint x="266" y="485" />
822
+ <di:waypoint x="266" y="160" />
823
+ <bpmndi:BPMNLabel>
824
+ <dc:Bounds x="686" y="438" width="60" height="42" />
825
+ </bpmndi:BPMNLabel>
826
+ </bpmndi:BPMNEdge>
793
827
  <bpmndi:BPMNEdge id="BPMNEdge_f_huskRetry" bpmnElement="f_huskRetry">
794
828
  <di:waypoint x="1741" y="1105" />
795
829
  <di:waypoint x="1741" y="1125" />
@@ -174,6 +174,7 @@
174
174
  <zeebe:calledElement processId="implement-cell" />
175
175
  <zeebe:ioMapping>
176
176
  <zeebe:input source="=task" target="task" />
177
+ <zeebe:input source="=baseBranch" target="baseBranch" />
177
178
  <zeebe:input source="=if (is defined(baseBranchBrief)) then baseBranchBrief else null" target="baseBranchBrief" />
178
179
  <zeebe:input source="=if (is defined(resolvedArtifacts)) then resolvedArtifacts else null" target="resolvedArtifacts" />
179
180
  <zeebe:input source="=if (is defined(customInstructions)) then customInstructions else null" target="customInstructions" />
@@ -37,6 +37,23 @@
37
37
  <bpmn:outgoing>ic_escalate</bpmn:outgoing>
38
38
  <bpmn:outgoing>ic_done</bpmn:outgoing>
39
39
  </bpmn:exclusiveGateway>
40
+ <bpmn:serviceTask id="reconcile-implement" name="Reconcile from GitHub">
41
+ <bpmn:extensionElements>
42
+ <zeebe:taskDefinition type="pr.reconcile-implement" />
43
+ <zeebe:ioMapping>
44
+ <zeebe:output source="=reconciled" target="reconciled" />
45
+ <zeebe:output source="=status" target="status" />
46
+ <zeebe:output source="=pr" target="pr" />
47
+ </zeebe:ioMapping>
48
+ </bpmn:extensionElements>
49
+ <bpmn:incoming>ic_escalate</bpmn:incoming>
50
+ <bpmn:outgoing>ic_toReconcileGw</bpmn:outgoing>
51
+ </bpmn:serviceTask>
52
+ <bpmn:exclusiveGateway id="ic_reconcile_gw" name="reconciled?" default="ic_toRecordEscalation">
53
+ <bpmn:incoming>ic_toReconcileGw</bpmn:incoming>
54
+ <bpmn:outgoing>ic_reconciled</bpmn:outgoing>
55
+ <bpmn:outgoing>ic_toRecordEscalation</bpmn:outgoing>
56
+ </bpmn:exclusiveGateway>
40
57
  <bpmn:callActivity id="escalate" name="Human escalation">
41
58
  <bpmn:extensionElements>
42
59
  <zeebe:calledElement processId="human-escalation" />
@@ -61,7 +78,7 @@
61
78
  <zeebe:output source="=question" target="question" />
62
79
  </zeebe:ioMapping>
63
80
  </bpmn:extensionElements>
64
- <bpmn:incoming>ic_escalate</bpmn:incoming>
81
+ <bpmn:incoming>ic_toRecordEscalation</bpmn:incoming>
65
82
  <bpmn:outgoing>ic_toEscalate</bpmn:outgoing>
66
83
  </bpmn:serviceTask>
67
84
  <bpmn:exclusiveGateway id="ic_gw_answer" name="answer?" default="ic_abandon">
@@ -81,6 +98,7 @@
81
98
  </bpmn:serviceTask>
82
99
  <bpmn:endEvent id="ic_end" name="Task done">
83
100
  <bpmn:incoming>ic_done</bpmn:incoming>
101
+ <bpmn:incoming>ic_reconciled</bpmn:incoming>
84
102
  <bpmn:incoming>ic_abandon</bpmn:incoming>
85
103
  </bpmn:endEvent>
86
104
  <bpmn:sequenceFlow id="ic_toImplement" sourceRef="Start" targetRef="implement-task" />
@@ -88,7 +106,12 @@
88
106
  <bpmn:sequenceFlow id="ic_done" name="done" sourceRef="ic_gw" targetRef="ic_end">
89
107
  <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=status = "opened" or status = "blocked" or status = "skipped"</bpmn:conditionExpression>
90
108
  </bpmn:sequenceFlow>
91
- <bpmn:sequenceFlow id="ic_escalate" name="escalated" sourceRef="ic_gw" targetRef="record-escalation" />
109
+ <bpmn:sequenceFlow id="ic_escalate" name="reconcile" sourceRef="ic_gw" targetRef="reconcile-implement" />
110
+ <bpmn:sequenceFlow id="ic_toReconcileGw" sourceRef="reconcile-implement" targetRef="ic_reconcile_gw" />
111
+ <bpmn:sequenceFlow id="ic_reconciled" name="adopted" sourceRef="ic_reconcile_gw" targetRef="ic_end">
112
+ <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=reconciled = true</bpmn:conditionExpression>
113
+ </bpmn:sequenceFlow>
114
+ <bpmn:sequenceFlow id="ic_toRecordEscalation" name="escalated" sourceRef="ic_reconcile_gw" targetRef="record-escalation" />
92
115
  <bpmn:sequenceFlow id="ic_toEscalate" sourceRef="record-escalation" targetRef="escalate" />
93
116
  <bpmn:sequenceFlow id="ic_toAnswerGw" sourceRef="escalate" targetRef="ic_gw_answer" />
94
117
  <bpmn:sequenceFlow id="ic_answerLoop" name="answer" sourceRef="ic_gw_answer" targetRef="record-implementing">
@@ -114,25 +137,34 @@
114
137
  <dc:Bounds x="406" y="62" width="70" height="28" />
115
138
  </bpmndi:BPMNLabel>
116
139
  </bpmndi:BPMNShape>
140
+ <bpmndi:BPMNShape id="BPMNShape_reconcile-implement" bpmnElement="reconcile-implement">
141
+ <dc:Bounds x="566" y="80" width="100" height="80" />
142
+ </bpmndi:BPMNShape>
143
+ <bpmndi:BPMNShape id="BPMNShape_ic_reconcile_gw" bpmnElement="ic_reconcile_gw" isMarkerVisible="true">
144
+ <dc:Bounds x="766" y="95" width="50" height="50" />
145
+ <bpmndi:BPMNLabel>
146
+ <dc:Bounds x="751" y="76" width="81" height="14" />
147
+ </bpmndi:BPMNLabel>
148
+ </bpmndi:BPMNShape>
117
149
  <bpmndi:BPMNShape id="BPMNShape_escalate" bpmnElement="escalate">
118
- <dc:Bounds x="766" y="80" width="100" height="80" />
150
+ <dc:Bounds x="1116" y="80" width="100" height="80" />
119
151
  </bpmndi:BPMNShape>
120
152
  <bpmndi:BPMNShape id="BPMNShape_record-escalation" bpmnElement="record-escalation">
121
- <dc:Bounds x="566" y="80" width="100" height="80" />
153
+ <dc:Bounds x="916" y="80" width="100" height="80" />
122
154
  </bpmndi:BPMNShape>
123
155
  <bpmndi:BPMNShape id="BPMNShape_ic_gw_answer" bpmnElement="ic_gw_answer" isMarkerVisible="true">
124
- <dc:Bounds x="966" y="95" width="50" height="50" />
156
+ <dc:Bounds x="1316" y="95" width="50" height="50" />
125
157
  <bpmndi:BPMNLabel>
126
- <dc:Bounds x="963" y="76" width="56" height="14" />
158
+ <dc:Bounds x="1313" y="76" width="56" height="14" />
127
159
  </bpmndi:BPMNLabel>
128
160
  </bpmndi:BPMNShape>
129
161
  <bpmndi:BPMNShape id="BPMNShape_record-implementing" bpmnElement="record-implementing">
130
- <dc:Bounds x="1116" y="240" width="100" height="80" />
162
+ <dc:Bounds x="1466" y="240" width="100" height="80" />
131
163
  </bpmndi:BPMNShape>
132
164
  <bpmndi:BPMNShape id="BPMNShape_ic_end" bpmnElement="ic_end">
133
- <dc:Bounds x="1148" y="102" width="36" height="36" />
165
+ <dc:Bounds x="1498" y="102" width="36" height="36" />
134
166
  <bpmndi:BPMNLabel>
135
- <dc:Bounds x="1133" y="83" width="66" height="14" />
167
+ <dc:Bounds x="1483" y="83" width="66" height="14" />
136
168
  </bpmndi:BPMNLabel>
137
169
  </bpmndi:BPMNShape>
138
170
  <bpmndi:BPMNEdge id="BPMNEdge_ic_toImplement" bpmnElement="ic_toImplement">
@@ -150,41 +182,61 @@
150
182
  <dc:Bounds x="483" y="128" width="67" height="14" />
151
183
  </bpmndi:BPMNLabel>
152
184
  </bpmndi:BPMNEdge>
153
- <bpmndi:BPMNEdge id="BPMNEdge_ic_toEscalate" bpmnElement="ic_toEscalate">
185
+ <bpmndi:BPMNEdge id="BPMNEdge_ic_toReconcileGw" bpmnElement="ic_toReconcileGw">
154
186
  <di:waypoint x="666" y="120" />
155
187
  <di:waypoint x="766" y="120" />
156
188
  </bpmndi:BPMNEdge>
189
+ <bpmndi:BPMNEdge id="BPMNEdge_ic_toRecordEscalation" bpmnElement="ic_toRecordEscalation">
190
+ <di:waypoint x="816" y="120" />
191
+ <di:waypoint x="916" y="120" />
192
+ <bpmndi:BPMNLabel>
193
+ <dc:Bounds x="833" y="128" width="67" height="14" />
194
+ </bpmndi:BPMNLabel>
195
+ </bpmndi:BPMNEdge>
196
+ <bpmndi:BPMNEdge id="BPMNEdge_ic_toEscalate" bpmnElement="ic_toEscalate">
197
+ <di:waypoint x="1016" y="120" />
198
+ <di:waypoint x="1116" y="120" />
199
+ </bpmndi:BPMNEdge>
157
200
  <bpmndi:BPMNEdge id="BPMNEdge_ic_toAnswerGw" bpmnElement="ic_toAnswerGw">
158
- <di:waypoint x="866" y="120" />
159
- <di:waypoint x="966" y="120" />
201
+ <di:waypoint x="1216" y="120" />
202
+ <di:waypoint x="1316" y="120" />
160
203
  </bpmndi:BPMNEdge>
161
204
  <bpmndi:BPMNEdge id="BPMNEdge_ic_abandon" bpmnElement="ic_abandon">
162
- <di:waypoint x="1016" y="120" />
163
- <di:waypoint x="1148" y="120" />
205
+ <di:waypoint x="1366" y="120" />
206
+ <di:waypoint x="1498" y="120" />
164
207
  <bpmndi:BPMNLabel>
165
- <dc:Bounds x="1056" y="98" width="53" height="14" />
208
+ <dc:Bounds x="1406" y="98" width="53" height="14" />
166
209
  </bpmndi:BPMNLabel>
167
210
  </bpmndi:BPMNEdge>
168
211
  <bpmndi:BPMNEdge id="BPMNEdge_ic_answerLoop" bpmnElement="ic_answerLoop">
169
- <di:waypoint x="991" y="145" />
170
- <di:waypoint x="991" y="280" />
171
- <di:waypoint x="1116" y="280" />
212
+ <di:waypoint x="1341" y="145" />
213
+ <di:waypoint x="1341" y="280" />
214
+ <di:waypoint x="1466" y="280" />
172
215
  <bpmndi:BPMNLabel>
173
- <dc:Bounds x="996" y="206" width="49" height="14" />
216
+ <dc:Bounds x="1346" y="206" width="49" height="14" />
174
217
  </bpmndi:BPMNLabel>
175
218
  </bpmndi:BPMNEdge>
176
219
  <bpmndi:BPMNEdge id="BPMNEdge_ic_done" bpmnElement="ic_done">
177
220
  <di:waypoint x="441" y="145" />
178
- <di:waypoint x="441" y="180" />
179
- <di:waypoint x="1166" y="180" />
180
- <di:waypoint x="1166" y="138" />
221
+ <di:waypoint x="441" y="200" />
222
+ <di:waypoint x="1516" y="200" />
223
+ <di:waypoint x="1516" y="138" />
224
+ <bpmndi:BPMNLabel>
225
+ <dc:Bounds x="963" y="208" width="32" height="14" />
226
+ </bpmndi:BPMNLabel>
227
+ </bpmndi:BPMNEdge>
228
+ <bpmndi:BPMNEdge id="BPMNEdge_ic_reconciled" bpmnElement="ic_reconciled">
229
+ <di:waypoint x="791" y="145" />
230
+ <di:waypoint x="791" y="180" />
231
+ <di:waypoint x="1516" y="180" />
232
+ <di:waypoint x="1516" y="138" />
181
233
  <bpmndi:BPMNLabel>
182
- <dc:Bounds x="788" y="188" width="32" height="14" />
234
+ <dc:Bounds x="1047" y="158" width="53" height="14" />
183
235
  </bpmndi:BPMNLabel>
184
236
  </bpmndi:BPMNEdge>
185
237
  <bpmndi:BPMNEdge id="BPMNEdge_ic_reImplement" bpmnElement="ic_reImplement">
186
- <di:waypoint x="1166" y="320" />
187
- <di:waypoint x="1166" y="340" />
238
+ <di:waypoint x="1516" y="320" />
239
+ <di:waypoint x="1516" y="340" />
188
240
  <di:waypoint x="266" y="340" />
189
241
  <di:waypoint x="266" y="160" />
190
242
  </bpmndi:BPMNEdge>
@@ -443,6 +443,7 @@
443
443
  <zeebe:ioMapping>
444
444
  <zeebe:input source="=task" target="task" />
445
445
  <zeebe:input source="=planKey" target="subjectKey" />
446
+ <zeebe:input source="=baseBranch" target="baseBranch" />
446
447
  <zeebe:input source="=escalationSlaTimeout" target="escalationSlaTimeout" />
447
448
  <zeebe:input source="=if (is defined(escalationAssignee)) then escalationAssignee else null" target="escalationAssignee" />
448
449
  <zeebe:input source="=if (is defined(baseBranchBrief)) then baseBranchBrief else null" target="baseBranchBrief" />
@@ -71,8 +71,11 @@ events` diagnostic pins this against the goldens themselves.
71
71
  sufficient_. Three golden features have no structured-builder derivation, each
72
72
  pinned by a diagnostic in `derivation-parity.test.ts`:
73
73
 
74
- 1. **Task-level back-edge merge.** The loop head `review-round` is a
75
- `serviceTask` that merges **four** back-edges directly (`in=4`). But `loop()`
74
+ 1. **Task-level back-edge merge.** The loop head `capture-head` is a
75
+ `serviceTask` that merges **five** back-edges directly (`in=5`: `f_start`,
76
+ `f_reviewLoop`, `f_answerLoop`, the #786 husk auto-retry `f_huskRetry`, and
77
+ the bounded auto-ack re-entry `f_ackRetry` added in #796); `review-round`
78
+ then takes its single `f_capture` in-edge. But `loop()`
76
79
  always inserts an exclusive-gateway loop head that absorbs the back-edge, so
77
80
  the body task stays `in=1` — empirically demonstrated by the `loop() inserts a
78
81
  gateway head` test.
@@ -125,13 +125,14 @@ test("convergence-loop golden has arbitrary-graph features the structured builde
125
125
  const body = rest.slice(0, close);
126
126
  return (body.match(new RegExp(`<bpmn:${tag}\\b`, "g")) ?? []).length;
127
127
  };
128
- // (a) the loop head is a serviceTask that MERGES four back-edges directly — after #786/#789 the
128
+ // (a) the loop head is a serviceTask that MERGES its back-edges directly — after #786/#789 the
129
129
  // round-entry head capture (`capture-head`) is the loop head sitting BEFORE `review-round`, so it
130
- // is `capture-head` that absorbs the four back-edges (the review loop, the answer resume, the
131
- // escalation re-enter, and the #786 husk auto-retry); `review-round` then takes its single
132
- // `f_capture` in-edge. A serviceTask merging four back-edges directly is the arbitrary-graph shape
133
- // the structured builder cannot emit — the feature this asserts, now on `capture-head`.
134
- assertEquals(between("capture-head", "serviceTask", "incoming"), 4, "capture-head should merge 4 flows on the task itself");
130
+ // is `capture-head` that absorbs the back-edges (f_start, the review loop, the answer resume, the
131
+ // #786 husk auto-retry, and the #796 bounded auto-ack re-entry f_ackRetry); `review-round` then
132
+ // takes its single `f_capture` in-edge. A serviceTask merging FIVE back-edges directly is the
133
+ // arbitrary-graph shape the structured builder cannot emit — the feature this asserts, now on
134
+ // `capture-head`.
135
+ assertEquals(between("capture-head", "serviceTask", "incoming"), 5, "capture-head should merge 5 flows on the task itself");
135
136
  assertEquals(between("review-round", "serviceTask", "incoming"), 1, "review-round now takes the single f_capture in-edge");
136
137
  // (b) a single exclusive gateway forks FOUR heterogeneous-condition out-edges.
137
138
  assertEquals(between("gw-status", "exclusiveGateway", "outgoing"), 4, "gw-status should be a 4-way exclusive gateway");
@@ -143,8 +144,9 @@ test("convergence-loop golden has arbitrary-graph features the structured builde
143
144
  // CLASS 2, empirical — demonstrate WHY the structured builder cannot reproduce
144
145
  // (a): a `loop()` whose body starts with a task derives an exclusive-gateway
145
146
  // loop head that absorbs the back-edge (in>=2), leaving the task itself at
146
- // in=1. The golden instead merges its back-edges directly into `review-round`
147
- // (in=4) with no loop-head gateway a shape the builder cannot express.
147
+ // in=1. The golden instead merges its back-edges directly into `capture-head`
148
+ // (capture-head in=5: f_start, f_reviewLoop, f_answerLoop, the #786 f_huskRetry, and the #796
149
+ // f_ackRetry re-entry) with no loop-head gateway — a shape the builder cannot express.
148
150
  test("loop() inserts a gateway head, so back-edges cannot merge into a task", () => {
149
151
  const probe = defineFlow("loop-head-probe", (w) => {
150
152
  w.loop((b) => {
@@ -23,8 +23,8 @@
23
23
  // (2) ARBITRARY control-flow graph (convergence-loop). Single start/end —
24
24
  // so it clears class (1) — but its topology is NOT expressible with the
25
25
  // structured-only builder (`loop`/`switch`/`branch`), empirically proven
26
- // (see `derivation-parity.test.ts`): its loop head `review-round` is a
27
- // serviceTask that MERGES four back-edges directly (in=4), whereas
26
+ // (see `derivation-parity.test.ts`): its loop head `capture-head` is a
27
+ // serviceTask that MERGES five back-edges directly (in=5), whereas
28
28
  // `loop()` always inserts an exclusive-gateway loop head (the task stays
29
29
  // in=1); `gw-status` is a single exclusive gateway with FOUR
30
30
  // heterogeneous-condition out-edges (two `=x = "v"`, one complex boolean,
@@ -207,8 +207,10 @@ export const PORTS: readonly PortEntry[] = [
207
207
  "blocked (arbitrary control-flow graph): single top-level start/end, but " +
208
208
  "its topology is not expressible with the published @nanobpm/workflow's " +
209
209
  "structured-only builder (loop/switch/branch). Proven in the test suite: " +
210
- "the loop head `review-round` is a serviceTask that merges 4 back-edges " +
211
- "directly (in=4), but loop() always inserts an exclusive-gateway head " +
210
+ "the loop head `capture-head` is a serviceTask that merges 5 back-edges " +
211
+ "directly (in=5: f_start, f_reviewLoop, f_answerLoop, the #786 husk " +
212
+ "auto-retry f_huskRetry, and the #796 bounded auto-ack re-entry " +
213
+ "f_ackRetry), but loop() always inserts an exclusive-gateway head " +
212
214
  "(task stays in=1); `gw-status` is one gateway with 4 heterogeneous-" +
213
215
  "condition out-edges (no switch/branch emits that); `gw-escalated` is one " +
214
216
  "gateway that is at once a 6-way merge and a 2-way split. Awaits an " +
@@ -6,15 +6,28 @@
6
6
  // the comment unaddressed). This step runs on the converged path, BEFORE the scope classifier and
7
7
  // pr.finalize hand off to the merge loop, and blocks handoff while GitHub still shows unaddressed
8
8
  // comments:
9
- // • any review THREAD is still unresolved (GraphQL `isResolved = false`), or
9
+ // • any SUBSTANTIVE review THREAD is still unresolved (GraphQL `isResolved = false`), or an
10
+ // unresolved `nano-ack:` ACK thread is open — an ack thread is a partially-completed
11
+ // acknowledgement the bounded #796 auto-ack retry can finish, so it still BLOCKS (a
12
+ // genuinely-open GitHub thread) but the block stays ack-only (recoverable) rather than escalating
13
+ // to a human; only a substantive open thread escalates, or
10
14
  // • any SUPPRESSED advisory in the latest Copilot review body lacks a matching RESOLVED ack
11
15
  // thread (a `nano-ack: <path> :: <verbatim advisory text>` marker whose line-stable prose
12
16
  // fingerprint matches Copilot's advisory). The bare legacy `nano-ack: <path>:<line>` form is
13
17
  // NOT honoured: keyed only on `path:line`, it is blind to the advisory prose and would let a
14
18
  // resolved ack for one advisory silently acknowledge a genuinely new advisory re-emitted at
15
19
  // that same line (a false-OPEN). Only the prose-keyed `<path> :: <text>` form acknowledges.
16
- // A blocked gate returns `convergeBlocked = true`; the model's `gw-converge-gate` gateway routes to
17
- // the human `wait-answer` escalation (recoverable), never a hard wedge.
20
+ // A blocked gate returns `convergeBlocked = true` (recoverable, never a hard wedge); the route then
21
+ // depends on WHY it blocked. An ack-only block (`convergeAckOnly = true` sole cause is unacked
22
+ // suppressed advisories and/or an unresolved `nano-ack:` ack thread) re-dispatches the `review-round`
23
+ // agent first, bounded by `ackRetryMax`, and only reaches the human `wait-answer` escalation once
24
+ // that budget is exhausted. A substantive unresolved thread routes to `wait-answer` immediately.
25
+ //
26
+ // STALE-REVIEW GUARD (issue #799): when the PR HEAD has advanced PAST the commit the latest Copilot
27
+ // review was submitted against, that review's body describes code the head has moved past, so the
28
+ // gate signals `reviewStale = true` (rather than blocking on the obsolete body) and the process
29
+ // re-enters the review wait for a fresh review of the current HEAD. The head read fails OPEN, so a
30
+ // transport hiccup can never fabricate a stale verdict.
18
31
  //
19
32
  // Scope integrity is NO LONGER judged here. A deterministic regex over the PR description could not
20
33
  // read the closed issue's acceptance criteria, so it false-positived on any body that merely
@@ -34,6 +47,7 @@ import {
34
47
  fetchLatestCopilotReview,
35
48
  fetchPrHead,
36
49
  fetchReviewThreads,
50
+ isAckThread,
37
51
  parseAckedAdvisories,
38
52
  parseSuppressedAdvisories,
39
53
  type ReviewThread,
@@ -85,23 +99,24 @@ export function makeHandler(deps: {
85
99
  const ghRepo = repo ?? parsed?.repo;
86
100
  const ghNumber = typeof prNumber === "number" ? prNumber : parsed?.number;
87
101
  if (!ghRepo || typeof ghNumber !== "number") {
88
- return { convergeBlocked: true, convergeBlockReason: BLOCK_UNVERIFIABLE, reviewStale: false };
102
+ return { convergeBlocked: true, convergeBlockReason: BLOCK_UNVERIFIABLE, convergeAckOnly: false, reviewStale: false };
89
103
  }
90
104
 
91
105
  let result: ConvergeGateResult;
92
106
  try {
93
107
  const threadsRead = await deps.readThreads(ghRepo, ghNumber);
94
108
  // A null threads read is an unverifiable gate — fail closed. (An empty ARRAY is a verified
95
- // "no threads" and is fine.)
109
+ // "no threads" and is fine.) An unverifiable block is NOT ack-only: it needs a human to
110
+ // confirm GitHub state, so it must not enter the bounded agent auto-ack path (#796).
96
111
  if (threadsRead === null) {
97
- return { convergeBlocked: true, convergeBlockReason: BLOCK_UNVERIFIABLE, reviewStale: false };
112
+ return { convergeBlocked: true, convergeBlockReason: BLOCK_UNVERIFIABLE, convergeAckOnly: false, reviewStale: false };
98
113
  }
99
114
  const review = await deps.readReview(ghRepo, ghNumber);
100
115
  // A null review read is an unverifiable read (no usable transport) — fail closed, same as a
101
116
  // null threads read. (A `{ body: "", commitId: null }` result is a verified "no Copilot
102
117
  // review / no advisories".)
103
118
  if (review === null) {
104
- return { convergeBlocked: true, convergeBlockReason: BLOCK_UNVERIFIABLE, reviewStale: false };
119
+ return { convergeBlocked: true, convergeBlockReason: BLOCK_UNVERIFIABLE, convergeAckOnly: false, reviewStale: false };
105
120
  }
106
121
  // STALE-REVIEW GUARD (issue #799). When the PR HEAD has advanced PAST the commit the latest
107
122
  // Copilot review was submitted against, that review's suppressed advisories describe code the
@@ -113,22 +128,39 @@ export function makeHandler(deps: {
113
128
  // fabricate a stale verdict; the ordinary gate below still runs on a HEAD-current review.
114
129
  const headSha = await readHeadSha(ghRepo, ghNumber).catch(() => null);
115
130
  if (isReviewStale(review.commitId, headSha)) {
116
- return { convergeBlocked: false, convergeBlockReason: "", reviewStale: true };
131
+ return { convergeBlocked: false, convergeBlockReason: "", convergeAckOnly: false, reviewStale: true };
117
132
  }
118
- const unresolvedThreadCount = threadsRead.filter((t) => !t.isResolved).length;
133
+ // Split the unresolved threads into SUBSTANTIVE reviewer findings vs partially-completed
134
+ // `nano-ack:` ACK threads (root carries a canonical `nano-ack: <path> :: <text>` marker — see
135
+ // `isAckThread`). Neither is ever dropped from the gate: a substantive unresolved thread blocks
136
+ // and escalates to a human; an unresolved ack thread ALSO blocks (it is a genuinely-open GitHub
137
+ // thread), but the block stays ack-only (recoverable by the bounded #796 auto-ack retry, which
138
+ // re-posts/resolves it). Keeping the ack thread a BLOCKING condition — rather than filtering it
139
+ // away — is what makes the root-marker classifier fail-CLOSED: a mislabelled substantive thread
140
+ // still blocks (as ack-only) instead of finalizing with the finding open, and the bounded retry
141
+ // cannot ack a non-advisory, so it escalates to a human on exhaustion.
142
+ const unresolved = threadsRead.filter((t) => !t.isResolved);
143
+ const unresolvedAckThreadCount = unresolved.filter((t) => isAckThread(t)).length;
144
+ const unresolvedThreadCount = unresolved.length - unresolvedAckThreadCount;
119
145
  const advisories = parseSuppressedAdvisories(review.body);
120
146
  result = evaluateConvergeGate({
121
147
  unresolvedThreadCount,
148
+ unresolvedAckThreadCount,
122
149
  suppressedAdvisories: advisories.map((a) => ({ key: a.key, label: a.label })),
123
150
  acknowledgedKeys: parseAckedAdvisories(threadsRead),
124
151
  });
125
152
  } catch {
126
- return { convergeBlocked: true, convergeBlockReason: BLOCK_UNVERIFIABLE, reviewStale: false };
153
+ return { convergeBlocked: true, convergeBlockReason: BLOCK_UNVERIFIABLE, convergeAckOnly: false, reviewStale: false };
127
154
  }
128
155
 
129
156
  return {
130
157
  convergeBlocked: result.convergeBlocked,
131
158
  convergeBlockReason: result.convergeBlockReason,
159
+ // Signals the bounded agent auto-ack path (#796): a block whose sole cause is recoverable —
160
+ // unacked suppressed advisories AND/OR a lone unresolved `nano-ack:` ack thread (a
161
+ // partially-completed acknowledgement), with NO substantive unresolved thread — re-dispatches
162
+ // the review-round agent before escalating to a human.
163
+ convergeAckOnly: result.ackOnly,
132
164
  reviewStale: false,
133
165
  };
134
166
  };