@nanobpm/nano-workforce 0.193.0 → 0.193.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
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.193.1](https://github.com/nanobpm/nano-workforce/compare/v0.193.0...v0.193.1) (2026-09-28)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* **convergence:** reset round output vars on entry so a no-result round can't re-escalate ([#823](https://github.com/nanobpm/nano-workforce/issues/823)) ([3154918](https://github.com/nanobpm/nano-workforce/commit/3154918f3aeea477733347277ec067c1e0359e84)), closes [nano-coder#35](https://github.com/nanobpm/nano-coder/issues/35) [#806](https://github.com/nanobpm/nano-workforce/issues/806) [#822](https://github.com/nanobpm/nano-workforce/issues/822) [#822](https://github.com/nanobpm/nano-workforce/issues/822)
|
|
6
|
+
|
|
1
7
|
## [0.193.0](https://github.com/nanobpm/nano-workforce/compare/v0.192.0...v0.193.0) (2026-09-28)
|
|
2
8
|
|
|
3
9
|
### Features
|
package/SPEC.md
CHANGED
|
@@ -300,6 +300,27 @@ Notes:
|
|
|
300
300
|
poller does not solicit a spurious review against the still-running round. The
|
|
301
301
|
round cap and the review-wait timeout remain the outer safety nets.
|
|
302
302
|
|
|
303
|
+
- **Stale-output reset on round entry (issue #822).** `pr.capture-head` — the
|
|
304
|
+
single task every entry into `review-round` routes through first (from `Start`,
|
|
305
|
+
the review-loop re-enter, the human-answer resume, the husk auto-retry, and the
|
|
306
|
+
ack-only retry) — additionally CLEARS the round's output variables on entry:
|
|
307
|
+
`status`, `summary`, and `question` (→ `null`) and `escalated` (→ `false`),
|
|
308
|
+
mirroring the merge loop's `arm-merge` (which already clears `status` on entry
|
|
309
|
+
before its agent runs). A `review-round` job taken by a misconfigured worker can
|
|
310
|
+
exit 0 with no result variables, empty output, and no push; because nothing
|
|
311
|
+
overwrote them, the PREVIOUS round's `status`/`summary`/`question` would
|
|
312
|
+
otherwise persist and `gw-status` would re-route on that stale decision —
|
|
313
|
+
re-opening a word-for-word duplicate of an escalation a human had just answered
|
|
314
|
+
(the answer never reaching an agent). Resetting on entry makes a no-result round
|
|
315
|
+
enter on a blank `status`/`question`, so it can no longer inherit the prior
|
|
316
|
+
round's decision: it falls through `gw-status`'s `f_addressed` default into
|
|
317
|
+
`persist-round` → `check-progress`, where the unchanged head is caught by the
|
|
318
|
+
no-progress guard above and classified (husk vs. no-advance) on its own merits.
|
|
319
|
+
These vars are reset on ENTRY here rather than on `review-round`'s own output
|
|
320
|
+
because a same-task output-clear would clobber the agent's real verdict;
|
|
321
|
+
`answer`/`scopePending` (inputs the agent consumes) stay cleared on the
|
|
322
|
+
review-round output.
|
|
323
|
+
|
|
303
324
|
|
|
304
325
|
## 5. Agent job contract (`senior:pr-review`)
|
|
305
326
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Regression guard for issue #822 — the convergence loop must not REUSE a previous round's
|
|
2
|
+
// status/question/summary/escalated when an agent round returns NO result.
|
|
3
|
+
//
|
|
4
|
+
// The defect (proc 86936 / nano-coder#35): a `review-round` job taken by a misconfigured worker
|
|
5
|
+
// exited 0 in ~11s with NO result vars, no output, no commits, no push. Because nothing overwrote
|
|
6
|
+
// them, the PREVIOUS round's `status = "needs_input"`, `summary` and `question` stayed in scope.
|
|
7
|
+
// `gw-status` then routed on that stale `status`/`question` to `persist-escalation`, opening a
|
|
8
|
+
// word-for-word DUPLICATE of the escalation a human had just answered — the human's answer never
|
|
9
|
+
// reached an agent.
|
|
10
|
+
//
|
|
11
|
+
// The fix (categorical, not a special-case): clear the round's OUTPUT variables ON ENTRY to the
|
|
12
|
+
// round, on `capture-head` — the single task every entry into `review-round` routes through first
|
|
13
|
+
// (from Start, the review-loop re-enter, the human-answer resume, the husk auto-retry, and the
|
|
14
|
+
// ack-only retry). This mirrors the merge loop's `arm-merge`, which clears `status` on entry before
|
|
15
|
+
// its agent runs. With the round entering on a blank `status`/`question`, a no-result round can no
|
|
16
|
+
// longer inherit the prior round's decision: it falls through `gw-status`'s `f_addressed` default
|
|
17
|
+
// into `persist-round` → `check-progress`, where the unchanged PR head is caught as a no-progress
|
|
18
|
+
// round. `decideProgress` then classifies it on positive evidence (`app/roundProgress.ts`): only a
|
|
19
|
+
// corroborated non-terminal completing `review-round` instance is a husk (auto-retried within the
|
|
20
|
+
// budget); a terminal instance OR an absent/unknown read fails safe to `no-advance` and is escalated
|
|
21
|
+
// immediately as "produced nothing" (never re-running, to avoid duplicating agent work that may have
|
|
22
|
+
// run). Either way the round is routed as no-progress and NOT re-asked as an already-answered
|
|
23
|
+
// question.
|
|
24
|
+
//
|
|
25
|
+
// `answer`/`scopePending` (INPUTS the agent CONSUMES) are cleared on the review-round OUTPUT so the
|
|
26
|
+
// NEXT round can't re-consume them; `status`/`summary`/`question`/`escalated` (OUTPUTS the agent
|
|
27
|
+
// PRODUCES) must instead be cleared on ENTRY, because a same-task output-clear would clobber the
|
|
28
|
+
// agent's real verdict. Hence they live on `capture-head`, not on `review-round`.
|
|
29
|
+
//
|
|
30
|
+
// Pure text assertions over the committed BPMN (no engine), matching the repo's lightweight
|
|
31
|
+
// model-guard style (see convergenceEscalationGuard.test.ts, roundResultDefault.test.ts).
|
|
32
|
+
|
|
33
|
+
import { test } from "node:test";
|
|
34
|
+
import { assert, assertStringIncludes } from "#test-assert";
|
|
35
|
+
import { readFileSync } from "node:fs";
|
|
36
|
+
|
|
37
|
+
const bpmn = readFileSync("resources/processes/convergence-loop.bpmn", "utf8");
|
|
38
|
+
// Collapse whitespace so attribute-order / line-wrapping churn doesn't make the assertions brittle.
|
|
39
|
+
const flat = bpmn.replace(/\s+/g, " ");
|
|
40
|
+
|
|
41
|
+
/** The <serviceTask> element for the given id (whole element, up to its close), or null. */
|
|
42
|
+
function serviceTask(id: string): string | null {
|
|
43
|
+
const m = flat.match(new RegExp(`<bpmn:serviceTask\\b[^>]*\\bid="${id}"[\\s\\S]*?</bpmn:serviceTask>`));
|
|
44
|
+
return m ? m[0] : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The round-output variables that must be reset on entry so a no-result round can't inherit them.
|
|
48
|
+
// `escalated` is reset to `false` (a boolean flag); the rest to `null` (absent).
|
|
49
|
+
const RESET_ON_ENTRY: ReadonlyArray<{ target: string; source: string }> = [
|
|
50
|
+
{ target: "status", source: "=null" },
|
|
51
|
+
{ target: "summary", source: "=null" },
|
|
52
|
+
{ target: "question", source: "=null" },
|
|
53
|
+
{ target: "escalated", source: "=false" },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
test("capture-head clears the round's output vars on entry to every review round", () => {
|
|
57
|
+
const el = serviceTask("capture-head");
|
|
58
|
+
assert(el, "capture-head service task must exist");
|
|
59
|
+
for (const { target, source } of RESET_ON_ENTRY) {
|
|
60
|
+
const re = new RegExp(`<zeebe:output\\b[^>]*\\bsource="${source}"[^>]*\\btarget="${target}"[^>]*/>`);
|
|
61
|
+
const alt = new RegExp(`<zeebe:output\\b[^>]*\\btarget="${target}"[^>]*\\bsource="${source}"[^>]*/>`);
|
|
62
|
+
assert(
|
|
63
|
+
re.test(el!) || alt.test(el!),
|
|
64
|
+
`capture-head must clear "${target}" (source ${source}) on entry so a no-result round can't reuse the prior round's ${target} (issue #822)`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("capture-head still precedes review-round on every loop entry (the reset is truly on entry)", () => {
|
|
70
|
+
// The reset is only "on entry to the round" if capture-head runs immediately before review-round
|
|
71
|
+
// for EVERY way the loop re-enters. capture-head's single outgoing must target review-round, and
|
|
72
|
+
// it must be the join for all the re-entry flows.
|
|
73
|
+
const el = serviceTask("capture-head");
|
|
74
|
+
assert(el, "capture-head service task must exist");
|
|
75
|
+
const incoming = [...el!.matchAll(/<bpmn:incoming>([^<]+)<\/bpmn:incoming>/g)].map((m) => m[1]);
|
|
76
|
+
for (const flow of ["f_start", "f_reviewLoop", "f_answerLoop", "f_huskRetry", "f_ackRetry"]) {
|
|
77
|
+
assert(incoming.includes(flow), `capture-head must be the entry join for ${flow} so the reset covers that re-entry (issue #822)`);
|
|
78
|
+
}
|
|
79
|
+
const capture = flat.match(/<bpmn:sequenceFlow[^>]*\bid="f_capture"[^>]*\/>/);
|
|
80
|
+
assert(capture, "f_capture flow missing");
|
|
81
|
+
assertStringIncludes(capture![0], 'sourceRef="capture-head"');
|
|
82
|
+
assertStringIncludes(capture![0], 'targetRef="review-round"');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("gw-status still defaults a blank status to the addressed (re-enter) arm, not escalation", () => {
|
|
86
|
+
// The reset only helps if a blank status routes to the no-progress/husk path rather than
|
|
87
|
+
// escalation. This pins the interaction: with status/question cleared, gw-status falls to
|
|
88
|
+
// f_addressed (persist-round → check-progress), where an unchanged head is caught as a husk.
|
|
89
|
+
const gw = flat.match(/<bpmn:exclusiveGateway\b[^>]*\bid="gw-status"[^>]*>/);
|
|
90
|
+
assert(gw, "gw-status gateway missing");
|
|
91
|
+
assertStringIncludes(gw![0], 'default="f_addressed"');
|
|
92
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.193.
|
|
3
|
+
"version": "0.193.1",
|
|
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",
|
|
@@ -145,6 +145,12 @@
|
|
|
145
145
|
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="PrCaptureHeadIn" />
|
|
146
146
|
<zeebe:property name="io.nanobpm.dataEnvelope.out" value="PrCaptureHeadOut" />
|
|
147
147
|
</zeebe:properties>
|
|
148
|
+
<zeebe:ioMapping>
|
|
149
|
+
<zeebe:output source="=null" target="status" />
|
|
150
|
+
<zeebe:output source="=null" target="summary" />
|
|
151
|
+
<zeebe:output source="=null" target="question" />
|
|
152
|
+
<zeebe:output source="=false" target="escalated" />
|
|
153
|
+
</zeebe:ioMapping>
|
|
148
154
|
</bpmn:extensionElements>
|
|
149
155
|
<bpmn:incoming>f_start</bpmn:incoming>
|
|
150
156
|
<bpmn:incoming>f_reviewLoop</bpmn:incoming>
|
|
@@ -23,6 +23,16 @@
|
|
|
23
23
|
// fresh on every round entry (including husk retries), so it never carries a stale prior-round value.
|
|
24
24
|
// The read fails OPEN to `null` (a transient GitHub hiccup must never fabricate a no-progress verdict);
|
|
25
25
|
// progress-check then falls back to the persisted `last_round_head`.
|
|
26
|
+
//
|
|
27
|
+
// Because this task is the single on-entry join before `review-round`, the model also uses it to RESET
|
|
28
|
+
// the round's OUTPUT variables on entry (issue #822): its `ioMapping` clears `status`, `summary`,
|
|
29
|
+
// `question` (→ null) and `escalated` (→ false) in convergence-loop.bpmn. A `review-round` that returns
|
|
30
|
+
// NO result (a misconfigured worker that exits 0 with empty output, no commits, no push) then can't
|
|
31
|
+
// inherit the PREVIOUS round's `status`/`question` and re-open a word-for-word duplicate of an
|
|
32
|
+
// already-answered escalation; instead it falls through `gw-status`'s addressed default into the
|
|
33
|
+
// no-progress (husk) path. These OUTPUT vars are reset on ENTRY (here) rather than on the review-round's
|
|
34
|
+
// own output — a same-task output-clear would clobber the agent's real verdict — mirroring the merge
|
|
35
|
+
// loop's `arm-merge`, which clears `status` on entry before its agent runs.
|
|
26
36
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
27
37
|
import { fetchBranchHead, fetchPrHead } from "../../app/github.ts";
|
|
28
38
|
import { parsePr } from "../../app/service.ts";
|