@nanobpm/nano-workforce 0.170.0 → 0.171.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [0.171.0](https://github.com/nanobpm/nano-workforce/compare/v0.170.1...v0.171.0) (2026-08-31)
2
+
3
+ ### Features
4
+
5
+ * **operations:** app-owned POST /actions/cancel door (cancelInstance) ([#671](https://github.com/nanobpm/nano-workforce/issues/671)) ([835afce](https://github.com/nanobpm/nano-workforce/commit/835afcea6be62fc768979b792217ad5cf246d008)), closes [#605](https://github.com/nanobpm/nano-workforce/issues/605) [#667](https://github.com/nanobpm/nano-workforce/issues/667)
6
+
7
+ ## [0.170.1](https://github.com/nanobpm/nano-workforce/compare/v0.170.0...v0.170.1) (2026-08-31)
8
+
9
+ ### Documentation
10
+
11
+ * **agent-guide:** make the operator guide tool-aware + tool↔curl crosswalk (N1) ([#669](https://github.com/nanobpm/nano-workforce/issues/669)) ([44c5937](https://github.com/nanobpm/nano-workforce/commit/44c5937fb3a1cdaa5a106d67ed16225c35d614f6)), closes [#664](https://github.com/nanobpm/nano-workforce/issues/664) [#666](https://github.com/nanobpm/nano-workforce/issues/666) [#667](https://github.com/nanobpm/nano-workforce/issues/667) [#665](https://github.com/nanobpm/nano-workforce/issues/665) [666/#667](https://github.com/666/nano-workforce/issues/667) [#666](https://github.com/nanobpm/nano-workforce/issues/666) [#667](https://github.com/nanobpm/nano-workforce/issues/667) [#665](https://github.com/nanobpm/nano-workforce/issues/665)
12
+
1
13
  ## [0.170.0](https://github.com/nanobpm/nano-workforce/compare/v0.169.0...v0.170.0) (2026-08-31)
2
14
 
3
15
  ### Features
package/app/agentGuide.ts CHANGED
@@ -106,6 +106,7 @@ export const GUIDE_SECTIONS: readonly GuideSectionMeta[] = [
106
106
  { id: "unstick", summary: "Unstick a wedged process — publish a correlating message, cancel, or otherwise recover a stalled instance." },
107
107
  { id: "raise-issue", summary: "Raise an issue or open a PR against the nano-workforce repository itself." },
108
108
  { id: "delivery-graphs", summary: "Author, preview, compile/stage and run an agent-authored delivery graph (ADR 0005): node/wait/connector vocabulary." },
109
+ { id: "tool-crosswalk", summary: "Tool↔curl crosswalk: map every guide action to its projected MCP tool (status, version, urban_debug_* engine reads, escalation answer, cancel) with the curl no-MCP fallback." },
109
110
  ] as const;
110
111
 
111
112
  /** A parsed section: its stable id + summary (from the registry), the derived heading `title` (the
@@ -25,18 +25,28 @@ in full so you can tell which are under the control-API base and which are not.
25
25
 
26
26
  ## 0. Orient yourself first
27
27
 
28
- Before acting, confirm what is running and what is in flight:
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.
29
32
 
30
- ```bash
33
+ ```text
31
34
  # Which code is live (app version, urban version, git sha/branch, uptime):
32
- curl -sS __BASE__/version | jq
35
+ tool: getVersion # GET /version
33
36
 
34
37
  # Every PR currently in flight (not converged/abandoned), with its engine
35
38
  # process key, status, round, and any open escalation:
39
+ tool: listActivePrs # GET /status
40
+ ```
41
+
42
+ ```bash
43
+ # No-MCP fallback (agent without the projected tools):
44
+ curl -sS __BASE__/version | jq
36
45
  curl -sS __BASE__/status | jq
37
46
  ```
38
47
 
39
- `/status` is your primary situational-awareness endpoint. Each entry carries:
48
+ `listActivePrs` (the `/status` endpoint) is your primary situational-awareness surface.
49
+ Each entry carries:
40
50
  `prKey` (`owner/repo#123`), `status`, `round`, `processKey` (the **engine process
41
51
  instance key** — the bridge to the engine REST API, §5), `openEscalation`,
42
52
  `activeWorker`/`leaseUntil` (is an agent actually working the round, or is the job
@@ -152,24 +162,36 @@ A loop escalates only when an agent returns `needs_input`/`blocked`, or a safety
152
162
  fires (round cap, a review that never arrives, a merge conflict, an unfixable CI
153
163
  failure). The parked process waits for a human answer.
154
164
 
155
- Find the open escalations, then answer them:
165
+ Find the open escalations, then answer them. **Prefer the `listEscalations` tool**
166
+ (the projected read tool that lists every open escalation with the `userTaskKey` and form —
167
+ tracked by sibling #666; on a deployment that predates it, use the `/status` / task-inbox
168
+ curl fallback below —
169
+ you need to answer it); curl is the no-MCP fallback.
170
+
171
+ ```text
172
+ # Every open escalation waiting for a human, across all four kinds — each with its
173
+ # userTaskKey, prKey, kind (elementId), question/findings/task, and formVariables:
174
+ tool: listEscalations
175
+ ```
156
176
 
157
177
  ```bash
158
- # Which in-flight PRs have an open escalation waiting for a human?
178
+ # No-MCP fallback: read the open escalations off /status
159
179
  curl -sS __BASE__/status | jq '.prs[] | select(.openEscalation != null)
160
180
  | { prKey, status, round, openEscalation }'
161
181
  ```
162
182
 
163
183
  The four decision-required escalation kinds — **PR review-loop**, **implementation
164
184
  (feature) task**, **plan-review**, and **trial-merge** — are now native BPMN
165
- `userTask`s bearing a linked `.form`, all answered the same way through the **task
166
- inbox** surface. There is no bespoke per-kind webhook or answer page any more.
185
+ `userTask`s bearing a linked `.form`, all answered the same way through the
186
+ `completeUserTask` tool (a human uses the same **task inbox** surface underneath).
187
+ There is no bespoke per-kind webhook or answer page any more.
167
188
 
168
- **List the open escalation tasks.** Each task carries its context (e.g. `prKey` /
169
- `question` / `findings` / `task`) in its `variables`, and its kind in `elementId`:
189
+ **List the open escalation tasks.** `listEscalations` returns each open task with its
190
+ context (`prKey` / `question` / `findings` / `task`) and its kind in `elementId` /
191
+ `kind`, so you do **not** need to reach for the raw Tasks inbox. No-MCP fallback only:
170
192
 
171
193
  ```bash
172
- # Every parked escalation, across all kinds:
194
+ # No-MCP fallback the un-projected engine task inbox (prefer `listEscalations`):
173
195
  curl -sS __BASE__/../../tasks/api/tasks | jq '.[] | { userTaskKey, elementId, variables }'
174
196
 
175
197
  # Filter to one kind (e.g. plan-review decisions) by elementId:
@@ -187,34 +209,52 @@ answer (assignee/candidate-group and age surface on each task once assignment la
187
209
  > node then runs against the unmet dependency. See §9.2 before clearing one.
188
210
 
189
211
  **Answer a task** by completing it with the typed variables its form expects — the
190
- completion resumes the parked process:
212
+ completion resumes the parked process. **Prefer the `completeUserTask` tool** (or
213
+ `agentCompleteEscalation` when you answer as an agent assignee, ADR 0046); the
214
+ `curl … /actions/complete-user-task` app-owned door is the no-MCP fallback.
191
215
 
192
- ```bash
216
+ ```text
193
217
  # PR review-loop (elementId `wait-answer`, pr-escalation form):
194
- curl -sS -X POST __BASE__/../../tasks/api/complete -H 'content-type: application/json' \
195
- -d '{ "userTaskKey": "<key>", "variables": { "answer": "Cap retries at 5 and proceed." } }'
218
+ tool: completeUserTask { "userTaskKey": "<key>",
219
+ "variables": { "answer": "Cap retries at 5 and proceed." } }
196
220
 
197
221
  # Implementation (feature) task (elementId `feature-escalation`):
198
222
  # { "resolution": "answer", "answer": "…" } to resume, or { "resolution": "abandon" }
199
- curl -sS -X POST __BASE__/../../tasks/api/complete -H 'content-type: application/json' \
200
- -d '{ "userTaskKey": "<key>", "variables": { "resolution": "answer", "answer": "Use v2." } }'
223
+ tool: completeUserTask { "userTaskKey": "<key>",
224
+ "variables": { "resolution": "answer", "answer": "Use v2." } }
201
225
 
202
226
  # Plan-review (elementId `plan-review-decision`):
203
227
  # { "directive": "revise", "notes": "…" } (fresh review budget) or { "directive": "proceed" }
204
- curl -sS -X POST __BASE__/../../tasks/api/complete -H 'content-type: application/json' \
205
- -d '{ "userTaskKey": "<key>", "variables": { "directive": "revise", "notes": "Make issue-7 the seam." } }'
228
+ tool: completeUserTask { "userTaskKey": "<key>",
229
+ "variables": { "directive": "revise", "notes": "Make issue-7 the seam." } }
206
230
 
207
231
  # Trial-merge (elementId `trial-merge-decision`):
208
232
  # { "action": "proceed" | "rebase" | "abandon", "notes"?: "…" }
209
- curl -sS -X POST __BASE__/../../tasks/api/complete -H 'content-type: application/json' \
210
- -d '{ "userTaskKey": "<key>", "variables": { "action": "rebase", "notes": "Re-run after the fix." } }'
233
+ tool: completeUserTask { "userTaskKey": "<key>",
234
+ "variables": { "action": "rebase", "notes": "Re-run after the fix." } }
235
+ ```
236
+
237
+ ```bash
238
+ # No-MCP fallback — the app-owned complete-user-task door (prefer the tool).
239
+ # `variables` must carry the shape THIS task's escalation kind expects (see the per-kind
240
+ # tool forms above): `{ answer }` here is the PR review-loop form — a feature task wants
241
+ # `{ resolution, answer }`, plan-review `{ directive, notes }`, trial-merge `{ action, notes? }`.
242
+ curl -sS -X POST __BASE__/actions/complete-user-task -H 'content-type: application/json' \
243
+ -d '{ "userTaskKey": "<key>", "variables": { "answer": "Cap retries at 5 and proceed." } }'
211
244
  ```
212
245
 
213
246
  **Answer a PR escalation** (both the review-loop `wait-answer` and the merge-loop
214
247
  `wait-merge-answer` — both are now native user tasks answered the same way, #256).
215
- Use the PR key's parked user task and submit the `pr-escalation` form's `{ answer }`:
248
+ Use the PR key's parked user task and submit the `pr-escalation` form's `{ answer }`
249
+ through the `completeUserTask` tool; curl is the no-MCP fallback:
250
+
251
+ ```text
252
+ tool: completeUserTask { "userTaskKey": "<key>",
253
+ "variables": { "answer": "Yes — cap the retries at 5 and proceed." } }
254
+ ```
216
255
 
217
256
  ```bash
257
+ # No-MCP fallback:
218
258
  curl -sS -X POST __BASE__/actions/complete-user-task \
219
259
  -H 'content-type: application/json' \
220
260
  -d '{
@@ -223,9 +263,11 @@ curl -sS -X POST __BASE__/actions/complete-user-task \
223
263
  }'
224
264
  ```
225
265
 
226
- The `userTaskKey` comes from `GET /status` or the Tasks inbox. This is the ONE
227
- canonical answer door for every escalation kind; the merge loop no longer uses a
228
- durable `escalation-answered` message catch.
266
+ The `userTaskKey` comes from `listEscalations` (or `GET /status`/the Tasks inbox). This
267
+ is the ONE canonical answer door (`completeUserTask`) for every escalation kind with
268
+ `agentCompleteEscalation` the agent-assignee variant that records identity for
269
+ attribution (ADR 0046); the merge loop no longer uses a durable `escalation-answered`
270
+ message catch.
229
271
 
230
272
  If `NANO_PR_WEBHOOK_SECRET` is set on the deployment, add `-H "x-hook-secret: <secret>"`.
231
273
 
@@ -266,10 +308,35 @@ The app stores each PR/plan's engine **process instance key** in its `process_ke
266
308
  column and surfaces it as `processKey` in `/status`. That key is the join between the
267
309
  app's business view and the engine's execution view.
268
310
 
269
- **Find the instance for a PR:** take `processKey` from `/status`, then query the
270
- engine's Camunda-8 v2 REST API:
311
+ **Find the instance for a PR:** take `processKey` from `listActivePrs` (`/status`), then
312
+ query engine truth. **Prefer the framework `urban_debug_*` engine-debug tools** (owned by
313
+ the nano-ide urban runtime — see the crosswalk in §10); the `curl __ENGINE__/…` engine
314
+ REST calls are the no-MCP fallback:
315
+
316
+ ```text
317
+ PK = <processKey from listActivePrs>
318
+
319
+ # The instance itself (state, the BPMN process it is running, start time):
320
+ tool: urban_debug_search_process_instances { "filter": { "processInstanceKey": PK } }
321
+
322
+ # Where is it parked? — active jobs on the instance (a CREATED senior:pr-review job
323
+ # with a `worker` set means an agent has leased the round; none means it is queued).
324
+ # `urban_debug_search_jobs`/`_search_variables` are projected only where the framework
325
+ # offers them (sibling nano-ide epic); else drop to the curl door (see §10 crosswalk):
326
+ tool: urban_debug_search_jobs { "filter": { "processInstanceKey": PK, "state": "CREATED" } } # where projected
327
+
328
+ # Its variables (the aggregate/job payload the engine holds):
329
+ tool: urban_debug_search_variables { "filter": { "processInstanceKey": PK } } # where projected
330
+
331
+ # Is it dead-in-the-water on a technical fault? — active incidents:
332
+ tool: urban_debug_search_incidents { "filter": { "processInstanceKey": PK, "state": "ACTIVE" } }
333
+
334
+ # Which BPMN element/wait-state is it sitting on?
335
+ tool: urban_debug_search_element_instance_wait_states { "filter": { "processInstanceKey": PK } }
336
+ ```
271
337
 
272
338
  ```bash
339
+ # No-MCP fallback — the raw engine Camunda-8 v2 REST API:
273
340
  PK=<processKey-from-status>
274
341
 
275
342
  # The instance itself (state, the BPMN process it is running, start time):
@@ -277,8 +344,7 @@ curl -sS -X POST __ENGINE__/process-instances/search \
277
344
  -H 'content-type: application/json' \
278
345
  -d "{ \"filter\": { \"processInstanceKey\": \"$PK\" } }" | jq
279
346
 
280
- # Where is it parked? — active jobs on the instance (a CREATED senior:pr-review job
281
- # with a `worker` set means an agent has leased the round; none means it is queued):
347
+ # Where is it parked? — active jobs on the instance:
282
348
  curl -sS -X POST __ENGINE__/jobs/search \
283
349
  -H 'content-type: application/json' \
284
350
  -d "{ \"filter\": { \"processInstanceKey\": \"$PK\", \"state\": \"CREATED\" } }" | jq
@@ -294,15 +360,23 @@ curl -sS -X POST __ENGINE__/element-instances/search \
294
360
  -d "{ \"filter\": { \"processInstanceKey\": \"$PK\" } }" | jq
295
361
  ```
296
362
 
363
+ > The `urban_debug_search_jobs`, `urban_debug_search_variables` and
364
+ > `urban_debug_get_process_definition_xml` reads are owned by the sibling nano-ide urban
365
+ > epic; where a deployment's framework has not yet projected them, use the curl fallback
366
+ > for that row. The three always-present engine reads are
367
+ > `urban_debug_search_process_instances`, `urban_debug_search_element_instance_wait_states`
368
+ > and `urban_debug_search_incidents`.
369
+
297
370
  The app already mirrors an ACTIVE incident onto the PR row (`incident`/incident
298
371
  message), so a PR that shows an incident in the UI is parked on an engine fault —
299
- inspect it with `incidents/search` above. If the engine is not at the default, the
300
- deployment's engine base is `__ENGINE__` (set via `NANOBPMN_BASE_URL` or
301
- `CAMUNDA_REST_ADDRESS`).
372
+ inspect it with `urban_debug_search_incidents` (or `incidents/search`) above. If the
373
+ engine is not at the default, the deployment's engine base is `__ENGINE__` (set via
374
+ `NANOBPMN_BASE_URL` or `CAMUNDA_REST_ADDRESS`).
302
375
 
303
376
  **Relate an instance back to a PR:** if you have a `processKey` but not the PR, match
304
- it against `/status` (`.prs[] | select(.processKey == "<PK>")`). A terminal PR is no
305
- longer in `/status`; its instance has already completed or been cancelled.
377
+ it against `listActivePrs`/`/status` (`.prs[] | select(.processKey == "<PK>")`). A
378
+ terminal PR is no longer in `/status`; its instance has already completed or been
379
+ cancelled.
306
380
 
307
381
  ---
308
382
 
@@ -311,9 +385,15 @@ longer in `/status`; its instance has already completed or been cancelled.
311
385
  The behaviour is defined by durable BPMN processes and model-authored agent prompts —
312
386
  both live in the source repo, not in the job payload.
313
387
 
314
- - **Processes:** `resources/processes/*.bpmn` `convergence-loop.bpmn` (review),
315
- `merge-loop.bpmn` (merge/CI-fix/rebase), `plan-fanout.bpmn` (planning),
316
- `retro.bpmn`. These are the source of truth for routing. To understand *why* an
388
+ - **Processes:** to inspect the model an instance is **actually running**, prefer the
389
+ framework tool **`urban_debug_get_process_definition_xml`** (owned by the sibling
390
+ nano-ide urban epic see §10), which returns the deployed BPMN XML for a process
391
+ definition; that is engine truth and cannot drift from what is live. The repo copies
392
+ `resources/processes/*.bpmn` — `convergence-loop.bpmn` (review), `merge-loop.bpmn`
393
+ (merge/CI-fix/rebase), `plan-fanout.bpmn` (planning), `retro.bpmn` — are the
394
+ **source of truth in git** for routing, read them from the checkout when you want the
395
+ authored form (or have no MCP). They can lag a running deployment, so confirm against
396
+ the deployed XML when *why did this instance route here* matters. To understand *why* an
317
397
  instance went where it did, read the gateway conditions (FEEL expressions on the
318
398
  sequence flows) for the element it is parked on (§5).
319
399
  - **Prompts (agent base instructions):** `resources/prompts/*.md` — `review-round.md`,
@@ -369,11 +449,24 @@ Work through this order:
369
449
  `NANO_PR_REVIEW_WAIT_TIMEOUT` (default `PT20M`); the poller also re-nudges the
370
450
  reviewer periodically. If the reviewer bot is not provisioned on the repo, no
371
451
  review will ever land — that is a repo-config problem, not an app bug.
372
- 5. **Cancel + resubmit** as a last resort. Cancel the instance via the app (the UI's
373
- per-row Cancel, `POST /app/actions/cancel { "processInstanceKey": "<PK>" }`), which
374
- marks the PR `abandoned`, then re-submit the PR (§1) to start a fresh loop. Do not
375
- cancel a raw engine instance out from under the app — go through the app so its
376
- record state stays consistent.
452
+ 5. **Cancel + resubmit** as a last resort. Cancel through the **app-owned** door
453
+ **prefer the `cancelInstance` tool** (projected by sibling #667; the same
454
+ record-consistent path as the UI's per-row Cancel, `POST /app/actions/cancel
455
+ { "processInstanceKey": "<PK>" }`), which cancels
456
+ the engine instance **and** marks the PR `abandoned` so it drops out of
457
+ `listActivePrs`. Then re-submit the PR (§1) to start a fresh loop. **Do not** cancel a
458
+ raw engine instance out from under the app with the engine-level
459
+ `urban_debug_cancel_instance` tool (or a direct engine REST cancel) — that cancels the
460
+ token but leaves the PR row inconsistent. Always go through `cancelInstance` /
461
+ `POST /app/actions/cancel` so the app's record state stays consistent. Note the door
462
+ sits at `/app/actions/cancel`, **outside** the `__BASE__` control-API base (ADR 0059) —
463
+ until #667 projects `cancelInstance`, that curl door is the only cancel path. No-MCP
464
+ fallback:
465
+
466
+ ```bash
467
+ curl -sS -X POST __BASE__/../actions/cancel -H 'content-type: application/json' \
468
+ -d '{ "processInstanceKey": "<PK>" }'
469
+ ```
377
470
 
378
471
  ---
379
472
 
@@ -767,3 +860,66 @@ Semantics:
767
860
  `stage:"merged"` once its PR merges. So `wait[epic]` targeting a feature/epic **root issue**
768
861
  observes that thread's aggregate frontier and releases on `stage:"merged" && active:false` either
769
862
  way — see `getDeliveryGraphVocabulary` (the `epic` probe entry) for the structured contract.
863
+
864
+ ---
865
+
866
+ ## 10. Tool ↔ curl crosswalk (MCP-first, curl is the no-MCP fallback)
867
+
868
+ When you drive this instance over MCP, **call the projected tool** — every recipe above
869
+ leads with it. `curl __BASE__/…`/`curl __ENGINE__/…` is only the **no-MCP fallback** for an
870
+ agent that has no tools (see the runbook, `docs/mcp-runbook.md` §2/§3, and [§5
871
+ Fallback](mcp-runbook.md#5-fallback)). This table maps every guide action to its projected
872
+ tool name and its no-MCP fallback underneath (usually a curl door; a few rows fall back to reading a repo file instead).
873
+
874
+ App-owned operations (projected from this app's `openapi.yaml`; the `__BASE__` control API).
875
+ Rows tagged *(projected by sibling #NNN)* name a tool that lands with that sibling task —
876
+ until it does, only the curl door in the third column exists on the deployment:
877
+
878
+ | Guide action | Projected tool | No-MCP fallback |
879
+ |---|---|---|
880
+ | Which code is live (§0) | `getVersion` | `curl __BASE__/version` |
881
+ | Every PR in flight (§0/§5) | `listActivePrs` | `curl __BASE__/status` |
882
+ | 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) |
883
+ | Answer an escalation (§3) | `completeUserTask` (agent-assignee: `agentCompleteEscalation`) | `curl -X POST __BASE__/actions/complete-user-task` |
884
+ | 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__`)* |
885
+ | Publish a BPMN message (§7) | `postMessage` | `curl -X POST __BASE__/actions/message` |
886
+ | The operator guide itself | `getAgentInstructions` (full) / `getAgentGuide(section?)` (addressable) | `curl __BASE__/agent` / `curl __BASE__/agent/guide` |
887
+
888
+ The `listEscalations` no-MCP fallback that filters `/status` with `jq` lives here, not inside the
889
+ table cell above: a literal `|` can't be written in a GFM table cell without escaping it as `\|`,
890
+ and this guide is served **raw** (byte-for-byte, `__BASE__`/`__ENGINE__` aside) to no-MCP agents, so
891
+ the escaped form would be copied verbatim and the shell would treat `\|` as a literal argument
892
+ rather than a pipe. It's only a **quick indicator** — `/status` surfaces `openEscalation` as a
893
+ question string with **no `userTaskKey`**, so to actually *answer* one you still need the tasks
894
+ inbox (`curl __BASE__/../../tasks/api/tasks`) or the `listEscalations` tool for its `userTaskKey`
895
+ (§3). It selects the same `{ prKey, status, round, openEscalation }` fields as the §3 fallback for
896
+ consistency. Copy it from here:
897
+
898
+ ```sh
899
+ curl __BASE__/status | jq '.prs[] | select(.openEscalation != null)
900
+ | { prKey, status, round, openEscalation }'
901
+ ```
902
+
903
+
904
+ Framework engine-debug tools (owned by the nano-ide urban runtime, not this app; they front
905
+ the engine's Camunda-8 v2 REST API at `__ENGINE__`). The first three are always projected;
906
+ `urban_debug_search_jobs` / `urban_debug_search_variables` /
907
+ `urban_debug_get_process_definition_xml` are owned by the sibling nano-ide urban epic — use
908
+ them by name where the framework projects them, else drop to the curl door:
909
+
910
+ | Guide action | Projected tool | No-MCP fallback |
911
+ |---|---|---|
912
+ | Search process instances (§5) | `urban_debug_search_process_instances` | `curl -X POST __ENGINE__/process-instances/search` |
913
+ | Inspect element/wait states (§5) | `urban_debug_search_element_instance_wait_states` | `curl -X POST __ENGINE__/element-instances/search` |
914
+ | Active incidents (§5) | `urban_debug_search_incidents` | `curl -X POST __ENGINE__/incidents/search` |
915
+ | Jobs on an instance (§5) | `urban_debug_search_jobs` | `curl -X POST __ENGINE__/jobs/search` |
916
+ | Instance variables (§5) | `urban_debug_search_variables` | `curl -X POST __ENGINE__/variables/search` |
917
+ | Deployed BPMN model XML (§6) | `urban_debug_get_process_definition_xml` | read the source copy `resources/processes/*.bpmn` from the checkout |
918
+ | Engine-level cancel — **avoid** (§7) | `urban_debug_cancel_instance` | *(use the app-owned `cancelInstance` instead — engine cancel leaves the PR row inconsistent)* |
919
+
920
+ **Rule of thumb (agent-guide §5 fallback discipline):** if a row has a projected tool, use
921
+ it; reach for its curl door only when you are genuinely not on MCP (or the tool's sibling
922
+ task has not yet landed — see the *(projected by sibling #NNN)* tags above). The app-owned
923
+ cancel door (`cancelInstance`, #667) is always preferred over the engine-level
924
+ `urban_debug_cancel_instance`,
925
+ because only the app door transitions the PR/plan record to `abandoned`.
@@ -91,7 +91,14 @@ family). Ask:
91
91
 
92
92
  > *"Using workforce-local, show what's in flight and any open escalations."*
93
93
 
94
- The agent should call the status operation tool, not curl.
94
+ The agent should call the status operation tool (`listActivePrs`), not curl. The operator
95
+ guide's **tool↔curl crosswalk** (agent-guide §10, via `getAgentGuide(section="tool-crosswalk")`)
96
+ maps every guide action to its projected tool name and the curl door underneath — so a
97
+ tool-aware agent leads with the tool (`listActivePrs`, `getVersion`, `completeUserTask`,
98
+ the `urban_debug_*` engine reads — plus `listEscalations` and `cancelInstance` *where the
99
+ deployment projects them*; those two are sibling tasks #666/#667 and are not on a
100
+ deployment that predates them, so fall back to the curl door there) and treats curl as
101
+ the no-MCP fallback only.
95
102
 
96
103
  ## 3. Debug a wedged instance
97
104
 
@@ -99,10 +106,19 @@ The agent should call the status operation tool, not curl.
99
106
  > instance, compare engine truth against the app's projections, and tell me where
100
107
  > it's stuck."*
101
108
 
102
- The agent has: instance search, wait states, variables, incidents (engine truth) +
109
+ The agent has: instance search, wait states, incidents (engine truth) — plus
110
+ `variables` where the framework projects the `urban_debug_search_variables` read
111
+ (otherwise via the engine curl fallback) — plus
103
112
  the `urban_*` projection reads (app belief) + the operator guide (the
104
113
  `getAgentInstructions` tool) for the convergence-loop-specific meaning of each wedge
105
- shape. A wedge is frequently exactly a disagreement between the two planes.
114
+ shape. A wedge is frequently exactly a disagreement between the two planes. The guide's
115
+ **tool↔curl crosswalk** (agent-guide §10) names each engine-truth tool
116
+ (`urban_debug_search_process_instances` / `urban_debug_search_element_instance_wait_states`
117
+ / `urban_debug_search_incidents`, and where projected `urban_debug_search_jobs` /
118
+ `urban_debug_search_variables` / `urban_debug_get_process_definition_xml`) so the agent
119
+ reaches for the tool, not the raw `__ENGINE__` curl — and steers cancels to the app-owned
120
+ `cancelInstance` (projected by sibling #667; until it lands, the `/app/actions/cancel`
121
+ door the UI's Cancel uses), never the record-desyncing engine-level `urban_debug_cancel_instance`.
106
122
 
107
123
  ## 4. Guard posture
108
124
 
@@ -166,7 +182,8 @@ limit. Over MCP, prefer the **addressable** companion tool `getAgentGuide(sectio
166
182
 
167
183
  - **No argument** → a compact **table of contents**: every stable section id
168
184
  (`orient`, `submit-pr`, `submit-epic`, `escalations`, `lifecycle`, `debug`,
169
- `debug-models`, `unstick`, `raise-issue`, `delivery-graphs`) with a one-line summary.
185
+ `debug-models`, `unstick`, `raise-issue`, `delivery-graphs`, `tool-crosswalk`) with a
186
+ one-line summary.
170
187
  - **`section=<id>`** → **only** that section's markdown, small enough to fit a typical
171
188
  limit. An unknown id is rejected with `issues[{path,message}]` listing the valid ids.
172
189
 
package/openapi.yaml CHANGED
@@ -3118,6 +3118,39 @@ components:
3118
3118
  abandoned:
3119
3119
  type: boolean
3120
3120
  description: Derived from pull_requests.status; true ⇒ the run was cancelled and the agent must stop.
3121
+ CancelInstanceIntent:
3122
+ type: object
3123
+ additionalProperties: false
3124
+ required:
3125
+ - processInstanceKey
3126
+ properties:
3127
+ processInstanceKey:
3128
+ type: string
3129
+ description: The engine process-instance key of the run to cancel (the `process_key` column surfaced by `listActivePrs`/the Convergence page). Must be a string — engine keys are 64-bit and a numeric value risks silent JS precision loss.
3130
+ CancelInstanceResult:
3131
+ type: object
3132
+ required:
3133
+ - ok
3134
+ - processInstanceKey
3135
+ - state
3136
+ - reconciled
3137
+ properties:
3138
+ ok:
3139
+ type: boolean
3140
+ description: True when the engine confirmed the instance is terminated (the record is now — or derives as — `abandoned`). False ⇒ the engine did NOT stop the instance (surfaced as a 502).
3141
+ processInstanceKey:
3142
+ type: string
3143
+ description: The cancelled instance key, echoed back.
3144
+ state:
3145
+ type: string
3146
+ enum: [ACTIVE, COMPLETED, TERMINATED, gone]
3147
+ description: The instance state read back from the engine after the cancel attempt. `gone` ⇒ the engine has no record of the key (already cleaned up / never existed).
3148
+ reconciled:
3149
+ type: integer
3150
+ description: 1 when the immediate reconcile recorded the terminal source into the canonical instance-state projection (so the PR's derived tracking status flips to `abandoned` at once), else 0.
3151
+ error:
3152
+ type: string
3153
+ description: Present only on a non-terminal failure — the reason the cancel did not take.
3121
3154
  # ─────────────────────────────────────────────────────────────────────────────────────────────
3122
3155
  # MCP tool-schema convention (epic nano-workforce#605, S0 — the shared invariant every later slice
3123
3156
  # inherits). The Urban runtime projects THIS document into MCP tools (ADR 0067 — zero MCP server
@@ -3159,6 +3192,59 @@ paths:
3159
3192
  application/json:
3160
3193
  schema:
3161
3194
  $ref: "#/components/schemas/ErrorBody"
3195
+ /actions/cancel:
3196
+ post:
3197
+ operationId: cancelInstance
3198
+ summary: "Cancel a wedged run the app-owned, record-consistent way: terminate the engine instance AND flip its PR/plan record to `abandoned` (issue #667). The correct unstick — go through the app so its record state stays consistent, rather than engine-level `urban_debug_cancel_instance`, which cancels out from under the app and leaves the PR row inconsistent."
3199
+ security:
3200
+ - hookSecret: []
3201
+ - {}
3202
+ requestBody:
3203
+ required: true
3204
+ content:
3205
+ application/json:
3206
+ schema:
3207
+ # BEGIN generated:mcp-body source=#/components/schemas/CancelInstanceIntent (scripts/inline-mcp-bodies.ts — do not hand-edit)
3208
+ type: object
3209
+ additionalProperties: false
3210
+ required:
3211
+ - processInstanceKey
3212
+ properties:
3213
+ processInstanceKey:
3214
+ type: string
3215
+ description: The engine process-instance key of the run to cancel (the `process_key` column surfaced by `listActivePrs`/the Convergence page). Must be a string — engine keys are 64-bit and a numeric value risks silent JS precision loss.
3216
+ # END generated:mcp-body
3217
+ responses:
3218
+ "200":
3219
+ description: The instance was terminated; its record derives/flips to `abandoned` and drops out of `listActivePrs`.
3220
+ content:
3221
+ application/json:
3222
+ schema:
3223
+ $ref: "#/components/schemas/CancelInstanceResult"
3224
+ "400":
3225
+ description: Missing/invalid `processInstanceKey`.
3226
+ content:
3227
+ application/json:
3228
+ schema:
3229
+ $ref: "#/components/schemas/ErrorBody"
3230
+ "401":
3231
+ description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
3232
+ content:
3233
+ application/json:
3234
+ schema:
3235
+ $ref: "#/components/schemas/ErrorBody"
3236
+ "502":
3237
+ description: The engine did NOT stop the instance (the cancel was not committed); the run may still be live.
3238
+ content:
3239
+ application/json:
3240
+ schema:
3241
+ $ref: "#/components/schemas/CancelInstanceResult"
3242
+ "503":
3243
+ description: The app has no data source configured, so the cancel cannot be reconciled into the record.
3244
+ content:
3245
+ application/json:
3246
+ schema:
3247
+ $ref: "#/components/schemas/ErrorBody"
3162
3248
  /escalations:
3163
3249
  get:
3164
3250
  operationId: listEscalations
@@ -3628,7 +3714,7 @@ paths:
3628
3714
  example: delivery-graphs
3629
3715
  description: OPTIONAL stable section id (from the table of contents `getAgentGuide` returns
3630
3716
  with no argument), e.g. `orient`, `submit-pr`, `submit-epic`, `escalations`, `lifecycle`,
3631
- `debug`, `debug-models`, `unstick`, `raise-issue`, `delivery-graphs`. Omit it to get the
3717
+ `debug`, `debug-models`, `unstick`, `raise-issue`, `delivery-graphs`, `tool-crosswalk`. Omit it to get the
3632
3718
  table of contents. An unknown id yields a 400 listing the valid ids.
3633
3719
  responses:
3634
3720
  "200":
@@ -0,0 +1,138 @@
1
+ // Tests for POST /app/api/actions/cancel — operationId `cancelInstance` (issue #667, epic #664).
2
+ //
3
+ // The delegate is a thin, record-consistent door over the SAME primitive the UI's per-row Cancel
4
+ // uses (urban's `cancelInstanceReconciling`). These tests drive the REAL primitive through a fake
5
+ // engine — no second source of truth for the cancel-then-reconcile dance — and pin the delegate's
6
+ // own contract: body validation, string-key enforcement, and the ok→200 / not-committed→502 mapping. The
7
+ // `abandoned` transition itself is DERIVED off the instance-state projection the primitive feeds
8
+ // (ADR 0065), so terminating the instance through this door is exactly what makes the PR drop out of
9
+ // `listActivePrs`; that derivation is the framework's, exercised via the shared primitive here.
10
+ import { test } from "node:test";
11
+ import { assert, assertEquals } from "#test-assert";
12
+ import type { AppApi } from "@nanobpm/urban";
13
+ import { noopLog } from "../test/log.ts";
14
+ import handler from "./cancelInstance.ts";
15
+
16
+ interface FakeEngineOpts {
17
+ /** Reject the cancel call (an uncommitted termination). */
18
+ cancelThrows?: boolean;
19
+ /** The state `searchProcessInstances` reads back for the key. `undefined` ⇒ "gone" (empty). */
20
+ readBackState?: "ACTIVE" | "COMPLETED" | "TERMINATED" | undefined;
21
+ }
22
+
23
+ function makeApp(opts: FakeEngineOpts = {}): { app: AppApi; cancelCalls: string[] } {
24
+ const cancelCalls: string[] = [];
25
+ const engine = {
26
+ async cancelInstance({ processInstanceKey }: { processInstanceKey: string }) {
27
+ cancelCalls.push(processInstanceKey);
28
+ if (opts.cancelThrows) throw new Error("engine refused");
29
+ },
30
+ async searchProcessInstances() {
31
+ return opts.readBackState ? [{ state: opts.readBackState }] : [];
32
+ },
33
+ };
34
+ // hasDefaultSource:false makes the primitive's projection feed a clean no-op (no derived-status
35
+ // store to fake here) — the reconcile-through-projection path is the framework's own tested seam.
36
+ const data = { hasDefaultSource: () => false };
37
+ const app = { engine, data, log: noopLog() } as unknown as AppApi;
38
+ return { app, cancelCalls };
39
+ }
40
+
41
+ function req(headers: Record<string, string> = {}) {
42
+ return { method: "POST", path: "/app/api/actions/cancel", headers: new Headers(headers) };
43
+ }
44
+
45
+ // biome-ignore lint/suspicious/noExplicitAny: test-only invocation shim for the delegate.
46
+ async function callHandler(
47
+ h: typeof handler,
48
+ app: AppApi,
49
+ body: unknown,
50
+ headers: Record<string, string> = {},
51
+ // biome-ignore lint/suspicious/noExplicitAny: test-only invocation shim for the delegate.
52
+ ): Promise<any> {
53
+ return await h({ req: req(headers) as any, params: {}, query: {}, body } as any, app);
54
+ }
55
+
56
+ // biome-ignore lint/suspicious/noExplicitAny: test-only invocation shim for the delegate.
57
+ async function call(app: AppApi, body: unknown): Promise<any> {
58
+ return await callHandler(handler, app, body);
59
+ }
60
+
61
+ test("a missing processInstanceKey → 400 and never touches the engine", async () => {
62
+ const { app, cancelCalls } = makeApp();
63
+ assertEquals((await call(app, {})).status, 400);
64
+ assertEquals((await call(app, { processInstanceKey: " " })).status, 400);
65
+ assertEquals((await call(app, undefined)).status, 400);
66
+ assertEquals(cancelCalls.length, 0, "no cancel attempted without a key");
67
+ });
68
+
69
+ test("a committed cancel (engine terminates) → 200 ok, echoes the key, and cancels via the engine", async () => {
70
+ const { app, cancelCalls } = makeApp({ readBackState: "TERMINATED" });
71
+ const res = await call(app, { processInstanceKey: "2985" });
72
+ assertEquals(res.status, 200);
73
+ assertEquals(res.body.ok, true);
74
+ assertEquals(res.body.processInstanceKey, "2985");
75
+ assertEquals(res.body.state, "TERMINATED");
76
+ assertEquals(cancelCalls, ["2985"], "the shared primitive issued the engine cancel for the key");
77
+ });
78
+
79
+ test("an accepted cancel whose read model lags at ACTIVE is still trusted → 200 ok", async () => {
80
+ // A non-throwing cancelInstance is a committed 204; the primitive trusts it even if the read
81
+ // model still reports ACTIVE. The door must surface that as success, not a spurious 502.
82
+ const { app } = makeApp({ readBackState: "ACTIVE" });
83
+ const res = await call(app, { processInstanceKey: "42" });
84
+ assertEquals(res.status, 200);
85
+ assertEquals(res.body.ok, true);
86
+ });
87
+
88
+ test("a numeric processInstanceKey is rejected → 400 and never touches the engine", async () => {
89
+ // Engine keys are 64-bit and can exceed JS's safe-integer range, so a numeric JSON value has
90
+ // already lost precision before we see it — the door requires a string (matching the OpenAPI
91
+ // contract) rather than coercing a possibly-corrupted number.
92
+ const { app, cancelCalls } = makeApp({ readBackState: "TERMINATED" });
93
+ const res = await call(app, { processInstanceKey: 2985 });
94
+ assertEquals(res.status, 400);
95
+ assertEquals(cancelCalls.length, 0, "a non-string key never reaches the engine");
96
+ });
97
+
98
+ test("an uncommitted cancel (engine throws, instance still ACTIVE) → 502 not-ok with the reason", async () => {
99
+ const { app } = makeApp({ cancelThrows: true, readBackState: "ACTIVE" });
100
+ const res = await call(app, { processInstanceKey: "77" });
101
+ assertEquals(res.status, 502, "an unconfirmed termination must NOT be reported as success");
102
+ assertEquals(res.body.ok, false);
103
+ assert(typeof res.body.error === "string" && res.body.error.length > 0, "carries the failure reason");
104
+ });
105
+
106
+ test("no data source configured → 503 and never touches the engine (cannot reconcile the record)", async () => {
107
+ // The record-consistent door refuses to cancel when it has no data source to reconcile the
108
+ // terminal state into — a live cancel with no derived-status update would leave the record lying.
109
+ const { app, cancelCalls } = makeApp();
110
+ // Strip the data source the primitive would reconcile against.
111
+ (app as unknown as { data?: unknown }).data = undefined;
112
+ const res = await call(app, { processInstanceKey: "2985" });
113
+ assertEquals(res.status, 503);
114
+ assert("error" in res.body, "carries an error reason");
115
+ assertEquals(cancelCalls.length, 0, "no cancel attempted without a data source");
116
+ });
117
+
118
+ test("shared-secret guard: when NANO_PR_WEBHOOK_SECRET is set, a missing/wrong secret → 401; the right one passes", async () => {
119
+ const prev = process.env["NANO_PR_WEBHOOK_SECRET"];
120
+ process.env["NANO_PR_WEBHOOK_SECRET"] = "s3cr3t";
121
+ try {
122
+ // SECRET is captured at module load, so import a cache-busted copy to observe the guard.
123
+ const mod = await import(`./cancelInstance.ts?guard=${Date.now()}`);
124
+ const guarded = mod.default as typeof handler;
125
+ const { app, cancelCalls } = makeApp({ readBackState: "TERMINATED" });
126
+ const missing = await callHandler(guarded, app, { processInstanceKey: "2985" });
127
+ assertEquals(missing.status, 401, "no secret header is rejected");
128
+ const wrong = await callHandler(guarded, app, { processInstanceKey: "2985" }, { "x-hook-secret": "nope" });
129
+ assertEquals(wrong.status, 401, "a wrong secret is rejected");
130
+ assertEquals(cancelCalls.length, 0, "a rejected request never reaches the engine");
131
+ const ok = await callHandler(guarded, app, { processInstanceKey: "2985" }, { "x-hook-secret": "s3cr3t" });
132
+ assertEquals(ok.status, 200, "the correct secret is accepted");
133
+ assertEquals(cancelCalls, ["2985"], "only the authorized request issues the engine cancel");
134
+ } finally {
135
+ if (prev === undefined) delete process.env["NANO_PR_WEBHOOK_SECRET"];
136
+ else process.env["NANO_PR_WEBHOOK_SECRET"] = prev;
137
+ }
138
+ });
@@ -0,0 +1,80 @@
1
+ // POST /app/api/actions/cancel → operationId `cancelInstance` (issue #667, epic #664).
2
+ //
3
+ // The app-owned, RECORD-CONSISTENT unstick for a wedged run. agent-guide §7 documents
4
+ // `POST /actions/cancel { processInstanceKey }` as the CORRECT way to abort a run — "go through the
5
+ // app so its record state stays consistent" — but until now the only cancel projected over MCP was
6
+ // the engine-level `urban_debug_cancel_instance`, which §7 warns against because it terminates the
7
+ // instance out from under the app and leaves the PR/plan row inconsistent. This delegate closes that
8
+ // doc/impl drift by exposing the app door as a projected MCP tool.
9
+ //
10
+ // Derivation over duplication (AGENTS.md): it does NOT re-implement the cancel-then-transition
11
+ // dance. It routes through the EXACT same primitive the UI's per-row Cancel button uses —
12
+ // urban's `cancelInstanceReconciling` (the handler wired to the built-in `/app/actions/cancel`
13
+ // page action) — so there is ONE source of truth for terminating the instance and recording the
14
+ // terminal source into the canonical instance-state projection. The PR's tracking status then
15
+ // derives to `abandoned` (ADR 0065) and the row drops out of `listActivePrs`. The `instanceTracking`
16
+ // bindings come from the app's single accessor (`engineBackedBindings`), so the set can never drift
17
+ // from the reconciler's registry.
18
+ //
19
+ // The runtime validates the body against openapi.yaml (`processInstanceKey` required); the optional
20
+ // shared-secret guard stays HERE (the runtime does not enforce OpenAPI `security`): as a MUTATING
21
+ // door, when NANO_PR_WEBHOOK_SECRET is set callers must present it via the x-hook-secret header —
22
+ // mirroring `reconcileEngineState`/`agentCompleteEscalation`.
23
+
24
+ import { cancelInstanceReconciling } from "@nanobpm/urban";
25
+ import { engineBackedBindings } from "../app/instanceTracking.ts";
26
+ import { envVar } from "../app/version.ts";
27
+ import { defineOperation } from "../nano-generated/operations.ts";
28
+
29
+ const SECRET = envVar("NANO_PR_WEBHOOK_SECRET") ?? "";
30
+
31
+ export default defineOperation("cancelInstance", async ({ req, body }, app) => {
32
+ if (SECRET && req.headers.get("x-hook-secret") !== SECRET) {
33
+ app.log.warn("cancelInstance rejected: missing/invalid shared secret");
34
+ return { status: 401, body: { error: "unauthorized" } };
35
+ }
36
+ if (!app.data) {
37
+ app.log.warn("cancelInstance: no data source configured — cannot reconcile the record");
38
+ return { status: 503, body: { error: "no data source configured" } };
39
+ }
40
+
41
+ // Require a STRING key. Engine instance keys are 64-bit and can exceed JS's safe-integer range,
42
+ // so a numeric JSON value would already have lost precision before it reached us — accepting it
43
+ // (and coercing it back to a string) would silently cancel the wrong instance and also contradicts
44
+ // the OpenAPI `string` contract. Reject a non-string with a 400 rather than coerce.
45
+ const raw = body && typeof body === "object" ? body.processInstanceKey : undefined;
46
+ const processInstanceKey = typeof raw === "string" ? raw.trim() : "";
47
+ if (!processInstanceKey) {
48
+ return { status: 400, body: { error: "processInstanceKey is required and must be a string" } };
49
+ }
50
+
51
+ const result = await cancelInstanceReconciling(
52
+ app,
53
+ [...engineBackedBindings()],
54
+ processInstanceKey,
55
+ );
56
+ const responseBody = {
57
+ ok: result.ok,
58
+ processInstanceKey: result.processInstanceKey,
59
+ state: result.state,
60
+ reconciled: result.reconciled,
61
+ ...(result.error !== undefined ? { error: result.error } : {}),
62
+ };
63
+ if (result.ok) {
64
+ app.log.info("cancelInstance: instance terminated", {
65
+ processInstanceKey,
66
+ state: result.state,
67
+ reconciled: result.reconciled,
68
+ });
69
+ return { status: 200, body: responseBody };
70
+ }
71
+ // A !ok result means the engine did NOT stop the instance (the cancel was not committed); the run
72
+ // may still be live, so surface 502 — the same non-committed-cancel signal the built-in page
73
+ // action returns.
74
+ app.log.warn("cancelInstance: engine did not confirm termination", {
75
+ processInstanceKey,
76
+ state: result.state,
77
+ error: result.error,
78
+ });
79
+ return { status: 502, body: responseBody };
80
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.170.0",
3
+ "version": "0.171.0",
4
4
  "description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
5
5
  "type": "module",
6
6
  "main": "main.ts",
@@ -82,9 +82,9 @@
82
82
  "variant": "ghost",
83
83
  "modal": {
84
84
  "title": "Point your agent at Nano Workforce",
85
- "description": "Copy this prompt and paste it into your coding agent (Copilot, Claude, etc.). It connects the agent to this instance's MCP server \u2014 the workforce's operations become native tools (including the live operator guide as `getAgentInstructions`), so it can drive and debug your workforce; agents with no MCP client fall back to fetching the operator skill. Note: `/app/mcp` is served on the same HTTP surface as the rest of the app \u2014 reachable on loopback by default, and from a remote instance (merlin, an ngrok tunnel) only when the app is bound wide (`network.bind`) or fronted by a reverse proxy, per the MCP runbook.",
85
+ "description": "Copy this prompt and paste it into your coding agent (Copilot, Claude, etc.). It connects the agent to this instance's MCP server \u2014 the workforce's operations become native tools (including the live operator guide as `getAgentInstructions`, or its addressable companion `getAgentGuide(section?)` \u2014 preferred over the ~43KB blob to avoid tool-result overrun), so it can drive and debug your workforce; agents with no MCP client fall back to fetching the operator skill. Note: `/app/mcp` is served on the same HTTP surface as the rest of the app \u2014 reachable on loopback by default, and from a remote instance (merlin, an ngrok tunnel) only when the app is bound wide (`network.bind`) or fronted by a reverse proxy, per the MCP runbook.",
86
86
  "copyLabel": "Copy prompt",
87
- "copyText": "Add this running Nano Workforce instance as an MCP server, then help me drive and debug my workforce \u2014 a durable orchestration app that drives pull requests to review convergence against an automated reviewer, merges them, and can take a whole issue and plan \u2192 implement \u2192 converge it across a fleet of coding agents.\n\nRegister ONE MCP server entry per instance (streamable-HTTP transport, URL {{appBase}}app/mcp). Naming the instance makes targeting the wrong one structurally impossible \u2014 its tools are namespaced under that name:\n\n copilot mcp add --transport http workforce-local {{appBase}}app/mcp\n\nGive each instance its own entry (e.g. `workforce-merlin` for a LAN/remote node). In config form (~/.copilot/mcp-config.json or repo-scoped .mcp.json):\n\n {\n \"mcpServers\": {\n \"workforce-local\": {\n \"type\": \"http\",\n \"url\": \"{{appBase}}app/mcp\",\n \"tools\": [\"*\"]\n }\n }\n }\n\nIf this instance is secured with a shared secret (NANO_PR_WEBHOOK_SECRET), pass it on the MCP connection \u2014 reads AND mutations both require it:\n\n copilot mcp add --transport http workforce-local {{appBase}}app/mcp \\\n --header \"x-hook-secret: $NANO_PR_WEBHOOK_SECRET\"\n\nIn config form, add a `headers` entry alongside `url` on that server (per the MCP runbook) \u2014 the config path has no header flag, so omitting this yields 401s:\n\n \"headers\": { \"x-hook-secret\": \"$NANO_PR_WEBHOOK_SECRET\" }\n\nA Basic-Auth-fronted instance (behind a reverse proxy) additionally needs `Authorization: Basic \u2026` on the connection.\n\nMCP servers register at host startup \u2014 add the entry, THEN start a new session so its tools load. The `workforce-*` tools then appear (the full set of operations projected from this instance's OpenAPI contract, including the live operator guide itself as the `getAgentInstructions` read tool, plus the `urban_debug_*` family for inspecting a wedged instance's process instances, wait states, variables, and incidents).\n\nThe instance's operations are now native tools. Ask, naming the instance: \"Using workforce-local, show what's in flight and any open escalations.\" It should call the status tool, not curl. Operator-only doors (the delivery-graph stage/dispatch/dismiss lifecycle \u2014 the human click IS the approval) are deliberately not tools.\n\nNo MCP client? The curl path is unchanged \u2014 fetch and follow the live guide (the response is JSON with a `skill` markdown field), adding `-H \"x-hook-secret: <secret>\"` if this instance is secured:\n\n curl -sS {{appBase}}app/api/agent/skill\n\nThat skill bootstraps you to this instance's live operator guide at {{appBase}}app/api/agent (the same guide MCP exposes as `getAgentInstructions`). If you find a bug or a stuck process, the guide explains how to raise an issue or a PR against nanobpm/nano-workforce."
87
+ "copyText": "Add this running Nano Workforce instance as an MCP server, then help me drive and debug my workforce \u2014 a durable orchestration app that drives pull requests to review convergence against an automated reviewer, merges them, and can take a whole issue and plan \u2192 implement \u2192 converge it across a fleet of coding agents.\n\nRegister ONE MCP server entry per instance (streamable-HTTP transport, URL {{appBase}}app/mcp). Naming the instance makes targeting the wrong one structurally impossible \u2014 its tools are namespaced under that name:\n\n copilot mcp add --transport http workforce-local {{appBase}}app/mcp\n\nGive each instance its own entry (e.g. `workforce-merlin` for a LAN/remote node). In config form (~/.copilot/mcp-config.json or repo-scoped .mcp.json):\n\n {\n \"mcpServers\": {\n \"workforce-local\": {\n \"type\": \"http\",\n \"url\": \"{{appBase}}app/mcp\",\n \"tools\": [\"*\"]\n }\n }\n }\n\nIf this instance is secured with a shared secret (NANO_PR_WEBHOOK_SECRET), pass it on the MCP connection \u2014 reads AND mutations both require it:\n\n copilot mcp add --transport http workforce-local {{appBase}}app/mcp \\\n --header \"x-hook-secret: $NANO_PR_WEBHOOK_SECRET\"\n\nIn config form, add a `headers` entry alongside `url` on that server (per the MCP runbook) \u2014 the config path has no header flag, so omitting this yields 401s:\n\n \"headers\": { \"x-hook-secret\": \"$NANO_PR_WEBHOOK_SECRET\" }\n\nA Basic-Auth-fronted instance (behind a reverse proxy) additionally needs `Authorization: Basic \u2026` on the connection.\n\nMCP servers register at host startup \u2014 add the entry, THEN start a new session so its tools load. The `workforce-*` tools then appear (the full set of operations projected from this instance's OpenAPI contract, including the live operator guide itself as the `getAgentInstructions` read tool \u2014 or its addressable companion `getAgentGuide(section?)`, which the MCP runbook recommends over the ~43KB blob to avoid tool-result overrun \u2014 plus the `urban_debug_*` family for inspecting a wedged instance's process instances, wait states, and incidents).\n\nThe instance's operations are now native tools. Ask, naming the instance: \"Using workforce-local, show what's in flight and any open escalations.\" It should call the status tool, not curl. Operator-only doors (the delivery-graph stage/dispatch/dismiss lifecycle \u2014 the human click IS the approval) are deliberately not tools.\n\nNo MCP client? The curl path is unchanged \u2014 fetch and follow the live guide (the response is JSON with a `skill` markdown field), adding `-H \"x-hook-secret: <secret>\"` if this instance is secured:\n\n curl -sS {{appBase}}app/api/agent/skill\n\nThat skill bootstraps you to this instance's live operator guide at {{appBase}}app/api/agent (the same guide MCP exposes as `getAgentInstructions`, or section-addressably as `getAgentGuide`). If you find a bug or a stuck process, the guide explains how to raise an issue or a PR against nanobpm/nano-workforce."
88
88
  }
89
89
  }
90
90
  },