@nanobpm/nano-workforce 0.200.8 → 0.201.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/SPEC.md +35 -0
  3. package/app/deliveryGraph.test.ts +10 -0
  4. package/app/deliveryGraphCompiler.test.ts +354 -10
  5. package/app/deliveryGraphCompiler.ts +296 -40
  6. package/app/deliveryRunner.ts +14 -5
  7. package/app/implementEscalationReason.test.ts +216 -0
  8. package/app/implementEscalationReason.ts +213 -0
  9. package/app/implementReconcile.test.ts +168 -11
  10. package/app/implementReconcile.ts +112 -18
  11. package/app/pullRequestReadModel.test.ts +1 -1
  12. package/app/roundChurn.test.ts +388 -0
  13. package/app/roundChurn.ts +265 -0
  14. package/app/roundProgress.test.ts +224 -1
  15. package/app/service.test.ts +7 -0
  16. package/app/service.ts +12 -0
  17. package/db/migrations/117_pull_requests_churn_escalated_round.sql +12 -0
  18. package/db/migrations/118_pull_requests_read_model_churn_escalated_round.sql +55 -0
  19. package/docs/agent-guide.md +10 -7
  20. package/e2e/delivery-graph.e2e.ts +570 -1
  21. package/e2e/feature-run.e2e.ts +8 -1
  22. package/e2e/plan-fanout.e2e.ts +75 -2
  23. package/package.json +1 -1
  24. package/resources/processes/implement-cell.bpmn +42 -17
  25. package/resources/processes/plan-fanout.bpmn +236 -211
  26. package/resources/prompts/feature.md +16 -1
  27. package/workers/progress-check/worker.ts +98 -1
  28. package/workers/readiness-probe/worker.test.ts +72 -1
  29. package/workers/readiness-probe/worker.ts +39 -0
  30. package/workers/reconcile-implement/worker.test.ts +31 -3
  31. package/workers/reconcile-implement/worker.ts +5 -0
  32. package/workers/record-feature-escalation/worker.test.ts +58 -3
  33. package/workers/record-feature-escalation/worker.ts +24 -10
@@ -587,11 +587,84 @@ describe("plan-fanout escalations (U2 — task + plan-review + trial-merge → u
587
587
 
588
588
  const answered = takenFlows(app);
589
589
  assert.ok(
590
- answered.includes("w_gw_answer->implement-cell-call") && answered.includes("Start->implement-task"),
591
- `answering the caps escalation released the barrier into the implement cell (flows: ${answered.join(", ")})`,
590
+ answered.includes("w_gw_answer->record-task-implementing") &&
591
+ answered.includes("record-task-implementing->implement-cell-call") &&
592
+ answered.includes("Start->implement-task"),
593
+ `answering the caps escalation reset the stale question and released the barrier into the implement cell (flows: ${answered.join(", ")})`,
592
594
  );
593
595
  assert.equal(featureRan, true, "the agent was dispatched once the operator answered the caps escalation");
594
596
  },
595
597
  );
596
598
  });
