@nanobpm/nano-workforce 0.77.0 → 0.79.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.
- package/CHANGELOG.md +14 -0
- package/SPEC.md +17 -14
- package/app/agentCompletion.ts +2 -0
- package/app/agentGuide.ts +2 -1
- package/app/agentic/README.md +20 -0
- package/app/agentic/cockpit/index.ts +5 -0
- package/app/agentic/cockpit/transcript-derive.test.ts +78 -0
- package/app/agentic/cockpit/transcript-derive.ts +90 -0
- package/app/agentic/transcript-events.drift.test.ts +55 -0
- package/app/agentic/transcript-events.test.ts +186 -0
- package/app/agentic/transcript-events.ts +470 -0
- package/app/agentic/transcript-fork.test.ts +156 -0
- package/app/agentic/transcript-fork.ts +151 -0
- package/app/agentic/transcript-read.ts +2 -1
- package/app/answer-escalation.test.ts +2 -2
- package/app/mergeEscalationUserTask.test.ts +77 -0
- package/app/pollUserTasks.test.ts +23 -0
- package/app/service.ts +8 -36
- package/app/userTasks.ts +7 -0
- package/docs/agent-guide.md +9 -6
- package/openapi.yaml +3 -5
- package/operations/getAgentInstructions.test.ts +1 -1
- package/operations/postMessage.ts +8 -24
- package/operations/startAndMessage.test.ts +15 -4
- package/package.json +1 -1
- package/pages/tasks.page.json +26 -3
- package/resources/processes/merge-loop.bpmn +47 -33
- package/workers/answer-escalation/worker.ts +9 -9
- package/app/answerEscalation.test.ts +0 -67
|
@@ -23,14 +23,6 @@
|
|
|
23
23
|
<zeebe:subscription correlationKey="=prKey" />
|
|
24
24
|
</bpmn:extensionElements>
|
|
25
25
|
</bpmn:message>
|
|
26
|
-
<bpmn:message id="Message_mergeEscAnswered" name="escalation-answered">
|
|
27
|
-
<bpmn:extensionElements>
|
|
28
|
-
<zeebe:subscription correlationKey="=prKey" />
|
|
29
|
-
<zeebe:properties>
|
|
30
|
-
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="MergeEscalationAnswered" />
|
|
31
|
-
</zeebe:properties>
|
|
32
|
-
</bpmn:extensionElements>
|
|
33
|
-
</bpmn:message>
|
|
34
26
|
<bpmn:process id="merge-loop" name="PR Merge" isExecutable="true">
|
|
35
27
|
<bpmn:extensionElements>
|
|
36
28
|
<nano:shapes>
|
|
@@ -87,10 +79,6 @@
|
|
|
87
79
|
<nano:extend name="prKey" type="string" />
|
|
88
80
|
<nano:extend name="dependsOn" type="string" optional="true" />
|
|
89
81
|
</nano:shape>
|
|
90
|
-
<nano:shape id="MergeEscalationAnswered" name="escalation-answered message payload">
|
|
91
|
-
<nano:extend name="answer" type="string" />
|
|
92
|
-
<nano:extend name="escalationId" type="integer" />
|
|
93
|
-
</nano:shape>
|
|
94
82
|
</nano:shapes>
|
|
95
83
|
</bpmn:extensionElements>
|
|
96
84
|
<bpmn:startEvent id="MergeStart" name="PR converged">
|
|
@@ -206,12 +194,33 @@
|
|
|
206
194
|
<bpmn:incoming>f_ci_sla</bpmn:incoming>
|
|
207
195
|
<bpmn:outgoing>f_m_escA</bpmn:outgoing>
|
|
208
196
|
</bpmn:serviceTask>
|
|
209
|
-
<bpmn:
|
|
197
|
+
<bpmn:userTask id="wait-merge-answer" name="Answer escalation">
|
|
198
|
+
<bpmn:extensionElements>
|
|
199
|
+
<zeebe:formDefinition formId="pr-escalation" />
|
|
200
|
+
<zeebe:userTask />
|
|
201
|
+
<zeebe:ioMapping>
|
|
202
|
+
<zeebe:input source="=question" target="question" />
|
|
203
|
+
<zeebe:input source="=prKey" target="prKey" />
|
|
204
|
+
</zeebe:ioMapping>
|
|
205
|
+
</bpmn:extensionElements>
|
|
210
206
|
<bpmn:incoming>f_m_escC</bpmn:incoming>
|
|
211
207
|
<bpmn:incoming>f_m_escA</bpmn:incoming>
|
|
208
|
+
<bpmn:outgoing>f_m_answerRecord</bpmn:outgoing>
|
|
209
|
+
</bpmn:userTask>
|
|
210
|
+
<bpmn:serviceTask id="record-merge-answer" name="Record answer">
|
|
211
|
+
<bpmn:extensionElements>
|
|
212
|
+
<zeebe:taskDefinition type="pr.answer-escalation" />
|
|
213
|
+
<zeebe:properties>
|
|
214
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="PrAnswerEscalationIn" />
|
|
215
|
+
</zeebe:properties>
|
|
216
|
+
<zeebe:ioMapping>
|
|
217
|
+
<zeebe:input source="=prKey" target="prKey" />
|
|
218
|
+
<zeebe:input source="=answer" target="answer" />
|
|
219
|
+
</zeebe:ioMapping>
|
|
220
|
+
</bpmn:extensionElements>
|
|
221
|
+
<bpmn:incoming>f_m_answerRecord</bpmn:incoming>
|
|
212
222
|
<bpmn:outgoing>f_m_answer</bpmn:outgoing>
|
|
213
|
-
|
|
214
|
-
</bpmn:intermediateCatchEvent>
|
|
223
|
+
</bpmn:serviceTask>
|
|
215
224
|
<bpmn:exclusiveGateway id="gw-ci-fix" name="auto-fix CI?" default="f_ci_giveup">
|
|
216
225
|
<bpmn:incoming>f_m_mCiFix</bpmn:incoming>
|
|
217
226
|
<bpmn:outgoing>f_ci_go</bpmn:outgoing>
|
|
@@ -355,7 +364,8 @@
|
|
|
355
364
|
<bpmn:sequenceFlow id="f_m_done" sourceRef="mark-merged" targetRef="MergeEnd" />
|
|
356
365
|
<bpmn:sequenceFlow id="f_m_escC" sourceRef="merge-esc-conflict" targetRef="wait-merge-answer" />
|
|
357
366
|
<bpmn:sequenceFlow id="f_m_escA" sourceRef="merge-esc-attempt" targetRef="wait-merge-answer" />
|
|
358
|
-
<bpmn:sequenceFlow id="
|
|
367
|
+
<bpmn:sequenceFlow id="f_m_answerRecord" sourceRef="wait-merge-answer" targetRef="record-merge-answer" />
|
|
368
|
+
<bpmn:sequenceFlow id="f_m_answer" sourceRef="record-merge-answer" targetRef="arm-merge" />
|
|
359
369
|
<bpmn:sequenceFlow id="f_reb_sla" name="agent SLA elapsed" sourceRef="be_rebase_sla" targetRef="merge-esc-conflict" />
|
|
360
370
|
<bpmn:sequenceFlow id="f_ci_sla" name="agent SLA elapsed" sourceRef="be_fixci_sla" targetRef="merge-esc-attempt" />
|
|
361
371
|
</bpmn:process>
|
|
@@ -419,9 +429,9 @@
|
|
|
419
429
|
<dc:Bounds x="1638" y="80" width="100" height="80" />
|
|
420
430
|
</bpmndi:BPMNShape>
|
|
421
431
|
<bpmndi:BPMNShape id="BPMNShape_MergeEnd" bpmnElement="MergeEnd">
|
|
422
|
-
<dc:Bounds x="
|
|
432
|
+
<dc:Bounds x="1870" y="102" width="36" height="36" />
|
|
423
433
|
<bpmndi:BPMNLabel>
|
|
424
|
-
<dc:Bounds x="
|
|
434
|
+
<dc:Bounds x="1863" y="143" width="50" height="14" />
|
|
425
435
|
</bpmndi:BPMNLabel>
|
|
426
436
|
</bpmndi:BPMNShape>
|
|
427
437
|
<bpmndi:BPMNShape id="BPMNShape_merge-esc-conflict" bpmnElement="merge-esc-conflict">
|
|
@@ -431,10 +441,10 @@
|
|
|
431
441
|
<dc:Bounds x="1438" y="1040" width="100" height="80" />
|
|
432
442
|
</bpmndi:BPMNShape>
|
|
433
443
|
<bpmndi:BPMNShape id="BPMNShape_wait-merge-answer" bpmnElement="wait-merge-answer">
|
|
434
|
-
<dc:Bounds x="
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
444
|
+
<dc:Bounds x="1638" y="1040" width="100" height="80" />
|
|
445
|
+
</bpmndi:BPMNShape>
|
|
446
|
+
<bpmndi:BPMNShape id="BPMNShape_record-merge-answer" bpmnElement="record-merge-answer">
|
|
447
|
+
<dc:Bounds x="1838" y="1040" width="100" height="80" />
|
|
438
448
|
</bpmndi:BPMNShape>
|
|
439
449
|
<bpmndi:BPMNShape id="BPMNShape_gw-ci-fix" bpmnElement="gw-ci-fix" isMarkerVisible="true">
|
|
440
450
|
<dc:Bounds x="863" y="1535" width="50" height="50" />
|
|
@@ -517,7 +527,7 @@
|
|
|
517
527
|
</bpmndi:BPMNEdge>
|
|
518
528
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_done" bpmnElement="f_m_done">
|
|
519
529
|
<di:waypoint x="1738" y="120" />
|
|
520
|
-
<di:waypoint x="
|
|
530
|
+
<di:waypoint x="1870" y="120" />
|
|
521
531
|
</bpmndi:BPMNEdge>
|
|
522
532
|
<bpmndi:BPMNEdge id="BPMNEdge_f_ci_giveup" bpmnElement="f_ci_giveup">
|
|
523
533
|
<di:waypoint x="913" y="1560" />
|
|
@@ -568,11 +578,15 @@
|
|
|
568
578
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_escC" bpmnElement="f_m_escC">
|
|
569
579
|
<di:waypoint x="1338" y="1560" />
|
|
570
580
|
<di:waypoint x="1688" y="1560" />
|
|
571
|
-
<di:waypoint x="1688" y="
|
|
581
|
+
<di:waypoint x="1688" y="1120" />
|
|
582
|
+
</bpmndi:BPMNEdge>
|
|
583
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_m_answerRecord" bpmnElement="f_m_answerRecord">
|
|
584
|
+
<di:waypoint x="1738" y="1080" />
|
|
585
|
+
<di:waypoint x="1838" y="1080" />
|
|
572
586
|
</bpmndi:BPMNEdge>
|
|
573
587
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_answer" bpmnElement="f_m_answer">
|
|
574
|
-
<di:waypoint x="
|
|
575
|
-
<di:waypoint x="
|
|
588
|
+
<di:waypoint x="1888" y="1120" />
|
|
589
|
+
<di:waypoint x="1888" y="1140" />
|
|
576
590
|
<di:waypoint x="402" y="1140" />
|
|
577
591
|
<di:waypoint x="402" y="160" />
|
|
578
592
|
</bpmndi:BPMNEdge>
|
|
@@ -612,13 +626,13 @@
|
|
|
612
626
|
<bpmndi:BPMNEdge id="BPMNEdge_f_ci_go" bpmnElement="f_ci_go">
|
|
613
627
|
<di:waypoint x="888" y="1585" />
|
|
614
628
|
<di:waypoint x="888" y="1605" />
|
|
615
|
-
<di:waypoint x="
|
|
616
|
-
<di:waypoint x="
|
|
629
|
+
<di:waypoint x="1958" y="1605" />
|
|
630
|
+
<di:waypoint x="1958" y="555" />
|
|
617
631
|
<di:waypoint x="1018" y="555" />
|
|
618
632
|
<di:waypoint x="1018" y="580" />
|
|
619
633
|
<di:waypoint x="1038" y="580" />
|
|
620
634
|
<bpmndi:BPMNLabel>
|
|
621
|
-
<dc:Bounds x="
|
|
635
|
+
<dc:Bounds x="1963" y="1066" width="49" height="28" />
|
|
622
636
|
</bpmndi:BPMNLabel>
|
|
623
637
|
</bpmndi:BPMNEdge>
|
|
624
638
|
<bpmndi:BPMNEdge id="BPMNEdge_f_ci_blocked" bpmnElement="f_ci_blocked">
|
|
@@ -685,13 +699,13 @@
|
|
|
685
699
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_gBlocked" bpmnElement="f_m_gBlocked">
|
|
686
700
|
<di:waypoint x="1088" y="95" />
|
|
687
701
|
<di:waypoint x="1088" y="75" />
|
|
688
|
-
<di:waypoint x="
|
|
689
|
-
<di:waypoint x="
|
|
702
|
+
<di:waypoint x="1926" y="75" />
|
|
703
|
+
<di:waypoint x="1926" y="820" />
|
|
690
704
|
<di:waypoint x="1418" y="820" />
|
|
691
705
|
<di:waypoint x="1418" y="1060" />
|
|
692
706
|
<di:waypoint x="1438" y="1060" />
|
|
693
707
|
<bpmndi:BPMNLabel>
|
|
694
|
-
<dc:Bounds x="
|
|
708
|
+
<dc:Bounds x="1931" y="441" width="53" height="14" />
|
|
695
709
|
</bpmndi:BPMNLabel>
|
|
696
710
|
</bpmndi:BPMNEdge>
|
|
697
711
|
<bpmndi:BPMNEdge id="BPMNEdge_f_ci_done" bpmnElement="f_ci_done">
|
|
@@ -704,7 +718,7 @@
|
|
|
704
718
|
</bpmndi:BPMNEdge>
|
|
705
719
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_escA" bpmnElement="f_m_escA">
|
|
706
720
|
<di:waypoint x="1538" y="1080" />
|
|
707
|
-
<di:waypoint x="
|
|
721
|
+
<di:waypoint x="1638" y="1080" />
|
|
708
722
|
</bpmndi:BPMNEdge>
|
|
709
723
|
<bpmndi:BPMNEdge id="BPMNEdge_f_reb_sla" bpmnElement="f_reb_sla">
|
|
710
724
|
<di:waypoint x="1088" y="978" />
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// pr.answer-escalation — retires the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// pr.answer-escalation — retires the escalation the operator just answered, for BOTH the review loop
|
|
2
|
+
// (`wait-answer`, convergence-loop.bpmn) and the merge loop (`wait-merge-answer`, merge-loop.bpmn).
|
|
3
|
+
// Both loops now park on a native `wait-*` userTask (backed by `pr-escalation.form`) and run this same
|
|
4
|
+
// reconcile step on completion (#256) — there is one answer path, not two.
|
|
4
5
|
// Completing that task resumes the token, but the engine folds the completed instance's variables
|
|
5
6
|
// away — so without this step the durable `escalations` audit row raised by `pr.persist-escalation`
|
|
6
7
|
// would stay `status="open"` forever, its `answer`/`answered_at` never recorded. That both loses the
|
|
@@ -8,9 +9,8 @@
|
|
|
8
9
|
// single source of truth, ADR "derivation over duplication") would keep surfacing a phantom open
|
|
9
10
|
// escalation on `/status` after it was answered.
|
|
10
11
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// retry of `pr.persist-escalation` can leave more than one open), AND move the `pull_requests` row
|
|
12
|
+
// It answers the newest still-open `escalations` row, marks any duplicate open rows `stale` (a
|
|
13
|
+
// retry of `pr.persist-escalation` can leave more than one open), AND moves the `pull_requests` row
|
|
14
14
|
// off `status="escalated"` back to `"converging"`, so an answered escalation is never left dangling
|
|
15
15
|
// and `/status` never shows an escalated PR with a null question or a phantom open row. The token
|
|
16
16
|
// resume itself is owned by the engine (userTask completion), so this worker only reconciles the
|
|
@@ -28,7 +28,7 @@ interface Escalation extends Record<string, unknown> {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// The PR-row fields this worker reconciles when an escalation is answered. Only `status`/`updated_at`
|
|
31
|
-
// are written
|
|
31
|
+
// are written; the rest of the row is untouched.
|
|
32
32
|
interface PullRequest extends Record<string, unknown> {
|
|
33
33
|
pr_key: string;
|
|
34
34
|
status: string;
|
|
@@ -51,7 +51,7 @@ const handler: AppJobHandler<In> = async (job, app) => {
|
|
|
51
51
|
// open row, so a retry/duplicate activation can leave more than one open — answering only the
|
|
52
52
|
// newest would leave an older duplicate `open`, a phantom `activePrs` keeps deriving while the PR
|
|
53
53
|
// is still `escalated`. Answer the newest (it carries the operator's reply) and mark any remaining
|
|
54
|
-
// open rows `stale`, mirroring `
|
|
54
|
+
// open rows `stale`, mirroring `submitPr`'s resubmit cleanup.
|
|
55
55
|
const open = (await escs.find({ pr_key: prKey, status: "open" })).sort((a, b) => b.id - a.id);
|
|
56
56
|
if (open.length > 0) {
|
|
57
57
|
const ts = new Date().toISOString();
|
|
@@ -63,7 +63,7 @@ const handler: AppJobHandler<In> = async (job, app) => {
|
|
|
63
63
|
for (const dup of open.slice(1)) {
|
|
64
64
|
await escs.update(dup.id, { status: "stale" });
|
|
65
65
|
}
|
|
66
|
-
//
|
|
66
|
+
// Move the PR off `status="escalated"` back to
|
|
67
67
|
// `"converging"` now that the question is answered. Without this the row stays `escalated` (with
|
|
68
68
|
// a now-null derived `openEscalation`) until the re-entered round's `persist-round` runs — a
|
|
69
69
|
// `/status` inconsistency and a divergence from the merge path both loops are meant to share.
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
// Red/green regression for answerEscalation (merge-loop message path) — Copilot review of PR #180.
|
|
2
|
-
//
|
|
3
|
-
// `pr.persist-escalation` always INSERTs a fresh `escalations` row with `status="open"`, so a
|
|
4
|
-
// retry/duplicate activation can leave more than one open row for the same PR. answerEscalation
|
|
5
|
-
// only closed the NEWEST open row, leaving older duplicates `open` — a phantom that keeps
|
|
6
|
-
// `activePrs` deriving an open escalation while the PR is still `escalated`. Every open row for the
|
|
7
|
-
// PR must leave `open` in this completion (newest answered, the rest marked `stale`), exactly as
|
|
8
|
-
// `submitPr`'s resubmit cleanup already does. Mirrors the review loop's `pr.answer-escalation`.
|
|
9
|
-
import { test } from "node:test";
|
|
10
|
-
import { assertEquals } from "#test-assert";
|
|
11
|
-
import type { DataLayer, EngineClient } from "@nanobpm/urban";
|
|
12
|
-
import { answerEscalation } from "./service.ts";
|
|
13
|
-
|
|
14
|
-
function memData(escalations: any[], prs: any[]): DataLayer {
|
|
15
|
-
const table = (name: string, key: string) => ({
|
|
16
|
-
async find(where: Record<string, unknown>) {
|
|
17
|
-
const src = name === "escalations" ? escalations : prs;
|
|
18
|
-
return src.filter((r) => Object.entries(where).every(([k, v]) => r[k] === v));
|
|
19
|
-
},
|
|
20
|
-
async update(k: unknown, patch: Record<string, unknown>) {
|
|
21
|
-
const src = name === "escalations" ? escalations : prs;
|
|
22
|
-
const r = src.find((x) => x[key] === k);
|
|
23
|
-
if (r) Object.assign(r, patch);
|
|
24
|
-
return r;
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
return { table } as any as DataLayer;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function fakeEngine(published: any[]): EngineClient {
|
|
31
|
-
return {
|
|
32
|
-
async publishMessage(m: any) {
|
|
33
|
-
published.push(m);
|
|
34
|
-
},
|
|
35
|
-
} as any as EngineClient;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
test("answerEscalation answers the newest open row and marks duplicate open rows stale", async () => {
|
|
39
|
-
const escalations = [
|
|
40
|
-
{ id: 3, pr_key: "o/r#1", status: "open", answer: null, answered_at: null },
|
|
41
|
-
{ id: 7, pr_key: "o/r#1", status: "open", answer: null, answered_at: null },
|
|
42
|
-
{ id: 9, pr_key: "o/r#2", status: "open", answer: null, answered_at: null },
|
|
43
|
-
];
|
|
44
|
-
const prs = [{ pr_key: "o/r#1", status: "escalated", updated_at: "t0" }];
|
|
45
|
-
const published: any[] = [];
|
|
46
|
-
const res = await answerEscalation(memData(escalations, prs), fakeEngine(published), "o/r#1", "Cap at 5.");
|
|
47
|
-
|
|
48
|
-
assertEquals(res, { ok: true, escalationId: 7 }, "the newest open row is the one answered");
|
|
49
|
-
const answered = escalations.find((e) => e.id === 7);
|
|
50
|
-
const stale = escalations.find((e) => e.id === 3);
|
|
51
|
-
const other = escalations.find((e) => e.id === 9);
|
|
52
|
-
assertEquals(answered?.status, "answered");
|
|
53
|
-
assertEquals(answered?.answer, "Cap at 5.");
|
|
54
|
-
assertEquals(stale?.status, "stale", "the older duplicate open row is retired to stale");
|
|
55
|
-
assertEquals(other?.status, "open", "the other PR's open row is untouched");
|
|
56
|
-
assertEquals(prs[0].status, "converging", "the PR row is moved off escalated");
|
|
57
|
-
assertEquals(published.length, 1, "the resume message is published once");
|
|
58
|
-
assertEquals(published[0].variables.escalationId, 7);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("answerEscalation is a no-op when no row is open", async () => {
|
|
62
|
-
const escalations = [{ id: 7, pr_key: "o/r#1", status: "answered", answer: "x", answered_at: "t" }];
|
|
63
|
-
const published: any[] = [];
|
|
64
|
-
const res = await answerEscalation(memData(escalations, []), fakeEngine(published), "o/r#1", "again");
|
|
65
|
-
assertEquals(res, { ok: false, reason: "no open escalation" });
|
|
66
|
-
assertEquals(published.length, 0);
|
|
67
|
-
});
|