@nanobpm/nano-workforce 0.150.3 → 0.150.4

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,9 @@
1
+ ## [0.150.4](https://github.com/nanobpm/nano-workforce/compare/v0.150.3...v0.150.4) (2026-08-28)
2
+
3
+ ### Documentation
4
+
5
+ * **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)
6
+
1
7
  ## [0.150.3](https://github.com/nanobpm/nano-workforce/compare/v0.150.2...v0.150.3) (2026-08-28)
2
8
 
3
9
  ### 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.150.4",
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",