599
+
600
+ // Regression (review: "Reset stale result fields on initial implementation entry"). The capability
601
+ // barrier's timeout seeds an operator `question` (`wait-caps-timeout`), and answering the
602
+ // `feature-escalation` loops back into the implement cell (`w_answerLoop`). The cell's
603
+ // `record-implementing` reset clears the result fields only on its OWN retry/answer re-entry — the
604
+ // INITIAL `Start -> implement-task` entry bypasses it, so the inherited caps-timeout question would
605
+ // still be in scope. If the agent then returns an off-vocabulary `completed` (no PR delivered), a
606
+ // stale question makes `shouldReconcileImplement` honour it as a genuine escalation — skipping the
607
+ // auto-retry and re-escalating the ALREADY-ANSWERED caps question. The fix clears `question` on the
608
+ // plan-fanout answer loop-back, so the cell is entered clean and the claimed completion auto-retries.
609
+ test("capability edge: an answered caps escalation enters the cell clean — a claimed completion auto-retries, not re-escalates the stale question", async () => {
610
+ const capTaskPlan: Stub = () => ({
611
+ tasks: [
612
+ {
613
+ id: "t1",
614
+ title: "T1",
615
+ prompt: "do t1",
616
+ // A bare handle names no owner/repo releases source — unresolvable, so the barrier times out.
617
+ needs: [{ capabilityRef: "#274", package: "@nanobpm/urban" }],
618
+ },
619
+ ],
620
+ });
621
+ let featureCalls = 0;
622
+ await withApp(
623
+ {
624
+ "senior:plan": capTaskPlan,
625
+ "senior:plan-review": approveReview,
626
+ // The implement agent claims completion WITHOUT opening a PR (the github stub lists none) — the
627
+ // #865 claimed-completion-without-delivery the reconcile step auto-retries ONCE.
628
+ "senior:feature": () => {
629
+ featureCalls += 1;
630
+ return { status: "completed", summary: "done, no PR" };
631
+ },
632
+ },
633
+ async ({ app, processKey }) => {
634
+ // Let the unresolvable barrier time out into the operator escalation.
635
+ await advancePastTimer(app, 25 * 60 * 60 * 1000);
636
+ const task = await openTask(app, processKey, "feature-escalation");
637
+ await app.engine.completeUserTask(task.userTaskKey, { resolution: "answer", answer: "shipped it manually" });
638
+ await settleFully(app);
639
+
640
+ // The answered barrier released the token through the answer loop-back's reset into the
641
+ // implement cell, and the agent ran.
642
+ const entered = takenFlows(app);
643
+ assert.ok(
644
+ entered.includes("w_gw_answer->record-task-implementing") &&
645
+ entered.includes("record-task-implementing->implement-cell-call") &&
646
+ entered.includes("Start->implement-task"),
647
+ `answering the caps escalation reset the stale question and released the barrier into the implement cell (flows: ${entered.join(", ")})`,
648
+ );
649
+
650
+ // With the stale caps-timeout question cleared on cell entry, the off-vocabulary `completed`
651
+ // (no adoptable PR) takes the reconcile AUTO-RETRY arm — re-dispatching the agent once — rather
652
+ // than re-escalating the ALREADY-ANSWERED caps question. `settleFully` above drove the whole
653
+ // loop to quiescence: the agent ran exactly TWICE (the initial dispatch + the one bounded
654
+ // auto-retry), and only then — the retry consumed — did the cell escalate (the bounded
655
+ // claimed-completion behaviour of #865). The retry is the proof the stale question was cleared:
656
+ // an inherited question would have routed the FIRST `completed` straight to escalation.
657
+ const flows = takenFlows(app);
658
+ assert.ok(
659
+ flows.includes("ic_reconcile_gw->record-implementing"),
660
+ `the claimed completion auto-retried through the cell's reset (flows: ${flows.join(", ")})`,
661
+ );
662
+ assert.ok(
663
+ flows.includes("record-implementing->implement-task"),
664
+ `the retry re-entered the implement task (flows: ${flows.join(", ")})`,
665
+ );
666
+ assert.equal(featureCalls, 2, "the auto-retry re-dispatched the implement agent exactly once");
667
+ },
668
+ );
669
+ });
597
670
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.200.8",
3
+ "version": "0.201.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",
@@ -7,6 +7,10 @@
7
7
  <nano:extend name="subjectKey" type="string" />
8
8
  <nano:extend name="status" type="string" optional="true" />
9
9
  <nano:extend name="question" type="string" optional="true" />
10
+ <nano:extend name="summary" type="string" optional="true" />
11
+ <nano:extend name="transcriptUrl" type="string" optional="true" />
12
+ <nano:extend name="pr" type="string" optional="true" />
13
+ <nano:extend name="deliveryVerified" type="boolean" optional="true" />
10
14
  </nano:shape>
11
15
  </nano:shapes>
12
16
  </bpmn:extensionElements>
@@ -21,10 +25,11 @@
21
25
  <zeebe:linkedResource resourceId="prompts/feature.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
