@nanobpm/nano-workforce 0.150.4 → 0.151.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [0.151.1](https://github.com/nanobpm/nano-workforce/compare/v0.151.0...v0.151.1) (2026-08-28)
2
+
3
+ ### Bug Fixes
4
+
5
+ * **delivery-graph:** read parked human nodes via searchUserTasks until engine projects USER_TASK wait states ([#595](https://github.com/nanobpm/nano-workforce/issues/595)) ([5dfb3fe](https://github.com/nanobpm/nano-workforce/commit/5dfb3fed1b0483c3a110979bd0aa7802ef3b17d2)), closes [#542](https://github.com/nanobpm/nano-workforce/issues/542) [Magikcraft/nano-bpm#1042](https://github.com/Magikcraft/nano-bpm/issues/1042) [nanobpm/nano-workforce#594](https://github.com/nanobpm/nano-workforce/issues/594)
6
+
7
+ ## [0.151.0](https://github.com/nanobpm/nano-workforce/compare/v0.150.4...v0.151.0) (2026-08-28)
8
+
9
+ ### Features
10
+
11
+ * make Agent Instructions modal MCP-first, skill-as-fallback ([#587](https://github.com/nanobpm/nano-workforce/issues/587)) ([cd2b613](https://github.com/nanobpm/nano-workforce/commit/cd2b613a35c0411ad5000dece36e54b584cffbc7)), closes [#575](https://github.com/nanobpm/nano-workforce/issues/575) [nanobpm/nano-workforce#586](https://github.com/nanobpm/nano-workforce/issues/586)
12
+
1
13
  ## [0.150.4](https://github.com/nanobpm/nano-workforce/compare/v0.150.3...v0.150.4) (2026-08-28)
2
14
 
3
15
  ### Documentation
@@ -115,7 +115,55 @@ test("pollDeliveryGraphPhase: a numeric engine processInstanceKey still matches
115
115
  // The engine can yield a NUMERIC key; the poller compares against the string process_key.
116
116
  const engine = {
117
117
  searchProcessInstances: async () => [{ processInstanceKey: 12345, state: "COMPLETED" }],
118
- searchElementInstanceWaitStates: async () => [],
118
+ searchUserTasks: async () => [],
119
+ };
120
+ await pollDeliveryGraphPhase(data, engine as never);
121
+ assertEquals((await runs.get("rk"))?.status, "done");
122
+ });
123
+ });
124
+
125
+ // ── pollDeliveryGraphPhase: engine 422 on USER_TASK wait-state read (nano-bpm#1042) ────────────
126
+ // The deployed engine's wait-state read model only accepts JOB | MESSAGE, so a `waitStateType:
127
+ // "USER_TASK"` filter 422s on a live gateway. This pass reads parks via `searchUserTasks({ state:
128
+ // "CREATED" })` — NOT `searchElementInstanceWaitStates` — precisely so that 422 can never reject the
129
+ // `Promise.all` and skip reconciliation. Guard both projections this pass owns against regressing back
130
+ // onto the wait-state channel: the parked-label projection AND the COMPLETED → `done` transition.
131
+ test("pollDeliveryGraphPhase: with an engine that 422s on waitStateType=USER_TASK, the parked label still projects (reads via searchUserTasks)", async () => {
132
+ await withData(async (data) => {
133
+ const runs = deliveryGraphRuns(data);
134
+ const humanEl = humanTaskElementId("publish");
135
+ await runs.insert({
136
+ ...claimRow("running"),
137
+ process_key: "PI-1",
138
+ human_labels: JSON.stringify({ [humanEl]: "run the manual OTP publish" }),
139
+ });
140
+ // A client that rejects the wait-state read the way a live gateway does (HTTP 422). If the poller
141
+ // reached for it, the throw would reject the Promise.all and reconciliation would be skipped.
142
+ const engine = {
143
+ searchProcessInstances: async () => [{ processInstanceKey: "PI-1", state: "ACTIVE" }],
144
+ searchUserTasks: async () => [{ userTaskKey: "ut-1", elementId: humanEl }],
145
+ searchElementInstanceWaitStates: async () => {
146
+ throw new Error("HttpSdkError: HTTP 422 — filter.waitStateType did not match any variant of untagged enum WaitStateTypeFilterProperty");
147
+ },
148
+ };
149
+ await pollDeliveryGraphPhase(data, engine as never);
150
+ const row = await runs.get("rk");
151
+ assertEquals(row?.status, "running");
152
+ assertEquals(row?.phase, "Parked on human node: run the manual OTP publish");
153
+ assertEquals(row?.phase_node_id, humanEl);
154
+ });
155
+ });
156
+
157
+ test("pollDeliveryGraphPhase: with an engine that 422s on waitStateType=USER_TASK, a COMPLETED instance still reconciles to done", async () => {
158
+ await withData(async (data) => {
159
+ const runs = deliveryGraphRuns(data);
160
+ await runs.insert({ ...claimRow("running"), process_key: "PI-2" });
161
+ const engine = {
162
+ searchProcessInstances: async () => [{ processInstanceKey: "PI-2", state: "COMPLETED" }],
163
+ searchUserTasks: async () => [],
164
+ searchElementInstanceWaitStates: async () => {
165
+ throw new Error("HttpSdkError: HTTP 422 — filter.waitStateType did not match any variant of untagged enum WaitStateTypeFilterProperty");
166
+ },
119
167
  };
120
168
  await pollDeliveryGraphPhase(data, engine as never);
121
169
  assertEquals((await runs.get("rk"))?.status, "done");
package/app/service.ts CHANGED
@@ -2391,13 +2391,19 @@ export async function pollEpicPhase(
2391
2391
  * a graph that ends normally would otherwise stay `running` forever). Generalises the `epic_phase`
2392
2392
  * derived-phase machinery to a graph whose element ids aren't known ahead of time: the parked-node
2393
2393
  * label is derived from the run row's stamped `human_labels` + the instance's live USER_TASK parks.
2394
- * The parked node is now sourced from the unified element-instance wait-state channel
2395
- * (`searchElementInstanceWaitStates`, nano-ide#473) rather than a separate user-task search, folding
2396
- * this read onto the same live element-instance model the epic derivation uses (S8, #542). Scoped to
2397
- * `running` rows (an `awaiting-approval` run has no instance yet), so it stays O(in-flight). */
2394
+ * The parked node is read via `searchUserTasks({ state: "CREATED" })` rather than the unified
2395
+ * element-instance wait-state channel (`searchElementInstanceWaitStates({ waitStateType: "USER_TASK" })`,
2396
+ * S8, #542/nano-ide#473): the deployed engine's wait-state read model only accepts `JOB | MESSAGE`, so
2397
+ * a `USER_TASK` filter 422s on a live gateway, rejecting the whole `Promise.all` and skipping this pass's
2398
+ * reconciliation entirely (the parked label never updates and the COMPLETED → `done` transition this pass
2399
+ * OWNS never fires, leaving a finished graph `running` forever). `deriveDeliveryPhase` consumes only
2400
+ * `elementId` from the park rows, which `searchUserTasks` supplies the same way.
2401
+ * TODO(Magikcraft/nano-bpm#1042): re-fold onto `searchElementInstanceWaitStates({ waitStateType:
2402
+ * "USER_TASK" })` once the engine projects `USER_TASK` wait states and that engine is deployed.
2403
+ * Scoped to `running` rows (an `awaiting-approval` run has no instance yet), so it stays O(in-flight). */
2398
2404
  export async function pollDeliveryGraphPhase(
2399
2405
  data: DataLayer,
2400
- engine: Pick<EngineClient, "searchProcessInstances" | "searchElementInstanceWaitStates">,
2406
+ engine: Pick<EngineClient, "searchProcessInstances" | "searchUserTasks">,
2401
2407
  ) {
2402
2408
  for (const run of await deliveryGraphRuns(data).find({ status: "running" })) {
2403
2409
  if (!run.process_key) continue;
@@ -2405,7 +2411,7 @@ export async function pollDeliveryGraphPhase(
2405
2411
  try {
2406
2412
  const [snapshots, parks] = await Promise.all([
2407
2413
  engine.searchProcessInstances({ processInstanceKeys: [processKey] }),
2408
- engine.searchElementInstanceWaitStates({ processInstanceKey: processKey, waitStateType: "USER_TASK" }),
2414
+ engine.searchUserTasks({ processInstanceKey: processKey, state: "CREATED" }),
2409
2415
  ]);
2410
2416
  const state = snapshots.find((s) => String(s.processInstanceKey) === processKey)?.state ?? null;
2411
2417
  const projection = deriveDeliveryPhase(state, parks, parseHumanLabels(run.human_labels));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.150.4",
3
+ "version": "0.151.1",
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 tells the agent to load this workforce's operator skill from this instance, then help you drive and debug it.",
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.",
86
86
  "copyLabel": "Copy prompt",
87
- "copyText": "Load the Nano Workforce operator skill from this running instance and 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\nThis is the instance to operate; its control-API base is {{appBase}}app/api. Fetch the skill and follow it (the response is JSON with a `skill` markdown field):\n\n curl -sS {{appBase}}app/api/agent/skill\n\nIf this instance is secured with a shared secret (NANO_PR_WEBHOOK_SECRET), add its value as an x-hook-secret header, otherwise the request returns 401:\n\n curl -sS -H \"x-hook-secret: <secret>\" {{appBase}}app/api/agent/skill\n\nThe skill is a thin bootstrap: it has you fetch this instance's live operator guide (at {{appBase}}app/api/agent) and then drive the workforce \u2014 submit PRs and epics for convergence, answer escalations, and debug stuck processes. 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, 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."
88
88
  }
89
89
  }
90
90
  },