@nanobpm/nano-workforce 0.203.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.
@@ -1,355 +1,161 @@
1
1
  # Nano Workforce — agent operator guide
2
2
 
3
- You are an AI assistant helping a human operate a running **Nano Workforce**
4
- instance. Nano Workforce is a durable orchestration app (a [Nano](https://nanobpm.io)
5
- Urban app) that drives pull requests to **review convergence** against an automated
6
- reviewer, then **merges** them, and can take a whole issue and **plan → implement →
7
- converge** it across a fleet of coding agents.
8
-
9
- This document is served live by the running app so you always match the deployed
10
- version. Use it to **drive** the workforce (submit work, answer escalations) and to
11
- **debug** it (find stuck instances, relate them to PRs, inspect the models/prompts,
12
- and unstick or report problems).
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 (Nano/Camunda-8 v2 REST) base:** `__ENGINE__`
9
+ - **Engine (Camunda-8 v2 REST) base:** `__ENGINE__`
16
10
  - **Source repository:** `nanobpm/nano-workforce`
17
11
 
18
- Everything below assumes the app control API is reachable at `__BASE__`. Most app
19
- endpoints are mounted under that base (ADR 0059), but a few siblings sit outside it —
20
- notably the interactive docs (Swagger UI) at `__BASE__/../api-docs` and the action
21
- endpoints (e.g. the cancel action at `/app/actions/cancel`). Paths below are written
22
- in full so you can tell which are under the control-API base and which are not.
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
- Before acting, confirm what is running and what is in flight. **Prefer the projected
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
- # No-MCP fallback (agent without the projected tools):
44
- curl -sS __BASE__/version | jq
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
- `listActivePrs` (the `/status` endpoint) is your primary situational-awareness surface.
49
- Each entry carries:
50
- `prKey` (`owner/repo#123`), `status`, `round`, `processKey` (the **engine process
51
- instance key** — the bridge to the engine REST API, §5), `openEscalation`,
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 dispatches a `senior:pr-review`
60
- agent; between rounds the process parks on a durable message-catch, so review latency
61
- never holds an agent slot. The app's poller watches GitHub and republishes
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
- # Minimal: converge, then merge (if NANO_PR_AUTO_MERGE is on — the default).
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#123" }'
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 | the PR — `owner/repo#123` or a full PR URL. Required. |
76
- | `autoMerge` | boolean | **Positive per-request setting.** `false` = review only, stopping at `converged`, regardless of the global default; `true` = converge **then merge** when `NANO_PR_AUTO_MERGE` is enabled. |
77
- | `maxRounds` | integer | per-submit cap before escalating (clamped 1–100; default from `NANO_PR_MAX_ROUNDS`, 20). |
78
- | `dependsOn` | string[] | other `prKey`s that must land before this one merges (merge-loop barrier). |
79
- | `autoResolveEscalations` | boolean | **Opt in to the escalation steward** (issue #889). When `true`, this loop's open escalations become eligible for the steward to review and auto-resolve every 30 min (recorded reviewably in **Auto-resolutions**, §3). Default `false` — escalations stay human-only. |
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
- **Converge-only vs. converge-and-merge — choose deliberately:**
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
- Hand the fleet a whole issue. A planning agent decomposes it into levelized tasks; a
105
- parallel fan-out drives one implementation agent per task (one PR each); every opened
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
- Both epic (`startPlanFanout`) and feature (`startFeature`) accept the optional
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
- ### Base-branch admission (ADR 0003)
136
-
137
- `startPlanFanout` admits the base through one fail-fast gate before any task fans out.
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
- ```bash
160
- curl -sS -X POST __BASE__/actions/start/plan-fanout \
161
- -H 'content-type: application/json' \
162
- -d '{ "issue": "owner/repo#123", "baseBranch": "main", "confirmDefaultBase": true }'
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
- Grandfathered: in-flight plans launched before this admission gate (with a `null` base branch)
166
- keep running unchanged — the requirement is enforced at admission of **new** launches, not by a
167
- database constraint.
168
-
169
- Track a plan the same way you track PRs — its `process_key` is an engine instance you
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 only when an agent returns `needs_input`/`blocked`, or a safety net
202
- fires (round cap, a review that never arrives, a merge conflict, an unfixable CI
203
- failure). The parked process waits for a human answer.
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
- # No-MCP fallback: read the open escalations off /status …
219
- curl -sS __BASE__/status | jq '.prs[] | select(.openEscalation != null)
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
- The four decision-required escalation kinds — **PR review-loop**, **implementation
224
- (feature) task**, **plan-review**, and **trial-merge** — are now native BPMN
225
- `userTask`s bearing a linked `.form`, all answered the same way through the
226
- `completeUserTask` tool (a human uses the same **task inbox** surface underneath).
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
- **List the open escalation tasks.** `listEscalations` returns each open task with its
230
- context (`prKey` / `question` / `findings` / `task`) and its kind in `elementId` /
231
- `kind`, so you do **not** need to reach for the raw Tasks inbox. No-MCP fallback only:
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
- ```bash
234
- # No-MCP fallback — the un-projected engine task inbox (prefer `listEscalations`):
235
- curl -sS __BASE__/../../tasks/api/tasks | jq '.[] | { userTaskKey, elementId, variables }'
236
-
237
- # Filter to one kind (e.g. plan-review decisions) by elementId:
238
- curl -sS __BASE__/../../tasks/api/tasks \
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
- # No-MCP fallback — the app-owned complete-user-task door (prefer the tool).
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
- **Answer a PR escalation** (both the review-loop `wait-answer` and the merge-loop
287
- `wait-merge-answer` — both are now native user tasks answered the same way, #256).
288
- Use the PR key's parked user task and submit the `pr-escalation` form's `{ answer }`
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
- ```text
292
- tool: completeUserTask { "userTaskKey": "<key>",
293
- "variables": { "answer": "Yes — cap the retries at 5 and proceed." } }
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
- ```bash
297
- # No-MCP fallback:
298
- curl -sS -X POST __BASE__/actions/complete-user-task \
299
- -H 'content-type: application/json' \
300
- -d '{
301
- "userTaskKey": "<key>",
302
- "variables": { "answer": "Yes — cap the retries at 5 and proceed." }
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
- The `userTaskKey` comes from `listEscalations` (or `GET /status`/the Tasks inbox). This
307
- is the ONE canonical answer door (`completeUserTask`) for every escalation kind — with
308
- `agentCompleteEscalation` the agent-assignee variant that records identity for
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 you will see in `/status`:
368
- `converging` (a review round is live), `waiting_review` (parked for a fresh review),
369
- `escalated` (waiting on a human answer), `converged`, `waiting_deps` / `waiting_merge`
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
- The app stores each PR/plan's engine **process instance key** in its `process_key`
379
- column and surfaces it as `processKey` in `/status`. That key is the join between the
380
- app's business view and the engine's execution view.
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
- # No-MCP fallback — the raw engine Camunda-8 v2 REST API:
411
- PK=<processKey-from-status>
412
-
413
- # The instance itself (state, the BPMN process it is running, start time):
414
- curl -sS -X POST __ENGINE__/process-instances/search \
415
- -H 'content-type: application/json' \
416
- -d "{ \"filter\": { \"processInstanceKey\": \"$PK\" } }" | jq
417
-
418
- # Where is it parked? — active jobs on the instance:
419
- curl -sS -X POST __ENGINE__/jobs/search \
420
- -H 'content-type: application/json' \
421
- -d "{ \"filter\": { \"processInstanceKey\": \"$PK\", \"state\": \"CREATED\" } }" | jq
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
- > The `urban_debug_search_jobs`, `urban_debug_search_variables` and
435
- > `urban_debug_get_process_definition_xml` reads are owned by the sibling nano-ide urban
436
- > epic; where a deployment's framework has not yet projected them, use the curl fallback
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
- The app already mirrors an ACTIVE incident onto the PR row (`incident`/incident
442
- message), so a PR that shows an incident in the UI is parked on an engine fault —
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
- The behaviour is defined by durable BPMN processes and model-authored agent prompts —
457
- both live in the source repo, not in the job payload.
458
-
459
- - **Processes:** to inspect the model an instance is **actually running**, prefer the
460
- framework tool **`urban_debug_get_process_definition_xml`** (owned by the sibling
461
- nano-ide urban epic — see §10), which returns the deployed BPMN XML for a process
462
- definition; that is engine truth and cannot drift from what is live. The repo copies
463
- `resources/processes/*.bpmn` — `convergence-loop.bpmn` (review), `merge-loop.bpmn`
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
- that the engine
484
- resolves to the latest deployed prompt at job activation, combined at runtime with the
485
- per-task `appendPrompt` FEEL (the task-specific slice appended to this base). Because
486
- the binding is `latest`, redeploying one of these files updates the prompt mid-epic
487
- without a process redeploy. If an agent misbehaves systematically, the prompt is the
488
- first thing to inspect/fix.
489
-
490
- > **Value-injection caveat:** never bake a per-run value (a URL, a flag) into a prompt
491
- > at deploy time — that path is gone. Pass it as a runtime job variable / `appendPrompt`
492
- > FEEL instead; deploy-time string substitution is not available.
493
- - **Job contract:** `senior:pr-review` receives `{ prUrl, repo, prNumber, round,
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
- Work through this order:
509
-
510
- 1. **Confirm it is actually stuck.** From `/status`, a PR `converging` with an
511
- `activeWorker` set is *working*, not stuck — an agent holds the round. No worker
512
- for a long time means the job is queued: is a fleet `c8ctl nano work` daemon
513
- running and subscribed to the `senior:*` task types?
514
- 2. **Check for an incident** (§5). An ACTIVE incident parks the token; the underlying
515
- fault must be resolved (or the instance cancelled and the work resubmitted). The
516
- app surfaces the incident message on the PR row.
517
- 3. **Check for an open escalation** (§3) — the process may simply be waiting for a
518
- human answer. Answer it.
519
- 4. **A review that never arrives** escalates on its own after
520
- `NANO_PR_REVIEW_WAIT_TIMEOUT` (default `PT30M`); the poller also re-nudges the
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__/../actions/cancel -H 'content-type: application/json' \
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
- When you find a genuine bug or a missing capability in the orchestration itself
547
- (not a transient repo/agent problem), help the human file it against
548
- `nanobpm/nano-workforce`:
549
-
550
- - **Every change needs a tracked issue or PR** — no silent fixes. Open an issue
551
- first if one does not exist.
552
- - **DCO is enforced:** every commit needs a `Signed-off-by` trailer — commit with
553
- `git commit -s` (or `git rebase --signoff`).
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
- The two workflows above (§1 convergence-loop, §2 plan-fanout) are each specialised to
573
- **one** node shape ("an agent implements a slice → opens a PR"). Real delivery is often a
574
- **heterogeneous, cross-repo, partly-human graph** — e.g. *merge PR #101 → un-draft+merge
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
- ### 9.1 The `DeliveryGraph` shape
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
- A `DeliveryGraph` is a JSON **DAG**: `{ name?, nodes[], edges[] }`.
599
-
600
- - **`nodes[]`** — each node has a unique `id`, a `kind` from the **closed allowlist**
601
- (`agent` | `wait` | `human` | `connector`), the matching per-kind config, and an
602
- optional typed `emits[]` declaration (the facts it hands forward).
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.2 The agent loop: draft → compile → stage → ask an operator to dispatch
289
+ ### 9.1 The `DeliveryGraph` shape
711
290
 
712
- ```
713
- GET __BASE__/agent # ← you are reading it; learn the vocabulary + endpoints
714
- └─ draft a DeliveryGraph JSON
715
- └─ POST __BASE__/actions/compile-delivery-graph # validate + preview + STAGE
716
- ├─ 400 { ok:false, errors:[{path,message}] } → fix the exact offending input, recompile
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
- **Compile (validate + preview + stage).** The compile door runs the semantic validator and
722
- the deterministic compiler and, on success, **stages** the compiled graph as a proposal a
723
- human can dispatch — it does **not** deploy or run anything. Recompiling a graph you are
724
- still drafting is safe: a re-compile of the same graph is idempotent, and a changed graph
725
- with the same `name` supersedes the prior staged proposal, so the cockpit shows exactly one
726
- live proposal per graph.
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
- - `200 { status:"ready", message, digest, preview, reviewUrl }` — the graph compiled and is
735
- **staged for operator review**. `digest` is the content-address that NAMES the proposal (tell the
736
- operator exactly which one to dispatch); `preview` is `{ diagram, sideEffects, humanNodes }` —
737
- `diagram` is a mermaid `flowchart` of the resolved graph, `humanNodes[]` are the stop-points where
738
- it waits for a person, and `sideEffects[]` are the `agent`/`connector` actions it **will** perform
739
- once dispatched; `reviewUrl` is a **navigational** cockpit deep-link (a pointer only — **not** a
740
- dispatch handle). The response carries **no run key, token, or process-instance key**: nothing you
741
- can replay to start a run. Hand the operator the `digest` (or `reviewUrl`) and ask them to dispatch.
742
- - `400 { ok:false, errors:[{ path, message }] }` — every error path-qualified
743
- (`nodes[2].kind`, `edges[1].from`, …) for unknown kind, dangling edge, a cycle, or an
744
- unresolvable `from` fact. Fix and recompile; nothing is staged.
745
-
746
- **Dispatch (operator-only — NOT on the agent surface).** There is deliberately no agent
747
- `start` endpoint (Decision 5/7, issue #460). Dispatch is a human action: an operator opens
748
- the **Delivery Graphs** page in the cockpit, reviews the staged proposal's rendered preview
749
- (its diagram, the human stop-points, and the side effects a dispatch authorises), and clicks
750
- **Dispatch** on the one they approve. That click **is** the approval — content-addressed to the
751
- exact digest previewed, so it cannot replay some other graph. Once dispatched, the graph deploys +
752
- runs engine-natively and registers as a run aggregate, so its current phase / parked node shows in
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
- *Merge PR #101 (repo 1) → un-draft+merge PR #202 (repo 2) → a **human** runs the manual OTP
785
- publish and records the version → open+merge PR #303 (repo 3) consuming that version.* The
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 #303",
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 and set up OIDC trusted publishing. Record the exact published version." },
800
- "emits": [ { "name": "publishedVersion", "type": "version", "description": "The version just published to npm." } ] },
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 #303." } },
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": "acme/repo-3#303", "match": { "prState": "merged" },
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
- `compile` returns this preview (abridged):
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
- To swap the manual PR-#303 path for a **capability** edge instead of a raw `pr` watch, make
836
- the consumer a `wait` node with `kind: "capability"` (resolving *which published
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
- ### 9.4 Connector targets — drive a PR to convergence + merge (`converge` / `converge-merge` / `merge-main`)
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
- A `connector` node with **`target: "converge-merge"`** (or **`"converge"`** / **`"merge-main"`**)
843
- enrolls an agent-opened PR into the app's **shared convergence loop** — the *same* enrollment §1 (a
844
- standalone submit) and a feature run use (`submitPr`), no duplicated machinery. This replaces
845
- the old habit of bridging an `agent`-opened PR to review with a **human `land-*` gate** whose
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 in acme/repo and open a PR." },
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
- The `merged` gate carries an explicit **`poll`** (re-probe every 5 minutes, budget 3 days:
897
- `timeoutMs: 259200000`) because a `wait[pr, merged]` waits on a human-paced merge — **omitting
898
- `poll` inherits the 30-minute default** (§9.1) and escalates mid-review. Set `poll.timeoutMs`
899
- to a realistic budget on any merge/epic gate.
900
-
901
- The `pr` fact is threaded along the **fact-qualified edges** (`open.pr → land`, `open.pr → merged`) —
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
- Sometimes the thing you must wait for is not one PR but a **whole epic** — an nwf
930
- `plan-fanout` that fans many slice PRs across waves whose numbers are unknown at compose
931
- time. The **`wait` kind `epic`** (issue #568) gates on that epic reaching **"fully merged"**
932
- (every opened slice landed), keyed by its durable **`planKey`** (`owner/repo#NN` — the epic
933
- issue), so *"start feature B once epic A has fully landed"* is an automated edge, not a human
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
- "name": "start #567 once epic #488 has fully merged",
939
- "nodes": [
940
- { "id": "gate-epic", "kind": "wait",
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
- When you drive this instance over MCP, **call the projected tool** — every recipe above
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
- | Search process instances (§5) | `urban_debug_search_process_instances` | `curl -X POST __ENGINE__/process-instances/search` |
1027
- | Inspect element/wait states (§5) | `urban_debug_search_element_instance_wait_states` | `curl -X POST __ENGINE__/element-instances/search` |
1028
- | Active incidents (§5) | `urban_debug_search_incidents` | `curl -X POST __ENGINE__/incidents/search` |
1029
- | Jobs on an instance (§5) | `urban_debug_search_jobs` | `curl -X POST __ENGINE__/jobs/search` |
1030
- | Instance variables (§5) | `urban_debug_search_variables` | `curl -X POST __ENGINE__/variables/search` |
1031
- | Deployed BPMN model XML (§6) | `urban_debug_get_process_definition_xml` | read the source copy `resources/processes/*.bpmn` from the checkout |
1032
- | Engine-level cancel — **avoid** (§7) | `urban_debug_cancel_instance` | *(use the app-owned `cancelInstance` instead — engine cancel leaves the PR row inconsistent)* |
1033
-
1034
- **Rule of thumb (agent-guide §5 fallback discipline):** if a row has a projected tool, use
1035
- it; reach for its curl door only when you are genuinely not on MCP (or the tool's sibling
1036
- task has not yet landed — see the *(projected by sibling #NNN)* tags above). The app-owned
1037
- cancel door (`cancelInstance`, #667) is always preferred over the engine-level
1038
- `urban_debug_cancel_instance`,
1039
- because only the app door transitions the PR/plan record to `abandoned`.
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).