22
26
  </zeebe:linkedResources>
23
27
  <zeebe:ioMapping>
24
- <zeebe:input source="=&#34;&#10;&#10;---&#10;&#10;&#34; + task.prompt + (if (is defined(baseBranchBrief) and baseBranchBrief != null) then baseBranchBrief else &#34;&#34;) + (if (is defined(blackboardBrief) and blackboardBrief != null) then blackboardBrief else &#34;&#34;) + (if (is defined(resolvedDepsBrief) and resolvedDepsBrief != null) then resolvedDepsBrief else &#34;&#34;) + (if (is defined(resolvedArtifacts) = false or resolvedArtifacts = null or count(resolvedArtifacts[item != null]) = 0) then &#34;&#34; else (&#34;&#10;&#10;---&#10;&#10;**Bound upstream readiness.** Build/install/clone against EXACTLY these resolved published versions (the ones first carrying the awaited capability), and bump the consumer dependency to them — never merely the newest:&#10;&#10;&#34; + string join(resolvedArtifacts[item != null], &#34;&#10;&#34;))) + (if (is defined(customInstructions) = false or customInstructions = null) then &#34;&#34; else &#34;&#10;&#10;---&#10;&#10;## Operator custom instructions&#10;&#10;The operator supplied these instructions for this run — follow them:&#10;&#10;&#34; + customInstructions)" target="appendPrompt" />
28
+ <zeebe:input source="=&#34;&#10;&#10;---&#10;&#10;&#34; + task.prompt + (if (is defined(baseBranchBrief) and baseBranchBrief != null) then baseBranchBrief else &#34;&#34;) + (if (is defined(blackboardBrief) and blackboardBrief != null) then blackboardBrief else &#34;&#34;) + (if (is defined(resolvedDepsBrief) and resolvedDepsBrief != null) then resolvedDepsBrief else &#34;&#34;) + (if (is defined(resolvedArtifacts) = false or resolvedArtifacts = null or count(resolvedArtifacts[item != null]) = 0) then &#34;&#34; else (&#34;&#10;&#10;---&#10;&#10;**Bound upstream readiness.** Build/install/clone against EXACTLY these resolved published versions (the ones first carrying the awaited capability), and bump the consumer dependency to them — never merely the newest:&#10;&#10;&#34; + string join(resolvedArtifacts[item != null], &#34;&#10;&#34;))) + (if (is defined(customInstructions) = false or customInstructions = null) then &#34;&#34; else &#34;&#10;&#10;---&#10;&#10;## Operator custom instructions&#10;&#10;The operator supplied these instructions for this run — follow them:&#10;&#10;&#34; + customInstructions) + (if (is defined(retryNudge) and retryNudge != null) then retryNudge else &#34;&#34;)" target="appendPrompt" />
25
29
  <zeebe:input source="=if (is defined(transcriptUrlBase)) then transcriptUrlBase else null" target="transcriptUrlBase" />
26
30
  <zeebe:output source="=status" target="status" />
27
31
  <zeebe:output source="=question" target="question" />
32
+ <zeebe:output source="=if (is defined(summary)) then summary else null" target="summary" />
28
33
  <zeebe:output source="=if (is defined(transcriptUrl)) then transcriptUrl else null" target="transcriptUrl" />
29
34
  </zeebe:ioMapping>
30
35
  </bpmn:extensionElements>
@@ -50,6 +55,10 @@
50
55
  <zeebe:output source="=reconciled" target="reconciled" />
51
56
  <zeebe:output source="=status" target="status" />
52
57
  <zeebe:output source="=pr" target="pr" />
58
+ <zeebe:output source="=retry" target="retry" />
59
+ <zeebe:output source="=retryNudge" target="retryNudge" />
60
+ <zeebe:output source="=retried" target="implementRetried" />
61
+ <zeebe:output source="=deliveryVerified" target="deliveryVerified" />
53
62
  </zeebe:ioMapping>
54
63
  </bpmn:extensionElements>
55
64
  <bpmn:incoming>ic_escalate</bpmn:incoming>
