@nanobpm/nano-workforce 0.187.2 → 0.187.3

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.187.3](https://github.com/nanobpm/nano-workforce/compare/v0.187.2...v0.187.3) (2026-09-14)
2
+
3
+ ### Bug Fixes
4
+
5
+ * 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)
6
+
1
7
  ## [0.187.2](https://github.com/nanobpm/nano-workforce/compare/v0.187.1...v0.187.2) (2026-09-13)
2
8
 
3
9
  ### Bug Fixes
@@ -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
+ });
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.3",
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
  }