@nanobpm/nano-workforce 0.98.1 → 0.99.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 +7 -0
- package/SPEC.md +16 -12
- package/app/github.ts +1 -1
- package/app/readiness.ts +8 -0
- package/app/reviewReadinessGate.test.ts +92 -0
- package/app/reviewWait.ts +1 -1
- package/app/service.ts +24 -4
- package/main.ts +3 -2
- package/package.json +1 -1
- package/resources/processes/convergence-loop.bpmn +3 -8
- package/workers/readiness-probe/worker.ts +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [0.99.0](https://github.com/nanobpm/nano-workforce/compare/v0.98.1...v0.99.0) (2026-08-19)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **review-loop:** re-express the review-ready wait on the ReadinessProbe gate ([#259](https://github.com/nanobpm/nano-workforce/issues/259)) ([#338](https://github.com/nanobpm/nano-workforce/issues/338)) ([4d86509](https://github.com/nanobpm/nano-workforce/commit/4d86509f1f55dd9b31417add323e04da5b574b3d)), closes [#258](https://github.com/nanobpm/nano-workforce/issues/258) [#258](https://github.com/nanobpm/nano-workforce/issues/258)
|
|
7
|
+
|
|
1
8
|
## [0.98.1](https://github.com/nanobpm/nano-workforce/compare/v0.98.0...v0.98.1) (2026-08-19)
|
|
2
9
|
|
|
3
10
|
|
package/SPEC.md
CHANGED
|
@@ -29,7 +29,7 @@ surface are proposed and open for adjustment.
|
|
|
29
29
|
submit (form / webhook)
|
|
30
30
|
│
|
|
31
31
|
▼
|
|
32
|
-
┌───────────────────┐
|
|
32
|
+
┌───────────────────┐ readiness-ready (msg) ┌──────────┐
|
|
33
33
|
│ convergence-loop │◀───────────────────────────────│ poller │
|
|
34
34
|
│ (BPMN) │ └────┬─────┘
|
|
35
35
|
└─────────┬─────────┘◀───────────────┐ │ polls
|
|
@@ -48,8 +48,10 @@ surface are proposed and open for adjustment.
|
|
|
48
48
|
agent worker slots and job timeouts are never held hostage to Copilot's reply
|
|
49
49
|
latency.
|
|
50
50
|
- **Poller**: an in-app background loop that watches waiting PRs and publishes
|
|
51
|
-
the `
|
|
52
|
-
|
|
51
|
+
the canonical `readiness-ready` wait-gate message (ADR 0001 §2) when a new
|
|
52
|
+
review lands — the "out-of-band poller-correlated shape" the review-ready wait
|
|
53
|
+
is re-expressed on (#259), so there is one "wait for the world" mechanism (no
|
|
54
|
+
GitHub webhook needed; works behind NAT).
|
|
53
55
|
|
|
54
56
|
## 3. Repository layout
|
|
55
57
|
|
|
@@ -100,7 +102,7 @@ known at submit time, carried as a process variable and stored on the DB row.
|
|
|
100
102
|
│ ├── converged → [Mark converged] → (end: converged)
|
|
101
103
|
│ │
|
|
102
104
|
│ ├── addressed → [Record round] → <event-based gateway: review ready or timeout?>
|
|
103
|
-
│ │ ├──
|
|
105
|
+
│ │ ├── readiness-ready (msg catch, key = prKey) → round++ ─┐
|
|
104
106
|
│ │ └── =reviewWaitTimeout (timer catch) │
|
|
105
107
|
│ │ → [Escalate: review stalled] (blocked) │
|
|
106
108
|
│ │ → [Wait: wait-answer userTask] ─────────────────────┤
|
|
@@ -127,8 +129,9 @@ Guard: before each Review round, if round > MAX_ROUNDS → force an escalation
|
|
|
127
129
|
```
|
|
128
130
|
|
|
129
131
|
Notes:
|
|
130
|
-
- On `addressed`, the loop parks at an **event-based gateway** that races
|
|
131
|
-
`
|
|
132
|
+
- On `addressed`, the loop parks at an **event-based gateway** that races the
|
|
133
|
+
canonical `readiness-ready` wait-gate message (ADR 0001 §2; correlated by the
|
|
134
|
+
poller when a fresh review lands)
|
|
132
135
|
against a `=reviewWaitTimeout` timer (seeded at submit from
|
|
133
136
|
`NANO_PR_REVIEW_WAIT_TIMEOUT`, default `PT20M`). Whichever fires first
|
|
134
137
|
withdraws the other — the message arm advances `round`, the timer arm escalates
|
|
@@ -136,7 +139,8 @@ Notes:
|
|
|
136
139
|
hanging forever. Because `persist-round` already recorded this `round` as
|
|
137
140
|
`addressed` before the gateway, the timer arm opens the escalation **without
|
|
138
141
|
re-recording the round** (it passes `recordRound=false`), so a single round is
|
|
139
|
-
never logged as both `addressed` and `blocked`. This
|
|
142
|
+
never logged as both `addressed` and `blocked`. This wait is re-expressed on the
|
|
143
|
+
ONE `ReadinessProbe` wait-gate primitive (#258, ADR 0001 §2); it replaced a bare
|
|
140
144
|
`review-ready` catch that could hang
|
|
141
145
|
indefinitely: Copilot won't re-review a round with no new commit and routinely
|
|
142
146
|
dismisses a re-request, so with no timeout a review that never arrives wedged
|
|
@@ -210,7 +214,7 @@ Consequences the prompt (`resources/prompts/review-round.md`) encodes:
|
|
|
210
214
|
| message | correlationKey | published by | payload |
|
|
211
215
|
|---|---|---|---|
|
|
212
216
|
| `pr-submitted` | — (start) | submit route/webhook | `{repo, prNumber, prUrl, prKey}` |
|
|
213
|
-
| `
|
|
217
|
+
| `readiness-ready` | `prKey` | **poller** | `{ready, detail?}` (ADR 0001 §2 wait-gate; the review-ready wait, re-expressed on the ReadinessProbe gate — #259) |
|
|
214
218
|
| `deps-cleared` | `prKey` | **poller** (merge) | — (all `Depends-on` PRs merged) |
|
|
215
219
|
| `merge-ready` | `prKey` | **poller** (merge) | `{mergeState}` (`ready` \| `conflict` \| `blocked`); when `blocked`, also `{failingChecks, failingChecksList}` for the `senior:fix-ci` branch |
|
|
216
220
|
| `merge-landed` | `prKey` | **poller** (merge) | — (queued PR merged, or merged out-of-band) |
|
|
@@ -368,8 +372,8 @@ An in-app loop (interval `NANO_PR_POLL_MS`, default 60s):
|
|
|
368
372
|
1. `SELECT pr_key, repo, number, waiting_since, last_review_id, last_nudge_at FROM pull_requests WHERE status = 'waiting_review'`.
|
|
369
373
|
2. For each, GET the PR's reviews from GitHub; find the newest review submitted
|
|
370
374
|
after `waiting_since` with id > `last_review_id`.
|
|
371
|
-
3. If found → publish `
|
|
372
|
-
`last_review_id`.
|
|
375
|
+
3. If found → publish the canonical `readiness-ready` wait-gate message (key =
|
|
376
|
+
`pr_key`, `{ready, detail}`) and set `last_review_id`.
|
|
373
377
|
4. If **not** found → ensure a review is in flight: unless Copilot is already a
|
|
374
378
|
pending reviewer, **re-request** it (REST `requested_reviewers`, exact login
|
|
375
379
|
`copilot-pull-request-reviewer[bot]`) and record `last_nudge_at`. This is
|
|
@@ -377,7 +381,7 @@ An in-app loop (interval `NANO_PR_POLL_MS`, default 60s):
|
|
|
377
381
|
so a re-request Copilot dismisses is retried without hammering the API. A repo
|
|
378
382
|
where Copilot isn't an assignable reviewer (HTTP 422) is left to the process's
|
|
379
383
|
review-wait timer (§4). This closes the stall where Copilot won't spontaneously
|
|
380
|
-
re-review and silently dismisses a re-request, so no `
|
|
384
|
+
re-review and silently dismisses a re-request, so no `readiness-ready` ever fires.
|
|
381
385
|
|
|
382
386
|
Requires a GitHub token (`GITHUB_TOKEN`) or the host `gh` CLI. One cheap API call
|
|
383
387
|
per waiting PR per interval (plus at most one reviewer-state check + re-request per
|
|
@@ -644,7 +648,7 @@ but encode incompatible decisions about a shared contract** — a genuine design
|
|
|
644
648
|
`baseRef`) that let large monorepos provision within the clone timeout (#287); the
|
|
645
649
|
harness is PR-agnostic and provisions from that envelope. The worker stays a pure
|
|
646
650
|
provisioner.
|
|
647
|
-
- **
|
|
651
|
+
- **readiness-ready via GitHub webhook** — same `readiness-ready` message, swappable faster trigger,
|
|
648
652
|
when the app is publicly reachable. Deferred (poller-only for v1).
|
|
649
653
|
- **Supervised vs external worker** — the agent runs as an external
|
|
650
654
|
`c8ctl nano work` daemon by default; a supervised in-server mode is possible
|
package/app/github.ts
CHANGED
|
@@ -348,7 +348,7 @@ export async function hasPendingCopilotReviewer(
|
|
|
348
348
|
}
|
|
349
349
|
|
|
350
350
|
/** Request a fresh Copilot review on the PR (REST reviewers endpoint, exact `[bot]` login), so
|
|
351
|
-
* the process's `
|
|
351
|
+
* the process's `readiness-ready` review wait-gate catch can eventually fire. Returns `"requested"` on success,
|
|
352
352
|
* `"unavailable"` when Copilot is not an assignable reviewer on that repo (HTTP 422 — e.g.
|
|
353
353
|
* Copilot review not enabled there), or `null` when no transport is usable. Never throws for the
|
|
354
354
|
* 422 "not assignable" case; only a genuine transport failure propagates. */
|
package/app/readiness.ts
CHANGED
|
@@ -150,6 +150,14 @@ export const DEFAULT_ATTEMPT_TIMEOUT_MS = 60_000;
|
|
|
150
150
|
/** Default gate timeout when neither the descriptor nor `NANO_READINESS_POLL_TIMEOUT` supplies one. */
|
|
151
151
|
export const DEFAULT_READINESS_TIMEOUT = "PT30M";
|
|
152
152
|
|
|
153
|
+
/** The ONE canonical readiness-signal message the wait-gate correlates on to release the wait
|
|
154
|
+
* (ADR 0001 §2) — the correlation key is per-caller (`=gateKey` for the in-flow probe worker,
|
|
155
|
+
* `=prKey` for the review-ready wait), not a single fixed key. Both the in-flow probe worker
|
|
156
|
+
* (`pr.readiness-probe`) and the out-of-band
|
|
157
|
+
* poller-correlated shape used by the review-ready migration (#259, per #258 pinned decision 3)
|
|
158
|
+
* publish under this ONE name — so there is a single "wait for the world" message, no bespoke twin. */
|
|
159
|
+
export const READINESS_READY_MESSAGE = "readiness-ready";
|
|
160
|
+
|
|
153
161
|
const str = (v: unknown): string => (typeof v === "string" ? v : v == null ? "" : String(v));
|
|
154
162
|
const num = (v: unknown): number | undefined =>
|
|
155
163
|
typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Structural guard for the review-ready wait re-expressed on the canonical ReadinessProbe
|
|
2
|
+
// wait-gate contract (ADR 0001 §2, issue #259).
|
|
3
|
+
//
|
|
4
|
+
// #258 landed the generic durable wait-gate: an event-based gateway racing a canonical
|
|
5
|
+
// `readiness-ready` signal against a bounded timer that escalates. #259 collapses nwf's bespoke
|
|
6
|
+
// review-ready poll path onto that ONE primitive — so there is a single "wait for the world"
|
|
7
|
+
// message, no drift-prone twin. The out-of-band poller-correlated shape (#258 pinned decision 3)
|
|
8
|
+
// keeps the canonical self-scheduling poller (`pollReviews` in app/service.ts) publishing the
|
|
9
|
+
// signal when a fresh review lands, while the convergence-loop parks on the canonical gate.
|
|
10
|
+
//
|
|
11
|
+
// These pure text assertions over the committed BPMN lock the migrated wait: it now catches the
|
|
12
|
+
// canonical `readiness-ready` message (the bespoke `review-ready` message is GONE), and it stays
|
|
13
|
+
// BOUNDED — the event-based gateway still races the signal against the `reviewWaitTimeout` timer,
|
|
14
|
+
// whose arm escalates to a human. Matches the repo's model-guard style (see roundResultDefault.test.ts).
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { test } from "node:test";
|
|
18
|
+
import { assert, assertStringIncludes } from "#test-assert";
|
|
19
|
+
import { READINESS_READY_MESSAGE } from "./readiness.ts";
|
|
20
|
+
|
|
21
|
+
const bpmn = readFileSync("resources/processes/convergence-loop.bpmn", "utf8");
|
|
22
|
+
const flat = bpmn.replace(/\s+/g, " ");
|
|
23
|
+
|
|
24
|
+
test("the review wait catches the canonical readiness-ready message, not a bespoke twin", () => {
|
|
25
|
+
// The one canonical wait-gate signal (#258), re-used here — no separate `review-ready` message.
|
|
26
|
+
const msg = flat.match(/<bpmn:message\b[^>]*\bname="readiness-ready"[\s\S]*?<\/bpmn:message>/);
|
|
27
|
+
assert(msg, "convergence-loop must declare the canonical readiness-ready message");
|
|
28
|
+
assertStringIncludes(msg![0], 'correlationKey="=prKey"', "correlation shape is preserved (=prKey)");
|
|
29
|
+
|
|
30
|
+
// The bespoke review-ready message/shape must be fully retired (one mechanism, no drift).
|
|
31
|
+
assert(!/name="review-ready"/.test(flat), "the bespoke review-ready message must be gone");
|
|
32
|
+
assert(!/id="Message_reviewReady"/.test(flat), "the bespoke Message_reviewReady must be gone");
|
|
33
|
+
assert(!/id="ReviewReady"/.test(flat), "the bespoke ReviewReady payload shape must be gone");
|
|
34
|
+
|
|
35
|
+
// And the constant the poller publishes under is the very same canonical name.
|
|
36
|
+
assert(new RegExp(`name="${READINESS_READY_MESSAGE}"`).test(flat), "message name matches the canonical constant");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("the review-ready catch subscribes to the canonical readiness-ready message", () => {
|
|
40
|
+
const wait = flat.match(/<bpmn:intermediateCatchEvent\b[^>]*\bid="wait-review"[\s\S]*?<\/bpmn:intermediateCatchEvent>/);
|
|
41
|
+
assert(wait, "the wait-review catch event must exist");
|
|
42
|
+
assertStringIncludes(
|
|
43
|
+
wait![0],
|
|
44
|
+
'messageRef="Message_readinessReady"',
|
|
45
|
+
"wait-review must catch the canonical readiness-ready message",
|
|
46
|
+
);
|
|
47
|
+
// The round increment (forward progress) is preserved on the same catch.
|
|
48
|
+
assertStringIncludes(wait![0], 'target="round"', "wait-review still advances the round on a fresh signal");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("the migrated wait stays bounded: an event-based gateway races the signal against a timer", () => {
|
|
52
|
+
// The gate shape from #258: an event-based gateway forks to the signal catch and a timer catch.
|
|
53
|
+
const gw = flat.match(/<bpmn:eventBasedGateway\b[^>]*\bid="gw-review-wait"[^>]*>/);
|
|
54
|
+
assert(gw, "the review wait must be an event-based gateway race (bounded gate)");
|
|
55
|
+
|
|
56
|
+
const timeout = flat.match(
|
|
57
|
+
/<bpmn:intermediateCatchEvent\b[^>]*\bid="wait-review-timeout"[\s\S]*?<\/bpmn:intermediateCatchEvent>/,
|
|
58
|
+
);
|
|
59
|
+
assert(timeout, "the bounded timer arm (wait-review-timeout) must exist");
|
|
60
|
+
assertStringIncludes(timeout![0], "<bpmn:timerEventDefinition", "the timer arm bounds the wait");
|
|
61
|
+
assertStringIncludes(timeout![0], "=reviewWaitTimeout", "the timer is seeded with the review-wait timeout");
|
|
62
|
+
|
|
63
|
+
// The gateway forks to BOTH the signal catch and the timer catch (the race).
|
|
64
|
+
assert(
|
|
65
|
+
/sourceRef="gw-review-wait"[^>]*targetRef="wait-review"|targetRef="wait-review"[^>]*sourceRef="gw-review-wait"/.test(
|
|
66
|
+
flat,
|
|
67
|
+
),
|
|
68
|
+
"the gateway races toward the readiness signal catch",
|
|
69
|
+
);
|
|
70
|
+
assert(
|
|
71
|
+
/sourceRef="gw-review-wait"[^>]*targetRef="wait-review-timeout"|targetRef="wait-review-timeout"[^>]*sourceRef="gw-review-wait"/.test(
|
|
72
|
+
flat,
|
|
73
|
+
),
|
|
74
|
+
"the gateway races toward the bounded timer catch",
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("the timeout arm escalates to a human (the wait cannot hang forever)", () => {
|
|
79
|
+
// timer catch → persist-review-stalled (records the escalation) → wait-answer (native userTask).
|
|
80
|
+
assert(
|
|
81
|
+
/sourceRef="wait-review-timeout"[^>]*targetRef="persist-review-stalled"|targetRef="persist-review-stalled"[^>]*sourceRef="wait-review-timeout"/.test(
|
|
82
|
+
flat,
|
|
83
|
+
),
|
|
84
|
+
"a timed-out review wait routes to the stalled-review escalation",
|
|
85
|
+
);
|
|
86
|
+
assert(
|
|
87
|
+
/sourceRef="persist-review-stalled"[^>]*targetRef="wait-answer"|targetRef="wait-answer"[^>]*sourceRef="persist-review-stalled"/.test(
|
|
88
|
+
flat,
|
|
89
|
+
),
|
|
90
|
+
"the stalled-review escalation parks on the human answer userTask",
|
|
91
|
+
);
|
|
92
|
+
});
|
package/app/reviewWait.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Two knobs govern the review-wait watchdog:
|
|
7
7
|
// • the *timeout* — an ISO-8601 duration handed to the process's `wait-review-timeout` timer
|
|
8
|
-
// catch (the far side of the event-based-gateway race against `
|
|
8
|
+
// catch (the far side of the event-based-gateway race against `readiness-ready`). If no fresh
|
|
9
9
|
// review arrives within it, the loop escalates to a human instead of hanging forever.
|
|
10
10
|
// • the *nudge cooldown* — how long the poller waits between automatic Copilot re-requests for
|
|
11
11
|
// one waiting PR, so a re-request that Copilot dismisses is retried without hammering the API.
|
package/app/service.ts
CHANGED
|
@@ -54,7 +54,14 @@ import {
|
|
|
54
54
|
planTasks,
|
|
55
55
|
} from "./plan.ts";
|
|
56
56
|
import { derivePromotionState, isPromotable, promotionPrBody, promotionPrTitle } from "./promotion.ts";
|
|
57
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
defaultProbeExec,
|
|
59
|
+
type ProbeExec,
|
|
60
|
+
probeOnce,
|
|
61
|
+
READINESS_READY_MESSAGE,
|
|
62
|
+
type ReadinessProbe,
|
|
63
|
+
readinessTimeout,
|
|
64
|
+
} from "./readiness.ts";
|
|
58
65
|
import { clampNudgeMinutes, reviewWaitTimeout } from "./reviewWait.ts";
|
|
59
66
|
import { trialMergeAudits } from "./trialMerge.ts";
|
|
60
67
|
import {
|
|
@@ -668,7 +675,17 @@ export async function activePrs(data: DataLayer): Promise<ActivePr[]> {
|
|
|
668
675
|
|
|
669
676
|
/** One review-ready poll pass (SPEC §10): for every PR waiting on a review, fetch its GitHub
|
|
670
677
|
* reviews (via the host `gh` CLI or a token — see `app/github.ts`) and, on a fresh one,
|
|
671
|
-
* correlate
|
|
678
|
+
* correlate the canonical `readiness-ready` message to resume the loop.
|
|
679
|
+
*
|
|
680
|
+
* This is the review-ready wait re-expressed on the ONE `ReadinessProbe` wait-gate contract
|
|
681
|
+
* (ADR 0001 §2, issue #259): the convergence-loop parks on the canonical `readiness-ready` gate
|
|
682
|
+
* (event-based gateway racing the signal against the bounded `reviewWaitTimeout` timer), and this
|
|
683
|
+
* canonical self-scheduling poller publishes that signal out-of-band when a fresh review lands —
|
|
684
|
+
* the "out-of-band poller-correlated shape" #258 pinned decision 3 reserved for exactly this
|
|
685
|
+
* migration. There is no bespoke `review-ready` message any more: one mechanism, no drift. The
|
|
686
|
+
* fresh-review detection + Copilot nudge below stay here because review freshness is inherently
|
|
687
|
+
* STATEFUL (keyed off `last_review_id`/`waiting_since`), which the stateless probe matchers cannot
|
|
688
|
+
* subsume — the poller owns the forward-progress guarantee, the gate owns the bounded wait. */
|
|
672
689
|
async function pollReviews(data: DataLayer, engine: EngineClient, token: string) {
|
|
673
690
|
const waiting = await prs(data).find({ status: "waiting_review" });
|
|
674
691
|
for (const pr of waiting) {
|
|
@@ -692,9 +709,12 @@ async function pollReviews(data: DataLayer, engine: EngineClient, token: string)
|
|
|
692
709
|
}
|
|
693
710
|
await prs(data).update(prKey, { last_review_id: fresh.id, status: "converging", updated_at: now() });
|
|
694
711
|
await engine.publishMessage({
|
|
695
|
-
name:
|
|
712
|
+
name: READINESS_READY_MESSAGE,
|
|
696
713
|
correlationKey: prKey,
|
|
697
|
-
|
|
714
|
+
// The canonical readiness-signal payload (`ReadinessReady` shape): the gate only needs to
|
|
715
|
+
// know the probe went green. The fresh review's id/state is bookkept above on the PR row,
|
|
716
|
+
// not carried on the message — the convergence-loop consumes only the round increment.
|
|
717
|
+
variables: { ready: true, detail: `review ${fresh.id} (${fresh.state})` },
|
|
698
718
|
});
|
|
699
719
|
console.log(`[poller] review ${fresh.id} (${fresh.state}) -> ${prKey}`);
|
|
700
720
|
} catch (err) {
|
package/main.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// control endpoints plus the webhook operations under `/app/api/hooks/*` (ADR 0059).
|
|
10
10
|
//
|
|
11
11
|
// The only thing that isn't declarative is the review-ready poller: it does arbitrary GitHub
|
|
12
|
-
// polling and then correlates
|
|
12
|
+
// polling and then correlates the canonical `readiness-ready` wait-gate message (#259). A cron
|
|
13
|
+
// trigger can only fire an engine
|
|
13
14
|
// start/message action, not this custom I/O glue, so it stays app-side here — driving the same
|
|
14
15
|
// engine client the runtime uses, over `app.data`.
|
|
15
16
|
//
|
|
@@ -89,7 +90,7 @@ if (httpServer instanceof Server) {
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
// Review-ready poller. Self-scheduling (not setInterval) so a slow GitHub call can never
|
|
92
|
-
// overlap two passes (which could double-signal `
|
|
93
|
+
// overlap two passes (which could double-signal `readiness-ready`); the next pass is scheduled
|
|
93
94
|
// only after the previous one settles.
|
|
94
95
|
let shuttingDown = false;
|
|
95
96
|
let pollTimer: ReturnType<typeof setTimeout> | null = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.99.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",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
2
|
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:nano="https://nanobpm.io/schema/shapes/1.0" id="Definitions_nano_workforce" targetNamespace="http://nanobpm.io/nano-workforce">
|
|
3
|
-
<bpmn:message id="
|
|
3
|
+
<bpmn:message id="Message_readinessReady" name="readiness-ready">
|
|
4
4
|
<bpmn:extensionElements>
|
|
5
5
|
<zeebe:subscription correlationKey="=prKey" />
|
|
6
6
|
<zeebe:properties>
|
|
7
|
-
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="
|
|
7
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="ReadinessReady" />
|
|
8
8
|
</zeebe:properties>
|
|
9
9
|
</bpmn:extensionElements>
|
|
10
10
|
</bpmn:message>
|
|
@@ -82,11 +82,6 @@
|
|
|
82
82
|
<nano:extend name="convergeOnly" type="boolean" optional="true" />
|
|
83
83
|
<nano:extend name="abandonUrl" type="string" optional="true" />
|
|
84
84
|
</nano:shape>
|
|
85
|
-
<nano:shape id="ReviewReady" name="review-ready message payload">
|
|
86
|
-
<nano:extend name="reviewId" type="integer" />
|
|
87
|
-
<nano:extend name="reviewState" type="string" />
|
|
88
|
-
<nano:extend name="submittedAt" type="datetime" optional="true" />
|
|
89
|
-
</nano:shape>
|
|
90
85
|
</nano:shapes>
|
|
91
86
|
</bpmn:extensionElements>
|
|
92
87
|
<bpmn:startEvent id="Start" name="PR submitted">
|
|
@@ -181,7 +176,7 @@
|
|
|
181
176
|
</bpmn:extensionElements>
|
|
182
177
|
<bpmn:incoming>f_toReviewWait</bpmn:incoming>
|
|
183
178
|
<bpmn:outgoing>f_reviewLoop</bpmn:outgoing>
|
|
184
|
-
<bpmn:messageEventDefinition id="
|
|
179
|
+
<bpmn:messageEventDefinition id="med_readinessReady" messageRef="Message_readinessReady" />
|
|
185
180
|
</bpmn:intermediateCatchEvent>
|
|
186
181
|
<bpmn:intermediateCatchEvent id="wait-review-timeout" name="Wait: review timeout">
|
|
187
182
|
<bpmn:incoming>f_toReviewTimeout</bpmn:incoming>
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
parseProbe,
|
|
26
26
|
probeBudgetMs,
|
|
27
27
|
probeOnce,
|
|
28
|
+
READINESS_READY_MESSAGE,
|
|
28
29
|
type ReadinessProbe,
|
|
29
30
|
redactTarget,
|
|
30
31
|
} from "../../app/readiness.ts";
|
|
@@ -36,8 +37,11 @@ import type { WorkerInputs, WorkerOutputs } from "../../nano-generated/worker-io
|
|
|
36
37
|
type In = WorkerInputs["pr.readiness-probe"];
|
|
37
38
|
type Out = WorkerOutputs["pr.readiness-probe"];
|
|
38
39
|
|
|
39
|
-
/** The message the gate's event-based gateway correlates on `=gateKey` to release the wait.
|
|
40
|
-
|
|
40
|
+
/** The message the gate's event-based gateway correlates on `=gateKey` to release the wait.
|
|
41
|
+
* Re-exported from the canonical source (`app/readiness.ts`) so the worker and every out-of-band
|
|
42
|
+
* publisher (the review-ready poller, #259) share ONE message name — no drift-prone local twin.
|
|
43
|
+
* Re-exports the binding already imported above rather than re-referencing the module. */
|
|
44
|
+
export { READINESS_READY_MESSAGE };
|
|
41
45
|
|
|
42
46
|
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
43
47
|
|