@@ -58,6 +67,7 @@
58
67
  <bpmn:exclusiveGateway id="ic_reconcile_gw" name="reconciled?" default="ic_toRecordEscalation">
59
68
  <bpmn:incoming>ic_toReconcileGw</bpmn:incoming>
60
69
  <bpmn:outgoing>ic_reconciled</bpmn:outgoing>
70
+ <bpmn:outgoing>ic_retry</bpmn:outgoing>
61
71
  <bpmn:outgoing>ic_toRecordEscalation</bpmn:outgoing>
62
72
  </bpmn:exclusiveGateway>
63
73
  <bpmn:callActivity id="escalate" name="Human escalation">
@@ -112,8 +122,16 @@
112
122
  <zeebe:properties>
113
123
  <zeebe:property name="io.nanobpm.dataEnvelope.in" value="RecordFeatureImplementingIn" />
114
124
  </zeebe:properties>
125
+ <zeebe:ioMapping>
126
+ <zeebe:output source="=null" target="status" />
127
+ <zeebe:output source="=null" target="question" />
128
+ <zeebe:output source="=null" target="summary" />
129
+ <zeebe:output source="=null" target="transcriptUrl" />
130
+ <zeebe:output source="=null" target="deliveryVerified" />
131
+ </zeebe:ioMapping>
115
132
  </bpmn:extensionElements>
116
133
  <bpmn:incoming>ic_answerLoop</bpmn:incoming>
134
+ <bpmn:incoming>ic_retry</bpmn:incoming>
117
135
  <bpmn:outgoing>ic_reImplement</bpmn:outgoing>
118
136
  </bpmn:serviceTask>
119
137
  <bpmn:endEvent id="ic_end" name="Task done">
@@ -134,6 +152,9 @@
134
152
  <bpmn:sequenceFlow id="ic_toRecordEscalation" name="escalated" sourceRef="ic_reconcile_gw" targetRef="record-escalation" />
135
153
  <bpmn:sequenceFlow id="ic_sla" name="agent SLA elapsed" sourceRef="be_implement_sla" targetRef="record-escalation-sla" />
136
154
  <bpmn:sequenceFlow id="ic_slaToEscalate" sourceRef="record-escalation-sla" targetRef="escalate" />
155
+ <bpmn:sequenceFlow id="ic_retry" name="retry" sourceRef="ic_reconcile_gw" targetRef="record-implementing">
156
+ <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=retry = true</bpmn:conditionExpression>
157
+ </bpmn:sequenceFlow>
137
158
  <bpmn:sequenceFlow id="ic_toEscalate" sourceRef="record-escalation" targetRef="escalate" />
138
159
  <bpmn:sequenceFlow id="ic_toAnswerGw" sourceRef="escalate" targetRef="ic_gw_answer" />
139
160
  <bpmn:sequenceFlow id="ic_answerLoop" name="answer" sourceRef="ic_gw_answer" targetRef="record-implementing">
@@ -156,7 +177,7 @@
156
177
  <bpmndi:BPMNShape id="BPMNShape_ic_gw" bpmnElement="ic_gw" isMarkerVisible="true">
157
178
  <dc:Bounds x="441" y="95" width="50" height="50" />
158
179
  <bpmndi:BPMNLabel>
159
- <dc:Bounds x="391" y="150" width="70" height="28" />
180
+ <dc:Bounds x="431" y="62" width="70" height="28" />
160
181
  </bpmndi:BPMNLabel>
161
182
  </bpmndi:BPMNShape>
162
183
  <bpmndi:BPMNShape id="BPMNShape_reconcile-implement" bpmnElement="reconcile-implement">
@@ -175,7 +196,7 @@
175
196
  <dc:Bounds x="966" y="80" width="100" height="80" />
176
197
  </bpmndi:BPMNShape>
177
198
  <bpmndi:BPMNShape id="BPMNShape_record-escalation-sla" bpmnElement="record-escalation-sla">
178
- <dc:Bounds x="416" y="240" width="100" height="80" />
199
+ <dc:Bounds x="416" y="400" width="100" height="80" />
179
200
  </bpmndi:BPMNShape>
