@nanobpm/nano-workforce 0.187.2 → 0.187.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,15 @@
1
+ ## [0.187.4](https://github.com/nanobpm/nano-workforce/compare/v0.187.3...v0.187.4) (2026-09-14)
2
+
3
+ ### Bug Fixes
4
+
5
+ * raise default review-wait timeout to PT30M ([#792](https://github.com/nanobpm/nano-workforce/issues/792)) ([042573d](https://github.com/nanobpm/nano-workforce/commit/042573dc5e691dc97efebc12da6a63f2cf398c2a)), closes [#783](https://github.com/nanobpm/nano-workforce/issues/783) [#791](https://github.com/nanobpm/nano-workforce/issues/791)
6
+
7
+ ## [0.187.3](https://github.com/nanobpm/nano-workforce/compare/v0.187.2...v0.187.3) (2026-09-14)
8
+
9
+ ### Bug Fixes
10
+
11
+ * pr-escalation Tasks form no longer renders a blank question field ([#785](https://github.com/nanobpm/nano-workforce/issues/785)) ([eb6bc0e](https://github.com/nanobpm/nano-workforce/commit/eb6bc0e9a677effed0e4d93a3c37033f7819d943)), closes [#767](https://github.com/nanobpm/nano-workforce/issues/767)
12
+
1
13
  ## [0.187.2](https://github.com/nanobpm/nano-workforce/compare/v0.187.1...v0.187.2) (2026-09-13)
2
14
 
3
15
  ### Bug Fixes
package/README.md CHANGED
@@ -360,7 +360,7 @@ agent at that URL to author, compile, and submit a graph unaided. See
360
360
  | `NANO_PR_MERGE_METHOD` | `squash` | merge method: `squash`, `merge`, or `rebase` |
361
361
  | `NANO_PR_MERGE_ADMIN` | `0` | pass `--admin` to override failing non-required checks (use with care) |
362
362
  | `NANO_PR_MAX_CI_FIX_ROUNDS` | `3` | max `senior:fix-ci` attempts to green a `blocked` PR before escalating; `0` disables (escalate immediately), clamped 0–20 |
363
- | `NANO_PR_REVIEW_WAIT_TIMEOUT` | `PT20M` | ISO-8601 duration the loop waits for a fresh review before escalating a stalled review (timer arm of the `wait-review` gateway) |
363
+ | `NANO_PR_REVIEW_WAIT_TIMEOUT` | `PT30M` | ISO-8601 duration the loop waits for a fresh review before escalating a stalled review (timer arm of the `wait-review` gateway) |
364
364
  | `NANO_PR_REVIEW_NUDGE_MINUTES` | `5` | cooldown between the poller's automatic reviewer re-request nudges for one waiting PR (clamped 1–1440) |
365
365
  | `NANO_WORKFORCE_BASE_URL` | `http://localhost:3000` | externally-reachable base URL for the capability hooks (`/app/api/hooks/*`). Must resolve from **wherever the agent runs** — set it to the app's LAN address (or console-proxy URL) for a remote fleet. See [Fleet networking](#fleet-networking-remote-workers) |
366
366
  | `NANO_AGENTIC_SECRET` | — | enables **secure mode** for the agentic visibility channel (`/agentic`): every peer must present the **same** `NANO_AGENTIC_SECRET` value (set the identical env var on the server and every worker box — Tab A → Slot A). Unset = on-by-default **LOCAL mode** — the well-known token is honoured from **any origin** (open on the trusted LAN, matching the engine's posture); exposure is governed by the server bind address, not a shared secret. Also accepts `NANO_PR_WEBHOOK_SECRET` |
package/SPEC.md CHANGED
@@ -133,7 +133,7 @@ Notes:
133
133
  canonical `readiness-ready` wait-gate message (ADR 0001 §2; correlated by the
134
134
  poller when a fresh review lands)
135
135
  against a `=reviewWaitTimeout` timer (seeded at submit from
136
- `NANO_PR_REVIEW_WAIT_TIMEOUT`, default `PT20M`). Whichever fires first
136
+ `NANO_PR_REVIEW_WAIT_TIMEOUT`, default `PT30M`). Whichever fires first
137
137
  withdraws the other — the message arm advances `round`, the timer arm escalates
138
138
  a **stalled review** (`blocked`) so a human decides rather than the instance
139
139
  hanging forever. Because `persist-round` already recorded this `round` as
@@ -544,7 +544,7 @@ queries skip (`merging`), so a slow pass can't double-signal.
544
544
  | `NANO_PR_AUTO_MERGE` | 1 | run the merge stage after convergence (`0` = review-only; per-submit `convergeOnly: true` override) |
545
545
  | `NANO_PR_MERGE_METHOD` | squash | `squash` \| `merge` \| `rebase` |
546
546
  | `NANO_PR_MERGE_ADMIN` | 0 | pass `--admin` on merge |
547
- | `NANO_PR_REVIEW_WAIT_TIMEOUT` | PT20M | ISO-8601 wait before a stalled review escalates (timer arm of the `wait-review` event-based gateway); malformed → default |
547
+ | `NANO_PR_REVIEW_WAIT_TIMEOUT` | PT30M | ISO-8601 wait before a stalled review escalates (timer arm of the `wait-review` event-based gateway); malformed → default |
548
548
  | `NANO_PR_REVIEW_NUDGE_MINUTES` | 5 | cooldown between poller Copilot re-request nudges per PR (clamped 1–1440) |
549
549
 
550
550
  ## 13. Planning fan-out (`plan-fanout.bpmn`) — issue #14
package/app/contracts.ts CHANGED
@@ -145,6 +145,7 @@ export const ENV_CONTRACTS = {
145
145
  name: "NANO_PR_REVIEW_WAIT_TIMEOUT",
146
146
  owner: "app/service.ts",
147
147
  semantics: "How long to wait for a review before nudging/escalating (FEEL/ISO-8601 duration).",
148
+ default: "PT30M",
148
149
  },
149
150
  NANO_PR_REVIEW_NUDGE_MINUTES: {
150
151
  category: "env",
@@ -190,7 +190,7 @@ test("persist-escalation heals from the prKey when repo/prNumber are absent", as
190
190
  // never a dead wait with a null question (the #333 defect).
191
191
  test("a control-flow arm with a concrete question opens an escalation gw-escalated can park", async () => {
192
192
  const { app, inserts } = fakeApp();
193
- const question = "No review arrived within the review-wait timeout (PT20M). A human must decide how to proceed.";
193
+ const question = "No review arrived within the review-wait timeout (PT30M). A human must decide how to proceed.";
194
194
  const job = { variables: { prKey: "o/r#5", round: 2, status: "blocked", question, recordRound: false } };
195
195
  const out = await handler(job as any, app as any);
196
196
  assertEquals((out as any).escalated, true, "a real control-flow escalation reports escalated:true");
@@ -0,0 +1,84 @@
1
+ // Regression guard for issue #767 — the shared PR-escalation Tasks form must not render a blank
2
+ // read-only "Escalation question" field. The Tasks surface (`detail.engineForm`) seeds NO form
3
+ // variables, so the form's `question` control could never be populated at render time: it showed up
4
+ // blank while the actual request was already rendered separately as "Decision context" (the
5
+ // `user_tasks.question` read-model column, sourced from `latestOpenEscalationQuestion`). This mirrors
6
+ // the fix for the sibling `delivery-human-generic.form` (#773): make the form static / input-only —
7
+ // no `{{tokens}}`, no data-dependent `conditional`, and exactly one editable, required `answer` — and
8
+ // rely on the already-populated Decision context as the read-only prompt.
9
+ //
10
+ // This is the task/form-boundary guard the issue asks for, distinct from the durable `user_tasks`
11
+ // projection coverage in `pollUserTasks.test.ts`: it ties the committed `.form`, the BPMN
12
+ // `formDefinition` linkage, and the completer's typed contract together so none can drift back to a
13
+ // blank-question render.
14
+
15
+ import { test } from "node:test";
16
+ import { readFileSync } from "node:fs";
17
+ import { assert, assertEquals } from "#test-assert";
18
+ import { validateEscalationVariables } from "./agentCompletion.ts";
19
+
20
+ // biome-ignore lint/suspicious/noExplicitAny: form-js component shape is untyped JSON.
21
+ type FormComponent = { type: string; key?: string; readonly?: boolean; conditional?: unknown; validate?: any };
22
+ type Form = { id: string; components: FormComponent[] };
23
+
24
+ const formText = readFileSync("resources/forms/pr-escalation.form", "utf8");
25
+ const form = JSON.parse(formText) as Form;
26
+
27
+ test("pr-escalation.form: has exactly one editable, required input — `answer` — and no blank readonly question field", () => {
28
+ assertEquals(form.id, "pr-escalation");
29
+ // Only ONE keyed (data-bearing) control, and it is the editable required `answer`.
30
+ const inputs = form.components.filter((c) => typeof c.key === "string");
31
+ assertEquals(
32
+ inputs.map((c) => c.key),
33
+ ["answer"],
34
+ "the form must expose exactly one input control, keyed `answer`",
35
+ );
36
+ const answer = inputs[0];
37
+ assert(answer.readonly !== true, "the `answer` control must be editable, not read-only");
38
+ assertEquals(answer.validate?.required, true, "the `answer` control must be required");
39
+ // The blank read-only `question` field the operator could never fill (issue #767) must be gone —
40
+ // Decision context is the canonical read-only prompt now.
41
+ assert(
42
+ !form.components.some((c) => c.key === "question"),
43
+ "the blank readonly `question` field must be removed (Decision context is the prompt)",
44
+ );
45
+ });
46
+
47
+ test("pr-escalation.form: is static — no `{{token}}` templating and no data-dependent `conditional`", () => {
48
+ // Deploy-time `{{token}}` templating is removed repo-wide (AGENTS.md), and the Tasks `engineForm`
49
+ // seeds no variables, so any data-dependent gate would never resolve — the exact failure mode #773
50
+ // fixed for the sibling form. Guard against a reappearance on this one.
51
+ assert(!/\{\{[^}]*\}\}/.test(formText), "the form must not contain `{{token}}` templating");
52
+ assert(
53
+ !form.components.some((c) => c.conditional !== undefined),
54
+ "the form must not use a data-dependent `conditional` the Tasks surface can never resolve",
55
+ );
56
+ });
57
+
58
+ test("pr-escalation.form: both PR escalation elements still complete with the `{ answer }` contract", () => {
59
+ // Preserve the existing typed completion contract: `wait-answer` and `wait-merge-answer` both map to
60
+ // this form and require a non-blank `answer` (validated via `validateEscalationVariables`). A missing
61
+ // answer is rejected; a present one accepted — proving the input-only rewrite kept the contract.
62
+ for (const element of ["wait-answer", "wait-merge-answer"]) {
63
+ assert(
64
+ validateEscalationVariables(element, {}) !== null,
65
+ `${element} must reject a missing answer (pr-escalation form contract)`,
66
+ );
67
+ assert(
68
+ validateEscalationVariables(element, { answer: "rerun the round" }) === null,
69
+ `${element} must accept a valid { answer }`,
70
+ );
71
+ }
72
+ });
73
+
74
+ test("drift guard: both PR escalation user tasks link the pr-escalation form", () => {
75
+ // The form the code validates against must be the SAME one both models render, or the operator sees
76
+ // a different (blank-question) form than the contract guards.
77
+ for (const model of ["convergence-loop", "merge-loop"]) {
78
+ const bpmn = readFileSync(`resources/processes/${model}.bpmn`, "utf8");
79
+ assert(
80
+ /formId="pr-escalation"/.test(bpmn),
81
+ `${model}.bpmn must link the pr-escalation form`,
82
+ );
83
+ }
84
+ });
@@ -13,6 +13,13 @@ import {
13
13
  reviewWaitTimeout,
14
14
  } from "./reviewWait.ts";
15
15
 
16
+ test("DEFAULT_REVIEW_WAIT_TIMEOUT: pins the user-visible default to PT30M", () => {
17
+ // A direct regression guard on the literal default — the fallback-based assertions below compare
18
+ // against DEFAULT_REVIEW_WAIT_TIMEOUT itself, so they would still pass if it were accidentally
19
+ // reverted to PT20M. This anchors the intended value so that change is caught.
20
+ assertEquals(DEFAULT_REVIEW_WAIT_TIMEOUT, "PT30M");
21
+ });
22
+
16
23
  test("reviewWaitTimeout: blank / absent / malformed → default", () => {
17
24
  assertEquals(reviewWaitTimeout(undefined), DEFAULT_REVIEW_WAIT_TIMEOUT);
18
25
  assertEquals(reviewWaitTimeout(""), DEFAULT_REVIEW_WAIT_TIMEOUT);
package/app/reviewWait.ts CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  /** Default review-wait timeout (ISO-8601 duration): how long the loop waits for a fresh review
14
14
  * before the timer arm of the event-based gateway fires and it escalates to a human. */
15
- export const DEFAULT_REVIEW_WAIT_TIMEOUT = "PT20M";
15
+ export const DEFAULT_REVIEW_WAIT_TIMEOUT = "PT30M";
16
16
 
17
17
  // A pragmatic ISO-8601 duration matcher: requires a leading `P`, at least one component, and a
18
18
  // `T` before any time components (with at least one time component after it). Good enough to
@@ -470,7 +470,7 @@ Work through this order:
470
470
  3. **Check for an open escalation** (§3) — the process may simply be waiting for a
471
471
  human answer. Answer it.
472
472
  4. **A review that never arrives** escalates on its own after
473
- `NANO_PR_REVIEW_WAIT_TIMEOUT` (default `PT20M`); the poller also re-nudges the
473
+ `NANO_PR_REVIEW_WAIT_TIMEOUT` (default `PT30M`); the poller also re-nudges the
474
474
  reviewer periodically. If the reviewer bot is not provisioned on the repo, no
475
475
  review will ever land — that is a repo-config problem, not an app bug.
476
476
  5. **Cancel + resubmit** as a last resort. Cancel through the **app-owned** door —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.187.2",
3
+ "version": "0.187.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",
@@ -4,17 +4,15 @@
4
4
  "type": "default",
5
5
  "components": [
6
6
  {
7
- "type": "textarea",
8
- "key": "question",
9
- "label": "Escalation question",
10
- "description": "A human decision is needed to resume the PR review-convergence loop.",
11
- "readonly": true
7
+ "type": "text",
8
+ "label": "Respond to this escalation",
9
+ "text": "A human decision is needed to resume the PR process. The requested decision is shown as **Decision context** above. Read it, then enter your reply below."
12
10
  },
13
11
  {
14
12
  "type": "textarea",
15
13
  "key": "answer",
16
14
  "label": "Your answer",
17
- "description": "Your reply resumes the review loop and is handed to the next review round.",
15
+ "description": "Your reply resumes the PR process and is handed to its next step. Answer the request shown in Decision context above.",
18
16
  "validate": {
19
17
  "required": true
20
18
  }