@nanobpm/nano-workforce 0.204.1 → 0.205.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/SPEC.md +10 -7
- package/app/agentOutputEnvelope.test.ts +50 -0
- package/app/feature.ts +3 -3
- package/app/featureDelivery.test.ts +2 -0
- package/app/github.test.ts +285 -1
- package/app/github.ts +160 -8
- package/app/mergeApproval.test.ts +20 -1
- package/app/mergeBlockedThreads.test.ts +551 -0
- package/app/mergeLoopBehaviour.test.ts +129 -0
- package/app/mergeStatus.test.ts +169 -0
- package/app/readiness.ts +1 -0
- package/app/service.test.ts +25 -0
- package/app/service.ts +257 -10
- package/app/userTasks.ts +1 -0
- package/docs/agent-guide.md +33 -0
- package/openapi.yaml +34 -0
- package/operations/getAgentInstructions.test.ts +16 -1
- package/operations/listActivePrs.ts +1 -1
- package/operations/startAndMessage.test.ts +13 -3
- package/operations/startConvergenceLoop.ts +10 -4
- package/operations/startFeature.readiness.integration.test.ts +15 -0
- package/operations/startFeature.ts +6 -0
- package/package.json +1 -1
- package/resources/processes/convergence-loop.bpmn +8 -0
- package/resources/processes/merge-loop.bpmn +55 -9
- package/skills/nano-workforce/SKILL.md +7 -0
- package/workers/converge-feature/worker.ts +8 -6
- package/workers/delivery-connector/worker.ts +1 -1
- package/workers/finalize/worker.test.ts +35 -0
- package/workers/finalize/worker.ts +9 -4
- package/workers/merge-stall-probe/worker.ts +17 -3
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
<nano:extend name="prUrl" type="string" />
|
|
104
104
|
<nano:extend name="round" type="integer" />
|
|
105
105
|
<nano:extend name="summary" type="string" optional="true" />
|
|
106
|
+
<nano:extend name="autoMerge" type="boolean" optional="true" />
|
|
106
107
|
<nano:extend name="convergeOnly" type="boolean" optional="true" />
|
|
107
108
|
<nano:extend name="abandonUrl" type="string" optional="true" />
|
|
108
109
|
</nano:shape>
|
|
@@ -161,6 +162,7 @@
|
|
|
161
162
|
<zeebe:input source="=reviewWaitTimeout" target="guardReviewWaitTimeout" />
|
|
162
163
|
<zeebe:input source="=humanApproval" target="guardHumanApproval" />
|
|
163
164
|
<zeebe:input source="=convergeOnly" target="guardConvergeOnly" />
|
|
165
|
+
<zeebe:input source="=autoMerge" target="guardAutoMerge" />
|
|
164
166
|
<zeebe:input source="=reviewStale" target="guardReviewStale" />
|
|
165
167
|
<zeebe:input source="=roundEntryHead" target="guardRoundEntryHead" />
|
|
166
168
|
<zeebe:input source="=abandonUrl" target="guardAbandonUrl" />
|
|
@@ -182,6 +184,7 @@
|
|
|
182
184
|
<zeebe:output source="=guardReviewWaitTimeout" target="reviewWaitTimeout" />
|
|
183
185
|
<zeebe:output source="=(guardHumanApproval = true) or (humanApproval = true)" target="humanApproval" />
|
|
184
186
|
<zeebe:output source="=guardConvergeOnly" target="convergeOnly" />
|
|
187
|
+
<zeebe:output source="=guardAutoMerge" target="autoMerge" />
|
|
185
188
|
<zeebe:output source="=guardReviewStale" target="reviewStale" />
|
|
186
189
|
<zeebe:output source="=guardRoundEntryHead" target="roundEntryHead" />
|
|
187
190
|
<zeebe:output source="=guardAbandonUrl" target="abandonUrl" />
|
|
@@ -255,6 +258,7 @@
|
|
|
255
258
|
<zeebe:input source="=reviewWaitTimeout" target="guardReviewWaitTimeout" />
|
|
256
259
|
<zeebe:input source="=humanApproval" target="guardHumanApproval" />
|
|
257
260
|
<zeebe:input source="=convergeOnly" target="guardConvergeOnly" />
|
|
261
|
+
<zeebe:input source="=autoMerge" target="guardAutoMerge" />
|
|
258
262
|
<zeebe:input source="=reviewStale" target="guardReviewStale" />
|
|
259
263
|
<zeebe:input source="=roundEntryHead" target="guardRoundEntryHead" />
|
|
260
264
|
<zeebe:input source="=abandonUrl" target="guardAbandonUrl" />
|
|
@@ -277,6 +281,7 @@
|
|
|
277
281
|
<zeebe:output source="=guardReviewWaitTimeout" target="reviewWaitTimeout" />
|
|
278
282
|
<zeebe:output source="=(guardHumanApproval = true) or (humanApproval = true)" target="humanApproval" />
|
|
279
283
|
<zeebe:output source="=guardConvergeOnly" target="convergeOnly" />
|
|
284
|
+
<zeebe:output source="=guardAutoMerge" target="autoMerge" />
|
|
280
285
|
<zeebe:output source="=guardReviewStale" target="reviewStale" />
|
|
281
286
|
<zeebe:output source="=guardRoundEntryHead" target="roundEntryHead" />
|
|
282
287
|
<zeebe:output source="=guardAbandonUrl" target="abandonUrl" />
|
|
@@ -474,6 +479,7 @@
|
|
|
474
479
|
<zeebe:ioMapping>
|
|
475
480
|
<zeebe:input source="=prKey" target="prKey" />
|
|
476
481
|
<zeebe:output source="=0" target="huskRetries" />
|
|
482
|
+
<zeebe:output source="=if mergeDecision = "approve" then false else humanApproval" target="humanApproval" />
|
|
477
483
|
</zeebe:ioMapping>
|
|
478
484
|
</bpmn:extensionElements>
|
|
479
485
|
<bpmn:incoming>f_needApproval</bpmn:incoming>
|
|
@@ -563,6 +569,7 @@
|
|
|
563
569
|
<zeebe:input source="=reviewWaitTimeout" target="guardReviewWaitTimeout" />
|
|
564
570
|
<zeebe:input source="=humanApproval" target="guardHumanApproval" />
|
|
565
571
|
<zeebe:input source="=convergeOnly" target="guardConvergeOnly" />
|
|
572
|
+
<zeebe:input source="=autoMerge" target="guardAutoMerge" />
|
|
566
573
|
<zeebe:input source="=reviewStale" target="guardReviewStale" />
|
|
567
574
|
<zeebe:input source="=roundEntryHead" target="guardRoundEntryHead" />
|
|
568
575
|
<zeebe:input source="=abandonUrl" target="guardAbandonUrl" />
|
|
@@ -582,6 +589,7 @@
|
|
|
582
589
|
<zeebe:output source="=guardReviewWaitTimeout" target="reviewWaitTimeout" />
|
|
583
590
|
<zeebe:output source="=(guardHumanApproval = true) or (humanApproval = true)" target="humanApproval" />
|
|
584
591
|
<zeebe:output source="=guardConvergeOnly" target="convergeOnly" />
|
|
592
|
+
<zeebe:output source="=guardAutoMerge" target="autoMerge" />
|
|
585
593
|
<zeebe:output source="=guardReviewStale" target="reviewStale" />
|
|
586
594
|
<zeebe:output source="=guardRoundEntryHead" target="roundEntryHead" />
|
|
587
595
|
<zeebe:output source="=guardAbandonUrl" target="abandonUrl" />
|
|
@@ -53,11 +53,15 @@
|
|
|
53
53
|
<nano:extend name="mergeState" type="string" />
|
|
54
54
|
<nano:extend name="failingChecks" type="integer" optional="true" />
|
|
55
55
|
<nano:extend name="failingChecksList" type="string" optional="true" />
|
|
56
|
+
<nano:extend name="reviewThreads" type="integer" optional="true" />
|
|
57
|
+
<nano:extend name="pendingChecks" type="integer" optional="true" />
|
|
56
58
|
</nano:shape>
|
|
57
59
|
<nano:shape id="MergeReady" name="merge-ready message payload">
|
|
58
60
|
<nano:extend name="mergeState" type="string" />
|
|
59
61
|
<nano:extend name="failingChecks" type="integer" optional="true" />
|
|
60
62
|
<nano:extend name="failingChecksList" type="string" optional="true" />
|
|
63
|
+
<nano:extend name="reviewThreads" type="integer" optional="true" />
|
|
64
|
+
<nano:extend name="pendingChecks" type="integer" optional="true" />
|
|
61
65
|
</nano:shape>
|
|
62
66
|
<nano:shape id="FixCiIn" name="CI fix — input">
|
|
63
67
|
<nano:extend name="prKey" type="string" />
|
|
@@ -179,6 +183,7 @@
|
|
|
179
183
|
<bpmn:outgoing>f_m_mRebase</bpmn:outgoing>
|
|
180
184
|
<bpmn:outgoing>f_m_mWaiting</bpmn:outgoing>
|
|
181
185
|
<bpmn:outgoing>f_m_mDraft</bpmn:outgoing>
|
|
186
|
+
<bpmn:outgoing>f_m_mThreads</bpmn:outgoing>
|
|
182
187
|
<bpmn:outgoing>f_m_mBlocked</bpmn:outgoing>
|
|
183
188
|
</bpmn:exclusiveGateway>
|
|
184
189
|
<bpmn:serviceTask id="attempt-merge" name="Merge PR">
|
|
@@ -258,7 +263,7 @@
|
|
|
258
263
|
</zeebe:properties>
|
|
259
264
|
<zeebe:ioMapping>
|
|
260
265
|
<zeebe:input source="="blocked"" target="status" />
|
|
261
|
-
<zeebe:input source="=if mergeState = "draft" then "This PR is in draft and can't be merged. Mark it ready (gh pr ready), then reply to retry." else if mergeState = "waiting" then "GitHub never settled this PR's mergeability (still computing after repeated re-polls, or the state could not be read). Check the PR on GitHub — resolve any conflict or failing required check if present — then reply to retry." else "This PR is not mergeable (state: " + mergeState + "). Resolve the conflict or failing required check on the branch, then reply to retry."" target="question" />
|
|
266
|
+
<zeebe:input source="=if mergeState = "draft" then "This PR is in draft and can't be merged. Mark it ready (gh pr ready), then reply to retry." else if mergeState = "waiting" and reviewThreads = 0 and failingChecks = 0 and pendingChecks = 0 then "This PR is merge-blocked with all checks green (none failing, none pending) and every review thread resolved. GitHub still reports BLOCKED, which means a required review approval (or code-owner review) is outstanding — nothing the automated loop can produce. Approve the PR (or adjust the branch-protection review requirement), then reply to retry." else if mergeState = "waiting" then "GitHub never settled this PR's mergeability (still computing or checks still pending after repeated re-polls, or the state could not be read). Check the PR on GitHub — resolve any conflict or failing required check if present — then reply to retry." else "This PR is not mergeable (state: " + mergeState + "). Resolve the conflict or failing required check on the branch, then reply to retry."" target="question" />
|
|
262
267
|
</zeebe:ioMapping>
|
|
263
268
|
</bpmn:extensionElements>
|
|
264
269
|
<bpmn:incoming>f_m_mDraft</bpmn:incoming>
|
|
@@ -287,9 +292,26 @@
|
|
|
287
292
|
<bpmn:incoming>f_mr_giveup</bpmn:incoming>
|
|
288
293
|
<bpmn:outgoing>f_m_escA</bpmn:outgoing>
|
|
289
294
|
</bpmn:serviceTask>
|
|
295
|
+
<bpmn:serviceTask id="merge-esc-threads" name="Escalate: unresolved review threads">
|
|
296
|
+
<bpmn:extensionElements>
|
|
297
|
+
<zeebe:taskDefinition type="pr.persist-escalation" />
|
|
298
|
+
<zeebe:properties>
|
|
299
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="EscalationIn" />
|
|
300
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.out" value="EscalationOut" />
|
|
301
|
+
</zeebe:properties>
|
|
302
|
+
<zeebe:ioMapping>
|
|
303
|
+
<zeebe:input source="="blocked"" target="status" />
|
|
304
|
+
<zeebe:input source="="This PR is merge-blocked by " + string(reviewThreads) + " unresolved review thread(s) (branch protection requires every conversation resolved; all checks are green and there is no conflict). These are review findings the loop already converged, so a fresh review round cannot clear them without a reply. Resolve the threads on the PR (or reply with guidance — e.g. which threads to push back on), then reply to retry."" target="question" />
|
|
305
|
+
<zeebe:input source="=null" target="agentSlaJobType" />
|
|
306
|
+
</zeebe:ioMapping>
|
|
307
|
+
</bpmn:extensionElements>
|
|
308
|
+
<bpmn:incoming>f_m_mThreads</bpmn:incoming>
|
|
309
|
+
<bpmn:outgoing>f_m_escT</bpmn:outgoing>
|
|
310
|
+
</bpmn:serviceTask>
|
|
290
311
|
<bpmn:exclusiveGateway id="gw-merge-escalated" name="escalation opened?" default="f_m_escReenter">
|
|
291
312
|
<bpmn:incoming>f_m_escA</bpmn:incoming>
|
|
292
313
|
<bpmn:incoming>f_m_escLanded</bpmn:incoming>
|
|
314
|
+
<bpmn:incoming>f_m_escT</bpmn:incoming>
|
|
293
315
|
<bpmn:outgoing>f_m_escWait</bpmn:outgoing>
|
|
294
316
|
<bpmn:outgoing>f_m_escReenter</bpmn:outgoing>
|
|
295
317
|
</bpmn:exclusiveGateway>
|
|
@@ -732,6 +754,9 @@
|
|
|
732
754
|
<bpmn:sequenceFlow id="f_m_mDraft" name="draft — mark ready (human)" sourceRef="gw-mergeable" targetRef="merge-esc-conflict">
|
|
733
755
|
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=mergeState = "draft"</bpmn:conditionExpression>
|
|
734
756
|
</bpmn:sequenceFlow>
|
|
757
|
+
<bpmn:sequenceFlow id="f_m_mThreads" name="blocked (unresolved review threads)" sourceRef="gw-mergeable" targetRef="merge-esc-threads">
|
|
758
|
+
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=mergeState = "review-threads"</bpmn:conditionExpression>
|
|
759
|
+
</bpmn:sequenceFlow>
|
|
735
760
|
<bpmn:sequenceFlow id="f_mrp_probe" sourceRef="wait-mergeable-repoll" targetRef="merge-stall-probe" />
|
|
736
761
|
<bpmn:sequenceFlow id="f_m_mBlocked" name="unclassified — auto-rebase" sourceRef="gw-mergeable" targetRef="gw-rebase" />
|
|
737
762
|
<bpmn:sequenceFlow id="f_ci_go" name="within budget" sourceRef="gw-ci-fix" targetRef="SP_cifix">
|
|
@@ -773,6 +798,7 @@
|
|
|
773
798
|
<bpmn:sequenceFlow id="f_m_done" sourceRef="mark-merged" targetRef="MergeEnd" />
|
|
774
799
|
<bpmn:sequenceFlow id="f_m_escC" sourceRef="merge-esc-conflict" targetRef="wait-merge-answer" />
|
|
775
800
|
<bpmn:sequenceFlow id="f_m_escA" sourceRef="merge-esc-attempt" targetRef="gw-merge-escalated" />
|
|
801
|
+
<bpmn:sequenceFlow id="f_m_escT" sourceRef="merge-esc-threads" targetRef="gw-merge-escalated" />
|
|
776
802
|
<bpmn:sequenceFlow id="f_m_escWait" sourceRef="gw-merge-escalated" targetRef="wait-merge-answer">
|
|
777
803
|
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=escalated = true</bpmn:conditionExpression>
|
|
778
804
|
</bpmn:sequenceFlow>
|
|
@@ -833,7 +859,7 @@
|
|
|
833
859
|
<bpmndi:BPMNShape id="BPMNShape_gw-merge-stall" bpmnElement="gw-merge-stall" isMarkerVisible="true">
|
|
834
860
|
<dc:Bounds x="1538" y="1535" width="50" height="50" />
|
|
835
861
|
<bpmndi:BPMNLabel>
|
|
836
|
-
<dc:Bounds x="1521" y="
|
|
862
|
+
<dc:Bounds x="1521" y="1630" width="85" height="42" />
|
|
837
863
|
</bpmndi:BPMNLabel>
|
|
838
864
|
</bpmndi:BPMNShape>
|
|
839
865
|
<bpmndi:BPMNShape id="BPMNShape_gw-mergeable" bpmnElement="gw-mergeable" isMarkerVisible="true">
|
|
@@ -902,6 +928,9 @@
|
|
|
902
928
|
<bpmndi:BPMNShape id="BPMNShape_merge-esc-attempt" bpmnElement="merge-esc-attempt">
|
|
903
929
|
<dc:Bounds x="1688" y="720" width="100" height="80" />
|
|
904
930
|
</bpmndi:BPMNShape>
|
|
931
|
+
<bpmndi:BPMNShape id="BPMNShape_merge-esc-threads" bpmnElement="merge-esc-threads">
|
|
932
|
+
<dc:Bounds x="1138" y="1680" width="100" height="80" />
|
|
933
|
+
</bpmndi:BPMNShape>
|
|
905
934
|
<bpmndi:BPMNShape id="BPMNShape_gw-merge-escalated" bpmnElement="gw-merge-escalated" isMarkerVisible="true">
|
|
906
935
|
<dc:Bounds x="2088" y="95" width="50" height="50" />
|
|
907
936
|
<bpmndi:BPMNLabel>
|
|
@@ -1002,7 +1031,7 @@
|
|
|
1002
1031
|
<di:waypoint x="402" y="1300" />
|
|
1003
1032
|
<di:waypoint x="402" y="320" />
|
|
1004
1033
|
<bpmndi:BPMNLabel>
|
|
1005
|
-
<dc:Bounds x="
|
|
1034
|
+
<dc:Bounds x="919" y="1239" width="88" height="56" />
|
|
1006
1035
|
</bpmndi:BPMNLabel>
|
|
1007
1036
|
</bpmndi:BPMNEdge>
|
|
1008
1037
|
<bpmndi:BPMNEdge id="BPMNEdge_f_reb_rearm" bpmnElement="f_reb_rearm">
|
|
@@ -1179,6 +1208,14 @@
|
|
|
1179
1208
|
<dc:Bounds x="1057" y="483" width="85" height="42" />
|
|
1180
1209
|
</bpmndi:BPMNLabel>
|
|
1181
1210
|
</bpmndi:BPMNEdge>
|
|
1211
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_m_mThreads" bpmnElement="f_m_mThreads">
|
|
1212
|
+
<di:waypoint x="1013" y="305" />
|
|
1213
|
+
<di:waypoint x="1013" y="1720" />
|
|
1214
|
+
<di:waypoint x="1138" y="1720" />
|
|
1215
|
+
<bpmndi:BPMNLabel>
|
|
1216
|
+
<dc:Bounds x="1018" y="1642" width="81" height="56" />
|
|
1217
|
+
</bpmndi:BPMNLabel>
|
|
1218
|
+
</bpmndi:BPMNEdge>
|
|
1182
1219
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_mBlocked" bpmnElement="f_m_mBlocked">
|
|
1183
1220
|
<di:waypoint x="1013" y="305" />
|
|
1184
1221
|
<di:waypoint x="1013" y="440" />
|
|
@@ -1255,7 +1292,7 @@
|
|
|
1255
1292
|
<di:waypoint x="1668" y="740" />
|
|
1256
1293
|
<di:waypoint x="1688" y="740" />
|
|
1257
1294
|
<bpmndi:BPMNLabel>
|
|
1258
|
-
<dc:Bounds x="
|
|
1295
|
+
<dc:Bounds x="1932" y="616" width="53" height="14" />
|
|
1259
1296
|
</bpmndi:BPMNLabel>
|
|
1260
1297
|
</bpmndi:BPMNEdge>
|
|
1261
1298
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_gAbandoned" bpmnElement="f_m_gAbandoned">
|
|
@@ -1281,8 +1318,9 @@
|
|
|
1281
1318
|
<di:waypoint x="870" y="618" />
|
|
1282
1319
|
<di:waypoint x="870" y="638" />
|
|
1283
1320
|
<di:waypoint x="888" y="638" />
|
|
1284
|
-
<di:waypoint x="888" y="
|
|
1285
|
-
<di:waypoint x="
|
|
1321
|
+
<di:waypoint x="888" y="1780" />
|
|
1322
|
+
<di:waypoint x="1388" y="1780" />
|
|
1323
|
+
<di:waypoint x="1388" y="1600" />
|
|
1286
1324
|
</bpmndi:BPMNEdge>
|
|
1287
1325
|
<bpmndi:BPMNEdge id="BPMNEdge_f_msp_gate" bpmnElement="f_msp_gate">
|
|
1288
1326
|
<di:waypoint x="1438" y="1560" />
|
|
@@ -1308,6 +1346,14 @@
|
|
|
1308
1346
|
<di:waypoint x="1988" y="120" />
|
|
1309
1347
|
<di:waypoint x="2088" y="120" />
|
|
1310
1348
|
</bpmndi:BPMNEdge>
|
|
1349
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_m_escT" bpmnElement="f_m_escT">
|
|
1350
|
+
<di:waypoint x="1238" y="1720" />
|
|
1351
|
+
<di:waypoint x="2008" y="1720" />
|
|
1352
|
+
<di:waypoint x="2008" y="145" />
|
|
1353
|
+
<di:waypoint x="2068" y="145" />
|
|
1354
|
+
<di:waypoint x="2068" y="120" />
|
|
1355
|
+
<di:waypoint x="2088" y="120" />
|
|
1356
|
+
</bpmndi:BPMNEdge>
|
|
1311
1357
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_escWait" bpmnElement="f_m_escWait">
|
|
1312
1358
|
<di:waypoint x="2138" y="120" />
|
|
1313
1359
|
<di:waypoint x="2238" y="120" />
|
|
@@ -1327,13 +1373,13 @@
|
|
|
1327
1373
|
<di:waypoint x="1013" y="1620" />
|
|
1328
1374
|
<di:waypoint x="1013" y="305" />
|
|
1329
1375
|
<bpmndi:BPMNLabel>
|
|
1330
|
-
<dc:Bounds x="
|
|
1376
|
+
<dc:Bounds x="1246" y="1587" width="85" height="28" />
|
|
1331
1377
|
</bpmndi:BPMNLabel>
|
|
1332
1378
|
</bpmndi:BPMNEdge>
|
|
1333
1379
|
<bpmndi:BPMNEdge id="BPMNEdge_f_dep_rewait" bpmnElement="f_dep_rewait">
|
|
1334
1380
|
<di:waypoint x="1738" y="1280" />
|
|
1335
|
-
<di:waypoint x="1738" y="
|
|
1336
|
-
<di:waypoint x="234" y="
|
|
1381
|
+
<di:waypoint x="1738" y="1300" />
|
|
1382
|
+
<di:waypoint x="234" y="1300" />
|
|
1337
1383
|
<di:waypoint x="234" y="298" />
|
|
1338
1384
|
</bpmndi:BPMNEdge>
|
|
1339
1385
|
<bpmndi:BPMNEdge id="BPMNEdge_f_mr_go" bpmnElement="f_mr_go">
|
|
@@ -126,3 +126,10 @@ debugging. **Re-fetch it every session.** For one section, use
|
|
|
126
126
|
- **Submissions are idempotent.** Re-submitting the same work must not double-dispatch.
|
|
127
127
|
- **Escalations are for humans.** Surface them with options; don't silently answer design or
|
|
128
128
|
product decisions.
|
|
129
|
+
- **Merge intent is a read, not a write.** Every `/status` PR row carries a `merge` block —
|
|
130
|
+
`requested` (the run's `autoMerge`), `enabled` (the deployment's `NANO_PR_AUTO_MERGE`),
|
|
131
|
+
`effective`, and `blockedBy` (`"env"` | `"human-approval"` | `null`). Read it there; never
|
|
132
|
+
write variables onto a running loop to change it. Such writes are unsupported and
|
|
133
|
+
timing-dependent, not reliable no-ops — one landing while the loop awaits merge approval
|
|
134
|
+
reaches `pr.finalize` unrestored and can change whether merging starts. Cancel and re-submit
|
|
135
|
+
instead.
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// Reached only via the `converge?` gateway's true branch (converge requested AND the agent opened a
|
|
4
4
|
// keyed PR). Reuses `submitPr` — the SAME enrollment the epic waves and the standalone
|
|
5
5
|
// `startConvergenceLoop` action use — so a feature run's PR gets identical review-round + merge
|
|
6
|
-
// behaviour with no duplicated machinery. `autoMerge`
|
|
7
|
-
// (
|
|
6
|
+
// behaviour with no duplicated machinery. The run's positive `autoMerge` is threaded to `submitPr`
|
|
7
|
+
// as-is (issue #916): auto-merge → drive the merge-loop; otherwise stop at `converged`.
|
|
8
8
|
//
|
|
9
9
|
// The feature-run's OWN process ends here; the PR's live convergence/merge state lives on the
|
|
10
10
|
// `pull_requests` row keyed by `pr_key`, which the feature page links to. We therefore leave the
|
|
@@ -40,8 +40,10 @@ const handler: AppJobHandler<In, Record<string, never>> = async (job, app) => {
|
|
|
40
40
|
// (edge 1) heals by re-enrolling when the PR row is still missing (idempotent, mirroring the
|
|
41
41
|
// promotion-PR re-enroll in `pollPromotion`). The reorder is safe: `submitPr` is idempotent on the PR
|
|
42
42
|
// key (it early-returns `alreadyRunning` for a non-terminal PR), and a `converging` row is owned by
|
|
43
|
-
// edge (1), never by the handoff edge.
|
|
44
|
-
//
|
|
43
|
+
// edge (1), never by the handoff edge.
|
|
44
|
+
//
|
|
45
|
+
// `autoMerge` is the canonical positive merge intent (issue #916): converge-only stops at
|
|
46
|
+
// `converged`; auto-merge lets the merge-loop drive the merge.
|
|
45
47
|
await featureRuns(app.data).update(featureKey, {
|
|
46
48
|
status: "converging",
|
|
47
49
|
pr_key: parsed.prKey,
|
|
@@ -51,11 +53,11 @@ const handler: AppJobHandler<In, Record<string, never>> = async (job, app) => {
|
|
|
51
53
|
// read the durable flag off the `feature_runs` row (the source of truth) and hand it to `submitPr`.
|
|
52
54
|
const featureRow = await featureRuns(app.data).get(featureKey);
|
|
53
55
|
const autoResolve = featureRow?.auto_resolve_escalations === 1;
|
|
54
|
-
await submitPr(app.data, app.engine, parsed, [], MAX_ROUNDS,
|
|
56
|
+
await submitPr(app.data, app.engine, parsed, [], MAX_ROUNDS, false, featureKey, humanApproval, autoResolve, autoMerge);
|
|
55
57
|
app.log.info("converge-feature: enrolled PR into convergence loop", {
|
|
56
58
|
featureKey,
|
|
57
59
|
prKey: parsed.prKey,
|
|
58
|
-
|
|
60
|
+
autoMerge,
|
|
59
61
|
humanApproval,
|
|
60
62
|
autoResolveEscalations: autoResolve,
|
|
61
63
|
});
|
|
@@ -217,7 +217,7 @@ const handler: AppJobHandler<In, ConnectorDispatchResult> = async (job, app) =>
|
|
|
217
217
|
// completion can clobber that variable, so the durable row is the only trustworthy source
|
|
218
218
|
// (issue #889 review). Fails closed (false) when no run row matches.
|
|
219
219
|
const autoResolve = await resolveAutoResolveEscalations(app.data, job.processInstanceKey != null ? String(job.processInstanceKey) : null);
|
|
220
|
-
await submitPr(app.data, app.engine, converge.parsed, converge.dependsOn, MAX_ROUNDS, converge.convergeOnly, dedupeKey, false, autoResolve);
|
|
220
|
+
await submitPr(app.data, app.engine, converge.parsed, converge.dependsOn, MAX_ROUNDS, converge.convergeOnly, dedupeKey, false, autoResolve, !converge.convergeOnly);
|
|
221
221
|
app.log.info("delivery-connector: enrolled PR into convergence loop", {
|
|
222
222
|
target,
|
|
223
223
|
dedupeKey,
|
|
@@ -103,3 +103,38 @@ test("finalize with convergeOnly absent hands off to the merge-loop when auto-me
|
|
|
103
103
|
assertEquals(pr.status, "waiting_deps");
|
|
104
104
|
});
|
|
105
105
|
});
|
|
106
|
+
|
|
107
|
+
// Issue #916: `autoMerge` is the CANONICAL positive merge-intent variable on the convergence loop.
|
|
108
|
+
// `pr.finalize` reads it first and falls back to the legacy inverted `convergeOnly` only when
|
|
109
|
+
// `autoMerge` is absent — so an in-flight loop seeded before `autoMerge` existed keeps its behaviour.
|
|
110
|
+
test("finalize with autoMerge=false rests at converged even when the legacy convergeOnly mirror is absent", async () => {
|
|
111
|
+
await withRetroOff(async () => {
|
|
112
|
+
const { app, stores, createdProcesses } = fakeApp();
|
|
113
|
+
// biome-ignore lint/plugin: constructing the framework's job envelope for the handler under test
|
|
114
|
+
await handler({ variables: { ...BASE_VARS, autoMerge: false } } as never, app as never);
|
|
115
|
+
assertEquals(createdProcesses.includes(MERGE_PROCESS_ID), false);
|
|
116
|
+
assertEquals(stores.pull_requests[0].status, "converged");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("finalize with autoMerge=true hands off to the merge-loop (canonical positive variable)", async () => {
|
|
121
|
+
await withRetroOff(async () => {
|
|
122
|
+
const { app, stores, createdProcesses } = fakeApp();
|
|
123
|
+
// biome-ignore lint/plugin: constructing the framework's job envelope for the handler under test
|
|
124
|
+
await handler({ variables: { ...BASE_VARS, autoMerge: true } } as never, app as never);
|
|
125
|
+
assertEquals(createdProcesses.includes(MERGE_PROCESS_ID), true);
|
|
126
|
+
assertEquals(stores.pull_requests[0].status, "waiting_deps");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("finalize prefers autoMerge over the legacy convergeOnly mirror when they disagree", async () => {
|
|
131
|
+
await withRetroOff(async () => {
|
|
132
|
+
const { app, stores, createdProcesses } = fakeApp();
|
|
133
|
+
// A contradictory pair (autoMerge=true, convergeOnly=true) can only arise from a hand-seeded or
|
|
134
|
+
// half-migrated instance: the positive canonical variable wins.
|
|
135
|
+
// biome-ignore lint/plugin: constructing the framework's job envelope for the handler under test
|
|
136
|
+
await handler({ variables: { ...BASE_VARS, autoMerge: true, convergeOnly: true } } as never, app as never);
|
|
137
|
+
assertEquals(createdProcesses.includes(MERGE_PROCESS_ID), true);
|
|
138
|
+
assertEquals(stores.pull_requests[0].status, "waiting_deps");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -22,7 +22,7 @@ const handler: AppJobHandler<In> = async (job, app) => {
|
|
|
22
22
|
// `summary` is left undefined when absent so the write boundary omits it: the
|
|
23
23
|
// nullable `rounds.summary` stays NULL and `pull_requests.outcome` is untouched
|
|
24
24
|
// rather than being coerced to "".
|
|
25
|
-
const { prKey, repo, prNumber, prUrl, round, summary, abandonUrl, convergeOnly } = job.variables;
|
|
25
|
+
const { prKey, repo, prNumber, prUrl, round, summary, abandonUrl, convergeOnly, autoMerge } = job.variables;
|
|
26
26
|
const now = new Date().toISOString();
|
|
27
27
|
|
|
28
28
|
// Heal a missing FK parent (engine/app.db desync) before the child `rounds` insert.
|
|
@@ -52,10 +52,15 @@ const handler: AppJobHandler<In> = async (job, app) => {
|
|
|
52
52
|
// with no process behind it, and `submitPr` refuses to restart it (only `cancel` recovers). On
|
|
53
53
|
// failure we leave the PR terminal as `converged` so a human/operator can (re)start merge.
|
|
54
54
|
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
55
|
+
// Merge intent (issue #916): `autoMerge` is the CANONICAL positive variable — read it first and
|
|
56
|
+
// fall back to the legacy inverted `convergeOnly` only when `autoMerge` is absent, so an in-flight
|
|
57
|
+
// loop seeded before `autoMerge` existed keeps its behaviour. A review-only intent
|
|
58
|
+
// (`autoMerge:false`, or legacy `convergeOnly:true`) forces the review-only path for this PR
|
|
59
|
+
// regardless of the global auto-merge default — the PR rests at `converged` and is never handed
|
|
60
|
+
// to the merge-loop.
|
|
61
|
+
const mergeRequested = autoMerge === true || (autoMerge == null && convergeOnly !== true);
|
|
57
62
|
let status = "converged";
|
|
58
|
-
if (AUTO_MERGE &&
|
|
63
|
+
if (AUTO_MERGE && mergeRequested) {
|
|
59
64
|
try {
|
|
60
65
|
const { mergeProcessKey } = await startMerge(app.data, app.engine, {
|
|
61
66
|
repo,
|
|
@@ -33,6 +33,14 @@ interface Out extends Record<string, unknown> {
|
|
|
33
33
|
mergeState: string;
|
|
34
34
|
failingChecks: number;
|
|
35
35
|
failingChecksList: string;
|
|
36
|
+
// The unresolved-review-thread count behind a `review-threads` verdict (issue #921), mirrored
|
|
37
|
+
// into the escalation question so a human sees exactly what blocks the merge. `-1` = unprobed.
|
|
38
|
+
reviewThreads: number;
|
|
39
|
+
// The pending-check count (PR #922 review): zero failing checks does not mean checks are green —
|
|
40
|
+
// a queued/running check leaves the BLOCKED cause undetermined, so the stall-escalation question
|
|
41
|
+
// gates its "required review approval" wording on this being 0 too (never claims a verified
|
|
42
|
+
// blocker off a half-settled rollup).
|
|
43
|
+
pendingChecks: number;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
const handler: AppJobHandler<In, Out> = async (job, app) => {
|
|
@@ -44,10 +52,14 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
|
|
|
44
52
|
// GitHub reports UNSTABLE. Any transport hiccup is treated as "still waiting" — never a spurious
|
|
45
53
|
// ready/conflict verdict — so a bad read is surfaced as `waiting`, which `gw-mergeable` routes to
|
|
46
54
|
// a bounded re-poll (#774) rather than misrouting the token to a wrong remediation arm; a
|
|
47
|
-
// genuinely stuck verdict still escalates once the `mergeStallMax` budget is exhausted.
|
|
48
|
-
|
|
55
|
+
// genuinely stuck verdict still escalates once the `mergeStallMax` budget is exhausted. The read
|
|
56
|
+
// opts into the review-threads probe (`withReviewThreads`) so a `BLOCKED` merge state is
|
|
57
|
+
// classified by its real cause (issue #921) — unresolved review threads surface as the
|
|
58
|
+
// `review-threads` verdict and route to their own arm instead of re-polling until the budget
|
|
59
|
+
// exhausts into the misleading "mergeability never settled" escalation.
|
|
60
|
+
const st = await fetchPrState(repo, prNumber, token, { withReviewThreads: true }).catch(() => null);
|
|
49
61
|
if (st === null) {
|
|
50
|
-
return { mergeState: "waiting", failingChecks: 0, failingChecksList: "" };
|
|
62
|
+
return { mergeState: "waiting", failingChecks: 0, failingChecksList: "", reviewThreads: -1, pendingChecks: 0 };
|
|
51
63
|
}
|
|
52
64
|
const protocol = await loadMergeProtocol(repo, token).catch(() => null);
|
|
53
65
|
const mergeState = classifyMergeability(st, protocol ?? undefined);
|
|
@@ -64,6 +76,8 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
|
|
|
64
76
|
// poller's `merge-ready` payload so fix-ci knows which gates to green.
|
|
65
77
|
failingChecks: st.failingChecks,
|
|
66
78
|
failingChecksList: st.failingCheckNames.join("\n"),
|
|
79
|
+
reviewThreads: st.unresolvedThreads,
|
|
80
|
+
pendingChecks: st.pendingCheckNames.length,
|
|
67
81
|
};
|
|
68
82
|
};
|
|
69
83
|
|