@nanobpm/nano-workforce 0.208.5 → 0.209.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/app/behindBaseRemedy.test.ts +115 -0
- package/app/behindBaseRemedy.ts +95 -0
- package/app/github.test.ts +137 -2
- package/app/github.ts +137 -1
- package/app/mergeLoopBehaviour.test.ts +49 -0
- package/app/service.ts +7 -0
- package/build-info.json +4 -4
- package/package.json +1 -1
- package/pages/_nav.json +12 -1
- package/pages/auto-resolutions.page.json +14 -1
- package/pages/board.page.json +14 -1
- package/pages/cockpit.page.json +14 -1
- package/pages/delivery-graph-detail.page.json +14 -1
- package/pages/delivery-graphs.page.json +14 -1
- package/pages/epic-detail.page.json +14 -1
- package/pages/epic.page.json +14 -1
- package/pages/feature.page.json +14 -1
- package/pages/home.page.json +14 -1
- package/pages/lineage.page.json +14 -1
- package/pages/mcp.page.json +14 -1
- package/pages/overview.page.json +14 -1
- package/pages/tasks.page.json +14 -1
- package/pages/velocity.page.json +14 -1
- package/resources/processes/merge-loop.bpmn +2 -1
- package/scripts/sync-nav.test.ts +38 -0
- package/workers/merge-stall-probe/worker.test.ts +119 -0
- package/workers/merge-stall-probe/worker.ts +69 -8
- package/workers/request-review/worker.test.ts +27 -0
- package/workers/request-review/worker.ts +42 -2
|
@@ -21,6 +21,7 @@ function ops(over: Partial<ReviewRequestOps>, calls: string[]): ReviewRequestOps
|
|
|
21
21
|
hasPending: async () => (calls.push("pending?"), false),
|
|
22
22
|
remove: async () => calls.push("remove"),
|
|
23
23
|
request: async () => (calls.push("request"), "requested"),
|
|
24
|
+
awaitStarted: async () => true,
|
|
24
25
|
...over,
|
|
25
26
|
};
|
|
26
27
|
}
|
|
@@ -52,3 +53,29 @@ test("Copilot not assignable reports unavailable; a transport error reports fail
|
|
|
52
53
|
);
|
|
53
54
|
assertEquals(await makeHandler(ops({ request: async () => null }, []))(job(0), app), { reviewRequest: "failed" });
|
|
54
55
|
});
|
|
56
|
+
|
|
57
|
+
// Issue #951: a request that produced `review_requested` but never `copilot_work_started` is stuck —
|
|
58
|
+
// verify via the timeline and withdraw + retry ONCE before leaving it to the bounded wait.
|
|
59
|
+
test("a request that never starts Copilot is withdrawn and retried once (#951)", async () => {
|
|
60
|
+
const calls: string[] = [];
|
|
61
|
+
const { app } = fakeApp();
|
|
62
|
+
const out = await makeHandler(
|
|
63
|
+
ops({ awaitStarted: async () => (calls.push("started?"), false) }, calls),
|
|
64
|
+
)(job(0), app);
|
|
65
|
+
assertEquals(out, { reviewRequest: "requested" });
|
|
66
|
+
assertEquals(calls, ["request", "started?", "remove", "request", "started?"]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("a request that starts Copilot is not retried (#951)", async () => {
|
|
70
|
+
const calls: string[] = [];
|
|
71
|
+
const { app } = fakeApp();
|
|
72
|
+
await makeHandler(ops({ awaitStarted: async () => (calls.push("started?"), true) }, calls))(job(0), app);
|
|
73
|
+
assertEquals(calls, ["request", "started?"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("an unverifiable timeline (null) is not treated as a dropped request (#951)", async () => {
|
|
77
|
+
const calls: string[] = [];
|
|
78
|
+
const { app } = fakeApp();
|
|
79
|
+
await makeHandler(ops({ awaitStarted: async () => (calls.push("started?"), null) }, calls))(job(0), app);
|
|
80
|
+
assertEquals(calls, ["request", "started?"]);
|
|
81
|
+
});
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
// second POST is then a no-op. On a re-request (round > 0) with Copilot still pending, withdraw the
|
|
12
12
|
// request first and re-add it — the equivalent that re-triggers the review.
|
|
13
13
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
hasCopilotWorkStarted,
|
|
16
|
+
hasPendingCopilotReviewer,
|
|
17
|
+
removeCopilotReviewer,
|
|
18
|
+
requestCopilotReview,
|
|
19
|
+
} from "../../app/github.ts";
|
|
15
20
|
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
16
21
|
|
|
17
22
|
// Input typed off the model data envelope (`RequestReviewIn` in merge-loop.bpmn) — ADR 0040.
|
|
@@ -28,6 +33,29 @@ export interface ReviewRequestOps {
|
|
|
28
33
|
hasPending: (repo: string, prNumber: number) => Promise<boolean | null>;
|
|
29
34
|
remove: (repo: string, prNumber: number) => Promise<unknown>;
|
|
30
35
|
request: (repo: string, prNumber: number) => Promise<"requested" | "unavailable" | null>;
|
|
36
|
+
/** Issue #951: poll the PR timeline until Copilot STARTS the requested review
|
|
37
|
+
* (`copilot_work_started` at/after `sinceIso`) or the verify window (~2 min) elapses. `true` =
|
|
38
|
+
* started, `false` = the request was silently dropped, `null` = unverifiable (treated as started). */
|
|
39
|
+
awaitStarted: (repo: string, prNumber: number, sinceIso: string) => Promise<boolean | null>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const VERIFY_WINDOW_MS = 120_000;
|
|
43
|
+
const VERIFY_POLL_MS = 15_000;
|
|
44
|
+
|
|
45
|
+
async function pollWorkStarted(repo: string, n: number, sinceIso: string): Promise<boolean | null> {
|
|
46
|
+
const deadline = Date.now() + VERIFY_WINDOW_MS;
|
|
47
|
+
for (;;) {
|
|
48
|
+
// Bound each read by the REMAINING verification budget (issue #951 review): the deadline is
|
|
49
|
+
// otherwise checked only AFTER the read returns, so a stalled `gh`/REST read could run past it
|
|
50
|
+
// and starve the BPMN review-wait timer. A read that exhausts its budget returns null (unknown)
|
|
51
|
+
// — surfaced as "started" (no spurious retry), and the loop stops because the deadline is met.
|
|
52
|
+
const remaining = deadline - Date.now();
|
|
53
|
+
if (remaining <= 0) return false;
|
|
54
|
+
const started = await hasCopilotWorkStarted(repo, n, token(), sinceIso, { timeoutMs: remaining });
|
|
55
|
+
if (started !== false) return started;
|
|
56
|
+
if (Date.now() >= deadline) return false;
|
|
57
|
+
await new Promise((r) => setTimeout(r, Math.min(VERIFY_POLL_MS, deadline - Date.now())));
|
|
58
|
+
}
|
|
31
59
|
}
|
|
32
60
|
|
|
33
61
|
const token = () => process.env.GITHUB_TOKEN ?? "";
|
|
@@ -35,6 +63,7 @@ const defaultOps: ReviewRequestOps = {
|
|
|
35
63
|
hasPending: (repo, n) => hasPendingCopilotReviewer(repo, n, token()),
|
|
36
64
|
remove: (repo, n) => removeCopilotReviewer(repo, n, token()),
|
|
37
65
|
request: (repo, n) => requestCopilotReview(repo, n, token()),
|
|
66
|
+
awaitStarted: pollWorkStarted,
|
|
38
67
|
};
|
|
39
68
|
|
|
40
69
|
export function makeHandler(ops: ReviewRequestOps): AppJobHandler<In, Out> {
|
|
@@ -47,7 +76,18 @@ export function makeHandler(ops: ReviewRequestOps): AppJobHandler<In, Out> {
|
|
|
47
76
|
if (round > 0 && (await ops.hasPending(repo, prNumber)) === true) {
|
|
48
77
|
await ops.remove(repo, prNumber);
|
|
49
78
|
}
|
|
50
|
-
|
|
79
|
+
let since = new Date().toISOString();
|
|
80
|
+
let res = await ops.request(repo, prNumber);
|
|
81
|
+
// Issue #951: verify the request actually STARTED a review. A request that only produced
|
|
82
|
+
// `review_requested` (no `copilot_work_started`) is stuck — withdraw it and retry ONCE before
|
|
83
|
+
// leaving it to the bounded wait (and, past budget, the escalation).
|
|
84
|
+
if (res === "requested" && (await ops.awaitStarted(repo, prNumber, since)) === false) {
|
|
85
|
+
app.log.warn(`request-review: ${prKey} Copilot review request did not start — withdrawing and retrying once`);
|
|
86
|
+
await ops.remove(repo, prNumber);
|
|
87
|
+
since = new Date().toISOString();
|
|
88
|
+
res = await ops.request(repo, prNumber);
|
|
89
|
+
if (res === "requested") await ops.awaitStarted(repo, prNumber, since);
|
|
90
|
+
}
|
|
51
91
|
result = res === "requested" ? "requested" : res === "unavailable" ? "unavailable" : "failed";
|
|
52
92
|
} catch (err) {
|
|
53
93
|
app.log.warn(`request-review: ${prKey} Copilot review request failed: ${err}`);
|