@nanobpm/nano-workforce 0.150.3 → 0.151.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.151.0](https://github.com/nanobpm/nano-workforce/compare/v0.150.4...v0.151.0) (2026-08-28)
2
+
3
+ ### Features
4
+
5
+ * 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)
6
+
7
+ ## [0.150.4](https://github.com/nanobpm/nano-workforce/compare/v0.150.3...v0.150.4) (2026-08-28)
8
+
9
+ ### Documentation
10
+
11
+ * **agent guide:** document the PT30M wait-gate default and poll bounds in §9 ([#585](https://github.com/nanobpm/nano-workforce/issues/585)) ([3092ae9](https://github.com/nanobpm/nano-workforce/commit/3092ae9c1448c8b540aa62abf58b268e14f3f0ee)), closes [nanobpm/nano-workforce#584](https://github.com/nanobpm/nano-workforce/issues/584)
12
+
1
13
  ## [0.150.3](https://github.com/nanobpm/nano-workforce/compare/v0.150.2...v0.150.3) (2026-08-28)
2
14
 
3
15
  ### Bug Fixes
@@ -180,6 +180,12 @@ curl -sS __BASE__/../../tasks/api/tasks \
180
180
  The inbox UI is also served at `__BASE__/../../tasks` for a human to browse, filter, and
181
181
  answer (assignee/candidate-group and age surface on each task once assignment lands).
182
182
 
183
+ > **`wait`-gate escalations are different — completing one does NOT re-arm the gate.** The
184
+ > escalation kinds above resume a loop with your answer. A **delivery-graph `wait` node**
185
+ > (§9) that elapses its bound also parks a task, but completing *that* one releases the token
186
+ > **as not-ready** and the graph proceeds **past the gate** — the downstream side-effecting
187
+ > node then runs against the unmet dependency. See §9.2 before clearing one.
188
+
183
189
  **Answer a task** by completing it with the typed variables its form expects — the
184
190
  completion resumes the parked process:
185
191
 
@@ -438,7 +444,20 @@ layer schedules, it does not re-implement execution):
438
444
  | `connector` | `connector: { target, dedupeKey?, payload? }` | an automated, side-effecting outbound action. Carries a `dedupeKey` (at-least-once safe). Two **real targets** ship today — **`converge`** and **`converge-merge`** (§9.4); other targets are a forward-declared stub. | yes |
439
445
 
440
446
  A **`wait` node's `wait` is a `ReadinessProbe` verbatim** (the same shape feature-run
441
- intake uses): `{ kind, target, onTimeout?, match?, poll? }`. The **`pr` kind** watches an
447
+ intake uses): `{ kind, target, onTimeout?, match?, poll? }`, where `poll` is
448
+ `{ everyMs?, timeoutMs?, backoff? }` — `everyMs` is the re-probe cadence, `timeoutMs` the
449
+ total bounded budget, and `backoff ∈ fixed|exponential`.
450
+
451
+ > **The bound is invisible unless you set it.** When `poll` (or `poll.timeoutMs`) is
452
+ > **omitted**, the gate inherits the built-in default budget of **`PT30M` (30 minutes),
453
+ > re-probing every 15s** (`DEFAULT_READINESS_TIMEOUT` / `DEFAULT_EVERY_MS`,
454
+ > `app/readiness.ts`). That default is right for *"is the package published yet"* and badly
455
+ > wrong for `wait[pr, merged]` / `wait[epic]`, which routinely wait **hours or days** — an
456
+ > unpopulated `poll` on such a gate escalates after 30 minutes for no visible reason. Neither
457
+ > `compile` nor `preview` surfaces the effective bound, so **set `poll.timeoutMs` explicitly**
458
+ > on any gate that waits on a merge or an epic (see §9.4 / §9.5).
459
+
460
+ The **`pr` kind** watches an
442
461
  in-flight PR — `target: "owner/repo#123"`, `match.prState ∈ ready|merged|mergeable|checks-green`
443
462
  (default `merged`) — and on a merged match binds `mergedSha` as an output fact. The **`epic`
444
463
  kind** (issue #568) gates on an **nwf plan-fanout epic reaching "fully merged"** — `target:
@@ -526,6 +545,18 @@ Graphs** grid (e.g. *"parked on human node: manual OTP publish"*). A `human` nod
526
545
  **Tasks** inbox and is answered exactly as an escalation is (§3) — its completion emits any
527
546
  declared facts, which downstream edges bind.
528
547
 
548
+ > **Completing a `wait` escalation proceeds *as not-ready* — it does NOT re-arm the gate.**
549
+ > A `wait` node with `onTimeout: "escalate"` (the default) that elapses its bound parks a
550
+ > human-completable escalation task on the Tasks inbox. **Completing that task does not retry
551
+ > the probe or wait for readiness** — it releases the token **as not-ready** and the graph
552
+ > proceeds **past the gate**, so the downstream side-effecting node then runs *against the
553
+ > unmet dependency* (`waitBodyLines`, `app/deliveryGraphCompiler.ts`). An operator clearing
554
+ > what looks like a stuck task therefore *launches the very work the gate was holding back*.
555
+ > If the dependency genuinely is not ready, do **not** complete the escalation to "unstick"
556
+ > it — extend the gate's `poll.timeoutMs` and re-dispatch, or abandon the run. (Same for
557
+ > `onTimeout: "continue"`, which proceeds past the gate as not-ready with **no** human stop
558
+ > at all.)
559
+
529
560
  > **Why the split?** Making the compile door the end of the agent surface closes a
530
561
  > self-approval hole: the old flow handed the same caller a content-addressed approval token
