@nanobpm/nano-workforce 0.138.2 → 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/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
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
+
1
7
  ## [0.138.2](https://github.com/nanobpm/nano-workforce/compare/v0.138.1...v0.138.2) (2026-08-24)
2
8
 
3
9
  ### Documentation
@@ -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.2",
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",