@nanobpm/nano-workforce 0.138.1 → 0.138.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/AGENTS.md +68 -3
- package/CHANGELOG.md +12 -0
- package/app/planReadModel.test.ts +23 -0
- package/db/migrations/084_plan_wave_tasks_effective_status.sql +51 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -211,6 +211,42 @@ Two different `nano:dataEnvelope` uses have different rules — don't conflate t
|
|
|
211
211
|
deriving a worker's array inputs from the model this way over a hand-typed
|
|
212
212
|
`interface In`; don't "fix" a modelled `list="true"` array back to a joined
|
|
213
213
|
scalar.
|
|
214
|
+
- **A service-task/worker `dataEnvelope.in` field is read from PROCESS scope, not
|
|
215
|
+
task-local `ioMapping`.** (This applies to service-task/worker job envelopes; a
|
|
216
|
+
`dataEnvelope.in` on a **message subscription** — e.g. `CapsResolved` at
|
|
217
|
+
`plan-fanout.bpmn:12` — is instead populated from the correlated **message
|
|
218
|
+
payload**, so this gotcha does not apply there.) At runtime the engine populates
|
|
219
|
+
each declared `.in` field *by name* from the process variables in scope — a
|
|
220
|
+
`<zeebe:ioMapping>` input that synthesises a **new** name with no backing process
|
|
221
|
+
variable is silently ignored, so the field arrives **blank**. The engine itself
|
|
222
|
+
raises no incident for that *omitted* field (an `.in` name with no backing
|
|
223
|
+
variable resolves to null and blanks) — but this is distinct from a FEEL
|
|
224
|
+
`ioMapping` that **errors**: on the pinned engine (`@nanobpm/engine-wasm` 0.8.1's
|
|
225
|
+
`IO_MAPPING_ERROR` fix — before it, a failed mapping silently blanked, e.g. a
|
|
226
|
+
blocked converge-gate's escalation *question*; see the `e2e/*escalation*.e2e.ts`
|
|
227
|
+
comments) a failed FEEL mapping now raises an **incident** rather than blanking
|
|
228
|
+
through. A consumer that fails fast on the blank value will also incident (e.g.
|
|
229
|
+
`pr.readiness-probe` once read an empty `gateKey`;
|
|
230
|
+
`readGateVars` in `workers/readiness-probe/worker.ts` now *throws* on a blank
|
|
231
|
+
`gateKey`, creating an incident). Tasks like `ensure-base-branch` work only because
|
|
232
|
+
`repo`/`baseBranch` already exist as process variables. Seed any field a task must
|
|
233
|
+
read — **that is not already created in the task's visible process scope** — as a
|
|
234
|
+
real process variable at plan start (e.g. in `startPlan`); don't rely on
|
|
235
|
+
`ioMapping` to synthesise it. (A field already materialised in scope by the
|
|
236
|
+
process — e.g. `ReadinessProbeIn.probe`, seeded per multi-instance child via
|
|
237
|
+
`inputElement="probe"` in `resources/processes/feature.bpmn` — needs no such
|
|
238
|
+
seeding.) A **constant/literal** ioMapping source is different again: a
|
|
239
|
+
`<zeebe:input source="=true" target="lastAttempt"/>` (the readiness-gate
|
|
240
|
+
boundary's last-attempt marker — `readiness-gate.bpmn:125`, `feature.bpmn:105`,
|
|
241
|
+
`plan-fanout.bpmn:198`, and the generated `deliveryGraphCompiler.ts:1011`) is
|
|
242
|
+
*evaluated* and materialised into the job's scope, so it needs **no** backing
|
|
243
|
+
process variable — its value is the literal, not a reference to an (absent)
|
|
244
|
+
variable. The "arrives blank" failure above is specific to a field whose value
|
|
245
|
+
must be *derived from a process variable that isn't in scope* (the `gateKey`
|
|
246
|
+
case). Do **not** "fix" a working literal marker like `lastAttempt` by seeding
|
|
247
|
+
it as a process variable: besides being unnecessary, seeding a var an
|
|
248
|
+
in-subprocess gateway reads re-introduces the multi-instance `=null`-shadow
|
|
249
|
+
gotcha above.
|
|
214
250
|
|
|
215
251
|
## Urban page runtime: rendering primitives are not JS-truthy
|
|
216
252
|
|
|
@@ -235,6 +271,21 @@ silently — cheap to avoid, annoying to debug after the fact.
|
|
|
235
271
|
`showWhenField` is hidden for `0`/`null`/`""` alike — so a `0`-or-`NULL` flag
|
|
236
272
|
correctly hides it either way. (This is why the same flag can need `NULL` for a
|
|
237
273
|
badge yet work as `0` for a `showWhenField` button.)
|
|
274
|
+
- **Renderer kinds are a version-pinned set — verify against the installed
|
|
275
|
+
package, not an issue reference.** The page renderers are a finite set fixed by
|
|
276
|
+
the pinned `@nanobpm/urban`; a version *range* only guarantees *some* matching
|
|
277
|
+
build is installed. Do **not** assume a primitive exists because an issue,
|
|
278
|
+
changelog, or *Links* section mentions it — `urban check` (a CI gate) rejects
|
|
279
|
+
unknown renderer kinds only *after* you've authored a page around a phantom one
|
|
280
|
+
(epic #254 bounced three review rounds asserting a page-level "stepper" that
|
|
281
|
+
never shipped). Confirm the kind against the installed `RENDERERS` map
|
|
282
|
+
(`node_modules/@nanobpm/urban/dist/runtime/core/modules/pages.js`) and the kinds
|
|
283
|
+
already used across `pages/*.page.json`. Note a **nano-ide issue can ship a data
|
|
284
|
+
read-model, not a page renderer** (nano-ide#254 shipped the lineage projection, a data
|
|
285
|
+
primitive) — read *what kind* of primitive it delivers. Build composite visuals
|
|
286
|
+
as a `dataGrid` column renderer over stored columns (e.g. the `"kind":
|
|
287
|
+
"pipeline"` column in `pages/feature.page.json`), not a page-level primitive,
|
|
288
|
+
unless you've verified that primitive exists in the installed build.
|
|
238
289
|
|
|
239
290
|
### The top nav has a single source of truth — edit `pages/_nav.json`
|
|
240
291
|
|
|
@@ -271,10 +322,24 @@ Migrations live in `db/migrations/*.sql` and are **auto-applied on boot** from
|
|
|
271
322
|
- Number a new migration after the current highest prefix (they apply in order).
|
|
272
323
|
Check `origin/main`, not your branch point — a fan-out epic branch forks at one
|
|
273
324
|
prefix while `main` keeps advancing, so the branch-local "next" number collides
|
|
274
|
-
on merge.
|
|
325
|
+
on merge. **In a *simultaneous* fan-out wave, checking `origin/main` isn't
|
|
326
|
+
enough**: every sibling forks at the same commit, sees the same highest prefix,
|
|
327
|
+
and independently takes the same next number (main hasn't advanced yet), so the
|
|
328
|
+
planner must **pre-assign each slice a disjoint prefix block at decomposition
|
|
329
|
+
time** (e.g. give slice A `NNN`–`NNN+1`, slice B `NNN+2`–`NNN+3`, plus a separate
|
|
330
|
+
cleanup block — start the whole allocation *after* the current highest committed
|
|
331
|
+
prefix, never at fixed literals like `060`, which are long occupied) rather than
|
|
332
|
+
have each
|
|
333
|
+
agent compute "the next free" prefix. (This is the same "coarsen parallel work on
|
|
334
|
+
a shared surface" principle as the one-task-owns-each-`.bpmn` rule above — a
|
|
335
|
+
migration-prefix block is a shared numbering surface just like a `.bpmn`
|
|
336
|
+
diagram.) Two files must never share a prefix; `npm run check:migrations`
|
|
275
337
|
(a CI gate) enforces this and fails the build on any new duplicate. Because a
|
|
276
|
-
prefix collision only exists in the *union* of two branches
|
|
277
|
-
|
|
338
|
+
**merge-skew** prefix collision only exists in the *union* of two branches (a
|
|
339
|
+
single branch that adds two same-prefix files is a self-collision
|
|
340
|
+
`check:migrations` already catches on the PR itself), this gate — like
|
|
341
|
+
`layout:check`, the navigation-index check (`sync:nav:check`), and a catch-all
|
|
342
|
+
committed-artifact backstop (`git diff --exit-code`) — is also re-run on the
|
|
278
343
|
merge queue's **prospective merged commit** and on **push to `main`** by
|
|
279
344
|
`.github/workflows/invariants.yml` (issue #366), so a merge-skew collision is
|
|
280
345
|
blocked at merge time or fails a `main`-scoped build within minutes rather than
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [0.138.3](https://github.com/nanobpm/nano-workforce/compare/v0.138.2...v0.138.3) (2026-08-24)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* **epic-detail:** derive plan_wave_tasks.status=merged so a landed slice isn't stranded at "opened" ([#530](https://github.com/nanobpm/nano-workforce/issues/530)) ([#531](https://github.com/nanobpm/nano-workforce/issues/531)) ([3893f7f](https://github.com/nanobpm/nano-workforce/commit/3893f7fe5534a1510b53a489a0aa83630c6207b9))
|
|
6
|
+
|
|
7
|
+
## [0.138.2](https://github.com/nanobpm/nano-workforce/compare/v0.138.1...v0.138.2) (2026-08-24)
|
|
8
|
+
|
|
9
|
+
### Documentation
|
|
10
|
+
|
|
11
|
+
* **agents:** consolidate retro feedback ([#268](https://github.com/nanobpm/nano-workforce/issues/268), [#322](https://github.com/nanobpm/nano-workforce/issues/322), [#449](https://github.com/nanobpm/nano-workforce/issues/449)); drop stale [#415](https://github.com/nanobpm/nano-workforce/issues/415) ([#518](https://github.com/nanobpm/nano-workforce/issues/518)) ([3c47cb2](https://github.com/nanobpm/nano-workforce/commit/3c47cb21d079b6b9f5470abacb0190373208d0ca)), closes [#416](https://github.com/nanobpm/nano-workforce/issues/416) [#887](https://github.com/nanobpm/nano-workforce/issues/887) [#254](https://github.com/nanobpm/nano-workforce/issues/254) [#254](https://github.com/nanobpm/nano-workforce/issues/254) [nano-ide#254](https://github.com/nanobpm/nano-ide/issues/254)
|
|
12
|
+
|
|
1
13
|
## [0.138.1](https://github.com/nanobpm/nano-workforce/compare/v0.138.0...v0.138.1) (2026-08-24)
|
|
2
14
|
|
|
3
15
|
### Bug Fixes
|
|
@@ -49,6 +49,7 @@ const MIGRATION_CHAIN = [
|
|
|
49
49
|
"080_plan_read_model_derive_terminal.sql",
|
|
50
50
|
ROLLUPS_MIGRATION,
|
|
51
51
|
READ_MODEL_MIGRATION,
|
|
52
|
+
"084_plan_wave_tasks_effective_status.sql",
|
|
52
53
|
];
|
|
53
54
|
|
|
54
55
|
// The base `plans` / `plan_tasks` / `pull_requests` shapes the VIEWs read, plus a stand-in for the
|
|
@@ -430,6 +431,28 @@ test("plan_wave_tasks carries each task's PR url + process_key link targets (unc
|
|
|
430
431
|
assertEquals({ pr_url: rows[1].pr_url, process_key: rows[1].process_key }, { pr_url: null, process_key: null });
|
|
431
432
|
});
|
|
432
433
|
|
|
434
|
+
test("plan_wave_tasks derives status=merged from the PR (never strands a landed slice at 'opened') — matching the summary bar", () => {
|
|
435
|
+
// The reported defect (#530): a slice whose PR converged + merged kept reading Status "opened" in
|
|
436
|
+
// the wave-state grid, because nothing writes `plan_tasks.status='merged'` on merge and the VIEW
|
|
437
|
+
// exposed the raw task status. The fix DERIVES the displayed status the SAME way the count VIEWs
|
|
438
|
+
// bucket `merged` — `pull_requests__tracking.derived_status = 'merged'` overrides the raw status —
|
|
439
|
+
// so the per-task grid and the per-wave summary bar agree.
|
|
440
|
+
const db = viewDb();
|
|
441
|
+
addPlan(db, "o/r#eff", { status: "done" });
|
|
442
|
+
addTask(db, "o/r#eff", { status: "opened", wave: 0, prStatus: "merged" }); // landed slice, task row frozen at "opened"
|
|
443
|
+
addTask(db, "o/r#eff", { status: "opened", wave: 0, prStatus: "converging" }); // still converging → stays "opened"
|
|
444
|
+
addTask(db, "o/r#eff", { status: "blocked", wave: 0 }); // no PR → raw status untouched
|
|
445
|
+
// A DERIVE-ONLY merged edge (base PR status frozen, tracking recomputes to `merged`) also reads merged.
|
|
446
|
+
addTask(db, "o/r#eff", { status: "opened", wave: 1, prStatus: "converging", prDerivedOverride: "merged" });
|
|
447
|
+
const byWaveIdx = db
|
|
448
|
+
.prepare("SELECT wave, task_index, status FROM plan_wave_tasks WHERE plan_key = ? ORDER BY task_index")
|
|
449
|
+
.all("o/r#eff") as Array<Record<string, unknown>>;
|
|
450
|
+
assertEquals(byWaveIdx.map((r) => r.status), ["merged", "opened", "blocked", "merged"]);
|
|
451
|
+
// The count VIEW and the per-task grid now agree on the merged tally for wave 0 (3 slices, 1 merged).
|
|
452
|
+
const c = db.prepare("SELECT merged, total FROM plan_wave_counts WHERE plan_key = ? AND wave = 0").get("o/r#eff") as Record<string, unknown>;
|
|
453
|
+
assertEquals({ merged: Number(c.merged), total: Number(c.total) }, { merged: 1, total: 3 });
|
|
454
|
+
});
|
|
455
|
+
|
|
433
456
|
test("the operator pages bind the derived plan-family VIEWs (never the raw plans table)", () => {
|
|
434
457
|
// Overview + Epic index + Epic detail all read the composite `plan_read_model`; the epic-detail
|
|
435
458
|
// per-wave summary reads `plan_wave_summary` (the bar), and the wave-state grid `plan_wave_tasks`.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
-- Epic-detail wave-state: a merged slice must not read "opened" (drift with the summary bar).
|
|
2
|
+
--
|
|
3
|
+
-- `plan_wave_tasks` (059) is the per-task display VIEW the Epic-detail "Wave state" grid binds
|
|
4
|
+
-- (pages/epic-detail.page.json → `wave-state`). Its `status` column exposed the RAW
|
|
5
|
+
-- `plan_tasks.status` verbatim — but nothing writes `plan_tasks.status = 'merged'` when a slice's PR
|
|
6
|
+
-- lands: the merge write-path flips `pull_requests.status` (and, for a close-without-merge, the
|
|
7
|
+
-- `abandonClosedPr` self-heal flips the task terminal), yet a *merged* PR leaves its `plan_tasks` row
|
|
8
|
+
-- frozen at `opened`. So a converged-and-merged slice kept showing Status "opened" in the grid, even
|
|
9
|
+
-- as its sibling summary VIEWs (`plan_wave_counts`/`plan_wave_summary`) already counted it `merged`
|
|
10
|
+
-- via the PR join. That is exactly the drift AGENTS.md forbids: two sibling VIEWs over the same join
|
|
11
|
+
-- disagreeing on whether a slice is merged.
|
|
12
|
+
--
|
|
13
|
+
-- Fix by DERIVING the displayed status the SAME way the count VIEWs bucket `merged` (082): a task is
|
|
14
|
+
-- `merged` iff its PR reached `pull_requests__tracking.derived_status = 'merged'` (the terminal-folded
|
|
15
|
+
-- status the canonical runtime reads, ADR-0065), otherwise it falls through to its own
|
|
16
|
+
-- `plan_tasks.status`. This keeps the per-task grid and the per-wave summary bar in exact agreement —
|
|
17
|
+
-- a single notion of "effective task status", still fully DERIVED with no write-path. `pr_url` /
|
|
18
|
+
-- `process_key` link targets are re-exported by the `pull_requests__tracking` VIEW (`p.*`), so the
|
|
19
|
+
-- single join now sources both the effective status and the link targets. The "Active" tab filter
|
|
20
|
+
-- (`status IN (pending,opened,escalated,waiting-for-lane,blocked)`) therefore drops a merged slice as
|
|
21
|
+
-- intended instead of stranding it under "opened".
|
|
22
|
+
--
|
|
23
|
+
-- Forward-only VIEW redefinition (DROP then CREATE); a merged VIEW is not editable in place. The
|
|
24
|
+
-- runner wraps each file in its own transaction, so this file must NOT contain BEGIN/COMMIT. Kept a
|
|
25
|
+
-- plain `CREATE VIEW … AS SELECT … FROM …` (no CTE / no select-list subquery) so the static
|
|
26
|
+
-- pages↔schema contract guard (scripts/pages-contract.test.ts) still parses it. Numbered after 083.
|
|
27
|
+
|
|
28
|
+
DROP VIEW IF EXISTS plan_wave_tasks;
|
|
29
|
+
|
|
30
|
+
CREATE VIEW plan_wave_tasks AS
|
|
31
|
+
SELECT
|
|
32
|
+
t.id AS id,
|
|
33
|
+
t.plan_key AS plan_key,
|
|
34
|
+
t.task_index AS task_index,
|
|
35
|
+
t.task_id AS task_id,
|
|
36
|
+
t.title AS title,
|
|
37
|
+
t.prompt AS prompt,
|
|
38
|
+
CASE WHEN p.derived_status = 'merged' THEN 'merged' ELSE t.status END AS status,
|
|
39
|
+
t.pr_key AS pr_key,
|
|
40
|
+
t.summary AS summary,
|
|
41
|
+
t.created_at AS created_at,
|
|
42
|
+
t.updated_at AS updated_at,
|
|
43
|
+
t.wave AS wave,
|
|
44
|
+
t.open_question AS open_question,
|
|
45
|
+
t.answer AS answer,
|
|
46
|
+
t.draft_pr_key AS draft_pr_key,
|
|
47
|
+
t.corr_key AS corr_key,
|
|
48
|
+
p.url AS pr_url,
|
|
49
|
+
p.process_key AS process_key
|
|
50
|
+
FROM plan_tasks t
|
|
51
|
+
LEFT JOIN pull_requests__tracking p ON p.pr_key = t.pr_key;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.138.
|
|
3
|
+
"version": "0.138.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",
|