@nanobpm/nano-workforce 0.189.2 → 0.189.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.
@@ -0,0 +1,63 @@
1
+ -- Feature-run read model: exclude a MID-HANDOFF `opened` run from the `list_bucket` / `ack_open`
2
+ -- DISMISSABLE-terminal predicate (issue #808 follow-up). SUPERSEDES 111's VIEW body — every DERIVED
3
+ -- column is emitted VERBATIM from the ONE declaration in app/featureReadModel.ts, now with `list_bucket`/
4
+ -- `ack_open` driven by the `featureDismissableTerminal` PREDICATE instead of a plain status set. 111 is a
5
+ -- MERGED, IMMUTABLE migration — never edited; this is a NEW migration superseding its VIEW body (the same
6
+ -- pattern by which 111 superseded 099, 099 superseded 081, 081 superseded 076).
7
+ --
8
+ -- WHY. 111 (issue #808) made EVERY `opened` row dismissable-terminal so a finished raise-only run stops
9
+ -- wedging in Active. But `record-feature` writes `status="opened"` for a converge-REQUESTED run TOO,
10
+ -- BEFORE the `gw-converge` gateway routes it into `converge-feature` (which flips it to `converging`).
11
+ -- In that in-between window the row reads `opened` while the feature engine instance is still ACTIVE, so
12
+ -- 111's "any opened is dismissable" rule offered Dismiss on an in-flight convergence handoff — and a
13
+ -- premature ack there stamps `acknowledged_at`, which would later drag a still-converging run to History.
14
+ -- This is the SAME mid-flight failure mode the epic read model already guards against by excluding its
15
+ -- own `converging` delivery (app/planReadModel.ts / app/listBucket.ts). The feature row distinguishes the
16
+ -- transient from a finished run by its own columns: a converge-requested run that actually handed off
17
+ -- carries `converge=1 AND pr_key IS NOT NULL` — exactly the window to exclude. Every other `opened` stays
18
+ -- dismissable: a raise-only run (`converge=0`), or a keyless `opened` that never satisfied the gateway's
19
+ -- `prKey != null` and fell through to `End` (`pr_key IS NULL`).
20
+ --
21
+ -- Every DERIVED column below is emitted VERBATIM from `featureReadModel.sqlSelectFor(col,
22
+ -- { baseAlias: "fr" })` (which ALSO drives the runtime TS via `fnFor`); the `stage`/`stage_state`/
23
+ -- `stage_skipped`/`attention` bodies are UNCHANGED from 111 and only `list_bucket`/`ack_open` gain the
24
+ -- mid-handoff exclusion. The drift guard (app/featureReadModel.test.ts) fails if this file stops matching
25
+ -- the declaration, and `assertReadModelParity` proves the SQL and TS lowerings agree.
26
+ --
27
+ -- SEMANTICS unchanged from 111 EXCEPT the two mid-handoff-narrowed columns: the status-classifying
28
+ -- columns still read the terminal-folded `feature_runs__tracking.derived_status`; base columns stay
29
+ -- aliased identity pass-throughs; `acknowledged_at` still passes through so the read model can classify
30
+ -- on it. The STAGE derivations are untouched, so a mid-handoff `opened` still renders `PR open`.
31
+ --
32
+ -- Forward-only VIEW redefinition (DROP then CREATE). `feature_runs__tracking` is the managed VIEW urban
33
+ -- provisions at mount; SQLite does not validate a view body at CREATE time, so this migration (which
34
+ -- runs before that mount) is created fine and resolves once the managed VIEW exists. The runner wraps
35
+ -- each file in its own transaction, so this file must NOT contain BEGIN/COMMIT. Numbered after 111.
36
+
37
+ DROP VIEW IF EXISTS feature_read_model;
38
+
39
+ CREATE VIEW feature_read_model AS
40
+ SELECT
41
+ fr.feature_key AS feature_key,
42
+ fr.repo AS repo,
43
+ fr.issue_number AS issue_number,
44
+ fr.issue_url AS issue_url,
45
+ fr.title AS title,
46
+ fr.base_branch AS base_branch,
47
+ fr.status AS status,
48
+ fr.process_key AS process_key,
49
+ fr.pr_key AS pr_key,
50
+ fr.converge AS converge,
51
+ fr.auto_merge AS auto_merge,
52
+ fr.outcome AS outcome,
53
+ fr.delivery_label AS delivery_label,
54
+ fr.acknowledged_at AS acknowledged_at,
55
+ fr.created_at AS created_at,
56
+ fr.updated_at AS updated_at,
57
+ CASE WHEN COALESCE((COALESCE(("fr"."derived_status" = 'merged'), 0) OR COALESCE(("fr"."derived_status" = 'converged'), 0) OR COALESCE(("fr"."derived_status" = 'blocked'), 0) OR COALESCE(("fr"."derived_status" = 'failed'), 0) OR COALESCE(("fr"."derived_status" = 'skipped'), 0) OR COALESCE(("fr"."derived_status" = 'abandoned'), 0)), 0) THEN 'Done' WHEN COALESCE(("fr"."derived_status" = 'converging'), 0) THEN 'Converging' WHEN COALESCE((COALESCE(("fr"."pr_key" <> ''), 0) OR COALESCE(("fr"."derived_status" = 'opened'), 0)), 0) THEN 'PR open' WHEN COALESCE((COALESCE(("fr"."derived_status" = 'running'), 0) OR COALESCE(("fr"."derived_status" = 'escalated'), 0) OR COALESCE(("fr"."derived_status" = 'awaiting_operator'), 0)), 0) THEN 'Implementing' ELSE 'Requested' END AS stage,
58
+ CASE WHEN COALESCE((COALESCE(("fr"."derived_status" = 'merged'), 0) OR COALESCE(("fr"."derived_status" = 'converged'), 0)), 0) THEN 'ok' WHEN COALESCE(("fr"."derived_status" = 'blocked'), 0) THEN 'blocked' WHEN COALESCE((COALESCE(("fr"."derived_status" = 'failed'), 0) OR COALESCE(("fr"."derived_status" = 'skipped'), 0) OR COALESCE(("fr"."derived_status" = 'abandoned'), 0)), 0) THEN 'failed' ELSE NULL END AS stage_state,
59
+ CASE WHEN (NOT COALESCE("fr"."converge", 0)) THEN 'Converging Merging' WHEN (NOT COALESCE("fr"."auto_merge", 0)) THEN 'Merging' ELSE '' END AS stage_skipped,
60
+ CASE WHEN EXISTS (SELECT 1 FROM "user_tasks" AS "__urban_proj_0" WHERE COALESCE((COALESCE(("__urban_proj_0"."subject_type" = 'feature'), 0) AND COALESCE(("__urban_proj_0"."subject_key" = "fr"."feature_key"), 0) AND COALESCE(("__urban_proj_0"."element_id" = 'feature-blocked'), 0)), 0)) THEN 'blocked' WHEN EXISTS (SELECT 1 FROM "user_tasks" AS "__urban_proj_0" WHERE COALESCE((COALESCE(("__urban_proj_0"."subject_type" = 'feature'), 0) AND COALESCE(("__urban_proj_0"."subject_key" = "fr"."feature_key"), 0) AND COALESCE(("__urban_proj_0"."element_id" = 'feature-escalation'), 0)), 0)) THEN '⚠' ELSE NULL END AS attention,
61
+ CASE WHEN COALESCE((COALESCE((COALESCE((COALESCE(("fr"."derived_status" = 'merged'), 0) OR COALESCE(("fr"."derived_status" = 'converged'), 0) OR COALESCE(("fr"."derived_status" = 'blocked'), 0) OR COALESCE(("fr"."derived_status" = 'failed'), 0) OR COALESCE(("fr"."derived_status" = 'skipped'), 0) OR COALESCE(("fr"."derived_status" = 'abandoned'), 0) OR COALESCE(("fr"."derived_status" = 'opened'), 0)), 0) AND (NOT COALESCE(COALESCE((COALESCE(("fr"."derived_status" = 'opened'), 0) AND COALESCE(("fr"."converge" = 1), 0) AND COALESCE(("fr"."pr_key" = "fr"."pr_key"), 0)), 0), 0))), 0) AND COALESCE(("fr"."acknowledged_at" = "fr"."acknowledged_at"), 0)), 0) THEN 'history' ELSE 'active' END AS list_bucket,
62
+ CASE WHEN COALESCE((COALESCE((COALESCE((COALESCE(("fr"."derived_status" = 'merged'), 0) OR COALESCE(("fr"."derived_status" = 'converged'), 0) OR COALESCE(("fr"."derived_status" = 'blocked'), 0) OR COALESCE(("fr"."derived_status" = 'failed'), 0) OR COALESCE(("fr"."derived_status" = 'skipped'), 0) OR COALESCE(("fr"."derived_status" = 'abandoned'), 0) OR COALESCE(("fr"."derived_status" = 'opened'), 0)), 0) AND (NOT COALESCE(COALESCE((COALESCE(("fr"."derived_status" = 'opened'), 0) AND COALESCE(("fr"."converge" = 1), 0) AND COALESCE(("fr"."pr_key" = "fr"."pr_key"), 0)), 0), 0))), 0) AND (NOT COALESCE(COALESCE(("fr"."acknowledged_at" = "fr"."acknowledged_at"), 0), 0))), 0) THEN 1 ELSE 0 END AS ack_open
63
+ FROM feature_runs__tracking fr;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.189.2",
3
+ "version": "0.189.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",
@@ -30,14 +30,23 @@ const handler: AppJobHandler<In, Record<string, never>> = async (job, app) => {
30
30
  await featureRuns(app.data).update(featureKey, { status: "opened", updated_at: new Date().toISOString() });
31
31
  return {};
32
32
  }
33
- // `convergeOnly` is the inverse of auto-merge: converge-only stops at `converged`; auto-merge lets
34
- // the merge-loop drive the merge. `submitPr` is idempotent on the PR key.
35
- await submitPr(app.data, app.engine, parsed, [], MAX_ROUNDS, !autoMerge, featureKey);
33
+ // Write `converging` BEFORE enrolling the PR (issue #808 / PR #809 review). The old order —
34
+ // `submitPr` then the status write — left an `opened` + `converge=1` + `pr_key` window spanning an
35
+ // ALREADY-SUBMITTED, live PR: a terminate/crash in that gap stranded a row whose PR was live while
36
+ // `pollFeatureDelivery`'s handoff edge (edge 3) read the FEATURE instance state alone and wrongly
37
+ // folded it to `abandoned`. Flipping to `converging` first shrinks the interrupted-handoff window to
38
+ // the (now converging-owned) gap BEFORE `submitPr`, which `pollFeatureDelivery`'s converging edge
39
+ // (edge 1) heals by re-enrolling when the PR row is still missing (idempotent, mirroring the
40
+ // promotion-PR re-enroll in `pollPromotion`). The reorder is safe: `submitPr` is idempotent on the PR
41
+ // key (it early-returns `alreadyRunning` for a non-terminal PR), and a `converging` row is owned by
42
+ // edge (1), never by the handoff edge. `convergeOnly` is the inverse of auto-merge: converge-only
43
+ // stops at `converged`; auto-merge lets the merge-loop drive the merge.
36
44
  await featureRuns(app.data).update(featureKey, {
37
45
  status: "converging",
38
46
  pr_key: parsed.prKey,
39
47
  updated_at: new Date().toISOString(),
40
48
  });
49
+ await submitPr(app.data, app.engine, parsed, [], MAX_ROUNDS, !autoMerge, featureKey);
41
50
  app.log.info("converge-feature: enrolled PR into convergence loop", {
42
51
  featureKey,
43
52
  prKey: parsed.prKey,