180
201
  <bpmndi:BPMNShape id="BPMNShape_ic_gw_answer" bpmnElement="ic_gw_answer" isMarkerVisible="true">
181
202
  <dc:Bounds x="1366" y="95" width="50" height="50" />
@@ -239,12 +260,20 @@
239
260
  <dc:Bounds x="1456" y="98" width="53" height="14" />
240
261
  </bpmndi:BPMNLabel>
241
262
  </bpmndi:BPMNEdge>
263
+ <bpmndi:BPMNEdge id="BPMNEdge_ic_retry" bpmnElement="ic_retry">
264
+ <di:waypoint x="841" y="145" />
265
+ <di:waypoint x="841" y="280" />
266
+ <di:waypoint x="1516" y="280" />
267
+ <bpmndi:BPMNLabel>
268
+ <dc:Bounds x="846" y="223" width="39" height="14" />
269
+ </bpmndi:BPMNLabel>
270
+ </bpmndi:BPMNEdge>
242
271
  <bpmndi:BPMNEdge id="BPMNEdge_ic_answerLoop" bpmnElement="ic_answerLoop">
243
272
  <di:waypoint x="1391" y="145" />
244
273
  <di:waypoint x="1391" y="280" />
245
274
  <di:waypoint x="1516" y="280" />
246
275
  <bpmndi:BPMNLabel>
247
- <dc:Bounds x="1337" y="206" width="49" height="14" />
276
+ <dc:Bounds x="1396" y="206" width="49" height="14" />
248
277
  </bpmndi:BPMNLabel>
249
278
  </bpmndi:BPMNEdge>
250
279
  <bpmndi:BPMNEdge id="BPMNEdge_ic_done" bpmnElement="ic_done">
@@ -266,27 +295,23 @@
266
295
  </bpmndi:BPMNLabel>
267
296
  </bpmndi:BPMNEdge>
268
297
  <bpmndi:BPMNEdge id="BPMNEdge_ic_slaToEscalate" bpmnElement="ic_slaToEscalate">
269
- <di:waypoint x="516" y="300" />
270
- <di:waypoint x="536" y="300" />
271
- <di:waypoint x="536" y="340" />
272
- <di:waypoint x="1436" y="340" />
273
- <di:waypoint x="1436" y="220" />
274
- <di:waypoint x="1604" y="220" />
275
- <di:waypoint x="1604" y="82" />
298
+ <di:waypoint x="516" y="440" />
299
+ <di:waypoint x="1636" y="440" />
300
+ <di:waypoint x="1636" y="82" />
276
301
  <di:waypoint x="1266" y="82" />
277
302
  </bpmndi:BPMNEdge>
278
303
  <bpmndi:BPMNEdge id="BPMNEdge_ic_sla" bpmnElement="ic_sla">
279
304
  <di:waypoint x="266" y="178" />
280
- <di:waypoint x="266" y="280" />
281
- <di:waypoint x="416" y="280" />
305
+ <di:waypoint x="266" y="440" />
306
+ <di:waypoint x="416" y="440" />
282
307
  <bpmndi:BPMNLabel>
283
- <dc:Bounds x="271" y="215" width="70" height="28" />
308
+ <dc:Bounds x="271" y="295" width="70" height="28" />
284
309
  </bpmndi:BPMNLabel>
285
310
  </bpmndi:BPMNEdge>
286
311
  <bpmndi:BPMNEdge id="BPMNEdge_ic_reImplement" bpmnElement="ic_reImplement">
287
- <di:waypoint x="1616" y="280" />
288
- <di:waypoint x="1636" y="280" />
289
- <di:waypoint x="1636" y="60" />
312
+ <di:waypoint x="1516" y="280" />
313
+ <di:waypoint x="60" y="280" />
314
+ <di:waypoint x="60" y="60" />
290
315
  <di:waypoint x="266" y="60" />
291
316
  <di:waypoint x="266" y="80" />
292
317
  </bpmndi:BPMNEdge>