531
562
  > to re-submit with, so any holder of the API credential approved its own graph. Removing the
@@ -544,7 +575,8 @@ publish and records the version → open+merge PR #303 (repo 3) consuming that v
544
575
  "name": "cross-repo release: merge #101 → un-draft+merge #202 → manual OTP publish → consume in #303",
545
576
  "nodes": [
546
577
  { "id": "merge-a", "kind": "wait",
547
- "wait": { "kind": "pr", "target": "acme/repo-1#101", "match": { "prState": "merged" }, "onTimeout": "escalate" } },
578
+ "wait": { "kind": "pr", "target": "acme/repo-1#101", "match": { "prState": "merged" },
579
+ "poll": { "everyMs": 300000, "timeoutMs": 259200000 }, "onTimeout": "escalate" } },
548
580
  { "id": "undraft-merge-b", "kind": "agent",
549
581
  "agent": { "jobType": "senior:merge", "prompt": "Take draft PR acme/repo-2#202 out of draft and merge it once its required checks are green." } },
550
582
  { "id": "manual-publish", "kind": "human",
@@ -553,7 +585,8 @@ publish and records the version → open+merge PR #303 (repo 3) consuming that v
553
585
  { "id": "open-pr-c", "kind": "agent",
554
586
  "agent": { "jobType": "senior:feature", "prompt": "Bump @acme/widget to the published version in acme/repo-3 and open PR #303." } },
555
587
  { "id": "merge-c", "kind": "wait",
556
- "wait": { "kind": "pr", "target": "acme/repo-3#303", "match": { "prState": "merged" }, "onTimeout": "escalate" } }
588
+ "wait": { "kind": "pr", "target": "acme/repo-3#303", "match": { "prState": "merged" },
589
+ "poll": { "everyMs": 300000, "timeoutMs": 259200000 }, "onTimeout": "escalate" } }
557
590
  ],
558
591
  "edges": [
559
592
  { "from": "merge-a", "to": "undraft-merge-b" },
@@ -622,7 +655,8 @@ author never knows the PR number at compose time, so reference it by fact:
622
655
  { "id": "land", "kind": "connector",
623
656
  "connector": { "target": "converge-merge", "payload": { "pr": "open.pr" } } },
624
657
  { "id": "merged", "kind": "wait",
625
- "wait": { "kind": "pr", "target": "open.pr", "match": { "prState": "merged" }, "onTimeout": "escalate" } }
658
+ "wait": { "kind": "pr", "target": "open.pr", "match": { "prState": "merged" },
659
+ "poll": { "everyMs": 300000, "timeoutMs": 259200000 }, "onTimeout": "escalate" } }
626
660
  ],
627
661
  "edges": [
628
662
  { "from": "open.pr", "to": "land" },
@@ -631,6 +665,11 @@ author never knows the PR number at compose time, so reference it by fact:
631
665
  }
632
666
  ```
633
667
 
668
+ The `merged` gate carries an explicit **`poll`** (re-probe every 5 minutes, budget 3 days:
669
+ `timeoutMs: 259200000`) because a `wait[pr, merged]` waits on a human-paced merge — **omitting
670
+ `poll` inherits the 30-minute default** (§9.1) and escalates mid-review. Set `poll.timeoutMs`
671
+ to a realistic budget on any merge/epic gate.
672
+
634
673
  The `pr` fact is threaded along the **fact-qualified edges** (`open.pr → land`, `open.pr → merged`) —
635
674
  those edges are what carry the observed PR into each consumer, so they are **required** when you
636
675
  reference `open.pr` (the validator rejects a reference with no threading edge, `unbound-pr`). Three
@@ -661,7 +700,9 @@ babysitting a `confirm` gate.
661
700
  "nodes": [
662
701
  { "id": "gate-epic", "kind": "wait",
663
702
  "wait": { "kind": "epic", "target": "nanobpm/nano-ide#488",
664
- "match": { "epicState": "merged" }, "onTimeout": "escalate" },
703
+ "match": { "epicState": "merged" },
704
+ "poll": { "everyMs": 300000, "timeoutMs": 259200000 },
705
+ "onTimeout": "escalate" },
665
706
  "emits": [ { "name": "prCount", "type": "number" } ] },
666
707
  { "id": "start-b", "kind": "agent",
667
708
  "agent": { "jobType": "senior:feature", "prompt": "Implement nanobpm/nano-workforce#567 and open a PR." } }
@@ -680,5 +721,11 @@ Semantics:
680
721
  - **A failed/abandoned/mixed epic never reports merged**, so it never falsely releases the
681
722
  gate; the **bounded** wait elapses and routes via **`onTimeout`** (`escalate`/`continue`) —
682
723
  it does **not** hang.
724
+ - **Set `poll.timeoutMs` to a realistic budget.** An epic reaching "fully merged" is a
725
+ multi-day, human-paced event, so the example gives it `poll: { everyMs: 300000, timeoutMs:
726
+ 259200000 }` (re-probe every 5 minutes, budget 3 days). **Omitting `poll` inherits the
727
+ 30-minute default** (§9.1) — the gate would escalate long before the epic lands, and (per
728
+ §9.2) completing that escalation would release `start-b` **as not-ready**, launching feature
729
+ B before its dependency merged. Size `timeoutMs` to how long the epic realistically takes.
683
730
  - On a fully-merged match it binds **`prCount`** (how many slice PRs the epic landed) as an
684
731
  output fact, so a downstream node can consume it (parity with the `pr` kind's `mergedSha`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.150.3",
3
+ "version": "0.151.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 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
  },