@nanobpm/nano-workforce 0.203.0 → 0.204.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 +12 -0
- package/app/feature.ts +9 -0
- package/app/github.ts +14 -6
- package/app/issuePrLink.test.ts +703 -0
- package/app/issuePrLink.ts +312 -0
- package/app/service.ts +3 -0
- package/db/migrations/125_feature_runs_pr_linked.sql +78 -0
- package/docs/agent-guide.md +282 -855
- package/e2e/feature-run.e2e.ts +58 -1
- package/e2e/plan-fanout.e2e.ts +24 -1
- package/e2e/support/github-admit.ts +33 -0
- package/package.json +1 -1
- package/resources/processes/feature.bpmn +1 -0
- package/resources/processes/implement-cell.bpmn +1 -0
- package/skills/nano-workforce/SKILL.md +20 -65
- package/workers/record-feature/worker.test.ts +256 -0
- package/workers/record-feature/worker.ts +27 -0
package/docs/agent-guide.md
CHANGED
|
@@ -1,355 +1,161 @@
|
|
|
1
1
|
# Nano Workforce — agent operator guide
|
|
2
2
|
|
|
3
|
-
You are
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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).
|
|
3
|
+
You are helping a human operate a running **Nano Workforce** instance: a durable
|
|
4
|
+
orchestration app that drives pull requests to **review convergence** and **merges** them,
|
|
5
|
+
takes whole issues through **plan → implement → converge** across a fleet of coding agents,
|
|
6
|
+
and runs agent-authored **delivery graphs**.
|
|
13
7
|
|
|
14
8
|
- **App control API base:** `__BASE__`
|
|
15
|
-
- **Engine (
|
|
9
|
+
- **Engine (Camunda-8 v2 REST) base:** `__ENGINE__`
|
|
16
10
|
- **Source repository:** `nanobpm/nano-workforce`
|
|
17
11
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
endpoints (
|
|
22
|
-
|
|
12
|
+
**Tools first, curl as fallback.** Over MCP, call the projected tool named in each
|
|
13
|
+
recipe. Without MCP, use the curl door in the §10 crosswalk. If the deployment sets
|
|
14
|
+
`NANO_PR_WEBHOOK_SECRET`, add `-H "x-hook-secret: <secret>"` to **every** request — the
|
|
15
|
+
read endpoints (`/version`, `/status`, `/escalations`, `/agent`, `/agent/guide`, …) are
|
|
16
|
+
guarded too, not only the POSTs.
|
|
23
17
|
|
|
24
18
|
---
|
|
25
19
|
|
|
26
20
|
## 0. Orient yourself first
|
|
27
21
|
|
|
28
|
-
|
|
29
|
-
tools** (`getVersion`, `listActivePrs`); curl is the no-MCP fallback only. Every step in
|
|
30
|
-
this guide that has a tool is mapped in the **tool↔curl crosswalk (§10)** — reach for the
|
|
31
|
-
tool first and drop to curl only when you are not driving this instance over MCP.
|
|
32
|
-
|
|
33
|
-
```text
|
|
34
|
-
# Which code is live (app version, urban version, git sha/branch, uptime):
|
|
35
|
-
tool: getVersion # GET /version
|
|
36
|
-
|
|
37
|
-
# Every PR currently in flight (not converged/abandoned), with its engine
|
|
38
|
-
# process key, status, round, and any open escalation:
|
|
39
|
-
tool: listActivePrs # GET /status
|
|
40
|
-
```
|
|
22
|
+
Confirm what is running and what is in flight before you act.
|
|
41
23
|
|
|
42
24
|
```bash
|
|
43
|
-
|
|
44
|
-
curl -sS __BASE__/
|
|
45
|
-
curl -sS __BASE__/status | jq
|
|
25
|
+
curl -sS __BASE__/version | jq # tool: getVersion: app/urban version, git sha, uptime
|
|
26
|
+
curl -sS __BASE__/status | jq # tool: listActivePrs: every PR in flight
|
|
46
27
|
```
|
|
47
28
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
`activeWorker`/`leaseUntil` (is an agent actually working the round, or is the job
|
|
53
|
-
just queued), and `updatedAt`.
|
|
29
|
+
Each `/status` entry carries `prKey` (`owner/repo#123`), `status`, `round`, `processKey`
|
|
30
|
+
(the **engine process-instance key**, the bridge to §5), `openEscalation`,
|
|
31
|
+
`activeWorker`/`leaseUntil` (an agent holds the round, or the job is just queued), and
|
|
32
|
+
`updatedAt`.
|
|
54
33
|
|
|
55
34
|
---
|
|
56
35
|
|
|
57
36
|
## 1. Submit a PR for review convergence
|
|
58
37
|
|
|
59
|
-
One PR → one durable `convergence-loop`. Each round
|
|
60
|
-
|
|
61
|
-
never holds an agent slot.
|
|
62
|
-
`review-ready` when a new review lands.
|
|
38
|
+
One PR → one durable `convergence-loop`. Each round runs a `senior:pr-review` agent. Between
|
|
39
|
+
rounds the process waits durably for the next review (the poller republishes `review-ready`),
|
|
40
|
+
so review latency never holds an agent slot.
|
|
63
41
|
|
|
64
42
|
```bash
|
|
65
|
-
#
|
|
43
|
+
# tool: startConvergenceLoop
|
|
66
44
|
curl -sS -X POST __BASE__/actions/start/convergence-loop \
|
|
67
45
|
-H 'content-type: application/json' \
|
|
68
|
-
-d '{ "pr": "owner/repo#
|
|
46
|
+
-d '{ "pr": "owner/repo#42", "autoMerge": true, "maxRounds": 8, "dependsOn": ["owner/repo#40"] }'
|
|
69
47
|
```
|
|
70
48
|
|
|
71
|
-
The body is flat. Fields:
|
|
72
|
-
|
|
73
49
|
| field | type | meaning |
|
|
74
50
|
|---|---|---|
|
|
75
|
-
| `pr` (or `url`) | string |
|
|
76
|
-
| `autoMerge` | boolean |
|
|
77
|
-
| `maxRounds` | integer |
|
|
78
|
-
| `dependsOn` | string[] |
|
|
79
|
-
| `autoResolveEscalations` | boolean |
|
|
51
|
+
| `pr` (or `url`) | string | `owner/repo#123` or a PR URL. Required. |
|
|
52
|
+
| `autoMerge` | boolean | `false` = review only, stop at `converged`. `true` = converge **then merge**. **Omitted = merge** (the merge path is the default). Merging also requires `NANO_PR_AUTO_MERGE` on (the default); a per-request value only ever narrows to review-only, it never forces a merge when the global switch is off. Choose deliberately — to guarantee review-only, set `autoMerge: false` explicitly. |
|
|
53
|
+
| `maxRounds` | integer | round cap before escalating (1–100; default `NANO_PR_MAX_ROUNDS`, 20). |
|
|
54
|
+
| `dependsOn` | string[] | `prKey`s that must land before this one merges. |
|
|
55
|
+
| `autoResolveEscalations` | boolean | opt in to the escalation steward (§3). Default `false`. |
|
|
80
56
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
# Review only — do NOT merge (use when the human wants to merge by hand,
|
|
85
|
-
# or is only after a review pass):
|
|
86
|
-
curl -sS -X POST __BASE__/actions/start/convergence-loop \
|
|
87
|
-
-H 'content-type: application/json' \
|
|
88
|
-
-d '{ "pr": "owner/repo#123", "autoMerge": false }'
|
|
89
|
-
|
|
90
|
-
# Converge then merge, with a dependency barrier and a tighter round cap:
|
|
91
|
-
curl -sS -X POST __BASE__/actions/start/convergence-loop \
|
|
92
|
-
-H 'content-type: application/json' \
|
|
93
|
-
-d '{ "pr": "owner/repo#42", "autoMerge": true, "maxRounds": 8, "dependsOn": ["owner/repo#40"] }'
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Submitting is **idempotent on the PR key** — re-POSTing the same PR refreshes the
|
|
97
|
-
aggregate rather than starting a duplicate loop. The response (202) echoes the
|
|
98
|
-
`prKey` and the engine `processKey`.
|
|
57
|
+
Idempotent on the PR key: re-POSTing refreshes the record instead of starting a second
|
|
58
|
+
loop. The 202 response echoes `prKey` and `processKey`.
|
|
99
59
|
|
|
100
60
|
---
|
|
101
61
|
|
|
102
62
|
## 2. Submit an epic (plan → implement → converge)
|
|
103
63
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
PR is then enrolled into its own convergence loop (§1).
|
|
64
|
+
A planning agent splits an issue into levelled tasks. One implementation agent per task
|
|
65
|
+
opens a PR, and every PR is enrolled in its own convergence loop (§1).
|
|
107
66
|
|
|
108
67
|
```bash
|
|
68
|
+
# tool: startPlanFanout
|
|
109
69
|
curl -sS -X POST __BASE__/actions/start/plan-fanout \
|
|
110
70
|
-H 'content-type: application/json' \
|
|
111
71
|
-d '{ "issue": "owner/repo#123", "baseBranch": "epic/agent-protocol" }'
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
The body is flat: `issue` (or `url`) — `owner/repo#123` or an issue URL — plus a REQUIRED
|
|
115
|
-
`baseBranch` (ADR 0003), the branch the fleet branches off and opens every PR against; a
|
|
116
|
-
blank/absent base is rejected with a 400. Starting a
|
|
117
|
-
plan is idempotent on the plan key; an already-running plan short-circuits. The
|
|
118
|
-
response (202) echoes the `planKey` and engine `processKey`.
|
|
119
72
|
|
|
120
|
-
|
|
121
|
-
**`autoResolveEscalations`** boolean (issue #889, default `false`). When `true`, the run opts in to
|
|
122
|
-
the escalation steward, and the opt-in **propagates to every child it spawns** — an epic's
|
|
123
|
-
implementation tasks and each enrolled convergence/merge loop, a feature run's handed-off PR. Only
|
|
124
|
-
escalations raised under opted-in work are eligible for the steward (§3).
|
|
125
|
-
|
|
126
|
-
**Single issue, one PR (feature run, `startFeature`)** — no planning or fan-out. Same body and
|
|
127
|
-
base-branch admission, plus `converge` (enrol the PR in §1) and `autoMerge`:
|
|
128
|
-
|
|
129
|
-
```bash
|
|
73
|
+
# Single issue → one PR, no planning (tool: startFeature). Adds `converge` and `autoMerge`.
|
|
130
74
|
curl -sS -X POST __BASE__/actions/start/feature \
|
|
131
75
|
-H 'content-type: application/json' \
|
|
132
76
|
-d '{ "issue": "owner/repo#123", "baseBranch": "main", "confirmDefaultBase": true, "converge": true, "autoMerge": true }'
|
|
133
77
|
```
|
|
134
78
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
`
|
|
138
|
-
Four ordered rules govern which base is accepted:
|
|
139
|
-
|
|
140
|
-
1. **Required + explicit.** `baseBranch` is mandatory — a blank/absent value is a `400`
|
|
141
|
-
(`MissingBaseBranchError`), and an implausible name is a `400` (`InvalidBaseBranchError`).
|
|
142
|
-
There is no silent "land on the default branch" fallback.
|
|
143
|
-
2. **Create-if-missing, `epic/*` only.** A missing `epic/*` base is **auto-created** off the
|
|
144
|
-
repository default branch's HEAD (idempotently — an existing branch is never reset). A
|
|
145
|
-
missing base that is **not** `epic/*` is a clean `400` (`BaseBranchMustExistError`): a typo
|
|
146
|
-
can't silently spawn a wrong-rooted branch, so any non-`epic/*` base must already exist.
|
|
147
|
-
3. **Confirm-default.** Naming the repository **default branch** as the base requires
|
|
148
|
-
`confirmDefaultBase: true`, else `400` (`DefaultBaseNotConfirmedError`). This is a
|
|
149
|
-
deliberate acknowledgement that every task lands directly on the default branch with no
|
|
150
|
-
integration buffer, and any merge-to-default side effect fires per task.
|
|
151
|
-
4. **Shared-base guard.** If another **active** epic (status not `done`/`failed`/`abandoned`)
|
|
152
|
-
already targets the **same repo + same custom base**, admission is a `409` (`SharedBaseError`)
|
|
153
|
-
unless you pass `allowSharedBase: true`. The default branch is exempt — many epics target it
|
|
154
|
-
concurrently without colliding.
|
|
155
|
-
|
|
156
|
-
So the body may also carry two optional booleans — `confirmDefaultBase` and `allowSharedBase` —
|
|
157
|
-
each a "warn you can't skip" for its rule:
|
|
79
|
+
Idempotent on the plan key. The 202 response echoes `planKey` and `processKey`, and the
|
|
80
|
+
plan's PRs appear in `/status` as ordinary convergence loops. Both doors accept
|
|
81
|
+
`autoResolveEscalations` (default `false`), and it propagates to every child the run spawns.
|
|
158
82
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
83
|
+
**Base-branch admission.** `baseBranch` is checked before anything fans out:
|
|
84
|
+
|
|
85
|
+
1. **Required.** Blank or absent → `400 MissingBaseBranchError`. An implausible name →
|
|
86
|
+
`400 InvalidBaseBranchError`. There is no silent default.
|
|
87
|
+
2. **Only `epic/*` is created if missing** (off the default branch's HEAD; an existing branch
|
|
88
|
+
is never reset). Any other missing base → `400 BaseBranchMustExistError`.
|
|
89
|
+
3. **The repo default branch needs `confirmDefaultBase: true`**, else
|
|
90
|
+
`400 DefaultBaseNotConfirmedError`, because every task then lands straight on it.
|
|
91
|
+
4. **Shared base.** Another active epic on the same repo and custom base →
|
|
92
|
+
`409 SharedBaseError`, unless you pass `allowSharedBase: true`. The default branch is exempt.
|
|
164
93
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
can inspect in §5, and the PRs it opens show up in `/status` as ordinary convergence
|
|
171
|
-
loops.
|
|
172
|
-
|
|
173
|
-
### Build hygiene — implementation agents warm up once, then build module-scoped
|
|
174
|
-
|
|
175
|
-
Implementation and CI-fix agents run on **stateless / ephemeral workers**: the
|
|
176
|
-
workspace, including its build outputs, is thrown away between runs, so each worker pays any
|
|
177
|
-
cold whole-reactor build tax independently. To keep iterations fast, the implementation
|
|
178
|
-
prompts (`resources/prompts/feature.md`, `resources/prompts/fix-ci.md`) instruct every
|
|
179
|
-
agent to, **before iterating on a build in the target repo**:
|
|
180
|
-
|
|
181
|
-
1. **Read the target repo's own `AGENTS.md` / `CONTRIBUTING.md` build section** — the
|
|
182
|
-
recipe is authored there, never duplicated into nano-workforce (a per-repo copy here
|
|
183
|
-
would rot as the target's build changes).
|
|
184
|
-
2. **Run the prescribed dependency warm-up once**, right after checkout. For a Maven
|
|
185
|
-
monorepo that ships the fast path (e.g. camunda), that is
|
|
186
|
-
`./mvnw install -Dquickly -T1C` — it installs every reactor SNAPSHOT into `~/.m2`.
|
|
187
|
-
3. **Then build only the changed module, offline, without `-am`** —
|
|
188
|
-
`./mvnw -Dquickly -o -pl <module> <goals>`. Never run a cold `-am` reactor build per
|
|
189
|
-
iteration.
|
|
190
|
-
|
|
191
|
-
The repo-agnostic infra levers behind this — a host-persisted `~/.m2` and a shared
|
|
192
|
-
remote Maven build cache for JVM-capable worker hosts — live in worker-host config, not
|
|
193
|
-
in per-repo files here. The retro loop (`resources/prompts/retro.md`) promotes any
|
|
194
|
-
durable "warm the cache, then build module-scoped" lesson into the *target repo's*
|
|
195
|
-
`AGENTS.md`, keeping each target's recipe sharp without nano-workforce owning it.
|
|
94
|
+
**Build hygiene.** Workers are ephemeral, so implementation prompts (`feature.md`,
|
|
95
|
+
`fix-ci.md`) tell agents to read the target repo's `AGENTS.md`/`CONTRIBUTING.md` build
|
|
96
|
+
recipe, run its dependency warm-up once (e.g. camunda: `./mvnw install -Dquickly -T1C`),
|
|
97
|
+
then build only the changed module offline (`./mvnw -Dquickly -o -pl <module> <goals>`),
|
|
98
|
+
never a cold `-am` reactor build per iteration. The recipe lives in the target repo, not here.
|
|
196
99
|
|
|
197
100
|
---
|
|
198
101
|
|
|
199
102
|
## 3. Answer escalations (unblock a human-in-the-loop wait)
|
|
200
103
|
|
|
201
|
-
A loop escalates
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
Find the open escalations, then answer them. **Prefer the `listEscalations` tool**
|
|
206
|
-
(the projected read tool that lists every open escalation with the `userTaskKey` and form —
|
|
207
|
-
tracked by sibling #666; on a deployment that predates it, use the `/status` / task-inbox
|
|
208
|
-
curl fallback below —
|
|
209
|
-
you need to answer it); curl is the no-MCP fallback.
|
|
210
|
-
|
|
211
|
-
```text
|
|
212
|
-
# Every open escalation waiting for a human, across all four kinds — each with its
|
|
213
|
-
# userTaskKey, prKey, kind (elementId), question/findings/task, and formVariables:
|
|
214
|
-
tool: listEscalations
|
|
215
|
-
```
|
|
104
|
+
A loop escalates when an agent returns `needs_input`/`blocked`, or when a safety net fires
|
|
105
|
+
(round cap, a review that never arrives, a merge conflict, unfixable CI). The process parks
|
|
106
|
+
on a native BPMN user task until someone answers.
|
|
216
107
|
|
|
217
108
|
```bash
|
|
218
|
-
#
|
|
219
|
-
curl -sS __BASE__/
|
|
220
|
-
| { prKey, status, round, openEscalation }'
|
|
109
|
+
# tool: listEscalations: every open escalation with userTaskKey, kind, subject, question
|
|
110
|
+
curl -sS __BASE__/escalations | jq
|
|
221
111
|
```
|
|
222
112
|
|
|
223
|
-
|
|
224
|
-
(
|
|
225
|
-
|
|
226
|
-
`
|
|
227
|
-
There is no bespoke per-kind webhook or answer page any more.
|
|
113
|
+
`/status` also carries a structured `openEscalation` pointer — `{ userTaskKey, kind, summary }`
|
|
114
|
+
(or `null`) — so you can answer straight from it. `listEscalations` remains the fuller
|
|
115
|
+
task-context surface (subject, question, findings). The raw task inbox is
|
|
116
|
+
`__BASE__/../../tasks/api/tasks`, and its UI for humans is at `__BASE__/../../tasks`.
|
|
228
117
|
|
|
229
|
-
**
|
|
230
|
-
|
|
231
|
-
|
|
118
|
+
**Answer** through the one door, `completeUserTask`, with the variables the task's kind
|
|
119
|
+
expects. Answering as an agent assignee? Use `agentCompleteEscalation` instead: it records
|
|
120
|
+
agent identity for attribution.
|
|
232
121
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
| jq '[.[] | select(.elementId == "plan-review-decision")]'
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
The inbox UI is also served at `__BASE__/../../tasks` for a human to browse, filter, and
|
|
243
|
-
answer (assignee/candidate-group and age surface on each task once assignment lands).
|
|
244
|
-
|
|
245
|
-
> **`wait`-gate escalations are different — completing one does NOT re-arm the gate.** The
|
|
246
|
-
> escalation kinds above resume a loop with your answer. A **delivery-graph `wait` node**
|
|
247
|
-
> (§9) that elapses its bound also parks a task, but completing *that* one releases the token
|
|
248
|
-
> **as not-ready** and the graph proceeds **past the gate** — the downstream side-effecting
|
|
249
|
-
> node then runs against the unmet dependency. See §9.2 before clearing one.
|
|
250
|
-
|
|
251
|
-
**Answer a task** by completing it with the typed variables its form expects — the
|
|
252
|
-
completion resumes the parked process. **Prefer the `completeUserTask` tool** (or
|
|
253
|
-
`agentCompleteEscalation` when you answer as an agent assignee, ADR 0046); the
|
|
254
|
-
`curl … /actions/complete-user-task` app-owned door is the no-MCP fallback.
|
|
255
|
-
|
|
256
|
-
```text
|
|
257
|
-
# PR review-loop (elementId `wait-answer`, pr-escalation form):
|
|
258
|
-
tool: completeUserTask { "userTaskKey": "<key>",
|
|
259
|
-
"variables": { "answer": "Cap retries at 5 and proceed." } }
|
|
260
|
-
|
|
261
|
-
# Implementation (feature) task (elementId `feature-escalation`):
|
|
262
|
-
# { "resolution": "answer", "answer": "…" } to resume, or { "resolution": "abandon" }
|
|
263
|
-
tool: completeUserTask { "userTaskKey": "<key>",
|
|
264
|
-
"variables": { "resolution": "answer", "answer": "Use v2." } }
|
|
265
|
-
|
|
266
|
-
# Plan-review (elementId `plan-review-decision`):
|
|
267
|
-
# { "directive": "revise", "notes": "…" } (fresh review budget) or { "directive": "proceed" }
|
|
268
|
-
tool: completeUserTask { "userTaskKey": "<key>",
|
|
269
|
-
"variables": { "directive": "revise", "notes": "Make issue-7 the seam." } }
|
|
270
|
-
|
|
271
|
-
# Trial-merge (elementId `trial-merge-decision`):
|
|
272
|
-
# { "action": "proceed" | "rebase" | "abandon", "notes"?: "…" }
|
|
273
|
-
tool: completeUserTask { "userTaskKey": "<key>",
|
|
274
|
-
"variables": { "action": "rebase", "notes": "Re-run after the fix." } }
|
|
275
|
-
```
|
|
122
|
+
| kind (`elementId`) | `variables` |
|
|
123
|
+
|---|---|
|
|
124
|
+
| PR review loop (`wait-answer`) and merge loop (`wait-merge-answer`) | `{ "answer": "…" }` |
|
|
125
|
+
| Implementation task (`feature-escalation`) | `{ "resolution": "answer", "answer": "…" }` or `{ "resolution": "abandon" }` |
|
|
126
|
+
| Plan review (`plan-review-decision`) | `{ "directive": "revise", "notes": "…" }` (fresh review budget) or `{ "directive": "proceed" }` |
|
|
127
|
+
| Trial merge (`trial-merge-decision`) | `{ "action": "rebase", "notes": "…" }`; `action` is `proceed`, `rebase` or `abandon` (`notes` optional) |
|
|
276
128
|
|
|
277
129
|
```bash
|
|
278
|
-
#
|
|
279
|
-
# `variables` must carry the shape THIS task's escalation kind expects (see the per-kind
|
|
280
|
-
# tool forms above): `{ answer }` here is the PR review-loop form — a feature task wants
|
|
281
|
-
# `{ resolution, answer }`, plan-review `{ directive, notes }`, trial-merge `{ action, notes? }`.
|
|
130
|
+
# tool: completeUserTask
|
|
282
131
|
curl -sS -X POST __BASE__/actions/complete-user-task -H 'content-type: application/json' \
|
|
283
132
|
-d '{ "userTaskKey": "<key>", "variables": { "answer": "Cap retries at 5 and proceed." } }'
|
|
284
133
|
```
|
|
285
134
|
|
|
286
|
-
|
|
287
|
-
`
|
|
288
|
-
|
|
289
|
-
through the `completeUserTask` tool; curl is the no-MCP fallback:
|
|
135
|
+
The answer becomes the agent's next-round context and the loop resumes. Read the
|
|
136
|
+
`question`/`findings` first and give the smallest answer that unblocks it. The history is
|
|
137
|
+
durable: completed user tasks, plus the `escalations` table shown per PR on the Convergence page.
|
|
290
138
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
```
|
|
139
|
+
> **A delivery-graph `wait` escalation is different.** Completing it releases the gate
|
|
140
|
+
> **as not-ready**, so the downstream work runs against the unmet dependency. Read §9.2
|
|
141
|
+
> before clearing one.
|
|
295
142
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
143
|
+
### The escalation steward (opt-in auto-resolution)
|
|
144
|
+
|
|
145
|
+
Work submitted with `autoResolveEscalations: true` has its open escalations reviewed by the
|
|
146
|
+
**steward** every 30 minutes (`NANO_ESCALATION_STEWARD_INTERVAL`; `NANO_ESCALATION_STEWARD=off`
|
|
147
|
+
disables it). The steward submits the recommended choice through `completeUserTask` with
|
|
148
|
+
**agent** attribution. It is conservative:
|
|
149
|
+
|
|
150
|
+
- It waits a minimum age, so a human already answering wins.
|
|
151
|
+
- It never touches human-only kinds: merge approval, permission prompts, readiness gates,
|
|
152
|
+
blocked/conformance acks, abandon.
|
|
153
|
+
- It **abstains** when the right choice is unclear, or when a subject has already been
|
|
154
|
+
auto-resolved twice and escalated again.
|
|
305
155
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
`
|
|
309
|
-
attribution (ADR 0046); the merge loop no longer uses a durable `escalation-answered`
|
|
310
|
-
message catch.
|
|
311
|
-
|
|
312
|
-
If `NANO_PR_WEBHOOK_SECRET` is set on the deployment, add `-H "x-hook-secret: <secret>"`.
|
|
313
|
-
|
|
314
|
-
The answer is delivered to the agent as its next-round context (e.g. the `answer`,
|
|
315
|
-
`directive`, or `action` variable), and the loop resumes. **Audit** is durable: the
|
|
316
|
-
completed user tasks form the escalation history, and the review/merge loops' rows
|
|
317
|
-
live in the `escalations` table (surfaced read-only per PR on the Convergence page).
|
|
318
|
-
|
|
319
|
-
Guidance for the human you assist: read the escalation `question` or plan-review
|
|
320
|
-
`findings` first, decide the smallest unblocking answer, and answer it precisely —
|
|
321
|
-
the answer becomes the agent's next-round context.
|
|
322
|
-
|
|
323
|
-
### The escalation steward (opt-in auto-resolution, issue #889)
|
|
324
|
-
|
|
325
|
-
Escalations are **still for humans** — unless the work opted in. Each initiation
|
|
326
|
-
pathway (§1, §2, §9) accepts `autoResolveEscalations` (default off). When a piece of
|
|
327
|
-
work opts in, the **escalation steward** reviews its open escalations every 30 minutes
|
|
328
|
-
(configurable via `NANO_ESCALATION_STEWARD_INTERVAL`; `NANO_ESCALATION_STEWARD=off`
|
|
329
|
-
disables the loop entirely) and submits a recommended choice for each — keeping the
|
|
330
|
-
fleet moving instead of parking for hours on a decision with a clear best option.
|
|
331
|
-
|
|
332
|
-
The steward is deliberately conservative:
|
|
333
|
-
|
|
334
|
-
- It only touches escalations on **opted-in** work, past a short minimum age (so a
|
|
335
|
-
human already answering wins), and never a human-only / irreversible / credential
|
|
336
|
-
kind (merge approval, a permission prompt, a readiness gate, a blocked/conformance
|
|
337
|
-
ack, an *abandon*).
|
|
338
|
-
- It **abstains** — leaving the escalation for a human — when the right choice is
|
|
339
|
-
unclear, and a **loop guard** abstains once a subject has been auto-resolved twice
|
|
340
|
-
and escalates again (the automatic answer isn't working).
|
|
341
|
-
- Every resolution goes through the canonical `completeUserTask` door with **agent**
|
|
342
|
-
attribution (ADR 0046) — never recorded as a human decision.
|
|
343
|
-
|
|
344
|
-
**Reviewing auto-resolutions.** Every resolution *and* abstention is written to a
|
|
345
|
-
durable record and shown in the **Auto-resolutions** view (a pending-count badge sits
|
|
346
|
-
in the nav). The default list shows `pending` records, newest first; open one to see
|
|
347
|
-
the full question, the options offered, the submitted variables, the rationale and the
|
|
348
|
-
evidence checked, plus links to the subject and the steward's transcript. From there
|
|
349
|
-
you can **Dismiss** it (reviewed, no action) or **Intervene** (mark the record and
|
|
350
|
-
record guidance for stepping in after the fact — reopening the completed user task is
|
|
351
|
-
not required). Over MCP / curl, `listAutoResolutions` reads the records and
|
|
352
|
-
`reviewAutoResolution` (`{ id, action: "dismiss" | "intervene", note? }`) reviews one.
|
|
156
|
+
Every resolution and abstention is recorded for review in **Auto-resolutions** (nav badge =
|
|
157
|
+
pending count). Read with `listAutoResolutions` (`GET __BASE__/auto-resolutions`), review with
|
|
158
|
+
`reviewAutoResolution` (`{ id, action: "dismiss" | "intervene", note? }`).
|
|
353
159
|
|
|
354
160
|
---
|
|
355
161
|
|
|
@@ -364,178 +170,89 @@ not required). Over MCP / curl, `listAutoResolutions` reads the records and
|
|
|
364
170
|
blocked (CI red) ─► senior:fix-ci ─► retry conflict ─► senior:rebase ─► retry
|
|
365
171
|
```
|
|
366
172
|
|
|
367
|
-
PR `status` values
|
|
368
|
-
`
|
|
369
|
-
|
|
370
|
-
/ `queued` / `merging` (merge stage), `merged`, `abandoned`. A separate
|
|
371
|
-
`incident` signal (§5) can overlay any live status when the engine parks the token on
|
|
372
|
-
a technical fault.
|
|
173
|
+
PR `status` values: `converging` (round live), `waiting_review`, `escalated`, `converged`,
|
|
174
|
+
`waiting_deps` / `waiting_merge` / `queued` / `merging`, `merged`, `abandoned`. An
|
|
175
|
+
**incident** (§5) can overlay any live status when the engine parks the token on a fault.
|
|
373
176
|
|
|
374
177
|
---
|
|
375
178
|
|
|
376
179
|
## 5. Debug: find engine instances and relate them to PRs
|
|
377
180
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
**Find the instance for a PR:** take `processKey` from `listActivePrs` (`/status`), then
|
|
383
|
-
query engine truth. **Prefer the framework `urban_debug_*` engine-debug tools** (owned by
|
|
384
|
-
the nano-ide urban runtime — see the crosswalk in §10); the `curl __ENGINE__/…` engine
|
|
385
|
-
REST calls are the no-MCP fallback:
|
|
386
|
-
|
|
387
|
-
```text
|
|
388
|
-
PK = <processKey from listActivePrs>
|
|
389
|
-
|
|
390
|
-
# The instance itself (state, the BPMN process it is running, start time):
|
|
391
|
-
tool: urban_debug_search_process_instances { "filter": { "processInstanceKey": PK } }
|
|
392
|
-
|
|
393
|
-
# Where is it parked? — active jobs on the instance (a CREATED senior:pr-review job
|
|
394
|
-
# with a `worker` set means an agent has leased the round; none means it is queued).
|
|
395
|
-
# `urban_debug_search_jobs`/`_search_variables` are projected only where the framework
|
|
396
|
-
# offers them (sibling nano-ide epic); else drop to the curl door (see §10 crosswalk):
|
|
397
|
-
tool: urban_debug_search_jobs { "filter": { "processInstanceKey": PK, "state": "CREATED" } } # where projected
|
|
398
|
-
|
|
399
|
-
# Its variables (the aggregate/job payload the engine holds):
|
|
400
|
-
tool: urban_debug_search_variables { "filter": { "processInstanceKey": PK } } # where projected
|
|
401
|
-
|
|
402
|
-
# Is it dead-in-the-water on a technical fault? — active incidents:
|
|
403
|
-
tool: urban_debug_search_incidents { "filter": { "processInstanceKey": PK, "state": "ACTIVE" } }
|
|
404
|
-
|
|
405
|
-
# Which BPMN element/wait-state is it sitting on?
|
|
406
|
-
tool: urban_debug_search_element_instance_wait_states { "filter": { "processInstanceKey": PK } }
|
|
407
|
-
```
|
|
181
|
+
`processKey` (DB column `process_key`) joins the app's record to the engine instance. Take it
|
|
182
|
+
from `/status`, then query the engine. Over MCP, the framework `urban_debug_*` tools front
|
|
183
|
+
these same searches (§10).
|
|
408
184
|
|
|
409
185
|
```bash
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
#
|
|
419
|
-
curl -sS -X POST __ENGINE__/
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
# Is it dead-in-the-water on a technical fault? — active incidents:
|
|
424
|
-
curl -sS -X POST __ENGINE__/incidents/search \
|
|
425
|
-
-H 'content-type: application/json' \
|
|
426
|
-
-d "{ \"filter\": { \"processInstanceKey\": \"$PK\", \"state\": \"ACTIVE\" } }" | jq
|
|
427
|
-
|
|
428
|
-
# What are the element/flow-node instances (which BPMN element is it sitting on)?
|
|
429
|
-
curl -sS -X POST __ENGINE__/element-instances/search \
|
|
430
|
-
-H 'content-type: application/json' \
|
|
431
|
-
-d "{ \"filter\": { \"processInstanceKey\": \"$PK\" } }" | jq
|
|
186
|
+
PK=<processKey>
|
|
187
|
+
H='content-type: application/json'
|
|
188
|
+
# State, BPMN process, start time:
|
|
189
|
+
curl -sS -X POST __ENGINE__/process-instances/search -H "$H" -d "{\"filter\":{\"processInstanceKey\":\"$PK\"}}" | jq
|
|
190
|
+
# Where it is parked (a job with a `worker` set is leased by an agent; none = queued):
|
|
191
|
+
curl -sS -X POST __ENGINE__/jobs/search -H "$H" -d "{\"filter\":{\"processInstanceKey\":\"$PK\",\"state\":\"CREATED\"}}" | jq
|
|
192
|
+
# Technical fault?
|
|
193
|
+
curl -sS -X POST __ENGINE__/incidents/search -H "$H" -d "{\"filter\":{\"processInstanceKey\":\"$PK\",\"state\":\"ACTIVE\"}}" | jq
|
|
194
|
+
# Which BPMN element it sits on:
|
|
195
|
+
curl -sS -X POST __ENGINE__/element-instances/search -H "$H" -d "{\"filter\":{\"processInstanceKey\":\"$PK\"}}" | jq
|
|
196
|
+
# The variables the engine holds:
|
|
197
|
+
curl -sS -X POST __ENGINE__/variables/search -H "$H" -d "{\"filter\":{\"processInstanceKey\":\"$PK\"}}" | jq
|
|
432
198
|
```
|
|
433
199
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
> for that row. The three always-present engine reads are
|
|
438
|
-
> `urban_debug_search_process_instances`, `urban_debug_search_element_instance_wait_states`
|
|
439
|
-
> and `urban_debug_search_incidents`.
|
|
200
|
+
An active incident is mirrored onto the PR row, so a PR showing an incident in the UI is
|
|
201
|
+
parked on an engine fault. The engine base comes from `NANOBPMN_BASE_URL` or
|
|
202
|
+
`CAMUNDA_REST_ADDRESS`.
|
|
440
203
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
inspect it with `urban_debug_search_incidents` (or `incidents/search`) above. If the
|
|
444
|
-
engine is not at the default, the deployment's engine base is `__ENGINE__` (set via
|
|
445
|
-
`NANOBPMN_BASE_URL` or `CAMUNDA_REST_ADDRESS`).
|
|
446
|
-
|
|
447
|
-
**Relate an instance back to a PR:** if you have a `processKey` but not the PR, match
|
|
448
|
-
it against `listActivePrs`/`/status` (`.prs[] | select(.processKey == "<PK>")`). A
|
|
449
|
-
terminal PR is no longer in `/status`; its instance has already completed or been
|
|
450
|
-
cancelled.
|
|
204
|
+
Have a `processKey` but not the PR? Use `/status | jq '.prs[] | select(.processKey == "<PK>")'`.
|
|
205
|
+
A terminal PR has left `/status`.
|
|
451
206
|
|
|
452
207
|
---
|
|
453
208
|
|
|
454
209
|
## 6. Debug the models and the prompts
|
|
455
210
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
(merge/CI-fix/rebase), `plan-fanout.bpmn` (planning), `retro.bpmn` — are the
|
|
465
|
-
**source of truth in git** for routing, read them from the checkout when you want the
|
|
466
|
-
authored form (or have no MCP). They can lag a running deployment, so confirm against
|
|
467
|
-
the deployed XML when *why did this instance route here* matters. To understand *why* an
|
|
468
|
-
instance went where it did, read the gateway conditions (FEEL expressions on the
|
|
469
|
-
sequence flows) for the element it is parked on (§5).
|
|
470
|
-
- **Prompts (agent base instructions):** `resources/prompts/*.md` — `review-round.md`,
|
|
471
|
-
`plan.md`, `feature.md`, `fix-ci.md`, `rebase.md`, `trial-merge.md`, etc. They live
|
|
472
|
-
under `resources/` so they deploy by convention (ADR 0062 — the app declares no
|
|
473
|
-
`models`; every file under `resources/` is deployed, one `.md` per task under
|
|
474
|
-
`resources/prompts/`). An agent's base prompt is **not** a job variable and is **not**
|
|
475
|
-
a deploy-time `{{token}}` substitution (that templating is removed — there is no
|
|
476
|
-
back-compat): it is delivered as a **linked resource**, the blessed **and only**
|
|
477
|
-
prompt-modularity path:
|
|
211
|
+
- **Processes.** Engine truth is the deployed XML (`urban_debug_get_process_definition_xml`).
|
|
212
|
+
The source in git is `resources/processes/*.bpmn`: `convergence-loop.bpmn` (review),
|
|
213
|
+
`merge-loop.bpmn` (merge/CI-fix/rebase), `plan-fanout.bpmn`, `retro.bpmn`. The repo copy can
|
|
214
|
+
lag a running deployment. To see *why* an instance routed somewhere, read the FEEL
|
|
215
|
+
conditions on the outgoing sequence flows of the element it is parked on (§5).
|
|
216
|
+
- **Prompts.** `resources/prompts/*.md` (`review-round.md`, `plan.md`, `feature.md`,
|
|
217
|
+
`fix-ci.md`, `rebase.md`, `trial-merge.md`, …) deploy by convention. Each agent task links its
|
|
218
|
+
base prompt as a linked resource that the engine resolves at job activation:
|
|
478
219
|
|
|
479
220
|
```xml
|
|
480
221
|
<zeebe:linkedResource resourceId="prompts/review-round.md" bindingType="latest" resourceType="GenericScript" linkName="prompt"/>
|
|
481
222
|
```
|
|
482
223
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
answer? }` and must return a flat result `{ status, summary, question? }` with
|
|
495
|
-
`status ∈ { converged, addressed, waiting, needs_input, blocked }`. A round that
|
|
496
|
-
pushes anything (including a rebase/force-push) is `addressed`; a round with an
|
|
497
|
-
unknown/empty result is treated as a safe `addressed` and re-enters the review wait
|
|
498
|
-
rather than escalating.
|
|
499
|
-
|
|
500
|
-
To validate a model/prompt change locally: `npm run layout:check` (BPMN diagram
|
|
501
|
-
freshness), `npm run check:prompts` (every prompt link resolves), `npm run check`
|
|
502
|
-
(manifest), `npm run typecheck`, `npm run lint`, `npm test`.
|
|
224
|
+
It is combined with the task's runtime `appendPrompt` FEEL. Because of `latest`, redeploying a
|
|
225
|
+
prompt changes it mid-epic with no process redeploy. A systematically misbehaving agent
|
|
226
|
+
usually means a prompt problem. There is no deploy-time `{{token}}` templating: pass per-run
|
|
227
|
+
values as job variables / `appendPrompt`.
|
|
228
|
+
- **Job contract.** `senior:pr-review` receives `{ prUrl, repo, prNumber, round, answer? }` and
|
|
229
|
+
returns `{ status, summary, question? }`, with `status ∈ { converged, addressed, waiting,
|
|
230
|
+
needs_input, blocked }`. Any push (including a rebase) is `addressed`. An empty or unknown
|
|
231
|
+
result is treated as `addressed` and re-enters the review wait.
|
|
232
|
+
|
|
233
|
+
Validate a model or prompt change with `npm run layout:check`, `check:prompts`, `check`,
|
|
234
|
+
`typecheck`, `lint` and `test`.
|
|
503
235
|
|
|
504
236
|
---
|
|
505
237
|
|
|
506
238
|
## 7. Unstick a stuck process
|
|
507
239
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
reviewer periodically. If the reviewer bot is not provisioned on the repo, no
|
|
522
|
-
review will ever land — that is a repo-config problem, not an app bug.
|
|
523
|
-
5. **Cancel + resubmit** as a last resort. Cancel through the **app-owned** door —
|
|
524
|
-
**prefer the `cancelInstance` tool** (projected by sibling #667; the same
|
|
525
|
-
record-consistent path as the UI's per-row Cancel, `POST /app/actions/cancel
|
|
526
|
-
{ "processInstanceKey": "<PK>" }`), which cancels
|
|
527
|
-
the engine instance **and** marks the PR `abandoned` so it drops out of
|
|
528
|
-
`listActivePrs`. Then re-submit the PR (§1) to start a fresh loop. **Do not** cancel a
|
|
529
|
-
raw engine instance out from under the app with the engine-level
|
|
530
|
-
`urban_debug_cancel_instance` tool (or a direct engine REST cancel) — that cancels the
|
|
531
|
-
token but leaves the PR row inconsistent. Always go through `cancelInstance` /
|
|
532
|
-
`POST /app/actions/cancel` so the app's record state stays consistent. Note the door
|
|
533
|
-
sits at `/app/actions/cancel`, **outside** the `__BASE__` control-API base (ADR 0059) —
|
|
534
|
-
until #667 projects `cancelInstance`, that curl door is the only cancel path. No-MCP
|
|
535
|
-
fallback:
|
|
240
|
+
1. **Is it actually stuck?** `converging` with an `activeWorker` means an agent is working.
|
|
241
|
+
No worker for a long time means the job is queued: check that a fleet `c8ctl nano work`
|
|
242
|
+
daemon is running and subscribed to the `senior:*` types.
|
|
243
|
+
2. **Incident?** (§5) It parks the token until the fault is resolved, or until you cancel and
|
|
244
|
+
resubmit.
|
|
245
|
+
3. **Open escalation?** (§3) Answer it.
|
|
246
|
+
4. **Review never arrives?** It escalates after `NANO_PR_REVIEW_WAIT_TIMEOUT` (default `PT30M`),
|
|
247
|
+
and the poller re-nudges the reviewer. If no reviewer bot is provisioned on the repo, that
|
|
248
|
+
is a repo-config problem.
|
|
249
|
+
5. **Last resort: cancel and resubmit** through the app-owned door, `cancelInstance`. It cancels
|
|
250
|
+
the engine instance **and** marks the record `abandoned`. Then resubmit (§1). Never use the
|
|
251
|
+
engine-level `urban_debug_cancel_instance` or a raw engine cancel: it leaves the PR row
|
|
252
|
+
inconsistent.
|
|
536
253
|
|
|
537
254
|
```bash
|
|
538
|
-
curl -sS -X POST __BASE__
|
|
255
|
+
curl -sS -X POST __BASE__/actions/cancel -H 'content-type: application/json' \
|
|
539
256
|
-d '{ "processInstanceKey": "<PK>" }'
|
|
540
257
|
```
|
|
541
258
|
|
|
@@ -543,252 +260,109 @@ Work through this order:
|
|
|
543
260
|
|
|
544
261
|
## 8. Raise an issue or a PR against nano-workforce
|
|
545
262
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
- **Work in a git worktree** off `origin/main`, on a `feat/*` or `fix/*` branch.
|
|
555
|
-
- **Author the BPMN semantics, generate the diagram.** Never hand-edit the
|
|
556
|
-
`bpmndi:BPMNDiagram`; run `npm run layout <file.bpmn>` and commit the result. CI
|
|
557
|
-
fails on stale DI.
|
|
558
|
-
- **Match the CI gates locally before pushing:** `npm run lint`, `npm run typecheck`,
|
|
559
|
-
`npm run check`, `npm run layout:check`, `npm run check:prompts`, `npm test`.
|
|
560
|
-
- **Copilot code review is provisioned** — drive the PR to convergence against it.
|
|
561
|
-
- Read `AGENTS.md` (engineering principles + gates) and `SPEC.md` (behavioural source
|
|
562
|
-
of truth) before proposing a change to a process.
|
|
563
|
-
|
|
564
|
-
When describing the bug, include the concrete evidence you gathered here: the
|
|
565
|
-
`prKey`, the engine `processKey`, the parked element / incident message (§5), and the
|
|
566
|
-
BPMN/prompt file you believe is responsible (§6).
|
|
263
|
+
For a genuine orchestration bug or gap (not a transient repo/agent problem), file it against
|
|
264
|
+
`nanobpm/nano-workforce`. Include the evidence: `prKey`, `processKey`, the parked element or
|
|
265
|
+
incident message (§5), and the BPMN or prompt file you suspect (§6).
|
|
266
|
+
|
|
267
|
+
If you change code: open or claim an issue first, work in a worktree off `origin/main`, and
|
|
268
|
+
sign off every commit (`git commit -s`, DCO). Never hand-edit `bpmndi` (run
|
|
269
|
+
`npm run layout <file.bpmn>`). Run the CI gates locally (§6 list). Drive the PR to convergence
|
|
270
|
+
against Copilot review. Read `AGENTS.md` and `SPEC.md` before changing a process.
|
|
567
271
|
|
|
568
272
|
---
|
|
569
273
|
|
|
570
274
|
## 9. Author and run a delivery graph (ADR 0005)
|
|
571
275
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
PR #202 → a human does a manual OTP publish → PR #303 consumes the just-published version*. A
|
|
576
|
-
**delivery graph** ([ADR 0005](https://github.com/nanobpm/nano-workforce/blob/main/docs/adr/0005-agent-authored-delivery-graphs.md))
|
|
577
|
-
lets you compose exactly that as **data** and hand it to a generic runner.
|
|
578
|
-
|
|
579
|
-
> **Discover the vocabulary from the surface.** Everything this section describes — the four
|
|
580
|
-
> node kinds and their body contracts, every `wait` probe kind and **what it observes**, the
|
|
581
|
-
> real-vs-stub connector targets, the `onTimeout` options, the poll-budget trap, and the
|
|
582
|
-
> fact-threading rules — is also available as **structured JSON** from the read tool
|
|
583
|
-
> **`getDeliveryGraphVocabulary`** (`GET __BASE__/delivery-graph/vocabulary`). It is derived
|
|
584
|
-
> from the implementing code (a drift test fails the build if the two disagree), so prose and
|
|
585
|
-
> data can never drift. Fetch it to author against the live vocabulary; this section is the
|
|
586
|
-
> narrative companion.
|
|
587
|
-
|
|
588
|
-
You author the graph as **JSON — never BPMN or code** (Decision 1: the agent must never
|
|
589
|
-
author the executable artifact; the closed node vocabulary is the trust boundary). Your
|
|
590
|
-
surface ends at **propose → compile → stage**: a single `compile` door validates the JSON,
|
|
591
|
-
renders a preview, and — when valid — **stages the compiled graph as a proposal** for a
|
|
592
|
-
human. **Dispatch is an operator action in the cockpit, not an agent endpoint** (issue #460):
|
|
593
|
-
there is deliberately no `start` door on the agent surface, so there is nothing an agent can
|
|
594
|
-
call — or replay — to launch a run. A human previews the staged proposal and dispatches it.
|
|
276
|
+
A delivery graph composes heterogeneous, cross-repo, partly-human work as **data**, for example:
|
|
277
|
+
*merge PR #101 → un-draft and merge PR #202 → a human does a manual OTP publish → PR #303
|
|
278
|
+
consumes that version.* You author **JSON only, never BPMN or code**.
|
|
595
279
|
|
|
596
|
-
|
|
280
|
+
Your surface is **propose → compile → stage**. **Dispatch is an operator action** in the
|
|
281
|
+
cockpit. There is no agent start door, so nothing an agent can call or replay launches a run.
|
|
597
282
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
- **`edges[]`** — each edge is `{ from, to }` meaning *"`to` proceeds once fact `from` is
|
|
604
|
-
observable"* (Decision 3 — edges are **discovered facts**, not declared values). `from`
|
|
605
|
-
is either a bare **`<nodeId>`** (the degenerate "wait for the upstream node's
|
|
606
|
-
completion" fact) or a **qualified `<nodeId>.<fact>`** referencing one of that node's
|
|
607
|
-
declared `emits`. The whole edge set must be a DAG. Omit / `[]` for independent roots.
|
|
608
|
-
|
|
609
|
-
**The four node kinds** (each delegates to an existing engine-native body — the graph
|
|
610
|
-
layer schedules, it does not re-implement execution):
|
|
611
|
-
|
|
612
|
-
| kind | config | what it does | may `emits`? |
|
|
613
|
-
|---|---|---|---|
|
|
614
|
-
| `agent` | `agent: { jobType, repository?, baseBranch?, prompt?, converge?, merge?, timeout? }` | a worker runs an agent job type (the fan-out body) in a fresh checkout of its `repository` (see *Provision every agent node* below). **Side-effecting.** First-class **`converge?` / `merge?`** cell policy (§9.4) — a **declared, compiler-validated** completion-policy flag that *declares* review-convergence / landing intent (`merge` requires `converge`); a raw `senior:converge`/`senior:merge` job is rejected. This slice adds + validates the flags; the delivery-graph execution wiring that consumes them lands in a follow-up slice. **`timeout?`** is an OPTIONAL per-node ISO-8601 SLA (#505) that overrides the run-level `nodeTimeout` (and the `PT1H` default) for this node's bounded-timeout → escalate boundary, so a legitimately-long node outlasts a quick gate without a spurious escalation. | yes |
|
|
615
|
-
| `wait` | `wait: <ReadinessProbe>` | a durable, bounded readiness probe — kind ∈ `http`, `command`, `npm`, `github-check`, `capability`, `pr`, `epic`. Read-only. | yes (binds observed facts) |
|
|
616
|
-
| `human` | `human?: { formKey?, prompt? }` | a scheduled user task + form (the Tasks inbox, §3). Blocks dependents, SLA-bounded, answerable by a human **or** an agent. | yes |
|
|
617
|
-
| `connector` | `connector: { target, dedupeKey?, payload?, timeout? }` | an automated, side-effecting outbound action. Carries a `dedupeKey` (at-least-once safe). Three **real targets** ship today — **`converge`**, **`converge-merge`** (unit → base branch) and **`merge-main`** (graph → `main`, the two-level top-level land) (§9.4); other targets are a forward-declared stub. **`timeout?`** — a per-node ISO-8601 SLA (#505), same semantics as the `agent` row. | yes |
|
|
618
|
-
|
|
619
|
-
> **Provision every `agent` node with a repository — compile will NOT catch it.** Each `agent` cell
|
|
620
|
-
> runs in an isolated checkout: the harness clones the node's **`repository`** (`owner/repo`, no
|
|
621
|
-
> `.git`/URL) at **`baseBranch`** and cuts `feat/<node.id>` off it (#739/#776). `baseBranch` resolves
|
|
622
|
-
> **per field** — the node's own, else the **run-level dispatch `baseBranch`**, else the repo default
|
|
623
|
-
> (an id yielding an ill-formed ref like `a..b`/`a.lock` makes the harness skip `branch.create`, so
|
|
624
|
-
> the agent cuts its own branch). A node with **no `repository`** is staged by compile **without a
|
|
625
|
-
> warning**, then — absent a **run-level fallback** supplied *at dispatch* — Dispatch fails with
|
|
626
|
-
> `N agent node(s) resolve to no repository (…)`. So:
|
|
627
|
-
>
|
|
628
|
-
> - **Declare `repository` on EVERY `agent` node** (and `baseBranch` when it is neither the run-level
|
|
629
|
-
> base nor the default): a single-repo graph repeats one value; a cross-repo graph names each node's
|
|
630
|
-
> own repo. Every repo a node's prompt changes must be that node's `repository`.
|
|
631
|
-
> - **`repoless` is a RUN-WIDE mode, not a per-node escape hatch** — it strips isolation from *every*
|
|
632
|
-
> node. Use it only when **every** `agent` node is checkout-less (pure research/comment work); if
|
|
633
|
-
> even one needs a checkout, provision repositories instead.
|
|
634
|
-
> - **Self-check before handing over the digest:** every `"kind": "agent"` node has a `repository`;
|
|
635
|
-
> if you leave some unset, tell the operator which, and that they must pick the run-level
|
|
636
|
-
> **repository + base-branch fallback** in the Dispatch form (the default mode rejects the graph).
|
|
637
|
-
>
|
|
638
|
-
> A **present** `repository`/`baseBranch` is validated at compile (`invalid-node-repository` /
|
|
639
|
-
> `invalid-node-base-branch`); an **absent `repository`** defers to dispatch (and can fail there); an
|
|
640
|
-
> **absent `baseBranch` is always valid** — it inherits the run-level base, else the repo default.
|
|
641
|
-
|
|
642
|
-
A **`wait` node's `wait` is a `ReadinessProbe` verbatim** (the same shape feature-run
|
|
643
|
-
intake uses): `{ kind, target, onTimeout?, match?, poll? }`, where `poll` is
|
|
644
|
-
`{ everyMs?, timeoutMs?, backoff? }` — `everyMs` is the re-probe cadence, `timeoutMs` the
|
|
645
|
-
total bounded budget, and `backoff ∈ fixed|exponential`.
|
|
646
|
-
|
|
647
|
-
> **The bound is invisible unless you set it.** When `poll` (or `poll.timeoutMs`) is
|
|
648
|
-
> **omitted**, the gate inherits the built-in default budget of **`PT30M` (30 minutes),
|
|
649
|
-
> re-probing every 15s** (`DEFAULT_READINESS_TIMEOUT` / `DEFAULT_EVERY_MS`,
|
|
650
|
-
> `app/readiness.ts`). That default is right for *"is the package published yet"* and badly
|
|
651
|
-
> wrong for `wait[pr, merged]` / `wait[epic]`, which routinely wait **hours or days** — an
|
|
652
|
-
> unpopulated `poll` on such a gate escalates after 30 minutes for no visible reason. Neither
|
|
653
|
-
> `compile` nor `preview` surfaces the effective bound, so **set `poll.timeoutMs` explicitly**
|
|
654
|
-
> on any gate that waits on a merge or an epic (see §9.4 / §9.5).
|
|
655
|
-
|
|
656
|
-
The **`pr` kind** watches an
|
|
657
|
-
in-flight PR — `target: "owner/repo#123"`, `match.prState ∈ ready|merged|mergeable|checks-green`
|
|
658
|
-
(default `merged`) — and on a merged match binds `mergedSha` as an output fact. The **`epic`
|
|
659
|
-
kind** (issue #568) gates on an **nwf plan-fanout epic reaching "fully merged"** — `target:
|
|
660
|
-
"owner/repo#NN"` is the epic's durable **`planKey`** (the epic issue, *not* the engine
|
|
661
|
-
`processInstanceKey`, so a resubmit/replay still resolves), `match.epicState ∈ merged|done`
|
|
662
|
-
(default `merged`, both mean "every opened slice landed"). It observes the app's own aggregate
|
|
663
|
-
(the lineage read-model), so a **failed/abandoned/mixed** epic never reports merged and the
|
|
664
|
-
bounded wait routes to **`onTimeout`** rather than hanging; on a fully-merged match it binds
|
|
665
|
-
`prCount` (how many slice PRs landed) as an output fact. Both `pr` and `epic` targets may also
|
|
666
|
-
be a **`<nodeId>.<fact>` late-binding reference** the compiler resolves at dispatch (§9.4),
|
|
667
|
-
rather than a literal handle.
|
|
668
|
-
|
|
669
|
-
A **typed fact** (`emits[]` entry) is `{ name, type, description? }` where
|
|
670
|
-
`type ∈ string|number|boolean|artifact|version|url|pr` (`artifact` = a `pkg@version` handle,
|
|
671
|
-
`version` = a bare version, `pr` = a PR reference `owner/repo#N` an `agent` node emits for the
|
|
672
|
-
PR it opened, late-bound by a downstream `wait[pr]`/`connector[converge*]` target — issue #548).
|
|
673
|
-
`name` matches `^[A-Za-z_][A-Za-z0-9_]*$` and is referenced
|
|
674
|
-
downstream as `<nodeId>.<name>`. A "click done" human node or a pass-through node declares
|
|
675
|
-
no facts.
|
|
676
|
-
|
|
677
|
-
**Guarded routing + the agent classifier-emit contract.** An edge may carry a **guard** —
|
|
678
|
-
`when: "<nodeId>.<fact>"` + `equals: <scalar>` — or be the split's single `default: true`
|
|
679
|
-
else-branch (S7). A node whose out-edges are guarded is a **data-based exclusive split**: at
|
|
680
|
-
runtime exactly one branch fires, chosen by the producer's emitted fact. For an **`agent`**
|
|
681
|
-
node the fact is late-bound from the servicing job's completion: the delivery output-mapping
|
|
682
|
-
publishes the engine variable named **exactly after the fact** (e.g. a fact `result` reads the
|
|
683
|
-
completion variable `result`). A real `senior:*` fleet agent completes with its **Output
|
|
684
|
-
contract** envelope (`{ status, summary, pr, question, delta }`), so — to make a guarded split
|
|
685
|
-
fire — an agent node that declares `emits` has a **classifier emit contract** automatically
|
|
686
|
-
appended to its prompt at dispatch: the agent MUST return each declared fact as an **extra
|
|
687
|
-
top-level field of the same result JSON** (the `AGENT_RESULT_FILE` it already writes
|
|
688
|
-
`status`/`summary`/`pr` to). Author side, this means: declare `emits: [{ name, type }]` on the
|
|
689
|
-
agent node and guard the downstream edge on `<node>.<name>`; a contract-following agent returns
|
|
690
|
-
`{ …, <name>: <value> }` and the split routes on it. If the agent cannot decide the fact it
|
|
691
|
-
**omits** it, and the split takes its `default` (else) branch — the deadlock-safe fallback. A
|
|
692
|
-
node that declares no `emits` gets no contract text and behaves exactly as before.
|
|
693
|
-
|
|
694
|
-
**Producer completion contract (auto-injected — do NOT hand-encode it).** Every `agent` node
|
|
695
|
-
*also* has a **producer completion contract** appended to its prompt at dispatch, alongside the
|
|
696
|
-
idempotency preflight and — only for a node that declares `emits` — the classifier emit contract.
|
|
697
|
-
It hands the agent the terminal-status vocabulary
|
|
698
|
-
the `#731` producer gate enforces: the injected text tells the agent it MUST end its result with
|
|
699
|
-
one of `AGENT_TERMINAL_SUCCESS_STATUSES` (`done` / `opened` / `skipped`) — never omit `status` or
|
|
700
|
-
invent one. The gate routes a completion onward only when its self-reported `status` is one of that
|
|
701
|
-
allowlist **and** every required emit is non-null; any *explicitly non-terminal* status parks the
|
|
702
|
-
run on a human `__contract` escalation (fail-closed). (As a backward-compat concession the gate
|
|
703
|
-
*also* routes onward an **absent/null** status — for legacy workers / stubs that report none — but
|
|
704
|
-
the injected contract never invites a real agent to lean on that: always return an allowlisted
|
|
705
|
-
status.) The wording is **derived from that single allowlist** (changing the
|
|
706
|
-
list changes the prompt — no second copy), so **authors must not hand-encode status vocabulary
|
|
707
|
-
in a node's prompt.** Unlike the emit contract, a no-emit node still receives the status block
|
|
708
|
-
(the gate applies to it too).
|
|
283
|
+
> **The structured vocabulary is authoritative:** `getDeliveryGraphVocabulary`
|
|
284
|
+
> (`GET __BASE__/delivery-graph/vocabulary`) returns the node kinds and fields, every `wait`
|
|
285
|
+
> probe kind and what it observes, connector targets, `onTimeout` options, poll defaults and
|
|
286
|
+
> fact-threading rules. It is derived from the code, so read it before authoring. This section
|
|
287
|
+
> covers the workflow and the traps.
|
|
709
288
|
|
|
710
|
-
### 9.
|
|
289
|
+
### 9.1 The `DeliveryGraph` shape
|
|
711
290
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
└─ 200 { status:"ready", message, digest, preview, reviewUrl }
|
|
718
|
-
└─ ask the operator to preview + dispatch it in the cockpit (there is no start door)
|
|
719
|
-
```
|
|
291
|
+
`{ name?, nodes[], edges[] }`, a DAG. Each node has a unique `id`, a `kind`, that kind's
|
|
292
|
+
config, and optional `emits[]` (typed facts `{ name, type, description? }`, with
|
|
293
|
+
`type ∈ string|number|boolean|artifact|version|url|pr`). For edges, each edge is `{ from, to }`:
|
|
294
|
+
`to` proceeds once `from` is observable. `from` is a bare `<nodeId>` (completion) or a
|
|
295
|
+
qualified `<nodeId>.<fact>` naming one of that node's `emits`.
|
|
720
296
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
297
|
+
| kind | config | does |
|
|
298
|
+
|---|---|---|
|
|
299
|
+
| `agent` | `agent: { jobType, repository?, baseBranch?, prompt?, converge?, merge?, timeout? }` | runs an agent job in a fresh checkout of `repository`. Side-effecting. `converge`/`merge` (merge requires converge) declare land intent; a raw `senior:converge`/`senior:merge` job is rejected. `timeout` is an ISO-8601 per-node SLA overriding the run's `nodeTimeout` (default `PT1H`). |
|
|
300
|
+
| `wait` | `wait: <ReadinessProbe>` | a durable, bounded, read-only readiness probe `{ kind, target, match?, poll?, onTimeout? }`. Kinds: `http`, `command`, `npm`, `github-check`, `capability`, `pr`, `epic`. |
|
|
301
|
+
| `human` | `human?: { formKey?, prompt? }` | a user task on the Tasks inbox (§3), answerable by a human or an agent. |
|
|
302
|
+
| `connector` | `connector: { target, dedupeKey?, payload?, timeout? }` | an at-least-once, deduped outbound action. Real targets: `converge`, `converge-merge`, `merge-main` (§9.4). |
|
|
303
|
+
|
|
304
|
+
> **Provision every `agent` node with a repository — compile will NOT catch it.** Each agent
|
|
305
|
+
> clones its node's `repository` (`owner/repo`) at `baseBranch` (node value, else the run-level
|
|
306
|
+
> dispatch base, else the repo default) and cuts `feat/<node.id>`. A node with no `repository`
|
|
307
|
+
> compiles cleanly, then Dispatch fails with `N agent node(s) resolve to no repository (…)`
|
|
308
|
+
> unless the operator supplies a run-level fallback. So set `repository` on **every** agent node.
|
|
309
|
+
> If you leave one unset, tell the operator. `repoless` is a run-wide mode for graphs where
|
|
310
|
+
> **no** node needs a checkout.
|
|
311
|
+
|
|
312
|
+
> **Set `poll.timeoutMs` on every merge or epic gate.** If `poll` is omitted, a `wait` inherits
|
|
313
|
+
> **30 minutes** (re-probing every 15 s). That suits "is the package published?", but a
|
|
314
|
+
> `wait[pr, merged]` or `wait[epic]` then escalates mid-review. Use e.g.
|
|
315
|
+
> `"poll": { "everyMs": 300000, "timeoutMs": 259200000 }` (5 min / 3 days).
|
|
316
|
+
|
|
317
|
+
**Guarded routing.** An edge may carry `when: "<nodeId>.<fact>"` + `equals: <scalar>`, or be the
|
|
318
|
+
split's single `default: true` branch. Exactly one branch fires. For an `agent` node, the fact
|
|
319
|
+
is read from a same-named top-level field in the agent's result JSON. The system adds that
|
|
320
|
+
instruction to the agent's prompt automatically when the node declares `emits`. If the agent
|
|
321
|
+
omits the fact, the `default` branch is taken.
|
|
322
|
+
|
|
323
|
+
**Result statuses are added automatically. Don't write them into your prompt.** Every agent
|
|
324
|
+
node's prompt gets the allowed terminal statuses (`done` / `opened` / `skipped`). A completion
|
|
325
|
+
moves on only with one of those statuses and every required emit present. Anything else parks
|
|
326
|
+
the run on a human `__contract` escalation — **except** an absent/null `status`, which a legacy
|
|
327
|
+
worker that populates its required emits may still advance with (the status contract binds only
|
|
328
|
+
when a status is actually reported).
|
|
329
|
+
|
|
330
|
+
### 9.2 The agent loop: draft → compile → stage → ask an operator to dispatch
|
|
727
331
|
|
|
728
332
|
```bash
|
|
333
|
+
# tool: compileDeliveryGraph: validate + preview + STAGE (never deploys or runs)
|
|
729
334
|
curl -sS -X POST __BASE__/actions/compile-delivery-graph \
|
|
730
|
-
-H 'content-type: application/json'
|
|
731
|
-
-d @graph.json | jq
|
|
335
|
+
-H 'content-type: application/json' -d @graph.json | jq
|
|
732
336
|
```
|
|
733
337
|
|
|
734
|
-
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
`
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
the cockpit's **Active Delivery Graphs** grid (e.g. *"parked on human node: manual OTP publish"*).
|
|
754
|
-
A `human` node parks on the **Tasks** inbox and is answered exactly as an escalation is (§3) — its
|
|
755
|
-
completion emits any declared facts, which downstream edges bind.
|
|
756
|
-
|
|
757
|
-
The dispatch body accepts the optional **`autoResolveEscalations`** boolean (issue #889, default
|
|
758
|
-
`false`): when `true`, the run opts in to the escalation steward (§3) — its `human` nodes, service-node
|
|
759
|
-
escalation twins, and any `converge` connector's enrolled PR become eligible for auto-resolution.
|
|
760
|
-
|
|
761
|
-
> **Completing a `wait` escalation proceeds *as not-ready* — it does NOT re-arm the gate.**
|
|
762
|
-
> A `wait` node with `onTimeout: "escalate"` (the default) that elapses its bound parks a
|
|
763
|
-
> human-completable escalation task on the Tasks inbox. **Completing that task does not retry
|
|
764
|
-
> the probe or wait for readiness** — it releases the token **as not-ready** and the graph
|
|
765
|
-
> proceeds **past the gate**, so the downstream side-effecting node then runs *against the
|
|
766
|
-
> unmet dependency* (`waitBodyLines`, `app/deliveryGraphCompiler.ts`). An operator clearing
|
|
767
|
-
> what looks like a stuck task therefore *launches the very work the gate was holding back*.
|
|
768
|
-
> If the dependency genuinely is not ready, do **not** complete the escalation to "unstick"
|
|
769
|
-
> it — extend the gate's `poll.timeoutMs` and re-dispatch, or abandon the run. (Same for
|
|
770
|
-
> `onTimeout: "continue"`, which proceeds past the gate as not-ready with **no** human stop
|
|
771
|
-
> at all.)
|
|
772
|
-
>
|
|
773
|
-
> **Exception (#872):** a `wait` that declares `emits` **validates** its resume value — an
|
|
774
|
-
> omitted/invalid value **re-parks** the task rather than releasing not-ready.
|
|
775
|
-
|
|
776
|
-
> **Why the split?** Making the compile door the end of the agent surface closes a
|
|
777
|
-
> self-approval hole: the old flow handed the same caller an approval token to re-submit with,
|
|
778
|
-
> so any holder of the API credential approved its own graph. With no dispatch affordance on the
|
|
779
|
-
> agent surface (capability by absence), the human in the cockpit is the only actor who can
|
|
780
|
-
> launch side effects.
|
|
338
|
+
- **`200 { status:"ready", message, digest, preview, reviewUrl }`**: staged.
|
|
339
|
+
`preview` is `{ diagram (mermaid), sideEffects[], humanNodes[] }`. `digest` names the proposal,
|
|
340
|
+
and `reviewUrl` is a cockpit link (navigation only, not a dispatch handle). Give the operator
|
|
341
|
+
the `digest`/`reviewUrl` and ask them to dispatch.
|
|
342
|
+
- **`400 { ok:false, errors:[{ path, message }] }`**: path-qualified errors (unknown kind,
|
|
343
|
+
dangling edge, cycle, unbound fact). Fix and recompile; nothing is staged.
|
|
344
|
+
|
|
345
|
+
Recompiling is safe: the same graph is idempotent, and a changed graph with the same `name`
|
|
346
|
+
replaces the staged proposal. The operator previews it on the **Delivery Graphs** page and
|
|
347
|
+
clicks **Dispatch**. That click is the approval, bound to the previewed digest. Dispatch also
|
|
348
|
+
accepts `autoResolveEscalations` (§3). The running graph shows in **Active Delivery Graphs**,
|
|
349
|
+
and `human` nodes are answered like escalations (§3), emitting their declared facts.
|
|
350
|
+
|
|
351
|
+
> **Completing a `wait` escalation proceeds *as not-ready*. It does NOT re-arm the gate.** With
|
|
352
|
+
> `onTimeout: "escalate"` (default), an elapsed bound parks a task. Completing it releases the
|
|
353
|
+
> token past the gate, so the downstream side-effecting node runs against the unmet dependency.
|
|
354
|
+
> If the dependency isn't ready, don't complete it: raise `poll.timeoutMs` and re-dispatch, or
|
|
355
|
+
> abandon. (`onTimeout: "continue"` proceeds the same way with no human stop.) **Exception:** a
|
|
356
|
+
> `wait` that declares `emits` validates the resume value and re-parks on a missing or invalid one.
|
|
781
357
|
|
|
782
358
|
### 9.3 Worked example — the cross-repo human-in-the-loop release
|
|
783
359
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
`human` node **emits** a typed `version` fact, and the downstream `from:
|
|
787
|
-
"manual-publish.publishedVersion"` edge binds it into the PR-#303 path:
|
|
360
|
+
The `human` node emits a `version` fact that the edge `manual-publish.publishedVersion` binds
|
|
361
|
+
into the next agent:
|
|
788
362
|
|
|
789
363
|
```json
|
|
790
364
|
{
|
|
791
|
-
"name": "cross-repo release: merge #101 → un-draft+merge #202 → manual OTP publish → consume in
|
|
365
|
+
"name": "cross-repo release: merge #101 → un-draft+merge #202 → manual OTP publish → consume in repo-3",
|
|
792
366
|
"nodes": [
|
|
793
367
|
{ "id": "merge-a", "kind": "wait",
|
|
794
368
|
"wait": { "kind": "pr", "target": "acme/repo-1#101", "match": { "prState": "merged" },
|
|
@@ -796,244 +370,97 @@ publish and records the version → open+merge PR #303 (repo 3) consuming that v
|
|
|
796
370
|
{ "id": "undraft-merge-b", "kind": "agent",
|
|
797
371
|
"agent": { "jobType": "senior:feature", "repository": "acme/repo-2", "converge": true, "merge": true, "prompt": "Take draft PR acme/repo-2#202 out of draft; converge it to green and land it." } },
|
|
798
372
|
{ "id": "manual-publish", "kind": "human",
|
|
799
|
-
"human": { "prompt": "Run the manual OTP-authenticated `npm publish` for @acme/widget
|
|
800
|
-
"emits": [ { "name": "publishedVersion", "type": "version"
|
|
373
|
+
"human": { "prompt": "Run the manual OTP-authenticated `npm publish` for @acme/widget. Record the exact published version." },
|
|
374
|
+
"emits": [ { "name": "publishedVersion", "type": "version" } ] },
|
|
801
375
|
{ "id": "open-pr-c", "kind": "agent",
|
|
802
|
-
"agent": { "jobType": "senior:feature", "repository": "acme/repo-3", "prompt": "Bump @acme/widget to the published version in acme/repo-3 and open PR
|
|
376
|
+
"agent": { "jobType": "senior:feature", "repository": "acme/repo-3", "prompt": "Bump @acme/widget to the published version in acme/repo-3 and open a PR." },
|
|
377
|
+
"emits": [ { "name": "pr", "type": "pr" } ] },
|
|
803
378
|
{ "id": "merge-c", "kind": "wait",
|
|
804
|
-
"wait": { "kind": "pr", "target": "
|
|
379
|
+
"wait": { "kind": "pr", "target": "open-pr-c.pr", "match": { "prState": "merged" },
|
|
805
380
|
"poll": { "everyMs": 300000, "timeoutMs": 259200000 }, "onTimeout": "escalate" } }
|
|
806
381
|
],
|
|
807
382
|
"edges": [
|
|
808
383
|
{ "from": "merge-a", "to": "undraft-merge-b" },
|
|
809
384
|
{ "from": "undraft-merge-b", "to": "manual-publish" },
|
|
810
385
|
{ "from": "manual-publish.publishedVersion", "to": "open-pr-c" },
|
|
811
|
-
{ "from": "open-pr-c", "to": "merge-c" }
|
|
386
|
+
{ "from": "open-pr-c.pr", "to": "merge-c" }
|
|
812
387
|
]
|
|
813
388
|
}
|
|
814
389
|
```
|
|
815
390
|
|
|
816
|
-
`
|
|
817
|
-
|
|
818
|
-
```
|
|
819
|
-
diagram (mermaid flowchart):
|
|
820
|
-
n4["agent: undraft-merge-b"] --> n0["human: manual-publish"]
|
|
821
|
-
n0 -- "publishedVersion" --> n3["agent: open-pr-c"]
|
|
822
|
-
n1["wait: merge-a"] --> n4
|
|
823
|
-
n3 --> n2["wait: merge-c"]
|
|
824
|
-
|
|
825
|
-
humanNodes: [ { nodeId: "manual-publish", emits: [ { name: "publishedVersion", type: "version" } ], … } ]
|
|
826
|
-
sideEffects: [ { nodeId: "open-pr-c", kind: "agent", … }, { nodeId: "undraft-merge-b", kind: "agent", … } ]
|
|
827
|
-
```
|
|
828
|
-
|
|
829
|
-
Two side-effecting `agent` nodes ⇒ the compile door **stages** the proposal and hands you a
|
|
830
|
-
`digest` + `reviewUrl`; ask an operator to preview and **Dispatch** it in the cockpit. Once
|
|
831
|
-
they do, the graph runs to `manual-publish`, parks it on the Tasks inbox (`now do X`), and —
|
|
832
|
-
once a human (or agent) completes it with the `publishedVersion` — binds that fact into
|
|
833
|
-
`open-pr-c` and carries on to `merge-c`.
|
|
391
|
+
### 9.4 Connector targets — drive a PR to convergence + merge (`converge` / `converge-merge` / `merge-main`)
|
|
834
392
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
`pkg@version` first carries the change*) fed by the same `manual-publish.publishedVersion`
|
|
838
|
-
fact — the fact-edge syntax is identical.
|
|
393
|
+
A connector with one of these targets enrolls a PR in the same convergence loop as §1
|
|
394
|
+
(idempotent; never double-enrolls). Payload: `{ pr, autoMerge?, dependsOn? }`.
|
|
839
395
|
|
|
840
|
-
|
|
396
|
+
- **`converge`**: review only, stop at `converged`.
|
|
397
|
+
- **`converge-merge`**: converge, then merge onto the PR's own base (inside an epic, that is the
|
|
398
|
+
integration branch).
|
|
399
|
+
- **`merge-main`**: same mechanics, for landing the graph's integration PR on `main`.
|
|
841
400
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
only job was "go run convergence yourself".
|
|
847
|
-
|
|
848
|
-
- **`converge-merge`** — the **unit-level** land: drive review convergence **and then the merge
|
|
849
|
-
loop**, landing the PR onto **its own base branch** (for a unit inside an epic that base is the
|
|
850
|
-
epic integration branch, never `main` directly — ADR 0003 base-branch admission). Equivalent to a
|
|
851
|
-
submit with `autoMerge: true`.
|
|
852
|
-
- **`merge-main`** — the **graph-level** top-level land: the second level of the two-level merge
|
|
853
|
-
(ADR 0006 §3), landing the graph/epic **integration** PR onto **`main`**. Dispatch-identical to
|
|
854
|
-
`converge-merge` (both enroll + merge); the distinction is the *level*, kept a first-class literal
|
|
855
|
-
so the two levels are authored explicitly rather than left emergent.
|
|
856
|
-
- **`converge`** — **converge-only**: drive review convergence and stop at `converged`, never
|
|
857
|
-
handing off to the merge loop (equivalent to `autoMerge: false`).
|
|
858
|
-
|
|
859
|
-
> **converge/merge are cell POLICY, not raw nodes.** A raw `senior:converge` / `senior:merge`
|
|
860
|
-
> **agent** job is **not expressible** — the compiler rejects it (`raw-converge-node`). Express
|
|
861
|
-
> "get to green, then land" via a cell node's first-class **`agent.converge` / `agent.merge`**
|
|
862
|
-
> policy flags (`merge` requires `converge`), or, for enrolling an already-open PR, the
|
|
863
|
-
> `connector` targets above (ADR 0006 §3 / S5).
|
|
864
|
-
|
|
865
|
-
**Payload:** `{ pr: "owner/repo#123", autoMerge?: boolean, dependsOn?: string[] }`. `pr` is
|
|
866
|
-
required (a literal `owner/repo#N`, identical to how a `wait: pr` node targets a known PR).
|
|
867
|
-
`autoMerge` defaults from the target and may be overridden per-node; `dependsOn` is unioned into
|
|
868
|
-
the PR's merge-stage dependency set. The enrollment is idempotent (the connector's
|
|
869
|
-
at-least-once dedupe fence **plus** `submitPr`'s own `prKey` idempotency), so a graph resume /
|
|
870
|
-
redelivery never double-enrolls.
|
|
871
|
-
|
|
872
|
-
**Canonical shape** — the agent opens the PR, emits it as a typed **`pr` fact**, and the connector
|
|
873
|
-
and `wait[pr, merged]` gate **late-bind** that fact (no hardcoded PR number, no human node). The
|
|
874
|
-
author never knows the PR number at compose time, so reference it by fact:
|
|
401
|
+
The PR number is unknown when you write the graph, so have the agent **emit a `pr` fact** and
|
|
402
|
+
reference it as `"<node>.pr"`. The reference needs a threading edge from that fact, otherwise
|
|
403
|
+
compile rejects it (`unbound-pr`). A connector with no `payload.pr` binds the single incoming
|
|
404
|
+
`pr` fact, and a literal `owner/repo#N` also works.
|
|
875
405
|
|
|
876
406
|
```json
|
|
877
407
|
{
|
|
878
408
|
"name": "open → converge+merge → wait merged",
|
|
879
409
|
"nodes": [
|
|
880
410
|
{ "id": "open", "kind": "agent",
|
|
881
|
-
"agent": { "jobType": "senior:feature", "repository": "acme/repo", "prompt": "Implement the change
|
|
411
|
+
"agent": { "jobType": "senior:feature", "repository": "acme/repo", "prompt": "Implement the change and open a PR." },
|
|
882
412
|
"emits": [ { "name": "pr", "type": "pr" } ] },
|
|
883
|
-
{ "id": "land", "kind": "connector",
|
|
884
|
-
"connector": { "target": "converge-merge", "payload": { "pr": "open.pr" } } },
|
|
413
|
+
{ "id": "land", "kind": "connector", "connector": { "target": "converge-merge", "payload": { "pr": "open.pr" } } },
|
|
885
414
|
{ "id": "merged", "kind": "wait",
|
|
886
415
|
"wait": { "kind": "pr", "target": "open.pr", "match": { "prState": "merged" },
|
|
887
416
|
"poll": { "everyMs": 300000, "timeoutMs": 259200000 }, "onTimeout": "escalate" } }
|
|
888
417
|
],
|
|
889
|
-
"edges": [
|
|
890
|
-
{ "from": "open.pr", "to": "land" },
|
|
891
|
-
{ "from": "open.pr", "to": "merged" }
|
|
892
|
-
]
|
|
418
|
+
"edges": [ { "from": "open.pr", "to": "land" }, { "from": "open.pr", "to": "merged" } ]
|
|
893
419
|
}
|
|
894
420
|
```
|
|
895
421
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
those edges are what carry the observed PR into each consumer, so they are **required** when you
|
|
903
|
-
reference `open.pr` (the validator rejects a reference with no threading edge, `unbound-pr`). Three
|
|
904
|
-
ways to name the target PR:
|
|
905
|
-
|
|
906
|
-
- **fact reference** `"<node>.pr"` — late-bound from the upstream `agent`'s emitted `pr` fact (the
|
|
907
|
-
shape above). The referenced fact must be declared **`pr`-typed** and threaded on an incoming edge.
|
|
908
|
-
- **omitted** (connector only) — `payload` without a `pr` auto-binds the **single** incoming `pr`
|
|
909
|
-
fact, so `"connector": { "target": "converge-merge" }` works when exactly one `pr` fact flows in.
|
|
910
|
-
- **literal** `"owner/repo#N"` — still accepted for a PR you already know (a real ref is never
|
|
911
|
-
`<node>.pr`-shaped, so it never collides with a reference).
|
|
912
|
-
|
|
913
|
-
`senior:feature` already returns the PR it opened, so declaring `emits: [{ "name": "pr", "type": "pr" }]`
|
|
914
|
-
on the agent node is all it takes to publish it (issue #548).
|
|
915
|
-
|
|
916
|
-
> **Don't hand-author this shape — generate it.** When your intent is simply "sequence these
|
|
917
|
-
> issues, each implemented → converged → merged (optionally behind a gate)", call the
|
|
918
|
-
> **`sequenceIssues`** door instead of assembling the nodes/edges by hand. Its body is the intent
|
|
919
|
-
> `{ "issues": ["owner/repo#A", "owner/repo#B", …] }` with an optional leading `"behind": "owner/repo#NN"`
|
|
920
|
-
> gate, and it GENERATES
|
|
921
|
-
> exactly the canonical chain above — for each issue `agent` (`senior:feature`, emits `pr`) →
|
|
922
|
-
> `connector[converge-merge]` → `wait[pr, merged]` with a realistic `poll.timeoutMs`, threading the
|
|
923
|
-
> `pr` fact, plus an optional leading `wait[epic]` gate (§9.5) when `behind` is given — then STAGES it
|
|
924
|
-
> through the same compile+stage path as `compileDeliveryGraph` (it never dispatches). The issues run
|
|
925
|
-
> in **sequence**: each issue's implementation starts once the prior issue has merged.
|
|
422
|
+
> **Don't hand-write that chain. Generate it.** To run issues one after another (each
|
|
423
|
+
> implemented, converged, then merged), call **`sequenceIssues`**
|
|
424
|
+
> (`POST __BASE__/actions/start/sequence-issues`, body
|
|
425
|
+
> `{ "issues": ["owner/repo#A", "owner/repo#B"], "behind"?: "owner/repo#NN" }`). It builds the
|
|
426
|
+
> chain above for each issue, with an optional leading `wait[epic]` gate, and stages it through
|
|
427
|
+
> compile (it never dispatches).
|
|
926
428
|
|
|
927
429
|
### 9.5 Gate a graph on an epic reaching "fully merged" (`wait[epic]`)
|
|
928
430
|
|
|
929
|
-
|
|
930
|
-
`
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
babysitting a gate.
|
|
431
|
+
`kind: "epic"` waits until every slice PR of an epic (or the PR of a feature run) has landed.
|
|
432
|
+
`target` is the **`planKey`** (`owner/repo#NN`, the root issue), not an engine key, so it
|
|
433
|
+
survives a resubmit. `match.epicState` is `merged` (default) or `done`. A failed or abandoned
|
|
434
|
+
epic never reports merged, so the bounded wait routes via `onTimeout`. On a match it binds
|
|
435
|
+
`prCount`.
|
|
935
436
|
|
|
936
437
|
```json
|
|
937
|
-
{
|
|
938
|
-
"
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
"wait": { "kind": "epic", "target": "nanobpm/nano-ide#488",
|
|
942
|
-
"match": { "epicState": "merged" },
|
|
943
|
-
"poll": { "everyMs": 300000, "timeoutMs": 259200000 },
|
|
944
|
-
"onTimeout": "escalate" },
|
|
945
|
-
"emits": [ { "name": "prCount", "type": "number" } ] },
|
|
946
|
-
{ "id": "start-b", "kind": "agent",
|
|
947
|
-
"agent": { "jobType": "senior:feature", "repository": "nanobpm/nano-workforce", "prompt": "Implement nanobpm/nano-workforce#567 and open a PR." } }
|
|
948
|
-
],
|
|
949
|
-
"edges": [ { "from": "gate-epic", "to": "start-b" } ]
|
|
950
|
-
}
|
|
438
|
+
{ "id": "gate-epic", "kind": "wait",
|
|
439
|
+
"wait": { "kind": "epic", "target": "nanobpm/nano-ide#488", "match": { "epicState": "merged" },
|
|
440
|
+
"poll": { "everyMs": 300000, "timeoutMs": 259200000 }, "onTimeout": "escalate" },
|
|
441
|
+
"emits": [ { "name": "prCount", "type": "number" } ] }
|
|
951
442
|
```
|
|
952
443
|
|
|
953
|
-
Semantics:
|
|
954
|
-
|
|
955
|
-
- **`target` is the `planKey`** (`owner/repo#NN`, the epic issue) — the *stable business id*,
|
|
956
|
-
not the engine `processInstanceKey` (`64200`), so a resubmit/replay still resolves.
|
|
957
|
-
- **`match.epicState`** is `merged` (default) or `done` — "every opened slice landed". The gate reads
|
|
958
|
-
the app's own **aggregate** (the lineage read-model over `NANO_WORKFORCE_BASE_URL`), so it is
|
|
959
|
-
level-triggered like `pr` (no missed edge). A failed/abandoned/mixed epic never reports merged, so
|
|
960
|
-
it never falsely releases the gate — the **bounded** wait routes via **`onTimeout`**, never hanging.
|
|
961
|
-
- **Set `poll.timeoutMs` to a realistic budget.** An epic reaching "fully merged" is a
|
|
962
|
-
multi-day, human-paced event, so the example budgets 3 days (`poll: { everyMs: 300000, timeoutMs:
|
|
963
|
-
259200000 }`). **Omitting `poll` inherits the 30-minute default** (§9.1) — the gate would escalate
|
|
964
|
-
long before the epic lands. Because this gate binds `prCount`, that escalation is **validated**
|
|
965
|
-
(§9.2): it **re-parks** unless the operator supplies a valid `prCount`, rather than silently
|
|
966
|
-
releasing `start-b` as not-ready.
|
|
967
|
-
- On a fully-merged match it binds **`prCount`** (how many slice PRs the epic landed) as an
|
|
968
|
-
output fact, so a downstream node can consume it (parity with the `pr` kind's `mergedSha`).
|
|
969
|
-
- **It also gates a single-PR *feature run*, not just a plan-fanout epic.** The gate resolves the
|
|
970
|
-
lineage thread whose **`rootRequestKey`** matches `target` **regardless of the thread's `kind`**
|
|
971
|
-
(`feature` | `epic` | `pr` | `delivery`), and `app/lineage.ts` lands a *feature* thread on
|
|
972
|
-
`stage:"merged"` once its PR merges. So `wait[epic]` targeting a feature/epic **root issue**
|
|
973
|
-
releases on `stage:"merged" && active:false` either way — see `getDeliveryGraphVocabulary` (the
|
|
974
|
-
`epic` probe entry) for the structured contract.
|
|
975
|
-
|
|
976
444
|
---
|
|
977
445
|
|
|
978
446
|
## 10. Tool ↔ curl crosswalk (MCP-first, curl is the no-MCP fallback)
|
|
979
447
|
|
|
980
|
-
|
|
981
|
-
leads with it. `curl __BASE__/…`/`curl __ENGINE__/…` is only the **no-MCP fallback** for an
|
|
982
|
-
agent that has no tools (see the runbook, `docs/mcp-runbook.md` §2/§3, and [§5
|
|
983
|
-
Fallback](mcp-runbook.md#5-fallback)). This table maps every guide action to its projected
|
|
984
|
-
tool name and its no-MCP fallback underneath (usually a curl door; a few rows fall back to reading a repo file instead).
|
|
985
|
-
|
|
986
|
-
App-owned operations (projected from this app's `openapi.yaml`; the `__BASE__` control API).
|
|
987
|
-
Rows tagged *(projected by sibling #NNN)* name a tool that lands with that sibling task —
|
|
988
|
-
until it does, only the curl door in the third column exists on the deployment:
|
|
989
|
-
|
|
990
|
-
| Guide action | Projected tool | No-MCP fallback |
|
|
991
|
-
|---|---|---|
|
|
992
|
-
| Which code is live (§0) | `getVersion` | `curl __BASE__/version` |
|
|
993
|
-
| Every PR in flight (§0/§5) | `listActivePrs` | `curl __BASE__/status` |
|
|
994
|
-
| List open escalations (§3) | `listEscalations` *(projected by sibling #666; curl door until then)* | `curl __BASE__/../../tasks/api/tasks` (the answerable list — carries each `userTaskKey`); `/status` filtered with the jq recipe just below this table is only a quick *indicator* (it surfaces `openEscalation` as a question string, **no `userTaskKey`**, so you can't answer from it) |
|
|
995
|
-
| Answer an escalation (§3) | `completeUserTask` (agent-assignee: `agentCompleteEscalation`) | `curl -X POST __BASE__/actions/complete-user-task` |
|
|
996
|
-
| List auto-resolutions (§3) | `listAutoResolutions` | `curl __BASE__/auto-resolutions` |
|
|
997
|
-
| Review an auto-resolution (dismiss / intervene) (§3) | `reviewAutoResolution` | `curl -X POST __BASE__/auto-resolutions/review` |
|
|
998
|
-
| Cancel an instance (record-consistent) (§7) | `cancelInstance` *(projected by sibling #667; curl door until then)* | `curl -X POST __BASE__/../actions/cancel` *(the `/app/actions/cancel` door, outside `__BASE__`)* |
|
|
999
|
-
| Publish a BPMN message (§7) | `postMessage` | `curl -X POST __BASE__/actions/message` |
|
|
1000
|
-
| The operator guide itself | `getAgentInstructions` (full) / `getAgentGuide(section?)` (addressable) | `curl __BASE__/agent` / `curl __BASE__/agent/guide` |
|
|
1001
|
-
|
|
1002
|
-
The `listEscalations` no-MCP fallback that filters `/status` with `jq` lives here, not inside the
|
|
1003
|
-
table cell above: a literal `|` can't be written in a GFM table cell without escaping it as `\|`,
|
|
1004
|
-
and this guide is served **raw** (byte-for-byte, `__BASE__`/`__ENGINE__` aside) to no-MCP agents, so
|
|
1005
|
-
the escaped form would be copied verbatim and the shell would treat `\|` as a literal argument
|
|
1006
|
-
rather than a pipe. It's only a **quick indicator** — `/status` surfaces `openEscalation` as a
|
|
1007
|
-
question string with **no `userTaskKey`**, so to actually *answer* one you still need the tasks
|
|
1008
|
-
inbox (`curl __BASE__/../../tasks/api/tasks`) or the `listEscalations` tool for its `userTaskKey`
|
|
1009
|
-
(§3). It selects the same `{ prKey, status, round, openEscalation }` fields as the §3 fallback for
|
|
1010
|
-
consistency. Copy it from here:
|
|
1011
|
-
|
|
1012
|
-
```sh
|
|
1013
|
-
curl __BASE__/status | jq '.prs[] | select(.openEscalation != null)
|
|
1014
|
-
| { prKey, status, round, openEscalation }'
|
|
1015
|
-
```
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
Framework engine-debug tools (owned by the nano-ide urban runtime, not this app; they front
|
|
1019
|
-
the engine's Camunda-8 v2 REST API at `__ENGINE__`). The first three are always projected;
|
|
1020
|
-
`urban_debug_search_jobs` / `urban_debug_search_variables` /
|
|
1021
|
-
`urban_debug_get_process_definition_xml` are owned by the sibling nano-ide urban epic — use
|
|
1022
|
-
them by name where the framework projects them, else drop to the curl door:
|
|
1023
|
-
|
|
1024
|
-
| Guide action | Projected tool | No-MCP fallback |
|
|
448
|
+
| Action | Tool | No-MCP fallback |
|
|
1025
449
|
|---|---|---|
|
|
1026
|
-
|
|
|
1027
|
-
|
|
|
1028
|
-
|
|
|
1029
|
-
|
|
|
1030
|
-
|
|
|
1031
|
-
|
|
|
1032
|
-
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
`
|
|
1039
|
-
|
|
450
|
+
| Live version (§0) | `getVersion` | `GET __BASE__/version` |
|
|
451
|
+
| PRs in flight (§0) | `listActivePrs` | `GET __BASE__/status` |
|
|
452
|
+
| Submit a PR / epic / feature (§1–2) | `startConvergenceLoop` / `startPlanFanout` / `startFeature` | `POST __BASE__/actions/start/{convergence-loop,plan-fanout,feature}` |
|
|
453
|
+
| List escalations (§3) | `listEscalations` | `GET __BASE__/escalations` |
|
|
454
|
+
| Answer an escalation (§3) | `completeUserTask` (as agent: `agentCompleteEscalation`) | `POST __BASE__/actions/complete-user-task` (as agent: `POST __BASE__/hooks/agent-complete` with `agentId` required) |
|
|
455
|
+
| Auto-resolutions (§3) | `listAutoResolutions` / `reviewAutoResolution` | `GET __BASE__/auto-resolutions` / `POST __BASE__/auto-resolutions/review` |
|
|
456
|
+
| Cancel a run (§7) | `cancelInstance` | `POST __BASE__/actions/cancel` |
|
|
457
|
+
| Publish a BPMN message | `postMessage` | `POST __BASE__/actions/message` |
|
|
458
|
+
| Delivery graphs (§9) | `getDeliveryGraphVocabulary` / `compileDeliveryGraph` / `sequenceIssues` | `GET __BASE__/delivery-graph/vocabulary`, `POST __BASE__/actions/{compile-delivery-graph,start/sequence-issues}` |
|
|
459
|
+
| This guide | `getAgentInstructions` (full) / `getAgentGuide(section?)` | `GET __BASE__/agent` / `GET __BASE__/agent/guide` |
|
|
460
|
+
|
|
461
|
+
The engine-debug tools come from the urban framework and front `__ENGINE__` (§5).
|
|
462
|
+
`urban_debug_search_process_instances`, `urban_debug_search_element_instance_wait_states` and
|
|
463
|
+
`urban_debug_search_incidents` are always available. `urban_debug_search_jobs`,
|
|
464
|
+
`urban_debug_search_variables` and `urban_debug_get_process_definition_xml` exist where the
|
|
465
|
+
framework provides them; otherwise use the §5 curl searches or read `resources/processes/` from
|
|
466
|
+
the checkout. **Never use `urban_debug_cancel_instance`**: use `cancelInstance` (§7).
|