@nanobpm/nano-workforce 0.149.0 → 0.150.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 +6 -0
- package/README.md +36 -3
- package/SPEC.md +1 -1
- package/app/mcpExclusions.test.ts +83 -0
- package/docs/mcp-runbook.md +133 -0
- package/openapi.yaml +16 -0
- package/package.json +2 -2
- package/skills/README.md +7 -3
- package/skills/nano-workforce/SKILL.md +122 -75
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.150.0](https://github.com/nanobpm/nano-workforce/compare/v0.149.0...v0.150.0) (2026-08-28)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* adopt Urban MCP surface — x-mcp exclusions, runbook, MCP-first skill ([#575](https://github.com/nanobpm/nano-workforce/issues/575)) ([44e89e4](https://github.com/nanobpm/nano-workforce/commit/44e89e40b6cbff0e4c7de32dad76d35f6d534cc6)), closes [nano-ide#488](https://github.com/nanobpm/nano-ide/issues/488) [nano-ide#488](https://github.com/nanobpm/nano-ide/issues/488) [#567](https://github.com/nanobpm/nano-workforce/issues/567) [#567](https://github.com/nanobpm/nano-workforce/issues/567) [nano-ide#488](https://github.com/nanobpm/nano-ide/issues/488)
|
|
6
|
+
|
|
1
7
|
## [0.149.0](https://github.com/nanobpm/nano-workforce/compare/v0.148.2...v0.149.0) (2026-08-27)
|
|
2
8
|
|
|
3
9
|
### Features
|
package/README.md
CHANGED
|
@@ -282,7 +282,7 @@ agent at that URL to author, compile, and submit a graph unaided. See
|
|
|
282
282
|
| `NANO_PR_GITHUB_TRANSPORT` | `auto` | how the poller reads GitHub: `gh` (host CLI), `token` (`GITHUB_TOKEN` over HTTP), or `auto` |
|
|
283
283
|
| `NANO_PR_POLL_MS` | `60000` | review-ready poll interval |
|
|
284
284
|
| `NANO_PR_MAX_ROUNDS` | `20` | default cap: escalate after N rounds (per-submit override via the form / the `maxRounds` field on `start/convergence-loop`; clamped 1–100) |
|
|
285
|
-
| `NANO_PR_WEBHOOK_SECRET` | — | optional shared secret (`
|
|
285
|
+
| `NANO_PR_WEBHOOK_SECRET` | — | optional shared secret (`x-hook-secret`) for guarded operations (e.g. `GET /app/api/agent`, `/app/api/version`, `/app/api/status`); unset = open |
|
|
286
286
|
| `NANO_PR_AUTO_MERGE` | `1` | after convergence, run the merge stage; `0` = stop at `converged` (review-only). Per-submit override via the `convergeOnly` field on `start/convergence-loop` (`true` forces review-only for that PR) |
|
|
287
287
|
| `NANO_PR_MERGE_METHOD` | `squash` | merge method: `squash`, `merge`, or `rebase` |
|
|
288
288
|
| `NANO_PR_MERGE_ADMIN` | `0` | pass `--admin` to override failing non-required checks (use with care) |
|
|
@@ -452,9 +452,42 @@ curl -sS http://localhost:3000/app/api/agent | jq -r .instructions
|
|
|
452
452
|
```
|
|
453
453
|
|
|
454
454
|
Like `/version` and `/status`, this endpoint honours the optional
|
|
455
|
-
`NANO_PR_WEBHOOK_SECRET` guard (`
|
|
455
|
+
`NANO_PR_WEBHOOK_SECRET` guard (`x-hook-secret` header): when that secret is set it
|
|
456
456
|
returns `401` without the matching header; unset = open. The source lives in
|
|
457
|
-
`
|
|
457
|
+
`docs/agent-guide.md`.
|
|
458
|
+
|
|
459
|
+
### Configure an agent over MCP
|
|
460
|
+
|
|
461
|
+
Where your agent supports **MCP**, prefer it over the curl path above. The Urban
|
|
462
|
+
runtime serves a Streamable-HTTP MCP endpoint at **`/app/mcp`** for every instance and
|
|
463
|
+
projects this app's `openapi.yaml` into tools with **zero MCP code in nwf** — the app
|
|
464
|
+
operations (including the operator guide, projected from `GET /app/api/agent` as the
|
|
465
|
+
`getAgentInstructions` read tool), a framework-owned engine-debug tool family (process
|
|
466
|
+
instances, wait states, variables, incidents), the `urban_*` projection reads, and the
|
|
467
|
+
runtime's derived **system brief** as an MCP resource plus an orientation prompt
|
|
468
|
+
(ADR 0067, nano-ide#488). Register one server entry per instance and name
|
|
469
|
+
it when you drive — tool calls are namespaced per entry, so the wrong-instance mistake
|
|
470
|
+
becomes impossible:
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
copilot mcp add --transport http workforce-local http://localhost:3000/app/mcp
|
|
474
|
+
# guarded instance: add the app secret as a header (never in chat)
|
|
475
|
+
copilot mcp add --transport http workforce-merlin http://merlin.local:3000/app/mcp \
|
|
476
|
+
--header "x-hook-secret: $NANO_PR_WEBHOOK_SECRET"
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
When `NANO_PR_WEBHOOK_SECRET` is unset, reads and mutations both work from loopback with
|
|
480
|
+
no credential; when it is set, that secret is required as an `x-hook-secret` header on
|
|
481
|
+
**both** — read endpoints like `GET /app/api/agent` and `GET /app/api/version` also
|
|
482
|
+
return `401` without it. **Operator-only doors stay operator-only** —
|
|
483
|
+
delivery-graph **dispatch** (and the stage/dismiss lifecycle) is `x-mcp`-excluded, so the
|
|
484
|
+
human clicking Dispatch in the cockpit remains the approval (ADR 0005). MCP is a **third
|
|
485
|
+
door**: `GET /app/api/agent` and `GET /app/api/agent/skill` are unchanged for agents
|
|
486
|
+
without it.
|
|
487
|
+
|
|
488
|
+
The full recipe — multiple instances, Basic-Auth-fronted instances, LAN exposure,
|
|
489
|
+
verification and wedged-instance debugging prompts — is the **agent-configuration
|
|
490
|
+
runbook**: [`docs/mcp-runbook.md`](docs/mcp-runbook.md).
|
|
458
491
|
|
|
459
492
|
---
|
|
460
493
|
|
package/SPEC.md
CHANGED
|
@@ -492,7 +492,7 @@ queries skip (`merging`), so a slow pass can't double-signal.
|
|
|
492
492
|
| `GITHUB_TOKEN` | — | GitHub API (poller + agent) |
|
|
493
493
|
| `NANO_PR_POLL_MS` | 60000 | poll interval |
|
|
494
494
|
| `NANO_PR_MAX_ROUNDS` | 20 | default round cap (per-submit `maxRounds` override, clamped 1–100) |
|
|
495
|
-
| `NANO_PR_WEBHOOK_SECRET` | — | optional shared secret (`
|
|
495
|
+
| `NANO_PR_WEBHOOK_SECRET` | — | optional shared secret (`x-hook-secret`) for guarded operations (e.g. `/app/api/agent`, `/app/api/version`, `/app/api/status`) |
|
|
496
496
|
| `NANO_PR_AUTO_MERGE` | 1 | run the merge stage after convergence (`0` = review-only; per-submit `convergeOnly: true` override) |
|
|
497
497
|
| `NANO_PR_MERGE_METHOD` | squash | `squash` \| `merge` \| `rebase` |
|
|
498
498
|
| `NANO_PR_MERGE_ADMIN` | 0 | pass `--admin` on merge |
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Authoring guard for the `x-mcp` MCP tool-exclusion switch (ADR 0067 §2, nano-ide#488 slice 3;
|
|
2
|
+
// adopted here as slice 4, issue #567).
|
|
3
|
+
//
|
|
4
|
+
// The runtime-served MCP surface (`/app/mcp`) projects every non-excluded `openapi.yaml` operation
|
|
5
|
+
// into an MCP tool. An operation opts OUT with the `x-mcp` extension — `x-mcp: false` or
|
|
6
|
+
// `x-mcp: { exclude: true }`; any other value (or its absence) leaves it exposed. That switch is
|
|
7
|
+
// security-relevant: it is the one authoring control that keeps an operator-only door off the
|
|
8
|
+
// agent-facing tool surface, so this test pins the intended exclusion set at the spec level.
|
|
9
|
+
//
|
|
10
|
+
// Derivation over duplication (AGENTS.md): we do NOT re-implement the exclusion rule or the
|
|
11
|
+
// path×method walk. We read the projection from the SAME `@nanobpm/urban` walker the runtime MCP
|
|
12
|
+
// module builds its live tool list from — `parseSpec` + `collectOperations`, whose `mcpExcluded`
|
|
13
|
+
// flag IS `isMcpExcluded` applied at parse time (openapi/spec.ts). This app-side test is therefore
|
|
14
|
+
// the authoring assertion that OUR operator doors carry the switch and that no agent-facing
|
|
15
|
+
// operation was excluded by accident, checked against the exact rule the runtime honours — no
|
|
16
|
+
// second source of truth to drift from it.
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { test } from "node:test";
|
|
21
|
+
import { collectOperations, parseSpec } from "@nanobpm/urban/toolkit";
|
|
22
|
+
import { assert, assertEquals } from "#test-assert";
|
|
23
|
+
|
|
24
|
+
const REPO_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
25
|
+
const SPEC_PATH = join(REPO_ROOT, "openapi.yaml");
|
|
26
|
+
|
|
27
|
+
// Every operation the runtime projection enumerates, with its `x-mcp` exclusion flag — read from
|
|
28
|
+
// the framework walker so this test and the live tool surface can never diverge.
|
|
29
|
+
function projectedOperations() {
|
|
30
|
+
return collectOperations(parseSpec(readFileSync(SPEC_PATH, "utf8")));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The operator-only cockpit doors: the staged delivery-graph lifecycle (stage -> dispatch ->
|
|
34
|
+
// dismiss) whose approval is a human click in the cockpit (ADR 0005 Decision 7). These — and ONLY
|
|
35
|
+
// these — must be excluded from the projected MCP tool surface.
|
|
36
|
+
const EXPECTED_EXCLUDED = ["stageDeliveryGraph", "dispatchDeliveryGraph", "dismissProposal"];
|
|
37
|
+
|
|
38
|
+
// A representative set of agent-facing operations that MUST stay exposed as tools — the drive doors
|
|
39
|
+
// (submit PR / epic / delivery-graph set), the pure compile/preview doors, and the read/orientation
|
|
40
|
+
// surface an agent debugging a wedged instance relies on.
|
|
41
|
+
const EXPECTED_EXPOSED = [
|
|
42
|
+
"startConvergenceLoop",
|
|
43
|
+
"startPlanFanout",
|
|
44
|
+
"startEpicSet",
|
|
45
|
+
"compileDeliveryGraph",
|
|
46
|
+
"previewDeliveryGraph",
|
|
47
|
+
"listStagedProposals",
|
|
48
|
+
"listActivePrs",
|
|
49
|
+
"getAgentInstructions",
|
|
50
|
+
"getVersion",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
test("x-mcp excludes exactly the operator-only delivery-graph lifecycle doors", () => {
|
|
54
|
+
const excluded = projectedOperations()
|
|
55
|
+
.filter((op) => op.mcpExcluded)
|
|
56
|
+
.map((op) => op.operationId)
|
|
57
|
+
.sort();
|
|
58
|
+
assertEquals(excluded, [...EXPECTED_EXCLUDED].sort());
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("x-mcp leaves the agent-facing drive/preview/read operations exposed", () => {
|
|
62
|
+
const byId = new Map(projectedOperations().map((op) => [op.operationId, op]));
|
|
63
|
+
for (const id of EXPECTED_EXPOSED) {
|
|
64
|
+
const op = byId.get(id);
|
|
65
|
+
assert(op, `expected operation ${id} to exist in openapi.yaml`);
|
|
66
|
+
assertEquals(op.mcpExcluded, false, `operation ${id} must NOT be x-mcp excluded`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// The operator guide (workflow knowledge) is discoverable over MCP as the projected
|
|
71
|
+
// `getAgentInstructions` READ tool — a safe GET, so the runtime projects it as a read tool
|
|
72
|
+
// (not `x-mcp`-excluded) on loopback, instance-keyed by the same `resolveApiBase` rewriting
|
|
73
|
+
// as its HTTP route. ("Read tool" describes the projected verb, not an auth posture: the
|
|
74
|
+
// route still honours the app's optional `x-hook-secret` guard when `NANO_PR_WEBHOOK_SECRET`
|
|
75
|
+
// is set.) This is how the guide's prose reaches an MCP agent; the framework separately
|
|
76
|
+
// serves its derived system brief as a resource. Pin the projection facts so an accidental
|
|
77
|
+
// exclusion (or a verb change that would flip it into a mutation) fails CI.
|
|
78
|
+
test("the operator guide is projected as a read tool (GET, not x-mcp excluded) over MCP", () => {
|
|
79
|
+
const op = projectedOperations().find((o) => o.operationId === "getAgentInstructions");
|
|
80
|
+
assert(op, "getAgentInstructions must exist in openapi.yaml");
|
|
81
|
+
assertEquals(op.mcpExcluded, false, "the operator guide must stay exposed over MCP");
|
|
82
|
+
assertEquals(op.method, "get", "the operator guide must be a safe GET so it projects as a read tool");
|
|
83
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Configure an agent to drive/debug this workforce over MCP
|
|
2
|
+
|
|
3
|
+
> Adoption of the Urban runtime-served MCP surface ([ADR 0067](https://github.com/nanobpm/nano-ide/blob/main/docs/adr/0067-runtime-served-mcp-surface.md),
|
|
4
|
+
> nano-ide#488) — first consumer, nano-workforce#567. Written against the
|
|
5
|
+
> [Copilot CLI](https://github.com/github/copilot-cli) (the harness nwf's fleet
|
|
6
|
+
> uses). Claude/Cursor equivalents use the same server entries.
|
|
7
|
+
|
|
8
|
+
The Urban runtime serves a Streamable-HTTP MCP endpoint at **`/app/mcp`** for every
|
|
9
|
+
hosted app and projects this app's `openapi.yaml` into tools — **zero MCP server code
|
|
10
|
+
in nwf**. An MCP-capable agent gets nwf's operations (submit work, answer escalations,
|
|
11
|
+
read status, and the operator **guide** itself — `GET /app/api/agent`, projected as the
|
|
12
|
+
`getAgentInstructions` read tool), the framework-owned engine-debug tool family (process
|
|
13
|
+
instances, wait states, variables, incidents), the `urban_*` projection reads, and the
|
|
14
|
+
runtime's derived **system brief** as an MCP resource plus an orientation prompt — all
|
|
15
|
+
namespaced per server entry.
|
|
16
|
+
|
|
17
|
+
This replaces the SKILL.md instance-probing dance for MCP clients: naming the
|
|
18
|
+
instance (`"drive workforce-merlin"`) makes the wrong-instance mistake structurally
|
|
19
|
+
impossible. The named-instance registry (`NANO_WORKFORCE_INSTANCES` /
|
|
20
|
+
`~/.config/nano-workforce/instances.json`) remains the source for the fallback path
|
|
21
|
+
and a handy list of the entries to register here.
|
|
22
|
+
|
|
23
|
+
MCP is a **third door**, not a replacement: `GET /app/api/agent` (the live guide) and
|
|
24
|
+
`GET /app/api/agent/skill` are unchanged for agents without MCP — see
|
|
25
|
+
[§5 Fallback](#5-fallback).
|
|
26
|
+
|
|
27
|
+
## 1. One MCP server entry per instance
|
|
28
|
+
|
|
29
|
+
In `~/.copilot/mcp-config.json` (user-wide) or `.mcp.json` (repo-scoped):
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"workforce-local": {
|
|
35
|
+
"type": "http",
|
|
36
|
+
"url": "http://localhost:3000/app/mcp",
|
|
37
|
+
"tools": ["*"]
|
|
38
|
+
},
|
|
39
|
+
"workforce-merlin": {
|
|
40
|
+
"type": "http",
|
|
41
|
+
"url": "http://merlin.local:3000/app/mcp",
|
|
42
|
+
"headers": { "x-hook-secret": "$NANO_PR_WEBHOOK_SECRET" },
|
|
43
|
+
"tools": ["*"]
|
|
44
|
+
},
|
|
45
|
+
"workforce-remote": {
|
|
46
|
+
"type": "http",
|
|
47
|
+
"url": "https://<subdomain>.ngrok.app/app/mcp",
|
|
48
|
+
"headers": { "x-hook-secret": "$NANO_PR_WEBHOOK_SECRET" },
|
|
49
|
+
"tools": ["*"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or from the terminal:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
copilot mcp add --transport http workforce-local http://localhost:3000/app/mcp
|
|
59
|
+
# add --header for a guarded instance:
|
|
60
|
+
copilot mcp add --transport http workforce-merlin http://merlin.local:3000/app/mcp \
|
|
61
|
+
--header "x-hook-secret: $NANO_PR_WEBHOOK_SECRET"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Tool calls are namespaced per server entry, so the instance you name is the instance
|
|
65
|
+
you drive.
|
|
66
|
+
|
|
67
|
+
### Instance behind Basic Auth? You need *both* headers
|
|
68
|
+
|
|
69
|
+
Two different layers. `x-hook-secret` is the **app's own** guard (checked by nwf in
|
|
70
|
+
the operation handler, only when `NANO_PR_WEBHOOK_SECRET` is set). **Basic Auth** is
|
|
71
|
+
enforced by whatever **fronts** the instance (ngrok edge, console proxy) and 401s
|
|
72
|
+
*before* the request ever reaches nwf:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
"headers": {
|
|
76
|
+
"Authorization": "Basic <base64(user:pass)>",
|
|
77
|
+
"x-hook-secret": "..."
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Generate the blob with `printf '%s' 'user:pass' | base64` — `echo | base64` appends a
|
|
82
|
+
newline and yields the wrong value. The proxy must forward custom headers for
|
|
83
|
+
`x-hook-secret` to survive (most do by default). Base64 is encoding, not encryption:
|
|
84
|
+
only use Basic Auth over HTTPS. The fallback curl path needs both too:
|
|
85
|
+
`curl -u user:pass -H "x-hook-secret: …"`.
|
|
86
|
+
|
|
87
|
+
## 2. Verify discovery
|
|
88
|
+
|
|
89
|
+
New agent session → the `workforce-*` tools appear (app operations + the engine-debug
|
|
90
|
+
family). Ask:
|
|
91
|
+
|
|
92
|
+
> *"Using workforce-local, show what's in flight and any open escalations."*
|
|
93
|
+
|
|
94
|
+
The agent should call the status operation tool, not curl.
|
|
95
|
+
|
|
96
|
+
## 3. Debug a wedged instance
|
|
97
|
+
|
|
98
|
+
> *"workforce-local: PR nanobpm/nano-workforce#123 looks wedged — find its process
|
|
99
|
+
> instance, compare engine truth against the app's projections, and tell me where
|
|
100
|
+
> it's stuck."*
|
|
101
|
+
|
|
102
|
+
The agent has: instance search, wait states, variables, incidents (engine truth) +
|
|
103
|
+
the `urban_*` projection reads (app belief) + the operator guide (the
|
|
104
|
+
`getAgentInstructions` tool) for the convergence-loop-specific meaning of each wedge
|
|
105
|
+
shape. A wedge is frequently exactly a disagreement between the two planes.
|
|
106
|
+
|
|
107
|
+
## 4. Guard posture
|
|
108
|
+
|
|
109
|
+
When `NANO_PR_WEBHOOK_SECRET` is **unset**, both reads (status, instances, incidents,
|
|
110
|
+
projections, the operator guide) and mutations (cancel/retry/resolve, `start/*`
|
|
111
|
+
operations, answering escalations) work from loopback with no credential. When it **is
|
|
112
|
+
set**, the guard is not mutation-only: that secret is required as an `x-hook-secret`
|
|
113
|
+
header on **both reads and mutations** — read endpoints like `GET /app/api/agent` and
|
|
114
|
+
`GET /app/api/version` also return `401` without it. Put it in the server entry's
|
|
115
|
+
`headers`, never in chat. For a remote fleet,
|
|
116
|
+
`NANO_WORKFORCE_BASE_URL` reachability rules apply unchanged, and LAN exposure of
|
|
117
|
+
`/app/mcp` follows the same `network.bind` manifest setting as the rest of the app's
|
|
118
|
+
HTTP surface.
|
|
119
|
+
|
|
120
|
+
**Operator-only doors stay operator-only.** The staged delivery-graph lifecycle —
|
|
121
|
+
`stageDeliveryGraph`, `dispatchDeliveryGraph`, `dismissProposal` — is `x-mcp`-excluded
|
|
122
|
+
from the projected tool surface (ADR 0067 §2): the human clicking **Dispatch** in the
|
|
123
|
+
cockpit *is* the approval (ADR 0005 Decision 7), so an agent cannot dispatch a delivery
|
|
124
|
+
graph through MCP. Agents author graphs through the pure `compileDeliveryGraph` /
|
|
125
|
+
`previewDeliveryGraph` doors, which stay exposed.
|
|
126
|
+
|
|
127
|
+
## 5. Fallback
|
|
128
|
+
|
|
129
|
+
Agents without MCP are unchanged — resolve the instance, then
|
|
130
|
+
`curl -sS $BASE/agent | jq -r .instructions`, or load the
|
|
131
|
+
[`nano-workforce` skill](../skills/nano-workforce/SKILL.md), which fetches the same
|
|
132
|
+
live guide. `GET /app/api/agent` and `GET /app/api/agent/skill` keep working exactly as
|
|
133
|
+
before.
|
package/openapi.yaml
CHANGED
|
@@ -3134,6 +3134,12 @@ paths:
|
|
|
3134
3134
|
/actions/delivery-graph/stage:
|
|
3135
3135
|
post:
|
|
3136
3136
|
operationId: stageDeliveryGraph
|
|
3137
|
+
# x-mcp exclusion (ADR 0067 §2 / nano-ide#488): operator-only cockpit door, kept OFF the
|
|
3138
|
+
# runtime-projected MCP tool surface. The staged-proposal lifecycle (stage -> dispatch ->
|
|
3139
|
+
# dismiss) is the human approval path (ADR 0005 Decision 7 — the operator's click IS the
|
|
3140
|
+
# approval); agents author graphs through the pure compile/preview doors only, never stage.
|
|
3141
|
+
x-mcp:
|
|
3142
|
+
exclude: true
|
|
3137
3143
|
summary: UI JSON-paste STAGE — parse a pasted delivery-graph JSON string, compile it and stage it for operator dispatch. (ADR 0005 Decision 7 / #460 / #516)
|
|
3138
3144
|
description: >-
|
|
3139
3145
|
The human-facing UI JSON-paste STAGE ingress — the deliberate commit half of the preview/stage
|
|
@@ -3167,6 +3173,11 @@ paths:
|
|
|
3167
3173
|
/actions/delivery-graph/dispatch:
|
|
3168
3174
|
post:
|
|
3169
3175
|
operationId: dispatchDeliveryGraph
|
|
3176
|
+
# x-mcp exclusion (ADR 0067 §2 / nano-ide#488): the canonical operator-only door. Dispatch
|
|
3177
|
+
# approval IS a human clicking Dispatch in the cockpit (ADR 0005 Decision 7) — there is no
|
|
3178
|
+
# replayable token and no agent-facing dispatch handle, so it is never a projected MCP tool.
|
|
3179
|
+
x-mcp:
|
|
3180
|
+
exclude: true
|
|
3170
3181
|
summary: OPERATOR DISPATCH — launch a staged delivery-graph proposal by its digest (idempotent). (ADR 0005 Decision 7 / #460)
|
|
3171
3182
|
description: >-
|
|
3172
3183
|
The OPERATOR-ONLY dispatch door (ADR 0005 Decision 7, issue #460). The cockpit's staged-proposals
|
|
@@ -3212,6 +3223,11 @@ paths:
|
|
|
3212
3223
|
/actions/delivery-graph/dismiss:
|
|
3213
3224
|
post:
|
|
3214
3225
|
operationId: dismissProposal
|
|
3226
|
+
# x-mcp exclusion (ADR 0067 §2 / nano-ide#488): operator-only cockpit door — discarding a
|
|
3227
|
+
# staged proposal as noise is a human decision in the same approval class as dispatch, so it
|
|
3228
|
+
# stays off the projected MCP tool surface.
|
|
3229
|
+
x-mcp:
|
|
3230
|
+
exclude: true
|
|
3215
3231
|
summary: OPERATOR DISMISS — discard a staged delivery-graph proposal by its digest as noise (idempotent). (#520)
|
|
3216
3232
|
description: >-
|
|
3217
3233
|
The OPERATOR-ONLY dismiss door (#520). The cockpit's staged-proposals grid posts the `digest` of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.150.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",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@nanobpm/agentic": "^0.4.0",
|
|
62
|
-
"@nanobpm/urban": "^0.
|
|
62
|
+
"@nanobpm/urban": "^0.86.0",
|
|
63
63
|
"bpmn-auto-layout": "^2.0.0-alpha.2"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
package/skills/README.md
CHANGED
|
@@ -7,9 +7,13 @@ loads on demand when its `description` matches the task.
|
|
|
7
7
|
## `nano-workforce`
|
|
8
8
|
|
|
9
9
|
A **thin bootstrap** that teaches any agent to operate a running Nano Workforce
|
|
10
|
-
instance
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
instance. Where the client supports **MCP**, it registers the instance's `/app/mcp`
|
|
11
|
+
server (its tools appear automatically, including the operator guide as the
|
|
12
|
+
`getAgentInstructions` tool — ADR 0067); where
|
|
13
|
+
it does not, it resolves the instance base URL and fetches the instance's *live*
|
|
14
|
+
operator guide (`GET /app/api/agent`), then follows it. Either way it deliberately holds
|
|
15
|
+
no endpoint detail of its own — the live, version-matched surface is the source of truth.
|
|
16
|
+
See [`docs/mcp-runbook.md`](../docs/mcp-runbook.md) for the MCP server-entry recipes.
|
|
13
17
|
|
|
14
18
|
### Install
|
|
15
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nano-workforce
|
|
3
|
-
description: Drive and debug a running Nano Workforce instance — submit PRs for review convergence, submit issues/epics for plan→implement→converge, submit agent-authored delivery graphs (ADR 0005), answer escalations, and unstick stuck instances. Use when the user asks to operate, drive, submit work to, or debug their Nano Workforce.
|
|
3
|
+
description: Drive and debug a running Nano Workforce instance — submit PRs for review convergence, submit issues/epics for plan→implement→converge, submit agent-authored delivery graphs (ADR 0005), answer escalations, and unstick stuck instances. Prefer the instance's MCP server (add it → its tools appear); fall back to the live operator guide. Use when the user asks to operate, drive, submit work to, or debug their Nano Workforce.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Nano Workforce operator skill
|
|
@@ -12,27 +12,88 @@ implements → converges** them across a fleet of coding agents, and runs
|
|
|
12
12
|
DAGs — ADR 0005).
|
|
13
13
|
|
|
14
14
|
**This skill is a thin bootstrap by design.** It does not describe the endpoints.
|
|
15
|
-
Every running nwf instance
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
versions and instances.
|
|
15
|
+
Every running nwf instance is self-describing — over **MCP** where your client
|
|
16
|
+
supports it, and over its **live operator guide** everywhere else. Your job is to
|
|
17
|
+
reach that live surface and follow it, never to work from a cached copy that drifts
|
|
18
|
+
across versions and instances.
|
|
19
|
+
|
|
20
|
+
There are two paths. **Prefer MCP (§A).** If your client has no MCP support, use the
|
|
21
|
+
fetch-the-live-guide fallback (§B). Both talk to the same app; MCP is a projection of
|
|
22
|
+
the same OpenAPI contract, not a different system (ADR 0067).
|
|
23
|
+
|
|
24
|
+
## A. Preferred — drive over MCP
|
|
25
|
+
|
|
26
|
+
The Urban runtime serves a Streamable-HTTP MCP endpoint at **`/app/mcp`** for every
|
|
27
|
+
instance, with **zero app-side MCP code**: the app's operations are projected into
|
|
28
|
+
tools from its OpenAPI spec, alongside a framework-owned engine-debug tool family
|
|
29
|
+
(process instances, wait states, variables, incidents) and the app's projection
|
|
30
|
+
reads. The operator **guide** (the same guide as §B) is itself one of those projected
|
|
31
|
+
tools — `GET /app/api/agent` becomes the `getAgentInstructions` read tool — so the
|
|
32
|
+
workflow knowledge (orient first, preview before dispatch, escalations are for humans)
|
|
33
|
+
is discoverable over the same channel as the drive tools; the runtime additionally
|
|
34
|
+
serves its derived **system brief** as an MCP resource plus an orientation prompt.
|
|
35
|
+
|
|
36
|
+
**Register one MCP server entry per instance.** Naming the instance
|
|
37
|
+
(`"drive workforce-merlin"`) makes the wrong-instance mistake structurally
|
|
38
|
+
impossible — tool calls are namespaced per server entry. For the Copilot CLI, in
|
|
39
|
+
`~/.copilot/mcp-config.json` (user-wide) or `.mcp.json` (repo-scoped):
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"mcpServers": {
|
|
44
|
+
"workforce-local": {
|
|
45
|
+
"type": "http",
|
|
46
|
+
"url": "http://localhost:3000/app/mcp",
|
|
47
|
+
"tools": ["*"]
|
|
48
|
+
},
|
|
49
|
+
"workforce-merlin": {
|
|
50
|
+
"type": "http",
|
|
51
|
+
"url": "http://merlin.local:3000/app/mcp",
|
|
52
|
+
"headers": { "x-hook-secret": "$NANO_PR_WEBHOOK_SECRET" },
|
|
53
|
+
"tools": ["*"]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or from the terminal:
|
|
60
|
+
`copilot mcp add --transport http workforce-local http://localhost:3000/app/mcp`
|
|
61
|
+
(add `--header` for a guarded instance). Claude/Cursor use the same server entries.
|
|
62
|
+
|
|
63
|
+
Then: start a session, confirm the `workforce-*` tools appear, and ask the agent to
|
|
64
|
+
use a **named** instance (`"Using workforce-local, show what's in flight and any open
|
|
65
|
+
escalations"`). It should call the status operation tool, not curl.
|
|
19
66
|
|
|
20
|
-
|
|
67
|
+
**Guard posture.** When `NANO_PR_WEBHOOK_SECRET` is **unset**, both reads (status,
|
|
68
|
+
instances, incidents, projections, the operator guide) and mutations (submit work,
|
|
69
|
+
answer escalations, cancel/retry/resolve) work from loopback with no credential. When it
|
|
70
|
+
**is set**, that secret is required as an `x-hook-secret` header on **both reads and
|
|
71
|
+
mutations** — read endpoints like `getAgentInstructions`/`getVersion` also return `401`
|
|
72
|
+
without it — put it in the server entry's `headers`, never in chat. **Operator-only doors stay operator-only:** the delivery-graph
|
|
73
|
+
dispatch/dismiss lifecycle (the human clicking Dispatch *is* the approval, ADR 0005)
|
|
74
|
+
is `x-mcp`-excluded and is **not** a tool — dispatch stays a human action in the
|
|
75
|
+
cockpit.
|
|
21
76
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
(an
|
|
25
|
-
submitting work, answering escalations, merging PRs — so targeting the wrong
|
|
26
|
-
instance is a real mistake, not a harmless one. **Never silently default to a
|
|
27
|
-
base URL.**
|
|
77
|
+
The full server-entry recipes (multiple instances, Basic-Auth-fronted instances,
|
|
78
|
+
LAN exposure) live in the **agent-configuration runbook** — see the repo README
|
|
79
|
+
("Configure an agent over MCP") and [`docs/mcp-runbook.md`](../../docs/mcp-runbook.md).
|
|
28
80
|
|
|
29
|
-
|
|
81
|
+
## B. Fallback — no MCP client? Fetch the live guide
|
|
30
82
|
|
|
31
|
-
|
|
83
|
+
Agents without MCP are unchanged: resolve the instance, then fetch and follow its
|
|
84
|
+
live guide.
|
|
32
85
|
|
|
33
|
-
1
|
|
34
|
-
|
|
35
|
-
|
|
86
|
+
### B.1 Confirm which instance you are driving — always
|
|
87
|
+
|
|
88
|
+
A user typically runs **several** instances — a local dev copy, one on the LAN
|
|
89
|
+
(`http://merlin.local:3000/app/api`), and a public tunnel (ngrok) when off the LAN.
|
|
90
|
+
Every action is **side-effecting**, so targeting the wrong instance is a real
|
|
91
|
+
mistake. **Never silently default to a base URL.** Gather candidates, in order:
|
|
92
|
+
|
|
93
|
+
1. A named-instance registry the user maintains — first that exists:
|
|
94
|
+
`$NANO_WORKFORCE_INSTANCES` (JSON `name → base URL`) or
|
|
95
|
+
`~/.config/nano-workforce/instances.json` (same shape). This is also the source
|
|
96
|
+
for the per-instance MCP server names in §A. Example:
|
|
36
97
|
|
|
37
98
|
```json
|
|
38
99
|
{ "local": "http://localhost:3000/app/api",
|
|
@@ -40,33 +101,25 @@ Gather candidates from, in order:
|
|
|
40
101
|
"remote": "https://<subdomain>.ngrok.app/app/api" }
|
|
41
102
|
```
|
|
42
103
|
|
|
43
|
-
2. `$NANO_WORKFORCE_URL`, if set (a single default;
|
|
44
|
-
|
|
104
|
+
2. `$NANO_WORKFORCE_URL`, if set (a single default; append `/app/api` only if it is
|
|
105
|
+
a bare origin).
|
|
45
106
|
3. Any URL the user names in the conversation.
|
|
46
107
|
4. Local fallback: `http://localhost:3000/app/api` (port `PR_REVIEW_PORT`, default `3000`).
|
|
47
108
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- If the user **named an instance** (by name from the registry, or by URL), use it.
|
|
51
|
-
- Otherwise, **probe the candidates for reachability** and ask the user which to
|
|
52
|
-
use, offering the candidates as choices and marking which are live. Reachability
|
|
53
|
-
disambiguates the common case — off the LAN, `merlin.local` won't resolve, so the
|
|
54
|
-
tunnel instance is the live one:
|
|
109
|
+
If the user named an instance, use it. Otherwise probe candidates for reachability
|
|
110
|
+
and ask which to use, marking which are live:
|
|
55
111
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
- Only skip the question when exactly **one** candidate exists and is reachable —
|
|
64
|
-
and even then, **name the instance you're about to drive** before acting.
|
|
112
|
+
```bash
|
|
113
|
+
curl -sS --max-time 3 \
|
|
114
|
+
${NANO_PR_WEBHOOK_SECRET:+-H "x-hook-secret: $NANO_PR_WEBHOOK_SECRET"} \
|
|
115
|
+
"$BASE/version" | jq '{appVersion, gitSha, uptimeSeconds}'
|
|
116
|
+
```
|
|
65
117
|
|
|
66
|
-
|
|
67
|
-
|
|
118
|
+
Only skip the question when exactly **one** candidate exists and is reachable — and
|
|
119
|
+
even then, name the instance you're about to drive before acting. If the user has
|
|
120
|
+
`$NANO_PR_WEBHOOK_SECRET` set, send it as `x-hook-secret` on every request.
|
|
68
121
|
|
|
69
|
-
|
|
122
|
+
### B.2 Fetch the live guide — your real playbook
|
|
70
123
|
|
|
71
124
|
```bash
|
|
72
125
|
curl -sS ${NANO_PR_WEBHOOK_SECRET:+-H "x-hook-secret: $NANO_PR_WEBHOOK_SECRET"} \
|
|
@@ -74,53 +127,47 @@ curl -sS ${NANO_PR_WEBHOOK_SECRET:+-H "x-hook-secret: $NANO_PR_WEBHOOK_SECRET"}
|
|
|
74
127
|
```
|
|
75
128
|
|
|
76
129
|
`GET /app/api/agent` (`getAgentInstructions`) returns
|
|
77
|
-
`{ format, appVersion, generatedAt, baseUrl, engineBase, instructions }
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
**Always re-fetch the guide at the start of a session.** It is the source of truth;
|
|
84
|
-
this skill only tells you how to find it.
|
|
85
|
-
|
|
86
|
-
## 3. Orient before acting
|
|
130
|
+
`{ format, appVersion, generatedAt, baseUrl, engineBase, instructions }` — the
|
|
131
|
+
authoritative, version-matched operator guide, every example keyed to this
|
|
132
|
+
instance. Read it in full and follow it for orientation, submitting work, answering
|
|
133
|
+
escalations, and debugging. **Re-fetch it at the start of every session.** (This is
|
|
134
|
+
the same prose the `getAgentInstructions` MCP tool serves in §A.)
|
|
87
135
|
|
|
88
|
-
|
|
136
|
+
### B.3 Orient before acting
|
|
89
137
|
|
|
90
138
|
```bash
|
|
91
139
|
curl -sS "$BASE/version" | jq # app/urban version, git sha, uptime
|
|
92
140
|
curl -sS "$BASE/status" | jq # every PR/instance in flight + open escalations
|
|
93
141
|
```
|
|
94
142
|
|
|
95
|
-
`/status` is the primary situational-awareness endpoint — check it before you
|
|
96
|
-
|
|
143
|
+
`/status` is the primary situational-awareness endpoint — check it before you submit
|
|
144
|
+
or unstick anything.
|
|
97
145
|
|
|
98
|
-
|
|
146
|
+
### B.4 What you can drive (all detailed in the live guide)
|
|
99
147
|
|
|
100
148
|
- **Submit a PR** for review convergence — `POST $BASE/actions/start/convergence-loop`.
|
|
101
|
-
- **Submit an issue/epic** for plan → implement → converge
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
from `/status`, then use the engine REST base (`engineBase` from the guide) to
|
|
111
|
-
inspect and unstick it.
|
|
149
|
+
- **Submit an issue/epic** for plan → implement → converge — `POST $BASE/actions/start/plan-fanout`.
|
|
150
|
+
- **Submit a delivery graph** (ADR 0005) — propose → preview → approve → **dispatch**.
|
|
151
|
+
Compile/preview is a pure, side-effect-free path; **dispatch is an operator action
|
|
152
|
+
in the cockpit**, not an agent door.
|
|
153
|
+
- **Answer an escalation** — `POST $BASE/actions/complete-user-task`, or the agent
|
|
154
|
+
hook `POST $BASE/hooks/agent-complete` (`agentCompleteEscalation`).
|
|
155
|
+
- **Debug** — relate an in-flight PR to its engine process instance via `processKey`
|
|
156
|
+
from `/status`, then inspect and unstick it against the engine REST base
|
|
157
|
+
(`engineBase` from the guide).
|
|
112
158
|
|
|
113
159
|
## Principles
|
|
114
160
|
|
|
115
|
-
- **Discover, don't declare.** Prefer the live
|
|
116
|
-
|
|
117
|
-
wins.
|
|
118
|
-
- **Confirm the target instance.** Never run a side-effecting call against an
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
side-effecting
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
-
|
|
126
|
-
|
|
161
|
+
- **Discover, don't declare.** Prefer the live surface (MCP tools including the
|
|
162
|
+
operator-guide tool, or the live guide and `/status`) over any assumption baked into
|
|
163
|
+
this file. If this skill and the live surface disagree, the live surface wins.
|
|
164
|
+
- **Confirm the target instance.** Never run a side-effecting call against an assumed
|
|
165
|
+
base URL. With MCP, name the server entry; with the fallback, know — and when
|
|
166
|
+
ambiguous, ask — which instance you're driving.
|
|
167
|
+
- **Preview before dispatch.** For delivery graphs and any bulk action, use the pure
|
|
168
|
+
preview/validate path first and show the user the plan before the side-effecting
|
|
169
|
+
start. Dispatch itself is the operator's call.
|
|
170
|
+
- **Idempotency.** Submissions carry dedupe keys; re-submitting the same work must not
|
|
171
|
+
double-dispatch. Honour the keys the guide documents.
|
|
172
|
+
- **Escalations are for humans.** When the workforce parks on a human node, surface it
|
|
173
|
+
with options; don't silently auto-answer design/product decisions.
|