@nanobpm/nano-workforce 0.187.5 → 0.187.6
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 +6 -0
- package/SPEC.md +24 -0
- package/app/convergeGate.test.ts +161 -4
- package/app/currentHead.ts +60 -0
- package/app/github.test.ts +65 -1
- package/app/github.ts +91 -19
- package/app/pollReviewsStale.test.ts +187 -0
- package/app/reviewWait.test.ts +33 -0
- package/app/reviewWait.ts +21 -0
- package/app/service.ts +37 -2
- package/package.json +1 -1
- package/resources/processes/convergence-loop.bpmn +138 -117
- package/resources/prompts/review-round.md +10 -0
- package/workers/converge-gate/worker.ts +48 -21
- package/workers/progress-check/worker.ts +6 -44
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Behavioral regression for the poller's stale-review branch (#799, FM2).
|
|
2
|
+
//
|
|
3
|
+
// `pollReviews` must only resume the convergence loop on a review of the CURRENT head. When the PR
|
|
4
|
+
// HEAD has advanced past the commit the newest review was submitted against, that review is STALE:
|
|
5
|
+
// its advisories describe code the head already moved past. Publishing `readiness-ready` on it would
|
|
6
|
+
// resume the loop on obsolete findings and re-escalate a human on an already-fixed advisory. The
|
|
7
|
+
// poller must instead treat a stale review like "no fresh review" — (re-)solicit and wait — WITHOUT
|
|
8
|
+
// advancing `last_review_id` or emitting the readiness signal.
|
|
9
|
+
//
|
|
10
|
+
// The pure `isReviewStale` predicate and the injected converge-gate already have coverage; this
|
|
11
|
+
// test locks the poller's own STATE TRANSITION (which those cannot), driving the real token-mode
|
|
12
|
+
// transport with a stubbed `fetch` so a differing branch-head SHA vs review `commit_id` is exercised
|
|
13
|
+
// end-to-end. A control with a CURRENT-head review asserts the loop still resumes.
|
|
14
|
+
import { test } from "node:test";
|
|
15
|
+
import { assertEquals } from "#test-assert";
|
|
16
|
+
import { withTrackingViews } from "../test/trackingViews.ts";
|
|
17
|
+
import { READINESS_READY_MESSAGE } from "./readiness.ts";
|
|
18
|
+
import { pollReviews } from "./service.ts";
|
|
19
|
+
|
|
20
|
+
function memTable(rows: any[], key: string) {
|
|
21
|
+
return {
|
|
22
|
+
get: (k: any) => Promise.resolve(rows.find((r) => r[key] === k) ?? null),
|
|
23
|
+
all: () => Promise.resolve([...rows]),
|
|
24
|
+
find: (q: any) =>
|
|
25
|
+
Promise.resolve(rows.filter((r) => Object.entries(q).every(([f, v]) => r[f] === v))),
|
|
26
|
+
findOne: (q: any) =>
|
|
27
|
+
Promise.resolve(rows.find((r) => Object.entries(q).every(([f, v]) => r[f] === v)) ?? null),
|
|
28
|
+
insert: (r: any) => {
|
|
29
|
+
rows.push(r);
|
|
30
|
+
return Promise.resolve(r);
|
|
31
|
+
},
|
|
32
|
+
update: (k: any, patch: any) => {
|
|
33
|
+
const r = rows.find((x) => x[key] === k);
|
|
34
|
+
if (r) Object.assign(r, patch);
|
|
35
|
+
return Promise.resolve(r);
|
|
36
|
+
},
|
|
37
|
+
delete: (k: any) => {
|
|
38
|
+
for (let i = rows.length - 1; i >= 0; i--) if (rows[i][key] === k) rows.splice(i, 1);
|
|
39
|
+
return Promise.resolve();
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const REPO = "owner/repo";
|
|
45
|
+
const NUMBER = 7;
|
|
46
|
+
const HEAD_REF = "feat/x";
|
|
47
|
+
|
|
48
|
+
/** Stub the token transport for exactly the endpoints the stale-review branch reads:
|
|
49
|
+
* - the paged reviews list (one short page → complete),
|
|
50
|
+
* - the PR object (for the head ref/repo),
|
|
51
|
+
* - the atomic branch ref (`git/ref/heads/<branch>`) the shared reader prefers, and
|
|
52
|
+
* - the requested-reviewers GET/POST the re-solicitation nudge uses.
|
|
53
|
+
* `branchHead` drives staleness: when it differs from the review's `commit_id` the review is stale.
|
|
54
|
+
* Every non-GET request is recorded in `posts` so a test can assert the nudge's reviewer-request POST
|
|
55
|
+
* actually fired (a regression that dropped the stale branch's nudge would leave `posts` empty). */
|
|
56
|
+
function reviewFetch(opts: { reviewCommitId: string; branchHead: string; posts: string[] }) {
|
|
57
|
+
return (url: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
58
|
+
const u = typeof url === "string" ? url : url.toString();
|
|
59
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
60
|
+
const json = (body: unknown, status = 200) =>
|
|
61
|
+
Promise.resolve(
|
|
62
|
+
new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }),
|
|
63
|
+
);
|
|
64
|
+
if (u.includes(`/pulls/${NUMBER}/requested_reviewers`)) {
|
|
65
|
+
if (method !== "GET") {
|
|
66
|
+
opts.posts.push(`${method} ${u}`);
|
|
67
|
+
return json({}, 201); // reviewer requested
|
|
68
|
+
}
|
|
69
|
+
return json({ users: [] }); // none pending → the nudge proceeds to the POST
|
|
70
|
+
}
|
|
71
|
+
if (u.includes(`/pulls/${NUMBER}/reviews`)) {
|
|
72
|
+
return json([
|
|
73
|
+
{ id: 5, state: "COMMENTED", submitted_at: "2026-09-16T00:45:33Z", commit_id: opts.reviewCommitId },
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
if (u.includes(`/git/ref/heads/${HEAD_REF}`)) {
|
|
77
|
+
return json({ object: { sha: opts.branchHead } });
|
|
78
|
+
}
|
|
79
|
+
if (u.endsWith(`/pulls/${NUMBER}`)) {
|
|
80
|
+
return json({ head: { ref: HEAD_REF, sha: opts.branchHead, repo: { full_name: REPO } }, base: { ref: "main" } });
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`unexpected fetch: ${method} ${u}`);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function makeEngine() {
|
|
87
|
+
const published: Array<{ name: string; correlationKey: string }> = [];
|
|
88
|
+
const engine = {
|
|
89
|
+
publishMessage: (m: { name: string; correlationKey: string }) => {
|
|
90
|
+
published.push({ name: m.name, correlationKey: m.correlationKey });
|
|
91
|
+
return Promise.resolve();
|
|
92
|
+
},
|
|
93
|
+
} as any;
|
|
94
|
+
return { engine, published };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function withTokenTransport<T>(run: () => Promise<T>): Promise<T> {
|
|
98
|
+
const prevMode = process.env["NANO_PR_GITHUB_TRANSPORT"];
|
|
99
|
+
const prevTok = process.env["GITHUB_TOKEN"];
|
|
100
|
+
process.env["NANO_PR_GITHUB_TRANSPORT"] = "token";
|
|
101
|
+
process.env["GITHUB_TOKEN"] = "test-token";
|
|
102
|
+
return run().finally(() => {
|
|
103
|
+
if (prevMode !== undefined) process.env["NANO_PR_GITHUB_TRANSPORT"] = prevMode;
|
|
104
|
+
else delete process.env["NANO_PR_GITHUB_TRANSPORT"];
|
|
105
|
+
if (prevTok !== undefined) process.env["GITHUB_TOKEN"] = prevTok;
|
|
106
|
+
else delete process.env["GITHUB_TOKEN"];
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function prRow() {
|
|
111
|
+
return {
|
|
112
|
+
pr_key: `${REPO}#${NUMBER}`,
|
|
113
|
+
repo: REPO,
|
|
114
|
+
number: NUMBER,
|
|
115
|
+
status: "waiting_review",
|
|
116
|
+
last_review_id: 0,
|
|
117
|
+
waiting_since: "2026-09-16T00:00:00Z",
|
|
118
|
+
// An EXPIRED nudge timestamp so `maybeRerequestReview` does NOT short-circuit at its cooldown —
|
|
119
|
+
// the stale branch must actually re-solicit a fresh review, and the test asserts that POST fired
|
|
120
|
+
// (a recent nudge would mask a regression that dropped the nudge entirely, #799 review).
|
|
121
|
+
last_nudge_at: "2000-01-01T00:00:00Z",
|
|
122
|
+
updated_at: "t0",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
test("pollReviews: a STALE review (branch head past the review's commit) nudges but does NOT resume the loop", async () => {
|
|
127
|
+
const row = prRow();
|
|
128
|
+
const stores: Record<string, { rows: unknown[]; key: string }> = {
|
|
129
|
+
pull_requests: { rows: [row], key: "pr_key" },
|
|
130
|
+
};
|
|
131
|
+
const data = {
|
|
132
|
+
table: withTrackingViews((name: string, key: string) =>
|
|
133
|
+
memTable(stores[name]?.rows ?? [], stores[name]?.key ?? key),
|
|
134
|
+
),
|
|
135
|
+
} as any;
|
|
136
|
+
const { engine, published } = makeEngine();
|
|
137
|
+
const posts: string[] = [];
|
|
138
|
+
const prevFetch = globalThis.fetch;
|
|
139
|
+
await withTokenTransport(async () => {
|
|
140
|
+
// review was submitted against SHA_OLD, but the branch head has advanced to SHA_NEW → stale.
|
|
141
|
+
globalThis.fetch = reviewFetch({ reviewCommitId: "SHA_OLD", branchHead: "SHA_NEW", posts }) as typeof fetch;
|
|
142
|
+
try {
|
|
143
|
+
await pollReviews(data, engine, "test-token");
|
|
144
|
+
} finally {
|
|
145
|
+
globalThis.fetch = prevFetch;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
assertEquals(published.length, 0, "no readiness-ready signal is published for a stale review");
|
|
149
|
+
assertEquals(row.last_review_id, 0, "last_review_id is NOT advanced past the stale review");
|
|
150
|
+
assertEquals(row.status, "waiting_review", "the PR stays parked awaiting a fresh review");
|
|
151
|
+
assertEquals(posts.length, 1, "the stale branch re-solicits a fresh Copilot review (one nudge POST)");
|
|
152
|
+
assertEquals(
|
|
153
|
+
posts[0],
|
|
154
|
+
`POST https://api.github.com/repos/${REPO}/pulls/${NUMBER}/requested_reviewers`,
|
|
155
|
+
"the nudge POSTs the requested-reviewers endpoint",
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("pollReviews: a CURRENT-head review (control) resumes the loop and publishes the readiness signal", async () => {
|
|
160
|
+
const row = prRow();
|
|
161
|
+
const stores: Record<string, { rows: unknown[]; key: string }> = {
|
|
162
|
+
pull_requests: { rows: [row], key: "pr_key" },
|
|
163
|
+
};
|
|
164
|
+
const data = {
|
|
165
|
+
table: withTrackingViews((name: string, key: string) =>
|
|
166
|
+
memTable(stores[name]?.rows ?? [], stores[name]?.key ?? key),
|
|
167
|
+
),
|
|
168
|
+
} as any;
|
|
169
|
+
const { engine, published } = makeEngine();
|
|
170
|
+
const posts: string[] = [];
|
|
171
|
+
const prevFetch = globalThis.fetch;
|
|
172
|
+
await withTokenTransport(async () => {
|
|
173
|
+
// review's commit_id equals the current branch head → NOT stale.
|
|
174
|
+
globalThis.fetch = reviewFetch({ reviewCommitId: "SHA_NEW", branchHead: "SHA_NEW", posts }) as typeof fetch;
|
|
175
|
+
try {
|
|
176
|
+
await pollReviews(data, engine, "test-token");
|
|
177
|
+
} finally {
|
|
178
|
+
globalThis.fetch = prevFetch;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
assertEquals(published.length, 1, "the readiness-ready signal is published for a fresh review");
|
|
182
|
+
assertEquals(published[0]?.name, READINESS_READY_MESSAGE);
|
|
183
|
+
assertEquals(published[0]?.correlationKey, `${REPO}#${NUMBER}`);
|
|
184
|
+
assertEquals(row.last_review_id, 5, "last_review_id advances to the fresh review");
|
|
185
|
+
assertEquals(row.status, "converging", "the loop resumes (status flips to converging)");
|
|
186
|
+
assertEquals(posts.length, 0, "a current-head review resumes directly — no re-solicitation nudge");
|
|
187
|
+
});
|
package/app/reviewWait.test.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
DEFAULT_REVIEW_NUDGE_MINUTES,
|
|
10
10
|
DEFAULT_REVIEW_WAIT_TIMEOUT,
|
|
11
11
|
isoDurationToMs,
|
|
12
|
+
isReviewStale,
|
|
12
13
|
MAX_REVIEW_NUDGE_MINUTES,
|
|
13
14
|
reviewWaitTimeout,
|
|
14
15
|
} from "./reviewWait.ts";
|
|
@@ -95,3 +96,35 @@ test("clampNudgeMinutes: above the ceiling is clamped, not rejected", () => {
|
|
|
95
96
|
test("clampNudgeMinutes: an oversized fallback is itself clamped to the ceiling", () => {
|
|
96
97
|
assertEquals(clampNudgeMinutes("", MAX_REVIEW_NUDGE_MINUTES + 50), MAX_REVIEW_NUDGE_MINUTES);
|
|
97
98
|
});
|
|
99
|
+
|
|
100
|
+
// ── isReviewStale (issue #799) ───────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
test("isReviewStale: differing review commit_id and HEAD sha is stale", () => {
|
|
103
|
+
assertEquals(isReviewStale("aaa1111", "bbb2222"), true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("isReviewStale: matching review commit_id and HEAD sha is NOT stale", () => {
|
|
107
|
+
assertEquals(isReviewStale("aaa1111", "aaa1111"), false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("isReviewStale: surrounding whitespace is ignored in the comparison", () => {
|
|
111
|
+
assertEquals(isReviewStale(" aaa1111 ", "aaa1111"), false);
|
|
112
|
+
assertEquals(isReviewStale("aaa1111", " bbb2222 "), true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("isReviewStale: a missing review commit_id fails safe to NOT stale", () => {
|
|
116
|
+
assertEquals(isReviewStale(null, "bbb2222"), false);
|
|
117
|
+
assertEquals(isReviewStale(undefined, "bbb2222"), false);
|
|
118
|
+
assertEquals(isReviewStale("", "bbb2222"), false);
|
|
119
|
+
assertEquals(isReviewStale(" ", "bbb2222"), false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("isReviewStale: a missing HEAD sha fails safe to NOT stale", () => {
|
|
123
|
+
assertEquals(isReviewStale("aaa1111", null), false);
|
|
124
|
+
assertEquals(isReviewStale("aaa1111", undefined), false);
|
|
125
|
+
assertEquals(isReviewStale("aaa1111", ""), false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("isReviewStale: both missing is NOT stale", () => {
|
|
129
|
+
assertEquals(isReviewStale(null, null), false);
|
|
130
|
+
});
|
package/app/reviewWait.ts
CHANGED
|
@@ -93,3 +93,24 @@ export function clampNudgeMinutes(
|
|
|
93
93
|
if (i < 1) return safeFallback;
|
|
94
94
|
return Math.min(i, MAX_REVIEW_NUDGE_MINUTES);
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
/** Is the latest Copilot review STALE relative to the PR's current HEAD? (issue #799)
|
|
98
|
+
*
|
|
99
|
+
* A review is stale when it was submitted against a commit the head has since moved past — its
|
|
100
|
+
* advisories describe code that no longer exists, so the convergence gate must NOT block/escalate
|
|
101
|
+
* against it and the poller must NOT unpark the loop on it; instead a fresh review of the current
|
|
102
|
+
* HEAD must be solicited and gated on. Staleness is a plain SHA inequality.
|
|
103
|
+
*
|
|
104
|
+
* Fails SAFE to "not stale" when either SHA is unknown (`null`/`undefined`/blank): GitHub did not
|
|
105
|
+
* carry a `commit_id` for the review, or the head could not be read. Without both SHAs we cannot
|
|
106
|
+
* prove the review predates the head, so we must not fabricate a stale verdict that would loop the
|
|
107
|
+
* loop re-soliciting forever — the review-wait timeout and the round cap remain the safety nets. */
|
|
108
|
+
export function isReviewStale(
|
|
109
|
+
reviewCommitId: string | null | undefined,
|
|
110
|
+
headSha: string | null | undefined,
|
|
111
|
+
): boolean {
|
|
112
|
+
const rev = (reviewCommitId ?? "").trim();
|
|
113
|
+
const head = (headSha ?? "").trim();
|
|
114
|
+
if (rev === "" || head === "") return false;
|
|
115
|
+
return rev !== head;
|
|
116
|
+
}
|
package/app/service.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
CONFORMANCE_ESCALATION_ELEMENT,
|
|
27
27
|
conformanceEscalationQuestion,
|
|
28
28
|
} from "./conformance.ts";
|
|
29
|
+
import { makeDefaultReadHead } from "./currentHead.ts";
|
|
29
30
|
import { isUniqueConstraintFence } from "./dbFence.ts";
|
|
30
31
|
import { deriveDelivery, EPIC_LIVE_STATUSES, TERMINAL_STATUSES } from "./delivery.ts";
|
|
31
32
|
import { sweepExpiredProposals } from "./deliveryGraphProposals.ts";
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
coalesceTitle,
|
|
41
42
|
ensureFreshHeadRun,
|
|
42
43
|
ensurePromotionPr,
|
|
44
|
+
fetchBranchHead,
|
|
43
45
|
fetchDefaultBranch,
|
|
44
46
|
fetchPrHead,
|
|
45
47
|
fetchPrMeta,
|
|
@@ -91,7 +93,7 @@ import {
|
|
|
91
93
|
// (submitPr/startMerge) and re-exported below so the long-standing `import { repoEnvelopeVars } from
|
|
92
94
|
// "./service.ts"` call sites (and its tests) keep resolving.
|
|
93
95
|
import { repoEnvelopeVars } from "./repoEnvelope.ts";
|
|
94
|
-
import { clampNudgeMinutes, reviewWaitTimeout } from "./reviewWait.ts";
|
|
96
|
+
import { clampNudgeMinutes, isReviewStale, reviewWaitTimeout } from "./reviewWait.ts";
|
|
95
97
|
import { trialMergeAudits } from "./trialMerge.ts";
|
|
96
98
|
import {
|
|
97
99
|
buildUserTaskRow,
|
|
@@ -833,7 +835,12 @@ export async function activePrs(data: DataLayer): Promise<ActivePr[]> {
|
|
|
833
835
|
* fresh-review detection + Copilot nudge below stay here because review freshness is inherently
|
|
834
836
|
* STATEFUL (keyed off `last_review_id`/`waiting_since`), which the stateless probe matchers cannot
|
|
835
837
|
* subsume — the poller owns the forward-progress guarantee, the gate owns the bounded wait. */
|
|
836
|
-
|
|
838
|
+
// The poller's PR current-head reader — the SAME branch-ref-preferring reader the converge-gate and
|
|
839
|
+
// progress-check bind (#786/#799), so the poller's stale-review detection sees the exact head those
|
|
840
|
+
// steps do. Fails OPEN to `null` (unreadable head → not stale, per `isReviewStale`).
|
|
841
|
+
const readCurrentHead = makeDefaultReadHead({ fetchPrHead, fetchBranchHead });
|
|
842
|
+
|
|
843
|
+
export async function pollReviews(data: DataLayer, engine: EngineClient, token: string) {
|
|
837
844
|
const waiting = await prs(data).find({ status: "waiting_review" });
|
|
838
845
|
for (const pr of waiting) {
|
|
839
846
|
const { repo, number, pr_key: prKey } = pr;
|
|
@@ -854,6 +861,34 @@ async function pollReviews(data: DataLayer, engine: EngineClient, token: string)
|
|
|
854
861
|
await maybeRerequestReview(data, pr, token);
|
|
855
862
|
continue;
|
|
856
863
|
}
|
|
864
|
+
// #799 (FM2): only resume the loop on a review of the CURRENT head. If the PR HEAD has
|
|
865
|
+
// advanced past the commit the newest review was submitted against, that review is STALE — its
|
|
866
|
+
// advisories describe code the head has moved past (e.g. an advisory the agent already fixed in
|
|
867
|
+
// a later commit). Publishing `readiness-ready` on it would resume the loop on obsolete
|
|
868
|
+
// findings and, at the converge-gate, re-escalate a human on an already-fixed advisory (PR
|
|
869
|
+
// #789). Treat a stale review like "no fresh review": (re-)solicit a fresh review of the
|
|
870
|
+
// current head and wait — without bumping `last_review_id`, so the next HEAD-current review is
|
|
871
|
+
// still detected. The head read fails OPEN (null → not stale), so a transport hiccup never
|
|
872
|
+
// strands a genuine review; the review-wait timer remains the backstop.
|
|
873
|
+
//
|
|
874
|
+
// Read the head via the SHARED branch-ref-preferring reader (`makeDefaultReadHead`, #786) — the
|
|
875
|
+
// exact reader the converge-gate uses — NOT `fetchPrHead(...).headSha` directly: the PR object's
|
|
876
|
+
// `head.sha` is an asynchronously-denormalized projection that can still equal `fresh.commit_id`
|
|
877
|
+
// right after a push, which would make the poller advance `last_review_id` on a stale review the
|
|
878
|
+
// gate would then classify stale, wedging the loop. Using the atomic branch ref makes the poller
|
|
879
|
+
// detect the same stale-head case the gate does.
|
|
880
|
+
// Read the head with the SAME per-call token the review fetch above uses (#799) — NOT the
|
|
881
|
+
// env-token default `makeDefaultReadHead` would otherwise fall back to. A caller that supplies
|
|
882
|
+
// a `token` without also setting GITHUB_TOKEN would otherwise get a `null` head here, which
|
|
883
|
+
// `isReviewStale` treats as not-stale (fails OPEN) and would advance `last_review_id` /
|
|
884
|
+
// publish readiness on a genuinely stale review. Passing `token` keeps both reads on one
|
|
885
|
+
// credential so the stale guard sees the real head.
|
|
886
|
+
const headSha = await readCurrentHead(repo, number, token).catch(() => null);
|
|
887
|
+
if (isReviewStale(fresh.commit_id, headSha)) {
|
|
888
|
+
console.log(`[poller] review ${fresh.id} is stale (predates HEAD) -> re-soliciting ${prKey}`);
|
|
889
|
+
await maybeRerequestReview(data, pr, token);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
857
892
|
await prs(data).update(prKey, { last_review_id: fresh.id, status: "converging", updated_at: now() });
|
|
858
893
|
await engine.publishMessage({
|
|
859
894
|
name: READINESS_READY_MESSAGE,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.187.
|
|
3
|
+
"version": "0.187.6",
|
|
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",
|