@nanobpm/nano-workforce 0.179.0 → 0.179.2
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 +12 -0
- package/app/tasksPage.test.ts +5 -0
- package/e2e/agent-answerable.e2e.ts +1 -2
- package/e2e/convergence-escalation.e2e.ts +2 -3
- package/e2e/feature-run.e2e.ts +4 -5
- package/e2e/retire-escalation-subsystem.e2e.ts +2 -3
- package/package.json +2 -2
- package/pages/tasks.page.json +3 -2
- package/e2e/support/engine-client.ts +0 -33
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [0.179.2](https://github.com/nanobpm/nano-workforce/compare/v0.179.1...v0.179.2) (2026-09-04)
|
|
2
|
+
|
|
3
|
+
### Code Refactoring
|
|
4
|
+
|
|
5
|
+
* drop redundant testkit engine-client shim in e2e ([#734](https://github.com/nanobpm/nano-workforce/issues/734)) ([8ea7ccd](https://github.com/nanobpm/nano-workforce/commit/8ea7ccd5c38a2b0e363cac74b126dd86bee0f0b1)), closes [#336](https://github.com/nanobpm/nano-workforce/issues/336)
|
|
6
|
+
|
|
7
|
+
## [0.179.1](https://github.com/nanobpm/nano-workforce/compare/v0.179.0...v0.179.1) (2026-09-04)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **tasks:** route Tasks-page completion through the canonical complete-user-task door ([#732](https://github.com/nanobpm/nano-workforce/issues/732)) ([98b59a5](https://github.com/nanobpm/nano-workforce/commit/98b59a5a30fb6c6038466da9ec71fc0557eb72e5)), closes [461/#484](https://github.com/461/nano-workforce/issues/484) [nano-ide#552](https://github.com/nanobpm/nano-ide/issues/552) [#728](https://github.com/nanobpm/nano-workforce/issues/728)
|
|
12
|
+
|
|
1
13
|
## [0.179.0](https://github.com/nanobpm/nano-workforce/compare/v0.178.5...v0.179.0) (2026-09-04)
|
|
2
14
|
|
|
3
15
|
### Features
|
package/app/tasksPage.test.ts
CHANGED
|
@@ -39,6 +39,11 @@ test("Tasks grid completes each row via its engine-declared form (nano-ide#457),
|
|
|
39
39
|
assert(detail?.engineForm, "the grid detail must opt into engineForm rendering");
|
|
40
40
|
assertEquals(detail.engineForm.formKeyField, "form_key");
|
|
41
41
|
assertEquals(detail.engineForm.userTaskKeyField, "user_task_key");
|
|
42
|
+
// Completion routes through nwf's canonical completion door (operations/completeUserTask.ts,
|
|
43
|
+
// issue #728 / nano-ide#552) — NOT the runtime's generic `/app/actions/complete` seam — so it
|
|
44
|
+
// records `task_completions` attribution, enforces HUMAN_COMPLETABLE_ELEMENTS, and drops the
|
|
45
|
+
// answered `user_tasks` row immediately instead of after a poll cycle.
|
|
46
|
+
assertEquals(detail.engineForm.completePath, "/app/api/actions/complete-user-task");
|
|
42
47
|
// The seven bespoke per-type detail.form blocks (each a copy of a deployed `.form`) are gone.
|
|
43
48
|
for (const g of grids) assert(!g.props.detail?.form, "no grid may carry a page-local detail.form");
|
|
44
49
|
});
|
|
@@ -26,7 +26,6 @@ import { fileURLToPath } from "node:url";
|
|
|
26
26
|
import type { EngineJob } from "@nanobpm/urban/runtime";
|
|
27
27
|
import { bootTestApp, type TestApp } from "@nanobpm/urban-testkit";
|
|
28
28
|
import { admitGithubState, installAdmitGithub } from "./support/github-admit.ts";
|
|
29
|
-
import { asEngineClient } from "./support/engine-client.ts";
|
|
30
29
|
import {
|
|
31
30
|
completeEscalationAsAgent,
|
|
32
31
|
latestCompletion,
|
|
@@ -142,7 +141,7 @@ describe("agent-answerable escalations (U6 — same form, agent completer, attri
|
|
|
142
141
|
|
|
143
142
|
// Complete AS AN AGENT through the host-side completer — the same typed `{resolution, answer}`
|
|
144
143
|
// a human submits through the inbox, only the caller differs.
|
|
145
|
-
const r = await completeEscalationAsAgent(app.db,
|
|
144
|
+
const r = await completeEscalationAsAgent(app.db, app.engine, {
|
|
146
145
|
userTaskKey: task.userTaskKey,
|
|
147
146
|
agentId: "senior:answer-bot",
|
|
148
147
|
variables: { resolution: "answer", answer: "use v2" },
|
|
@@ -29,7 +29,6 @@ import { after, before, describe, test } from "node:test";
|
|
|
29
29
|
import { fileURLToPath } from "node:url";
|
|
30
30
|
import { bootTestApp, type TestApp } from "@nanobpm/urban-testkit";
|
|
31
31
|
import { pollUserTasks } from "../app/service.ts";
|
|
32
|
-
import { asEngineClient } from "./support/engine-client.ts";
|
|
33
32
|
|
|
34
33
|
const APP_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
35
34
|
const DB_DIR = mkdtempSync(join(tmpdir(), "nwf-u4-"));
|
|
@@ -159,7 +158,7 @@ describe("nano-workforce PR review-loop escalation (U4 userTask)", () => {
|
|
|
159
158
|
// denormalised `open_escalation_*` pointer is written or read. That read model is projected by
|
|
160
159
|
// `pollUserTasks` (part of the imperative main.ts poll loop the testkit does not run), so drive it
|
|
161
160
|
// explicitly (mirrors feature-run.e2e.ts) to project the parked `wait-answer` task first.
|
|
162
|
-
await pollUserTasks(app.db,
|
|
161
|
+
await pollUserTasks(app.db, app.engine);
|
|
163
162
|
const status = await app.callRoute<StatusBody>({ method: "GET", path: "/app/api/status" });
|
|
164
163
|
assert.equal(status.status, 200, "the status endpoint responds");
|
|
165
164
|
const statusRow = status.body.prs.find((p) => p.prKey === prKey);
|
|
@@ -198,7 +197,7 @@ describe("nano-workforce PR review-loop escalation (U4 userTask)", () => {
|
|
|
198
197
|
// reconciles away, so no open escalation lingers on the status endpoint once answered + resumed.
|
|
199
198
|
// Re-run the projection (as the durable poller would) so the closed task's read-model row is
|
|
200
199
|
// deleted before re-reading /status.
|
|
201
|
-
await pollUserTasks(app.db,
|
|
200
|
+
await pollUserTasks(app.db, app.engine);
|
|
202
201
|
const afterStatus = await app.callRoute<StatusBody>({ method: "GET", path: "/app/api/status" });
|
|
203
202
|
const afterRow = afterStatus.body.prs.find((p) => p.prKey === prKey);
|
|
204
203
|
if (afterRow) {
|
package/e2e/feature-run.e2e.ts
CHANGED
|
@@ -23,7 +23,6 @@ import { fileURLToPath } from "node:url";
|
|
|
23
23
|
import type { EngineJob } from "@nanobpm/urban/runtime";
|
|
24
24
|
import { bootTestApp, type TestApp } from "@nanobpm/urban-testkit";
|
|
25
25
|
import { admitGithubState, installAdmitGithub } from "./support/github-admit.ts";
|
|
26
|
-
import { asEngineClient } from "./support/engine-client.ts";
|
|
27
26
|
import { pollUserTasks } from "../app/service.ts";
|
|
28
27
|
|
|
29
28
|
const APP_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -208,7 +207,7 @@ describe("single-issue feature run (#172 — feature.bpmn)", () => {
|
|
|
208
207
|
// read-model (issue #332 dropped the denormalised `feature_runs.blocked_user_task_key` pointer),
|
|
209
208
|
// so `pollUserTasks` projects the parked `feature-blocked` task onto `user_tasks` by reading the
|
|
210
209
|
// engine directly — no per-run column write.
|
|
211
|
-
await pollUserTasks(app.db,
|
|
210
|
+
await pollUserTasks(app.db, app.engine);
|
|
212
211
|
const inboxRow = await app.db
|
|
213
212
|
.table<{ user_task_key: string; element_id: string }>("user_tasks", "user_task_key")
|
|
214
213
|
.findOne({ user_task_key: task!.userTaskKey });
|
|
@@ -233,7 +232,7 @@ describe("single-issue feature run (#172 — feature.bpmn)", () => {
|
|
|
233
232
|
|
|
234
233
|
// A further poll pass is an idempotent no-op — the task is completed, so the read-model row is
|
|
235
234
|
// reconciled away and a terminal run is not a candidate.
|
|
236
|
-
await pollUserTasks(app.db,
|
|
235
|
+
await pollUserTasks(app.db, app.engine);
|
|
237
236
|
assert.equal((await featureRow(app, featureKey)).status, "blocked");
|
|
238
237
|
},
|
|
239
238
|
);
|
|
@@ -385,7 +384,7 @@ describe("single-issue feature run (#172 — feature.bpmn)", () => {
|
|
|
385
384
|
|
|
386
385
|
// The poller projects the parked task onto the Tasks inbox `user_tasks` read-model by reading
|
|
387
386
|
// the engine directly, sourcing the question from the `feature_escalations` audit log.
|
|
388
|
-
await pollUserTasks(app.db,
|
|
387
|
+
await pollUserTasks(app.db, app.engine);
|
|
389
388
|
const inboxRow = await app.db
|
|
390
389
|
.table<{ user_task_key: string; element_id: string; question: string | null }>("user_tasks", "user_task_key")
|
|
391
390
|
.findOne({ user_task_key: task!.userTaskKey });
|
|
@@ -414,7 +413,7 @@ describe("single-issue feature run (#172 — feature.bpmn)", () => {
|
|
|
414
413
|
assert.equal(settled.status, "opened", "the resumed run opened its PR");
|
|
415
414
|
|
|
416
415
|
// A further poll pass reconciles the completed task's read-model row away.
|
|
417
|
-
await pollUserTasks(app.db,
|
|
416
|
+
await pollUserTasks(app.db, app.engine);
|
|
418
417
|
const gone = await app.db
|
|
419
418
|
.table<{ user_task_key: string }>("user_tasks", "user_task_key")
|
|
420
419
|
.findOne({ user_task_key: task!.userTaskKey });
|
|
@@ -26,7 +26,6 @@ import { after, before, describe, test } from "node:test";
|
|
|
26
26
|
import { fileURLToPath } from "node:url";
|
|
27
27
|
import { bootTestApp, type TestApp } from "@nanobpm/urban-testkit";
|
|
28
28
|
import { pollUserTasks } from "../app/service.ts";
|
|
29
|
-
import { asEngineClient } from "./support/engine-client.ts";
|
|
30
29
|
|
|
31
30
|
const APP_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
32
31
|
const DB_DIR = mkdtempSync(join(tmpdir(), "nwf-u7-"));
|
|
@@ -237,7 +236,7 @@ describe("retire escalation subsystem (U7 — destructive contract phase)", () =
|
|
|
237
236
|
// written. That read model is projected by `pollUserTasks` (part of the imperative main.ts poll
|
|
238
237
|
// loop the testkit does not run), so drive it explicitly (mirrors feature-run.e2e.ts) to project
|
|
239
238
|
// the parked `wait-answer` task before reading /status.
|
|
240
|
-
await pollUserTasks(app.db,
|
|
239
|
+
await pollUserTasks(app.db, app.engine);
|
|
241
240
|
const status = await app.callRoute<StatusBody>({ method: "GET", path: "/app/api/status" });
|
|
242
241
|
const statusRow = status.body.prs.find((p) => p.prKey === prKey);
|
|
243
242
|
assert.equal(statusRow?.status, "escalated", "the PR reads as escalated");
|
|
@@ -265,7 +264,7 @@ describe("retire escalation subsystem (U7 — destructive contract phase)", () =
|
|
|
265
264
|
// No addressed escalation lingers — the completed task's `user_tasks` row reconciles away. Re-run
|
|
266
265
|
// the projection (as the durable poller would) so the closed task's read-model row is deleted
|
|
267
266
|
// before re-reading /status.
|
|
268
|
-
await pollUserTasks(app.db,
|
|
267
|
+
await pollUserTasks(app.db, app.engine);
|
|
269
268
|
const afterStatus = await app.callRoute<StatusBody>({ method: "GET", path: "/app/api/status" });
|
|
270
269
|
const afterRow = afterStatus.body.prs.find((p) => p.prKey === prKey);
|
|
271
270
|
if (afterRow) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.179.
|
|
3
|
+
"version": "0.179.2",
|
|
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",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@nanobpm/agentic": "^0.12.0",
|
|
68
|
-
"@nanobpm/urban": "^0.
|
|
68
|
+
"@nanobpm/urban": "^0.91.0",
|
|
69
69
|
"bpmn-auto-layout": "^2.0.0-alpha.2"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
package/pages/tasks.page.json
CHANGED
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"type": "text",
|
|
75
75
|
"id": "subtitle",
|
|
76
76
|
"props": {
|
|
77
|
-
"text": "Every open native user-task escalation awaiting a human decision — feature runs, epics, PR review/merge loops, conformance reviews, and delivery-graph human/escalation gates — in one list. The \"Type\" column tells them apart; open a row to complete it via the process's own deployed form,
|
|
77
|
+
"text": "Every open native user-task escalation awaiting a human decision — feature runs, epics, PR review/merge loops, conformance reviews, and delivery-graph human/escalation gates — in one list. The \"Type\" column tells them apart; open a row to complete it via the process's own deployed form, through the nwf canonical completion door (operations/completeUserTask.ts) — recording who answered and dropping the row immediately. This list is exactly the set the nav badge counts, so a badge always maps to a visible, completable row. Rows disappear once completed (here, or out-of-band).",
|
|
78
78
|
"variant": "sub"
|
|
79
79
|
}
|
|
80
80
|
},
|
|
@@ -116,7 +116,8 @@
|
|
|
116
116
|
],
|
|
117
117
|
"engineForm": {
|
|
118
118
|
"formKeyField": "form_key",
|
|
119
|
-
"userTaskKeyField": "user_task_key"
|
|
119
|
+
"userTaskKeyField": "user_task_key",
|
|
120
|
+
"completePath": "/app/api/actions/complete-user-task"
|
|
120
121
|
}
|
|
121
122
|
},
|
|
122
123
|
"refreshMs": 5000
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { EngineClient } from "@nanobpm/urban";
|
|
2
|
-
import type { TestApp } from "@nanobpm/urban-testkit";
|
|
3
|
-
|
|
4
|
-
// `@nanobpm/urban-testkit`'s `WasmEngineClient` has historically lagged urban's `EngineClient`
|
|
5
|
-
// interface: a method lands on the real seam a release or two before the testkit fake grows it. Both
|
|
6
|
-
// guards below defensively COMPLETE the contract for any method the *installed* testkit hasn't
|
|
7
|
-
// implemented yet — each is an idempotent `if (typeof … !== "function")`, so it no-ops the moment a
|
|
8
|
-
// testkit release ships the real method (no version pins to drift). Scoped to the test harness;
|
|
9
|
-
// production adapters (`SdkEngineClient`) implement both for real.
|
|
10
|
-
//
|
|
11
|
-
// - `getForm` (urban 0.49.0, ADR 0062): the original instance of this lag. Now implemented by the
|
|
12
|
-
// currently pinned testkit, so this guard is a no-op there; kept as defence against version skew.
|
|
13
|
-
// These hermetic flows drive completion directly and never resolve a form schema, so the fallback
|
|
14
|
-
// returns `null` — the documented "no matching form" path.
|
|
15
|
-
// - `openUserTasks` (issue #294 moved the pollers onto it): the CURRENT gap — the pinned testkit has
|
|
16
|
-
// `searchUserTasks` but not the open-task-scoped `openUserTasks`, so a poller call throws
|
|
17
|
-
// `not a function` (swallowed by the poller's try/catch) and the read-model denormalisation
|
|
18
|
-
// silently no-ops. Polyfill it as `searchUserTasks({ state: "CREATED" })` — byte-for-byte what
|
|
19
|
-
// urban's real `SdkEngineClient.openUserTasks` does, so the two cannot drift. Fix: nano-workforce
|
|
20
|
-
// #309; categorical (testkit) fix upstream in nanobpm/nano-ide#341.
|
|
21
|
-
export function asEngineClient(engine: TestApp["engine"]): EngineClient {
|
|
22
|
-
const e = engine as unknown as EngineClient & {
|
|
23
|
-
getForm?: EngineClient["getForm"];
|
|
24
|
-
openUserTasks?: EngineClient["openUserTasks"];
|
|
25
|
-
};
|
|
26
|
-
if (typeof e.getForm !== "function") {
|
|
27
|
-
e.getForm = async () => null;
|
|
28
|
-
}
|
|
29
|
-
if (typeof e.openUserTasks !== "function") {
|
|
30
|
-
e.openUserTasks = (filter) => e.searchUserTasks({ ...filter, state: "CREATED" });
|
|
31
|
-
}
|
|
32
|
-
return e;
|
|
33
|
-
}
|