@nanobpm/nano-workforce 0.40.1 → 0.41.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/README.md +20 -0
- package/app/agentGuide.ts +64 -0
- package/app/queuedVerdict.test.ts +40 -0
- package/app/service.ts +45 -9
- package/openapi.yaml +57 -0
- package/operations/getAgentInstructions.test.ts +100 -0
- package/operations/getAgentInstructions.ts +52 -0
- package/package.json +1 -1
- package/resources/agent-guide.md +302 -0
- package/resources/processes/merge-loop.bpmn +133 -89
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [0.41.0](https://github.com/nanobpm/nano-workforce/compare/v0.40.2...v0.41.0) (2026-08-11)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **api:** serve an agent operator guide at GET /app/api/agent ([#118](https://github.com/nanobpm/nano-workforce/issues/118)) ([6768407](https://github.com/nanobpm/nano-workforce/commit/67684075cfe4706db7dfc63f8c536990254a7480))
|
|
7
|
+
|
|
8
|
+
## [0.40.2](https://github.com/nanobpm/nano-workforce/compare/v0.40.1...v0.40.2) (2026-08-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **merge-loop:** escape wait-landed when a queued PR is evicted on conflict ([#117](https://github.com/nanobpm/nano-workforce/issues/117)) ([df7f850](https://github.com/nanobpm/nano-workforce/commit/df7f85050624f11490d715c90e054a5c7fb6d0fa)), closes [Magikcraft/nano-bpm#727](https://github.com/Magikcraft/nano-bpm/issues/727)
|
|
14
|
+
|
|
1
15
|
## [0.40.1](https://github.com/nanobpm/nano-workforce/compare/v0.40.0...v0.40.1) (2026-08-11)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -279,6 +279,26 @@ per-instance context (e.g. a human's escalation answer) is appended by the harne
|
|
|
279
279
|
|
|
280
280
|
---
|
|
281
281
|
|
|
282
|
+
## Point an agent at it (self-serve guide)
|
|
283
|
+
|
|
284
|
+
The running app serves a live **agent operator guide** at
|
|
285
|
+
`GET /app/api/agent` — how to submit PRs (review-only vs. merge), hand over an epic,
|
|
286
|
+
answer escalations, and **debug** the system (find engine instances, relate them to
|
|
287
|
+
PRs, inspect the models/prompts, unstick stuck processes, and raise issues/PRs). Its
|
|
288
|
+
examples are keyed to the instance you fetched it from, so a coding agent can drive
|
|
289
|
+
**and** debug the workforce with no extra context:
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
curl -sS http://localhost:3000/app/api/agent | jq -r .instructions
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Like `/version` and `/status`, this endpoint honours the optional
|
|
296
|
+
`NANO_PR_WEBHOOK_SECRET` guard (`X-Hook-Secret` header): when that secret is set it
|
|
297
|
+
returns `401` without the matching header; unset = open. The source lives in
|
|
298
|
+
`resources/agent-guide.md`.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
282
302
|
## Architecture & contributing
|
|
283
303
|
|
|
284
304
|
- **[SPEC.md](SPEC.md)** — the behavioural source of truth for the processes.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The agent operator guide served by GET /app/api/agent (operationId `getAgentInstructions`).
|
|
2
|
+
//
|
|
3
|
+
// The guide itself is authored as plain markdown in `resources/agent-guide.md` (kept out of
|
|
4
|
+
// `prompts/` so it is NOT treated as a deployable agent template) and read from the checkout at
|
|
5
|
+
// module load — same "run the .ts sources directly, inspect the working tree at runtime" approach
|
|
6
|
+
// as version.ts. Two placeholders are substituted per request/deployment so the embedded examples
|
|
7
|
+
// are copy-pasteable against THIS instance:
|
|
8
|
+
// • __BASE__ → the app control-API base the caller reached us on (e.g. https://host/app/api)
|
|
9
|
+
// • __ENGINE__ → the engine's Camunda-8 v2 REST base this app is configured to talk to
|
|
10
|
+
//
|
|
11
|
+
// Reading is best-effort: a missing file yields a short built-in fallback rather than throwing, so
|
|
12
|
+
// the endpoint never 500s just because the doc is absent from a stripped-down deploy.
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const REPO_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
18
|
+
const GUIDE_PATH = join(REPO_ROOT, "resources", "agent-guide.md");
|
|
19
|
+
|
|
20
|
+
// Read the raw guide once, at module load. Frozen for the life of the process.
|
|
21
|
+
const RAW_GUIDE: string = (() => {
|
|
22
|
+
try {
|
|
23
|
+
return readFileSync(GUIDE_PATH, "utf8");
|
|
24
|
+
} catch {
|
|
25
|
+
return [
|
|
26
|
+
"# Nano Workforce — agent operator guide",
|
|
27
|
+
"",
|
|
28
|
+
"The full guide document could not be read from this deployment.",
|
|
29
|
+
"",
|
|
30
|
+
"Key endpoints (under the app control-API base `__BASE__`):",
|
|
31
|
+
"- `GET /status` — every PR in flight, with its engine `processKey` and any open escalation.",
|
|
32
|
+
"- `GET /version` — which code is live.",
|
|
33
|
+
"- `POST /actions/start/convergence-loop` — submit a PR (`{ pr, convergeOnly?, maxRounds?, dependsOn? }`).",
|
|
34
|
+
"- `POST /actions/start/plan-fanout` — submit an epic (`{ issue }`).",
|
|
35
|
+
"- `POST /actions/message` — answer an escalation (`escalation-answered`, correlate by PR key).",
|
|
36
|
+
"",
|
|
37
|
+
"Engine (Camunda-8 v2 REST) base for debugging: `__ENGINE__`.",
|
|
38
|
+
"Source repository: `nanobpm/nano-workforce`.",
|
|
39
|
+
"",
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The engine's Camunda-8 v2 REST base this app talks to, resolved exactly as `main.ts` does:
|
|
46
|
+
* an explicit `CAMUNDA_REST_ADDRESS` wins, else `${NANOBPMN_BASE_URL}/v2` (default localhost:8080).
|
|
47
|
+
* Trailing slashes are trimmed so the guide's `__ENGINE__/jobs/search` examples are well-formed.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveEngineBase(): string {
|
|
50
|
+
const explicit = process.env.CAMUNDA_REST_ADDRESS;
|
|
51
|
+
const base = explicit?.trim()
|
|
52
|
+
? explicit.trim()
|
|
53
|
+
: `${(process.env.NANOBPMN_BASE_URL ?? "http://localhost:8080").replace(/\/+$/, "")}/v2`;
|
|
54
|
+
return base.replace(/\/+$/, "");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Render the guide for a given app control-API base (e.g. "https://host/app/api"). The engine base
|
|
59
|
+
* is resolved from the environment. Substitutes every `__BASE__`/`__ENGINE__` occurrence.
|
|
60
|
+
*/
|
|
61
|
+
export function renderAgentGuide(apiBase: string): string {
|
|
62
|
+
const base = apiBase.replace(/\/+$/, "");
|
|
63
|
+
return RAW_GUIDE.replaceAll("__BASE__", base).replaceAll("__ENGINE__", resolveEngineBase());
|
|
64
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The merge-loop enqueues a "ready" PR (mergeStatus="queued") and parks at `wait-landed`. This
|
|
2
|
+
// decision drives what the poller does next from the PR's live GitHub state. The regression it
|
|
3
|
+
// guards: a PR that develops a merge CONFLICT after being enqueued (#727/instance 729) must be
|
|
4
|
+
// EVICTED back to the mergeable gate, not left waiting forever — while a PR still legitimately in
|
|
5
|
+
// the queue (reported BLOCKED/UNSTABLE by GitHub) must keep waiting, never be falsely evicted.
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import { assertEquals } from "#test-assert";
|
|
8
|
+
import type { PrState } from "./github.ts";
|
|
9
|
+
import { queuedVerdict } from "./service.ts";
|
|
10
|
+
|
|
11
|
+
function st(over: Partial<PrState>): PrState {
|
|
12
|
+
return {
|
|
13
|
+
merged: false,
|
|
14
|
+
mergeStateStatus: "CLEAN",
|
|
15
|
+
failingChecks: 0,
|
|
16
|
+
failingCheckNames: [],
|
|
17
|
+
presentCheckNames: [],
|
|
18
|
+
totalChecks: 0,
|
|
19
|
+
isDraft: false,
|
|
20
|
+
headRefOid: null,
|
|
21
|
+
...over,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("a landed PR advances (merge-landed)", () => {
|
|
26
|
+
assertEquals(queuedVerdict(st({ merged: true, mergeStateStatus: "CLEAN" })), "landed");
|
|
27
|
+
// `merged` wins even if a stale mergeStateStatus lags.
|
|
28
|
+
assertEquals(queuedVerdict(st({ merged: true, mergeStateStatus: "DIRTY" })), "landed");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("a DIRTY (conflicting) PR is evicted — this is the #727 wedge", () => {
|
|
32
|
+
assertEquals(queuedVerdict(st({ merged: false, mergeStateStatus: "DIRTY" })), "evicted");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("a PR still legitimately in the queue keeps waiting (never falsely evicted)", () => {
|
|
36
|
+
// Queuing PRs commonly report these; none is a conflict, so none may evict.
|
|
37
|
+
for (const s of ["CLEAN", "BLOCKED", "UNSTABLE", "BEHIND", "HAS_HOOKS", "UNKNOWN", "DRAFT"]) {
|
|
38
|
+
assertEquals(queuedVerdict(st({ merged: false, mergeStateStatus: s })), "waiting", s);
|
|
39
|
+
}
|
|
40
|
+
});
|
package/app/service.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
fetchPrState,
|
|
18
18
|
hasPendingCopilotReviewer,
|
|
19
19
|
type MergeMethod,
|
|
20
|
+
type PrState,
|
|
20
21
|
requestCopilotReview,
|
|
21
22
|
} from "./github.ts";
|
|
22
23
|
import { mergeLanes, readExclusions } from "./mergeExclusion.ts";
|
|
@@ -637,7 +638,7 @@ async function mirrorTaskStatusForPr(data: DataLayer, prKey: string, status: "op
|
|
|
637
638
|
* • waiting_deps → every declared dependency has merged → `deps-cleared`
|
|
638
639
|
* • waiting_merge → GitHub settled the PR as mergeable/blocked → `merge-ready` {mergeState}
|
|
639
640
|
* • waiting_lane → predecessor in same exclusion lane merged → re-arm `waiting_merge`
|
|
640
|
-
* • queued → the queued PR
|
|
641
|
+
* • queued → the queued PR landed → `merge-landed`; or it conflicts (DIRTY) → `merge-evicted`
|
|
641
642
|
* On publish we flip status to the transient `merging` (which no branch scans) so a slow pass
|
|
642
643
|
* can't double-signal, exactly as `pollReviews` flips to `converging`; `flipToMergingThenPublish`
|
|
643
644
|
* reverts the flip if the publish fails so a failed handoff can't wedge the PR. */
|
|
@@ -756,25 +757,60 @@ async function pollMerges(data: DataLayer, engine: EngineClient, token: string)
|
|
|
756
757
|
}
|
|
757
758
|
}
|
|
758
759
|
|
|
759
|
-
// 4) Queued PR landed?
|
|
760
|
+
// 4) Queued PR landed — or fell out of the merge queue?
|
|
761
|
+
//
|
|
762
|
+
// A PR enqueued by `attempt-merge` (mergeStatus="queued") parks the process at `wait-landed`.
|
|
763
|
+
// Two things can end that wait: the queue lands the PR (→ `merge-landed`), or the PR is EVICTED
|
|
764
|
+
// from the queue because its base moved under it and it now conflicts (`DIRTY`). Without the
|
|
765
|
+
// eviction path a conflicted-after-enqueue PR waits forever (nothing ever publishes
|
|
766
|
+
// `merge-landed`), which is exactly how #727/instance 729 wedged. We must NOT treat a merely
|
|
767
|
+
// "not yet landed" PR as evicted: while it is legitimately queuing GitHub often reports it as
|
|
768
|
+
// BLOCKED (a pending queue check) or UNSTABLE, which `classifyMergeability` calls not-ready. Only
|
|
769
|
+
// a genuine merge CONFLICT (`DIRTY`) means it has dropped out — evict on that alone and re-arm the
|
|
770
|
+
// merge poller (`merge-evicted` → `arm-merge`), which re-runs the mergeable gate so the existing
|
|
771
|
+
// auto-rebase / escalate machinery resolves the conflict.
|
|
760
772
|
for (const pr of await prs(data).find({ status: "queued" })) {
|
|
761
773
|
const { repo, number, pr_key: prKey } = pr;
|
|
762
774
|
try {
|
|
763
775
|
const st = await fetchPrState(repo, number, token);
|
|
764
776
|
if (st === null) continue; // no transport → skip this PR (others may still advance)
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
777
|
+
const verdict = queuedVerdict(st);
|
|
778
|
+
if (verdict === "landed") {
|
|
779
|
+
await flipToMergingThenPublish(data, engine, prKey, "queued", {
|
|
780
|
+
name: "merge-landed",
|
|
781
|
+
correlationKey: prKey,
|
|
782
|
+
variables: {},
|
|
783
|
+
});
|
|
784
|
+
console.log(`[poller] queued PR landed -> ${prKey}`);
|
|
785
|
+
} else if (verdict === "evicted") {
|
|
786
|
+
await flipToMergingThenPublish(data, engine, prKey, "queued", {
|
|
787
|
+
name: "merge-evicted",
|
|
788
|
+
correlationKey: prKey,
|
|
789
|
+
variables: {},
|
|
790
|
+
});
|
|
791
|
+
console.log(`[poller] queued PR evicted (conflict) -> ${prKey}`);
|
|
792
|
+
}
|
|
793
|
+
// otherwise: still legitimately in the queue — keep waiting.
|
|
772
794
|
} catch (err) {
|
|
773
795
|
console.error(`[poller] queued ${prKey}: ${err}`);
|
|
774
796
|
}
|
|
775
797
|
}
|
|
776
798
|
}
|
|
777
799
|
|
|
800
|
+
/** Decide what to do with a PR the process enqueued (parked at `wait-landed`), from its current
|
|
801
|
+
* GitHub merge state:
|
|
802
|
+
* • `landed` — the queue merged it → publish `merge-landed` (advance to mark-merged).
|
|
803
|
+
* • `evicted` — it fell out of the queue with a real merge CONFLICT (`DIRTY`) → publish
|
|
804
|
+
* `merge-evicted` so the process re-arms the merge poller and the mergeable gate re-runs
|
|
805
|
+
* (auto-rebase / escalate). Without this a conflicted-after-enqueue PR waits forever.
|
|
806
|
+
* • `waiting` — still legitimately in the queue. Crucially, a queuing PR is frequently reported
|
|
807
|
+
* BLOCKED/UNSTABLE (a pending queue check), which is NOT eviction — only `DIRTY` is. */
|
|
808
|
+
export function queuedVerdict(st: PrState): "landed" | "evicted" | "waiting" {
|
|
809
|
+
if (st.merged) return "landed";
|
|
810
|
+
if (st.mergeStateStatus === "DIRTY") return "evicted";
|
|
811
|
+
return "waiting";
|
|
812
|
+
}
|
|
813
|
+
|
|
778
814
|
/** The subset of a Camunda-8 `/v2/jobs/search` result item this app reads. `worker` is the
|
|
779
815
|
* leasing worker's name (empty/absent until an agent activates the job); `deadline` is the
|
|
780
816
|
* activation lock's expiry (ISO ts). */
|
package/openapi.yaml
CHANGED
|
@@ -137,6 +137,42 @@ components:
|
|
|
137
137
|
type: string
|
|
138
138
|
uptimeSeconds:
|
|
139
139
|
type: integer
|
|
140
|
+
AgentInstructions:
|
|
141
|
+
type: object
|
|
142
|
+
description: The agent operator guide — how to drive (submit PRs/epics, answer escalations)
|
|
143
|
+
and debug (find engine instances, relate them to PRs, inspect models/prompts, unstick stuck
|
|
144
|
+
processes, raise issues/PRs) this Nano Workforce instance. The `instructions` markdown has
|
|
145
|
+
its example commands keyed to this instance's `baseUrl`/`engineBase`.
|
|
146
|
+
additionalProperties: false
|
|
147
|
+
required:
|
|
148
|
+
- format
|
|
149
|
+
- appVersion
|
|
150
|
+
- generatedAt
|
|
151
|
+
- baseUrl
|
|
152
|
+
- engineBase
|
|
153
|
+
- instructions
|
|
154
|
+
properties:
|
|
155
|
+
format:
|
|
156
|
+
type: string
|
|
157
|
+
description: The `instructions` media format. Always "markdown".
|
|
158
|
+
enum:
|
|
159
|
+
- markdown
|
|
160
|
+
appVersion:
|
|
161
|
+
type: string
|
|
162
|
+
nullable: true
|
|
163
|
+
description: The running app version this guide matches (null when unreadable).
|
|
164
|
+
generatedAt:
|
|
165
|
+
type: string
|
|
166
|
+
description: When this response was rendered (ISO-8601).
|
|
167
|
+
baseUrl:
|
|
168
|
+
type: string
|
|
169
|
+
description: The app control-API base the examples target (e.g. https://host/app/api).
|
|
170
|
+
engineBase:
|
|
171
|
+
type: string
|
|
172
|
+
description: The engine's Camunda-8 v2 REST base this app talks to, for debugging queries.
|
|
173
|
+
instructions:
|
|
174
|
+
type: string
|
|
175
|
+
description: The full operator guide as markdown.
|
|
140
176
|
SubmitResult:
|
|
141
177
|
type: object
|
|
142
178
|
required:
|
|
@@ -387,6 +423,27 @@ paths:
|
|
|
387
423
|
application/json:
|
|
388
424
|
schema:
|
|
389
425
|
$ref: "#/components/schemas/ErrorBody"
|
|
426
|
+
/agent:
|
|
427
|
+
get:
|
|
428
|
+
operationId: getAgentInstructions
|
|
429
|
+
summary: The agent operator guide (markdown) — how to submit PRs/epics, answer escalations, and
|
|
430
|
+
debug this instance. Point a coding agent at this URL to drive and debug the workforce.
|
|
431
|
+
security:
|
|
432
|
+
- hookSecret: []
|
|
433
|
+
- {}
|
|
434
|
+
responses:
|
|
435
|
+
"200":
|
|
436
|
+
description: The operator guide, with examples keyed to this instance.
|
|
437
|
+
content:
|
|
438
|
+
application/json:
|
|
439
|
+
schema:
|
|
440
|
+
$ref: "#/components/schemas/AgentInstructions"
|
|
441
|
+
"401":
|
|
442
|
+
description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
|
|
443
|
+
content:
|
|
444
|
+
application/json:
|
|
445
|
+
schema:
|
|
446
|
+
$ref: "#/components/schemas/ErrorBody"
|
|
390
447
|
/actions/start/convergence-loop:
|
|
391
448
|
post:
|
|
392
449
|
operationId: startConvergenceLoop
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Tests for GET /app/api/agent → operation `getAgentInstructions` (ADR 0058 OpenAPI surface).
|
|
2
|
+
// The guide markdown is served as the `instructions` field with its examples keyed to the request's
|
|
3
|
+
// control-API base + the configured engine base. Mirrors the getVersion test's request shape and
|
|
4
|
+
// shared-secret guard pattern.
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import { assert, assertEquals } from "#test-assert";
|
|
7
|
+
import type { AppApi } from "@nanobpm/urban";
|
|
8
|
+
import handler from "./getAgentInstructions.ts";
|
|
9
|
+
|
|
10
|
+
const app = {} as any as AppApi;
|
|
11
|
+
|
|
12
|
+
function input(headers: Record<string, string> = {}, path = "/app/api/agent") {
|
|
13
|
+
return {
|
|
14
|
+
req: {
|
|
15
|
+
method: "GET",
|
|
16
|
+
path,
|
|
17
|
+
query: new URLSearchParams(),
|
|
18
|
+
headers: new Headers(headers),
|
|
19
|
+
text: async () => "",
|
|
20
|
+
} as any,
|
|
21
|
+
params: {},
|
|
22
|
+
query: {},
|
|
23
|
+
body: undefined,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("returns 200 with the markdown guide and metadata", async () => {
|
|
28
|
+
const r = (await handler(input(), app)) as any;
|
|
29
|
+
assertEquals(r.status, 200);
|
|
30
|
+
assertEquals(r.body.format, "markdown");
|
|
31
|
+
assert("appVersion" in r.body); // nullable, but always present
|
|
32
|
+
assert(typeof r.body.generatedAt === "string" && r.body.generatedAt.length > 0);
|
|
33
|
+
assert(typeof r.body.baseUrl === "string" && r.body.baseUrl.length > 0);
|
|
34
|
+
assert(typeof r.body.engineBase === "string" && r.body.engineBase.length > 0);
|
|
35
|
+
assert(typeof r.body.instructions === "string" && r.body.instructions.length > 200);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("the guide covers every capability the endpoint promises", async () => {
|
|
39
|
+
const md = ((await handler(input(), app)) as any).body.instructions as string;
|
|
40
|
+
// Submit a PR (converge vs. merge), submit an epic, answer escalations…
|
|
41
|
+
assert(md.includes("start/convergence-loop"), "covers submitting a PR for convergence");
|
|
42
|
+
assert(md.includes("convergeOnly"), "documents review-only vs. merge");
|
|
43
|
+
assert(md.includes("start/plan-fanout"), "covers submitting an epic");
|
|
44
|
+
assert(md.includes("escalation-answered"), "covers answering escalations");
|
|
45
|
+
// …debug the system.
|
|
46
|
+
assert(md.includes("/jobs/search") && md.includes("/incidents/search"), "covers engine REST debugging");
|
|
47
|
+
assert(md.includes("processKey") || md.includes("process_key"), "relates instances to PRs");
|
|
48
|
+
assert(md.includes("resources/processes") && md.includes("prompts/"), "covers debugging models + prompts");
|
|
49
|
+
assert(md.includes("nanobpm/nano-workforce"), "covers raising issues/PRs against the repo");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("examples are keyed to the request's control-API base and leave no placeholders", async () => {
|
|
53
|
+
const forwarded = input({ host: "wf.example.com", "x-forwarded-proto": "https" });
|
|
54
|
+
const md = ((await handler(forwarded, app)) as any).body.instructions as string;
|
|
55
|
+
const body = (await handler(forwarded, app)) as any;
|
|
56
|
+
assertEquals(body.body.baseUrl, "https://wf.example.com/app/api");
|
|
57
|
+
assert(md.includes("https://wf.example.com/app/api/version"), "base URL substituted into examples");
|
|
58
|
+
assert(!md.includes("__BASE__"), "no unsubstituted __BASE__ placeholder");
|
|
59
|
+
assert(!md.includes("__ENGINE__"), "no unsubstituted __ENGINE__ placeholder");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("x-forwarded-proto is restricted to http/https", async () => {
|
|
63
|
+
const spoofed = input({ host: "wf.example.com", "x-forwarded-proto": "javascript" });
|
|
64
|
+
const body = (await handler(spoofed, app)) as any;
|
|
65
|
+
assertEquals(body.body.baseUrl, "http://wf.example.com/app/api", "unsafe scheme falls back to http");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("engine base follows CAMUNDA_REST_ADDRESS / NANOBPMN_BASE_URL", async () => {
|
|
69
|
+
const prevCamunda = process.env["CAMUNDA_REST_ADDRESS"];
|
|
70
|
+
const prevBase = process.env["NANOBPMN_BASE_URL"];
|
|
71
|
+
try {
|
|
72
|
+
delete process.env["CAMUNDA_REST_ADDRESS"];
|
|
73
|
+
process.env["NANOBPMN_BASE_URL"] = "http://engine.internal:8080";
|
|
74
|
+
const r = (await handler(input(), app)) as any;
|
|
75
|
+
assertEquals(r.body.engineBase, "http://engine.internal:8080/v2");
|
|
76
|
+
assert(r.body.instructions.includes("http://engine.internal:8080/v2/jobs/search"));
|
|
77
|
+
} finally {
|
|
78
|
+
if (prevCamunda === undefined) delete process.env["CAMUNDA_REST_ADDRESS"];
|
|
79
|
+
else process.env["CAMUNDA_REST_ADDRESS"] = prevCamunda;
|
|
80
|
+
if (prevBase === undefined) delete process.env["NANOBPMN_BASE_URL"];
|
|
81
|
+
else process.env["NANOBPMN_BASE_URL"] = prevBase;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("shared-secret guard rejects a missing/wrong secret when configured", async () => {
|
|
86
|
+
const prev = process.env["NANO_PR_WEBHOOK_SECRET"];
|
|
87
|
+
process.env["NANO_PR_WEBHOOK_SECRET"] = "s3cr3t";
|
|
88
|
+
try {
|
|
89
|
+
// SECRET is bound at import time, so import a cache-busted copy to observe the guard.
|
|
90
|
+
const mod = await import(`./getAgentInstructions.ts?guard=${Date.now()}`);
|
|
91
|
+
const guarded = mod.default as typeof handler;
|
|
92
|
+
const bad = (await guarded(input(), app)) as any;
|
|
93
|
+
assertEquals(bad.status, 401);
|
|
94
|
+
const ok = (await guarded(input({ "x-hook-secret": "s3cr3t" }), app)) as any;
|
|
95
|
+
assertEquals(ok.status, 200);
|
|
96
|
+
} finally {
|
|
97
|
+
if (prev === undefined) delete process.env["NANO_PR_WEBHOOK_SECRET"];
|
|
98
|
+
else process.env["NANO_PR_WEBHOOK_SECRET"] = prev;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// GET /app/api/agent → operationId `getAgentInstructions` (ADR 0058/0059 OpenAPI surface, base
|
|
2
|
+
// /app/api). Serves the agent operator guide: how to submit a PR for convergence (review-only vs.
|
|
3
|
+
// merge), submit an epic, answer escalations, and debug the system (find engine instances, relate
|
|
4
|
+
// them to PRs, inspect the models/prompts, unstick stuck processes, and raise issues/PRs). A user
|
|
5
|
+
// can point their coding agent at this URL and it can drive AND debug the workforce.
|
|
6
|
+
//
|
|
7
|
+
// The runtime serializes an operation body as JSON, so the markdown guide is returned as the
|
|
8
|
+
// `instructions` string field (alongside the app version + the base URLs the examples are keyed
|
|
9
|
+
// to), rather than as a raw text/markdown body. The embedded examples are rewritten to THIS
|
|
10
|
+
// instance's control-API base (derived from the request) and engine base (from the environment).
|
|
11
|
+
//
|
|
12
|
+
// Read-only. The optional shared-secret guard mirrors /version: enforced HERE only when
|
|
13
|
+
// NANO_PR_WEBHOOK_SECRET is set (the runtime does not enforce OpenAPI `security`).
|
|
14
|
+
|
|
15
|
+
import { renderAgentGuide, resolveEngineBase } from "../app/agentGuide.ts";
|
|
16
|
+
import { buildVersionInfo, envVar } from "../app/version.ts";
|
|
17
|
+
import { defineOperation } from "../nano-generated/operations.ts";
|
|
18
|
+
|
|
19
|
+
const SECRET = envVar("NANO_PR_WEBHOOK_SECRET") ?? "";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Reconstruct the app control-API base the caller reached us on (e.g. "https://host/app/api"), so
|
|
23
|
+
* the guide's example commands are copy-pasteable. Honour reverse-proxy forwarding headers; fall
|
|
24
|
+
* back to a localhost default when the Host header is absent (e.g. a raw unit-test request).
|
|
25
|
+
*/
|
|
26
|
+
function resolveApiBase(req: { path: string; headers: Headers }): string {
|
|
27
|
+
const rawProto = (req.headers.get("x-forwarded-proto") ?? "http").split(",")[0].trim().toLowerCase();
|
|
28
|
+
// x-forwarded-proto is user-controlled behind some proxies; only trust http/https.
|
|
29
|
+
const proto = rawProto === "http" || rawProto === "https" ? rawProto : "http";
|
|
30
|
+
const host = (req.headers.get("x-forwarded-host") ?? req.headers.get("host") ?? "").split(",")[0].trim();
|
|
31
|
+
// The op is mounted at "<base>/agent"; strip the trailing segment to recover the base path.
|
|
32
|
+
const basePath = req.path.replace(/\/agent\/?$/, "") || "/app/api";
|
|
33
|
+
return host ? `${proto}://${host}${basePath}` : `http://localhost:3000${basePath}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default defineOperation("getAgentInstructions", ({ req }) => {
|
|
37
|
+
if (SECRET && req.headers.get("x-hook-secret") !== SECRET) {
|
|
38
|
+
return { status: 401, body: { error: "unauthorized" } };
|
|
39
|
+
}
|
|
40
|
+
const baseUrl = resolveApiBase(req);
|
|
41
|
+
return {
|
|
42
|
+
status: 200,
|
|
43
|
+
body: {
|
|
44
|
+
format: "markdown",
|
|
45
|
+
appVersion: buildVersionInfo().version,
|
|
46
|
+
generatedAt: new Date().toISOString(),
|
|
47
|
+
baseUrl,
|
|
48
|
+
engineBase: resolveEngineBase(),
|
|
49
|
+
instructions: renderAgentGuide(baseUrl),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.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",
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# Nano Workforce — agent operator guide
|
|
2
|
+
|
|
3
|
+
You are an AI assistant helping a human operate a running **Nano Workforce**
|
|
4
|
+
instance. Nano Workforce is a durable orchestration app (a [Nano](https://nanobpm.io)
|
|
5
|
+
Urban app) that drives pull requests to **review convergence** against an automated
|
|
6
|
+
reviewer, then **merges** them, and can take a whole issue and **plan → implement →
|
|
7
|
+
converge** it across a fleet of coding agents.
|
|
8
|
+
|
|
9
|
+
This document is served live by the running app so you always match the deployed
|
|
10
|
+
version. Use it to **drive** the workforce (submit work, answer escalations) and to
|
|
11
|
+
**debug** it (find stuck instances, relate them to PRs, inspect the models/prompts,
|
|
12
|
+
and unstick or report problems).
|
|
13
|
+
|
|
14
|
+
- **App control API base:** `__BASE__`
|
|
15
|
+
- **Engine (Nano/Camunda-8 v2 REST) base:** `__ENGINE__`
|
|
16
|
+
- **Source repository:** `nanobpm/nano-workforce`
|
|
17
|
+
|
|
18
|
+
Everything below assumes the app control API is reachable at `__BASE__`. Most app
|
|
19
|
+
endpoints are mounted under that base (ADR 0059), but a few siblings sit outside it —
|
|
20
|
+
notably the interactive docs (Swagger UI) at `__BASE__/../api-docs` and the action
|
|
21
|
+
endpoints (e.g. the cancel action at `/app/actions/cancel`). Paths below are written
|
|
22
|
+
in full so you can tell which are under the control-API base and which are not.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 0. Orient yourself first
|
|
27
|
+
|
|
28
|
+
Before acting, confirm what is running and what is in flight:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Which code is live (app version, urban version, git sha/branch, uptime):
|
|
32
|
+
curl -sS __BASE__/version | jq
|
|
33
|
+
|
|
34
|
+
# Every PR currently in flight (not converged/abandoned), with its engine
|
|
35
|
+
# process key, status, round, and any open escalation:
|
|
36
|
+
curl -sS __BASE__/status | jq
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`/status` is your primary situational-awareness endpoint. Each entry carries:
|
|
40
|
+
`prKey` (`owner/repo#123`), `status`, `round`, `processKey` (the **engine process
|
|
41
|
+
instance key** — the bridge to the engine REST API, §5), `openEscalation`,
|
|
42
|
+
`activeWorker`/`leaseUntil` (is an agent actually working the round, or is the job
|
|
43
|
+
just queued), and `updatedAt`.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 1. Submit a PR for review convergence
|
|
48
|
+
|
|
49
|
+
One PR → one durable `convergence-loop`. Each round dispatches a `senior:pr-review`
|
|
50
|
+
agent; between rounds the process parks on a durable message-catch, so review latency
|
|
51
|
+
never holds an agent slot. The app's poller watches GitHub and republishes
|
|
52
|
+
`review-ready` when a new review lands.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Minimal: converge, then merge (if NANO_PR_AUTO_MERGE is on — the default).
|
|
56
|
+
curl -sS -X POST __BASE__/actions/start/convergence-loop \
|
|
57
|
+
-H 'content-type: application/json' \
|
|
58
|
+
-d '{ "pr": "owner/repo#123" }'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The body is flat. Fields:
|
|
62
|
+
|
|
63
|
+
| field | type | meaning |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `pr` (or `url`) | string | the PR — `owner/repo#123` or a full PR URL. Required. |
|
|
66
|
+
| `convergeOnly` | boolean | **`true` = review only.** The PR stops at `converged` and is **never** handed to the merge loop, even when `NANO_PR_AUTO_MERGE` is on. Omit / `false` = converge **then merge**. |
|
|
67
|
+
| `maxRounds` | integer | per-submit cap before escalating (clamped 1–100; default from `NANO_PR_MAX_ROUNDS`, 20). |
|
|
68
|
+
| `dependsOn` | string[] | other `prKey`s that must land before this one merges (merge-loop barrier). |
|
|
69
|
+
|
|
70
|
+
**Converge-only vs. converge-and-merge — choose deliberately:**
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Review only — do NOT merge (use when the human wants to merge by hand,
|
|
74
|
+
# or is only after a review pass):
|
|
75
|
+
curl -sS -X POST __BASE__/actions/start/convergence-loop \
|
|
76
|
+
-H 'content-type: application/json' \
|
|
77
|
+
-d '{ "pr": "owner/repo#123", "convergeOnly": true }'
|
|
78
|
+
|
|
79
|
+
# Converge then merge, with a dependency barrier and a tighter round cap:
|
|
80
|
+
curl -sS -X POST __BASE__/actions/start/convergence-loop \
|
|
81
|
+
-H 'content-type: application/json' \
|
|
82
|
+
-d '{ "pr": "owner/repo#42", "maxRounds": 8, "dependsOn": ["owner/repo#40"] }'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Submitting is **idempotent on the PR key** — re-POSTing the same PR refreshes the
|
|
86
|
+
aggregate rather than starting a duplicate loop. The response (202) echoes the
|
|
87
|
+
`prKey` and the engine `processKey`.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 2. Submit an epic (plan → implement → converge)
|
|
92
|
+
|
|
93
|
+
Hand the fleet a whole issue. A planning agent decomposes it into levelized tasks; a
|
|
94
|
+
parallel fan-out drives one implementation agent per task (one PR each); every opened
|
|
95
|
+
PR is then enrolled into its own convergence loop (§1).
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
curl -sS -X POST __BASE__/actions/start/plan-fanout \
|
|
99
|
+
-H 'content-type: application/json' \
|
|
100
|
+
-d '{ "issue": "owner/repo#123" }'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The body is flat: `issue` (or `url`) — `owner/repo#123` or an issue URL. Starting a
|
|
104
|
+
plan is idempotent on the plan key; an already-running plan short-circuits. The
|
|
105
|
+
response (202) echoes the `planKey` and engine `processKey`.
|
|
106
|
+
|
|
107
|
+
Track a plan the same way you track PRs — its `process_key` is an engine instance you
|
|
108
|
+
can inspect in §5, and the PRs it opens show up in `/status` as ordinary convergence
|
|
109
|
+
loops.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 3. Answer escalations (unblock a human-in-the-loop wait)
|
|
114
|
+
|
|
115
|
+
A loop escalates only when an agent returns `needs_input`/`blocked`, or a safety net
|
|
116
|
+
fires (round cap, a review that never arrives, a merge conflict, an unfixable CI
|
|
117
|
+
failure). The parked process waits for a human answer.
|
|
118
|
+
|
|
119
|
+
Find the open escalations, then answer them:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# Which in-flight PRs have an open escalation waiting for a human?
|
|
123
|
+
curl -sS __BASE__/status | jq '.prs[] | select(.openEscalation != null)
|
|
124
|
+
| { prKey, status, round, openEscalation }'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Answer a PR/merge escalation** (convergence-loop or merge-loop). Use the message
|
|
128
|
+
name `escalation-answered`; correlate by the PR key:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
curl -sS -X POST __BASE__/actions/message \
|
|
132
|
+
-H 'content-type: application/json' \
|
|
133
|
+
-d '{
|
|
134
|
+
"name": "escalation-answered",
|
|
135
|
+
"correlationKey": "owner/repo#123",
|
|
136
|
+
"variables": { "answer": "Yes — cap the retries at 5 and proceed." }
|
|
137
|
+
}'
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The answer is delivered to the agent as the `answer` variable on its next round, and
|
|
141
|
+
the loop resumes.
|
|
142
|
+
|
|
143
|
+
**Answer an implementation-phase (feature) task escalation** raised during a
|
|
144
|
+
plan fan-out — a dedicated webhook operation:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
curl -sS -X POST __BASE__/hooks/feature-answer \
|
|
148
|
+
-H 'content-type: application/json' \
|
|
149
|
+
-d '{ "correlationKey": "<task-or-pr-key>", "answer": "…" }'
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
If `NANO_PR_WEBHOOK_SECRET` is set on the deployment, add `-H "x-hook-secret: <secret>"`.
|
|
153
|
+
|
|
154
|
+
Guidance for the human you assist: read the escalation `question` first (it is the
|
|
155
|
+
exact blocker text the agent surfaced), decide the smallest unblocking answer, and
|
|
156
|
+
answer it precisely — the answer becomes the agent's next-round context.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 4. The lifecycle & statuses (so you can reason about state)
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
submit ──► convergence-loop
|
|
164
|
+
round (senior:pr-review) ──► addressed ──► wait review-ready ─┐
|
|
165
|
+
├─ converged ──► finalize ──► merge-loop (unless convergeOnly)
|
|
166
|
+
└─ needs_input/blocked ──► escalate ──► wait escalation-answered
|
|
167
|
+
merge-loop: wait deps ─► arm merge ─► (queue-aware) merge / land
|
|
168
|
+
blocked (CI red) ─► senior:fix-ci ─► retry conflict ─► senior:rebase ─► retry
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
PR `status` values you will see in `/status`:
|
|
172
|
+
`converging` (a review round is live), `waiting_review` (parked for a fresh review),
|
|
173
|
+
`escalated` (waiting on a human answer), `converged`, `waiting_deps` / `waiting_merge`
|
|
174
|
+
/ `queued` / `merging` (merge stage), `merged`, `abandoned`. A separate
|
|
175
|
+
`incident` signal (§5) can overlay any live status when the engine parks the token on
|
|
176
|
+
a technical fault.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 5. Debug: find engine instances and relate them to PRs
|
|
181
|
+
|
|
182
|
+
The app stores each PR/plan's engine **process instance key** in its `process_key`
|
|
183
|
+
column and surfaces it as `processKey` in `/status`. That key is the join between the
|
|
184
|
+
app's business view and the engine's execution view.
|
|
185
|
+
|
|
186
|
+
**Find the instance for a PR:** take `processKey` from `/status`, then query the
|
|
187
|
+
engine's Camunda-8 v2 REST API:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
PK=<processKey-from-status>
|
|
191
|
+
|
|
192
|
+
# The instance itself (state, the BPMN process it is running, start time):
|
|
193
|
+
curl -sS -X POST __ENGINE__/process-instances/search \
|
|
194
|
+
-H 'content-type: application/json' \
|
|
195
|
+
-d "{ \"filter\": { \"processInstanceKey\": \"$PK\" } }" | jq
|
|
196
|
+
|
|
197
|
+
# Where is it parked? — active jobs on the instance (a CREATED senior:pr-review job
|
|
198
|
+
# with a `worker` set means an agent has leased the round; none means it is queued):
|
|
199
|
+
curl -sS -X POST __ENGINE__/jobs/search \
|
|
200
|
+
-H 'content-type: application/json' \
|
|
201
|
+
-d "{ \"filter\": { \"processInstanceKey\": \"$PK\", \"state\": \"CREATED\" } }" | jq
|
|
202
|
+
|
|
203
|
+
# Is it dead-in-the-water on a technical fault? — active incidents:
|
|
204
|
+
curl -sS -X POST __ENGINE__/incidents/search \
|
|
205
|
+
-H 'content-type: application/json' \
|
|
206
|
+
-d "{ \"filter\": { \"processInstanceKey\": \"$PK\", \"state\": \"ACTIVE\" } }" | jq
|
|
207
|
+
|
|
208
|
+
# What are the element/flow-node instances (which BPMN element is it sitting on)?
|
|
209
|
+
curl -sS -X POST __ENGINE__/element-instances/search \
|
|
210
|
+
-H 'content-type: application/json' \
|
|
211
|
+
-d "{ \"filter\": { \"processInstanceKey\": \"$PK\" } }" | jq
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The app already mirrors an ACTIVE incident onto the PR row (`incident`/incident
|
|
215
|
+
message), so a PR that shows an incident in the UI is parked on an engine fault —
|
|
216
|
+
inspect it with `incidents/search` above. If the engine is not at the default, the
|
|
217
|
+
deployment's engine base is `__ENGINE__` (set via `NANOBPMN_BASE_URL` or
|
|
218
|
+
`CAMUNDA_REST_ADDRESS`).
|
|
219
|
+
|
|
220
|
+
**Relate an instance back to a PR:** if you have a `processKey` but not the PR, match
|
|
221
|
+
it against `/status` (`.prs[] | select(.processKey == "<PK>")`). A terminal PR is no
|
|
222
|
+
longer in `/status`; its instance has already completed or been cancelled.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 6. Debug the models and the prompts
|
|
227
|
+
|
|
228
|
+
The behaviour is defined by durable BPMN processes and model-authored agent prompts —
|
|
229
|
+
both live in the source repo, not in the job payload.
|
|
230
|
+
|
|
231
|
+
- **Processes:** `resources/processes/*.bpmn` — `convergence-loop.bpmn` (review),
|
|
232
|
+
`merge-loop.bpmn` (merge/CI-fix/rebase), `plan-fanout.bpmn` (planning),
|
|
233
|
+
`retro.bpmn`. These are the source of truth for routing. To understand *why* an
|
|
234
|
+
instance went where it did, read the gateway conditions (FEEL expressions on the
|
|
235
|
+
sequence flows) for the element it is parked on (§5).
|
|
236
|
+
- **Prompts (agent base instructions):** `prompts/*.md` — `review-round.md`,
|
|
237
|
+
`plan.md`, `feature.md`, `fix-ci.md`, `rebase.md`, `trial-merge.md`, etc. An
|
|
238
|
+
agent's base prompt is **not** a job variable: it is delivered as a model
|
|
239
|
+
**template header** (`{{review-round}}`, `{{plan}}`, …) substituted from these files
|
|
240
|
+
at deploy time. If an agent misbehaves systematically, the prompt is the first thing
|
|
241
|
+
to inspect/fix.
|
|
242
|
+
- **Job contract:** `senior:pr-review` receives `{ prUrl, repo, prNumber, round,
|
|
243
|
+
answer? }` and must return a flat result `{ status, summary, question? }` with
|
|
244
|
+
`status ∈ { converged, addressed, waiting, needs_input, blocked }`. A round that
|
|
245
|
+
pushes anything (including a rebase/force-push) is `addressed`; a round with an
|
|
246
|
+
unknown/empty result is treated as a safe `addressed` and re-enters the review wait
|
|
247
|
+
rather than escalating.
|
|
248
|
+
|
|
249
|
+
To validate a model/prompt change locally: `npm run layout:check` (BPMN diagram
|
|
250
|
+
freshness), `npm run check:prompts` (every template resolves), `npm run check`
|
|
251
|
+
(manifest), `npm run typecheck`, `npm run lint`, `npm test`.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## 7. Unstick a stuck process
|
|
256
|
+
|
|
257
|
+
Work through this order:
|
|
258
|
+
|
|
259
|
+
1. **Confirm it is actually stuck.** From `/status`, a PR `converging` with an
|
|
260
|
+
`activeWorker` set is *working*, not stuck — an agent holds the round. No worker
|
|
261
|
+
for a long time means the job is queued: is a fleet `c8ctl nano work` daemon
|
|
262
|
+
running and subscribed to the `senior:*` task types?
|
|
263
|
+
2. **Check for an incident** (§5). An ACTIVE incident parks the token; the underlying
|
|
264
|
+
fault must be resolved (or the instance cancelled and the work resubmitted). The
|
|
265
|
+
app surfaces the incident message on the PR row.
|
|
266
|
+
3. **Check for an open escalation** (§3) — the process may simply be waiting for a
|
|
267
|
+
human answer. Answer it.
|
|
268
|
+
4. **A review that never arrives** escalates on its own after
|
|
269
|
+
`NANO_PR_REVIEW_WAIT_TIMEOUT` (default `PT20M`); the poller also re-nudges the
|
|
270
|
+
reviewer periodically. If the reviewer bot is not provisioned on the repo, no
|
|
271
|
+
review will ever land — that is a repo-config problem, not an app bug.
|
|
272
|
+
5. **Cancel + resubmit** as a last resort. Cancel the instance via the app (the UI's
|
|
273
|
+
per-row Cancel, `POST /app/actions/cancel { "processInstanceKey": "<PK>" }`), which
|
|
274
|
+
marks the PR `abandoned`, then re-submit the PR (§1) to start a fresh loop. Do not
|
|
275
|
+
cancel a raw engine instance out from under the app — go through the app so its
|
|
276
|
+
record state stays consistent.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## 8. Raise an issue or a PR against nano-workforce
|
|
281
|
+
|
|
282
|
+
When you find a genuine bug or a missing capability in the orchestration itself
|
|
283
|
+
(not a transient repo/agent problem), help the human file it against
|
|
284
|
+
`nanobpm/nano-workforce`:
|
|
285
|
+
|
|
286
|
+
- **Every change needs a tracked issue or PR** — no silent fixes. Open an issue
|
|
287
|
+
first if one does not exist.
|
|
288
|
+
- **DCO is enforced:** every commit needs a `Signed-off-by` trailer — commit with
|
|
289
|
+
`git commit -s` (or `git rebase --signoff`).
|
|
290
|
+
- **Work in a git worktree** off `origin/main`, on a `feat/*` or `fix/*` branch.
|
|
291
|
+
- **Author the BPMN semantics, generate the diagram.** Never hand-edit the
|
|
292
|
+
`bpmndi:BPMNDiagram`; run `npm run layout <file.bpmn>` and commit the result. CI
|
|
293
|
+
fails on stale DI.
|
|
294
|
+
- **Match the CI gates locally before pushing:** `npm run lint`, `npm run typecheck`,
|
|
295
|
+
`npm run check`, `npm run layout:check`, `npm run check:prompts`, `npm test`.
|
|
296
|
+
- **Copilot code review is provisioned** — drive the PR to convergence against it.
|
|
297
|
+
- Read `AGENTS.md` (engineering principles + gates) and `SPEC.md` (behavioural source
|
|
298
|
+
of truth) before proposing a change to a process.
|
|
299
|
+
|
|
300
|
+
When describing the bug, include the concrete evidence you gathered here: the
|
|
301
|
+
`prKey`, the engine `processKey`, the parked element / incident message (§5), and the
|
|
302
|
+
BPMN/prompt file you believe is responsible (§6).
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
<zeebe:subscription correlationKey="=prKey" />
|
|
19
19
|
</bpmn:extensionElements>
|
|
20
20
|
</bpmn:message>
|
|
21
|
+
<bpmn:message id="Message_mergeEvicted" name="merge-evicted">
|
|
22
|
+
<bpmn:extensionElements>
|
|
23
|
+
<zeebe:subscription correlationKey="=prKey" />
|
|
24
|
+
</bpmn:extensionElements>
|
|
25
|
+
</bpmn:message>
|
|
21
26
|
<bpmn:message id="Message_mergeEscAnswered" name="escalation-answered">
|
|
22
27
|
<bpmn:extensionElements>
|
|
23
28
|
<zeebe:subscription correlationKey="=prKey" />
|
|
@@ -109,6 +114,7 @@
|
|
|
109
114
|
<bpmn:incoming>f_m_answer</bpmn:incoming>
|
|
110
115
|
<bpmn:incoming>f_ci_fixed</bpmn:incoming>
|
|
111
116
|
<bpmn:incoming>f_reb_rebased</bpmn:incoming>
|
|
117
|
+
<bpmn:incoming>f_m_evicted</bpmn:incoming>
|
|
112
118
|
<bpmn:outgoing>f_m_arm</bpmn:outgoing>
|
|
113
119
|
</bpmn:serviceTask>
|
|
114
120
|
<bpmn:intermediateCatchEvent id="wait-mergeable" name="Wait: mergeable">
|
|
@@ -140,11 +146,21 @@
|
|
|
140
146
|
<bpmn:outgoing>f_m_gQueued</bpmn:outgoing>
|
|
141
147
|
<bpmn:outgoing>f_m_gBlocked</bpmn:outgoing>
|
|
142
148
|
</bpmn:exclusiveGateway>
|
|
143
|
-
<bpmn:
|
|
149
|
+
<bpmn:eventBasedGateway id="eg-landed" name="landed or evicted?">
|
|
144
150
|
<bpmn:incoming>f_m_gQueued</bpmn:incoming>
|
|
151
|
+
<bpmn:outgoing>f_eg_landed</bpmn:outgoing>
|
|
152
|
+
<bpmn:outgoing>f_eg_evicted</bpmn:outgoing>
|
|
153
|
+
</bpmn:eventBasedGateway>
|
|
154
|
+
<bpmn:intermediateCatchEvent id="wait-landed" name="Wait: merge queue landed">
|
|
155
|
+
<bpmn:incoming>f_eg_landed</bpmn:incoming>
|
|
145
156
|
<bpmn:outgoing>f_m_landed</bpmn:outgoing>
|
|
146
157
|
<bpmn:messageEventDefinition id="med_mergeLanded" messageRef="Message_mergeLanded" />
|
|
147
158
|
</bpmn:intermediateCatchEvent>
|
|
159
|
+
<bpmn:intermediateCatchEvent id="wait-evicted" name="Wait: evicted from queue">
|
|
160
|
+
<bpmn:incoming>f_eg_evicted</bpmn:incoming>
|
|
161
|
+
<bpmn:outgoing>f_m_evicted</bpmn:outgoing>
|
|
162
|
+
<bpmn:messageEventDefinition id="med_mergeEvicted" messageRef="Message_mergeEvicted" />
|
|
163
|
+
</bpmn:intermediateCatchEvent>
|
|
148
164
|
<bpmn:serviceTask id="mark-merged" name="Mark merged">
|
|
149
165
|
<bpmn:extensionElements>
|
|
150
166
|
<zeebe:taskDefinition type="pr.mark-merged" />
|
|
@@ -287,9 +303,12 @@
|
|
|
287
303
|
<bpmn:sequenceFlow id="f_m_gMerged" name="merged" sourceRef="gw-merge" targetRef="mark-merged">
|
|
288
304
|
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=mergeStatus = "merged"</bpmn:conditionExpression>
|
|
289
305
|
</bpmn:sequenceFlow>
|
|
290
|
-
<bpmn:sequenceFlow id="f_m_gQueued" name="queued" sourceRef="gw-merge" targetRef="
|
|
306
|
+
<bpmn:sequenceFlow id="f_m_gQueued" name="queued" sourceRef="gw-merge" targetRef="eg-landed">
|
|
291
307
|
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=mergeStatus = "queued"</bpmn:conditionExpression>
|
|
292
308
|
</bpmn:sequenceFlow>
|
|
309
|
+
<bpmn:sequenceFlow id="f_eg_landed" sourceRef="eg-landed" targetRef="wait-landed" />
|
|
310
|
+
<bpmn:sequenceFlow id="f_eg_evicted" sourceRef="eg-landed" targetRef="wait-evicted" />
|
|
311
|
+
<bpmn:sequenceFlow id="f_m_evicted" sourceRef="wait-evicted" targetRef="arm-merge" />
|
|
293
312
|
<bpmn:sequenceFlow id="f_m_gBlocked" name="blocked" sourceRef="gw-merge" targetRef="merge-esc-attempt" />
|
|
294
313
|
<bpmn:sequenceFlow id="f_m_landed" sourceRef="wait-landed" targetRef="mark-merged" />
|
|
295
314
|
<bpmn:sequenceFlow id="f_m_done" sourceRef="mark-merged" targetRef="MergeEnd" />
|
|
@@ -332,49 +351,61 @@
|
|
|
332
351
|
<bpmndi:BPMNShape id="BPMNShape_gw-merge" bpmnElement="gw-merge" isMarkerVisible="true">
|
|
333
352
|
<dc:Bounds x="1063" y="95" width="50" height="50" />
|
|
334
353
|
<bpmndi:BPMNLabel>
|
|
335
|
-
<dc:Bounds x="
|
|
354
|
+
<dc:Bounds x="1022" y="150" width="53" height="28" />
|
|
355
|
+
</bpmndi:BPMNLabel>
|
|
356
|
+
</bpmndi:BPMNShape>
|
|
357
|
+
<bpmndi:BPMNShape id="BPMNShape_eg-landed" bpmnElement="eg-landed" isMarkerVisible="true">
|
|
358
|
+
<dc:Bounds x="1238" y="255" width="50" height="50" />
|
|
359
|
+
<bpmndi:BPMNLabel>
|
|
360
|
+
<dc:Bounds x="1231" y="222" width="64" height="28" />
|
|
336
361
|
</bpmndi:BPMNLabel>
|
|
337
362
|
</bpmndi:BPMNShape>
|
|
338
363
|
<bpmndi:BPMNShape id="BPMNShape_wait-landed" bpmnElement="wait-landed">
|
|
339
|
-
<dc:Bounds x="
|
|
364
|
+
<dc:Bounds x="1420" y="262" width="36" height="36" />
|
|
340
365
|
<bpmndi:BPMNLabel>
|
|
341
|
-
<dc:Bounds x="
|
|
366
|
+
<dc:Bounds x="1396" y="303" width="85" height="28" />
|
|
367
|
+
</bpmndi:BPMNLabel>
|
|
368
|
+
</bpmndi:BPMNShape>
|
|
369
|
+
<bpmndi:BPMNShape id="BPMNShape_wait-evicted" bpmnElement="wait-evicted">
|
|
370
|
+
<dc:Bounds x="1420" y="422" width="36" height="36" />
|
|
371
|
+
<bpmndi:BPMNLabel>
|
|
372
|
+
<dc:Bounds x="1394" y="375" width="88" height="42" />
|
|
342
373
|
</bpmndi:BPMNLabel>
|
|
343
374
|
</bpmndi:BPMNShape>
|
|
344
375
|
<bpmndi:BPMNShape id="BPMNShape_mark-merged" bpmnElement="mark-merged">
|
|
345
|
-
<dc:Bounds x="
|
|
376
|
+
<dc:Bounds x="1588" y="80" width="100" height="80" />
|
|
346
377
|
</bpmndi:BPMNShape>
|
|
347
378
|
<bpmndi:BPMNShape id="BPMNShape_MergeEnd" bpmnElement="MergeEnd">
|
|
348
|
-
<dc:Bounds x="
|
|
379
|
+
<dc:Bounds x="1788" y="102" width="36" height="36" />
|
|
349
380
|
<bpmndi:BPMNLabel>
|
|
350
|
-
<dc:Bounds x="
|
|
381
|
+
<dc:Bounds x="1781" y="143" width="50" height="14" />
|
|
351
382
|
</bpmndi:BPMNLabel>
|
|
352
383
|
</bpmndi:BPMNShape>
|
|
353
384
|
<bpmndi:BPMNShape id="BPMNShape_merge-esc-conflict" bpmnElement="merge-esc-conflict">
|
|
354
|
-
<dc:Bounds x="1038" y="
|
|
385
|
+
<dc:Bounds x="1038" y="720" width="100" height="80" />
|
|
355
386
|
</bpmndi:BPMNShape>
|
|
356
387
|
<bpmndi:BPMNShape id="BPMNShape_merge-esc-attempt" bpmnElement="merge-esc-attempt">
|
|
357
|
-
<dc:Bounds x="1388" y="
|
|
388
|
+
<dc:Bounds x="1388" y="560" width="100" height="80" />
|
|
358
389
|
</bpmndi:BPMNShape>
|
|
359
390
|
<bpmndi:BPMNShape id="BPMNShape_wait-merge-answer" bpmnElement="wait-merge-answer">
|
|
360
|
-
<dc:Bounds x="
|
|
391
|
+
<dc:Bounds x="1620" y="582" width="36" height="36" />
|
|
361
392
|
<bpmndi:BPMNLabel>
|
|
362
|
-
<dc:Bounds x="
|
|
393
|
+
<dc:Bounds x="1661" y="579" width="74" height="42" />
|
|
363
394
|
</bpmndi:BPMNLabel>
|
|
364
395
|
</bpmndi:BPMNShape>
|
|
365
396
|
<bpmndi:BPMNShape id="BPMNShape_gw-ci-fix" bpmnElement="gw-ci-fix" isMarkerVisible="true">
|
|
366
|
-
<dc:Bounds x="863" y="
|
|
397
|
+
<dc:Bounds x="863" y="735" width="50" height="50" />
|
|
367
398
|
<bpmndi:BPMNLabel>
|
|
368
|
-
<dc:Bounds x="844" y="
|
|
399
|
+
<dc:Bounds x="844" y="716" width="89" height="14" />
|
|
369
400
|
</bpmndi:BPMNLabel>
|
|
370
401
|
</bpmndi:BPMNShape>
|
|
371
402
|
<bpmndi:BPMNShape id="BPMNShape_fix-ci" bpmnElement="fix-ci">
|
|
372
|
-
<dc:Bounds x="1038" y="
|
|
403
|
+
<dc:Bounds x="1038" y="880" width="100" height="80" />
|
|
373
404
|
</bpmndi:BPMNShape>
|
|
374
405
|
<bpmndi:BPMNShape id="BPMNShape_gw-ci-result" bpmnElement="gw-ci-result" isMarkerVisible="true">
|
|
375
|
-
<dc:Bounds x="1238" y="
|
|
406
|
+
<dc:Bounds x="1238" y="895" width="50" height="50" />
|
|
376
407
|
<bpmndi:BPMNLabel>
|
|
377
|
-
<dc:Bounds x="1240" y="
|
|
408
|
+
<dc:Bounds x="1240" y="876" width="46" height="14" />
|
|
378
409
|
</bpmndi:BPMNLabel>
|
|
379
410
|
</bpmndi:BPMNShape>
|
|
380
411
|
<bpmndi:BPMNShape id="BPMNShape_gw-rebase" bpmnElement="gw-rebase" isMarkerVisible="true">
|
|
@@ -384,12 +415,12 @@
|
|
|
384
415
|
</bpmndi:BPMNLabel>
|
|
385
416
|
</bpmndi:BPMNShape>
|
|
386
417
|
<bpmndi:BPMNShape id="BPMNShape_rebase" bpmnElement="rebase">
|
|
387
|
-
<dc:Bounds x="1038" y="
|
|
418
|
+
<dc:Bounds x="1038" y="1040" width="100" height="80" />
|
|
388
419
|
</bpmndi:BPMNShape>
|
|
389
420
|
<bpmndi:BPMNShape id="BPMNShape_gw-rebase-result" bpmnElement="gw-rebase-result" isMarkerVisible="true">
|
|
390
|
-
<dc:Bounds x="1238" y="
|
|
421
|
+
<dc:Bounds x="1238" y="1055" width="50" height="50" />
|
|
391
422
|
<bpmndi:BPMNLabel>
|
|
392
|
-
<dc:Bounds x="1233" y="
|
|
423
|
+
<dc:Bounds x="1233" y="1036" width="60" height="14" />
|
|
393
424
|
</bpmndi:BPMNLabel>
|
|
394
425
|
</bpmndi:BPMNShape>
|
|
395
426
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_start" bpmnElement="f_m_start">
|
|
@@ -421,69 +452,78 @@
|
|
|
421
452
|
</bpmndi:BPMNEdge>
|
|
422
453
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_gMerged" bpmnElement="f_m_gMerged">
|
|
423
454
|
<di:waypoint x="1113" y="120" />
|
|
424
|
-
<di:waypoint x="
|
|
455
|
+
<di:waypoint x="1588" y="120" />
|
|
425
456
|
<bpmndi:BPMNLabel>
|
|
426
|
-
<dc:Bounds x="
|
|
457
|
+
<dc:Bounds x="1326" y="98" width="49" height="14" />
|
|
427
458
|
</bpmndi:BPMNLabel>
|
|
428
459
|
</bpmndi:BPMNEdge>
|
|
429
460
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_done" bpmnElement="f_m_done">
|
|
430
|
-
<di:waypoint x="
|
|
431
|
-
<di:waypoint x="
|
|
461
|
+
<di:waypoint x="1688" y="120" />
|
|
462
|
+
<di:waypoint x="1788" y="120" />
|
|
432
463
|
</bpmndi:BPMNEdge>
|
|
433
464
|
<bpmndi:BPMNEdge id="BPMNEdge_f_ci_giveup" bpmnElement="f_ci_giveup">
|
|
434
|
-
<di:waypoint x="913" y="
|
|
435
|
-
<di:waypoint x="1038" y="
|
|
465
|
+
<di:waypoint x="913" y="760" />
|
|
466
|
+
<di:waypoint x="1038" y="760" />
|
|
436
467
|
<bpmndi:BPMNLabel>
|
|
437
|
-
<dc:Bounds x="942" y="
|
|
468
|
+
<dc:Bounds x="942" y="765" width="67" height="28" />
|
|
438
469
|
</bpmndi:BPMNLabel>
|
|
439
470
|
</bpmndi:BPMNEdge>
|
|
440
471
|
<bpmndi:BPMNEdge id="BPMNEdge_f_ci_blocked" bpmnElement="f_ci_blocked">
|
|
441
|
-
<di:waypoint x="1288" y="
|
|
442
|
-
<di:waypoint x="1438" y="
|
|
443
|
-
<di:waypoint x="1438" y="
|
|
472
|
+
<di:waypoint x="1288" y="920" />
|
|
473
|
+
<di:waypoint x="1438" y="920" />
|
|
474
|
+
<di:waypoint x="1438" y="640" />
|
|
444
475
|
<bpmndi:BPMNLabel>
|
|
445
|
-
<dc:Bounds x="1319" y="
|
|
476
|
+
<dc:Bounds x="1319" y="898" width="89" height="14" />
|
|
446
477
|
</bpmndi:BPMNLabel>
|
|
447
478
|
</bpmndi:BPMNEdge>
|
|
448
479
|
<bpmndi:BPMNEdge id="BPMNEdge_f_reb_giveup" bpmnElement="f_reb_giveup">
|
|
449
480
|
<di:waypoint x="913" y="280" />
|
|
450
481
|
<di:waypoint x="1088" y="280" />
|
|
451
|
-
<di:waypoint x="1088" y="
|
|
482
|
+
<di:waypoint x="1088" y="720" />
|
|
452
483
|
<bpmndi:BPMNLabel>
|
|
453
484
|
<dc:Bounds x="977" y="247" width="67" height="28" />
|
|
454
485
|
</bpmndi:BPMNLabel>
|
|
455
486
|
</bpmndi:BPMNEdge>
|
|
456
487
|
<bpmndi:BPMNEdge id="BPMNEdge_f_reb_blocked" bpmnElement="f_reb_blocked">
|
|
457
|
-
<di:waypoint x="1288" y="
|
|
458
|
-
<di:waypoint x="1438" y="
|
|
459
|
-
<di:waypoint x="1438" y="
|
|
488
|
+
<di:waypoint x="1288" y="1080" />
|
|
489
|
+
<di:waypoint x="1438" y="1080" />
|
|
490
|
+
<di:waypoint x="1438" y="640" />
|
|
460
491
|
<bpmndi:BPMNLabel>
|
|
461
|
-
<dc:Bounds x="1331" y="
|
|
492
|
+
<dc:Bounds x="1331" y="1047" width="64" height="28" />
|
|
462
493
|
</bpmndi:BPMNLabel>
|
|
463
494
|
</bpmndi:BPMNEdge>
|
|
495
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_eg_landed" bpmnElement="f_eg_landed">
|
|
496
|
+
<di:waypoint x="1288" y="280" />
|
|
497
|
+
<di:waypoint x="1420" y="280" />
|
|
498
|
+
</bpmndi:BPMNEdge>
|
|
499
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_m_landed" bpmnElement="f_m_landed">
|
|
500
|
+
<di:waypoint x="1456" y="280" />
|
|
501
|
+
<di:waypoint x="1638" y="280" />
|
|
502
|
+
<di:waypoint x="1638" y="160" />
|
|
503
|
+
</bpmndi:BPMNEdge>
|
|
464
504
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_escC" bpmnElement="f_m_escC">
|
|
465
|
-
<di:waypoint x="1138" y="
|
|
466
|
-
<di:waypoint x="1158" y="
|
|
467
|
-
<di:waypoint x="1158" y="
|
|
468
|
-
<di:waypoint x="
|
|
469
|
-
<di:waypoint x="
|
|
505
|
+
<di:waypoint x="1138" y="740" />
|
|
506
|
+
<di:waypoint x="1158" y="740" />
|
|
507
|
+
<di:waypoint x="1158" y="540" />
|
|
508
|
+
<di:waypoint x="1638" y="540" />
|
|
509
|
+
<di:waypoint x="1638" y="582" />
|
|
470
510
|
</bpmndi:BPMNEdge>
|
|
471
511
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_escA" bpmnElement="f_m_escA">
|
|
472
|
-
<di:waypoint x="1488" y="
|
|
473
|
-
<di:waypoint x="
|
|
512
|
+
<di:waypoint x="1488" y="600" />
|
|
513
|
+
<di:waypoint x="1620" y="600" />
|
|
474
514
|
</bpmndi:BPMNEdge>
|
|
475
515
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_answer" bpmnElement="f_m_answer">
|
|
476
|
-
<di:waypoint x="
|
|
477
|
-
<di:waypoint x="
|
|
478
|
-
<di:waypoint x="402" y="
|
|
516
|
+
<di:waypoint x="1638" y="618" />
|
|
517
|
+
<di:waypoint x="1638" y="660" />
|
|
518
|
+
<di:waypoint x="402" y="660" />
|
|
479
519
|
<di:waypoint x="402" y="160" />
|
|
480
520
|
</bpmndi:BPMNEdge>
|
|
481
521
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_mCiFix" bpmnElement="f_m_mCiFix">
|
|
482
522
|
<di:waypoint x="713" y="145" />
|
|
483
|
-
<di:waypoint x="713" y="
|
|
484
|
-
<di:waypoint x="863" y="
|
|
523
|
+
<di:waypoint x="713" y="760" />
|
|
524
|
+
<di:waypoint x="863" y="760" />
|
|
485
525
|
<bpmndi:BPMNLabel>
|
|
486
|
-
<dc:Bounds x="718" y="
|
|
526
|
+
<dc:Bounds x="718" y="518" width="60" height="42" />
|
|
487
527
|
</bpmndi:BPMNLabel>
|
|
488
528
|
</bpmndi:BPMNEdge>
|
|
489
529
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_mRebase" bpmnElement="f_m_mRebase">
|
|
@@ -498,18 +538,18 @@
|
|
|
498
538
|
<di:waypoint x="713" y="145" />
|
|
499
539
|
<di:waypoint x="713" y="318" />
|
|
500
540
|
<di:waypoint x="1018" y="318" />
|
|
501
|
-
<di:waypoint x="1018" y="
|
|
502
|
-
<di:waypoint x="1038" y="
|
|
541
|
+
<di:waypoint x="1018" y="740" />
|
|
542
|
+
<di:waypoint x="1038" y="740" />
|
|
503
543
|
<bpmndi:BPMNLabel>
|
|
504
544
|
<dc:Bounds x="823" y="326" width="85" height="14" />
|
|
505
545
|
</bpmndi:BPMNLabel>
|
|
506
546
|
</bpmndi:BPMNEdge>
|
|
507
547
|
<bpmndi:BPMNEdge id="BPMNEdge_f_ci_go" bpmnElement="f_ci_go">
|
|
508
|
-
<di:waypoint x="888" y="
|
|
509
|
-
<di:waypoint x="888" y="
|
|
510
|
-
<di:waypoint x="1038" y="
|
|
548
|
+
<di:waypoint x="888" y="785" />
|
|
549
|
+
<di:waypoint x="888" y="920" />
|
|
550
|
+
<di:waypoint x="1038" y="920" />
|
|
511
551
|
<bpmndi:BPMNLabel>
|
|
512
|
-
<dc:Bounds x="893" y="
|
|
552
|
+
<dc:Bounds x="893" y="839" width="49" height="28" />
|
|
513
553
|
</bpmndi:BPMNLabel>
|
|
514
554
|
</bpmndi:BPMNEdge>
|
|
515
555
|
<bpmndi:BPMNEdge id="BPMNEdge_f_reb_go" bpmnElement="f_reb_go">
|
|
@@ -517,8 +557,8 @@
|
|
|
517
557
|
<di:waypoint x="933" y="280" />
|
|
518
558
|
<di:waypoint x="933" y="305" />
|
|
519
559
|
<di:waypoint x="1158" y="305" />
|
|
520
|
-
<di:waypoint x="1158" y="
|
|
521
|
-
<di:waypoint x="1138" y="
|
|
560
|
+
<di:waypoint x="1158" y="1080" />
|
|
561
|
+
<di:waypoint x="1138" y="1080" />
|
|
522
562
|
<bpmndi:BPMNLabel>
|
|
523
563
|
<dc:Bounds x="1101" y="310" width="49" height="28" />
|
|
524
564
|
</bpmndi:BPMNLabel>
|
|
@@ -526,60 +566,64 @@
|
|
|
526
566
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_gQueued" bpmnElement="f_m_gQueued">
|
|
527
567
|
<di:waypoint x="1088" y="145" />
|
|
528
568
|
<di:waypoint x="1088" y="280" />
|
|
529
|
-
<di:waypoint x="
|
|
569
|
+
<di:waypoint x="1238" y="280" />
|
|
530
570
|
<bpmndi:BPMNLabel>
|
|
531
571
|
<dc:Bounds x="1093" y="206" width="46" height="14" />
|
|
532
572
|
</bpmndi:BPMNLabel>
|
|
533
573
|
</bpmndi:BPMNEdge>
|
|
574
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_eg_evicted" bpmnElement="f_eg_evicted">
|
|
575
|
+
<di:waypoint x="1263" y="305" />
|
|
576
|
+
<di:waypoint x="1263" y="440" />
|
|
577
|
+
<di:waypoint x="1420" y="440" />
|
|
578
|
+
</bpmndi:BPMNEdge>
|
|
534
579
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_gBlocked" bpmnElement="f_m_gBlocked">
|
|
535
|
-
<di:waypoint x="
|
|
536
|
-
<di:waypoint x="
|
|
537
|
-
<di:waypoint x="
|
|
538
|
-
<di:waypoint x="
|
|
580
|
+
<di:waypoint x="1088" y="95" />
|
|
581
|
+
<di:waypoint x="1088" y="75" />
|
|
582
|
+
<di:waypoint x="1844" y="75" />
|
|
583
|
+
<di:waypoint x="1844" y="478" />
|
|
584
|
+
<di:waypoint x="1368" y="478" />
|
|
585
|
+
<di:waypoint x="1368" y="580" />
|
|
586
|
+
<di:waypoint x="1388" y="580" />
|
|
539
587
|
<bpmndi:BPMNLabel>
|
|
540
|
-
<dc:Bounds x="
|
|
588
|
+
<dc:Bounds x="1849" y="270" width="53" height="14" />
|
|
541
589
|
</bpmndi:BPMNLabel>
|
|
542
590
|
</bpmndi:BPMNEdge>
|
|
543
591
|
<bpmndi:BPMNEdge id="BPMNEdge_f_ci_done" bpmnElement="f_ci_done">
|
|
544
|
-
<di:waypoint x="1138" y="
|
|
545
|
-
<di:waypoint x="1238" y="
|
|
592
|
+
<di:waypoint x="1138" y="920" />
|
|
593
|
+
<di:waypoint x="1238" y="920" />
|
|
546
594
|
</bpmndi:BPMNEdge>
|
|
547
595
|
<bpmndi:BPMNEdge id="BPMNEdge_f_reb_done" bpmnElement="f_reb_done">
|
|
548
|
-
<di:waypoint x="1138" y="
|
|
549
|
-
<di:waypoint x="1158" y="
|
|
550
|
-
<di:waypoint x="1158" y="
|
|
551
|
-
<di:waypoint x="1238" y="
|
|
552
|
-
<di:waypoint x="1263" y="
|
|
553
|
-
<di:waypoint x="1263" y="
|
|
554
|
-
</bpmndi:BPMNEdge>
|
|
555
|
-
<bpmndi:BPMNEdge id="BPMNEdge_f_m_landed" bpmnElement="f_m_landed">
|
|
556
|
-
<di:waypoint x="1263" y="298" />
|
|
557
|
-
<di:waypoint x="1263" y="318" />
|
|
558
|
-
<di:waypoint x="1281" y="318" />
|
|
559
|
-
<di:waypoint x="1281" y="540" />
|
|
560
|
-
<di:waypoint x="1644" y="540" />
|
|
561
|
-
<di:waypoint x="1644" y="180" />
|
|
562
|
-
<di:waypoint x="1468" y="180" />
|
|
563
|
-
<di:waypoint x="1468" y="160" />
|
|
596
|
+
<di:waypoint x="1138" y="1100" />
|
|
597
|
+
<di:waypoint x="1158" y="1100" />
|
|
598
|
+
<di:waypoint x="1158" y="1125" />
|
|
599
|
+
<di:waypoint x="1238" y="1125" />
|
|
600
|
+
<di:waypoint x="1263" y="1125" />
|
|
601
|
+
<di:waypoint x="1263" y="1105" />
|
|
564
602
|
</bpmndi:BPMNEdge>
|
|
565
603
|
<bpmndi:BPMNEdge id="BPMNEdge_f_ci_fixed" bpmnElement="f_ci_fixed">
|
|
566
|
-
<di:waypoint x="1263" y="
|
|
567
|
-
<di:waypoint x="1263" y="
|
|
568
|
-
<di:waypoint x="402" y="
|
|
604
|
+
<di:waypoint x="1263" y="945" />
|
|
605
|
+
<di:waypoint x="1263" y="980" />
|
|
606
|
+
<di:waypoint x="402" y="980" />
|
|
569
607
|
<di:waypoint x="402" y="160" />
|
|
570
608
|
<bpmndi:BPMNLabel>
|
|
571
|
-
<dc:Bounds x="813" y="
|
|
609
|
+
<dc:Bounds x="813" y="958" width="39" height="14" />
|
|
572
610
|
</bpmndi:BPMNLabel>
|
|
573
611
|
</bpmndi:BPMNEdge>
|
|
574
612
|
<bpmndi:BPMNEdge id="BPMNEdge_f_reb_rebased" bpmnElement="f_reb_rebased">
|
|
575
|
-
<di:waypoint x="1263" y="
|
|
576
|
-
<di:waypoint x="1263" y="
|
|
577
|
-
<di:waypoint x="402" y="
|
|
613
|
+
<di:waypoint x="1263" y="1105" />
|
|
614
|
+
<di:waypoint x="1263" y="1140" />
|
|
615
|
+
<di:waypoint x="402" y="1140" />
|
|
578
616
|
<di:waypoint x="402" y="160" />
|
|
579
617
|
<bpmndi:BPMNLabel>
|
|
580
|
-
<dc:Bounds x="806" y="
|
|
618
|
+
<dc:Bounds x="806" y="1118" width="53" height="14" />
|
|
581
619
|
</bpmndi:BPMNLabel>
|
|
582
620
|
</bpmndi:BPMNEdge>
|
|
621
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_m_evicted" bpmnElement="f_m_evicted">
|
|
622
|
+
<di:waypoint x="1438" y="458" />
|
|
623
|
+
<di:waypoint x="1438" y="478" />
|
|
624
|
+
<di:waypoint x="402" y="478" />
|
|
625
|
+
<di:waypoint x="402" y="160" />
|
|
626
|
+
</bpmndi:BPMNEdge>
|
|
583
627
|
</bpmndi:BPMNPlane>
|
|
584
628
|
</bpmndi:BPMNDiagram>
|
|
585
629
|
</bpmn:definitions>
|