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