@nanobpm/nano-workforce 0.131.1 → 0.133.0

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,153 @@
1
+ // Read-model guard for migration 079's extended `lineage_thread_view` VIEW (issue #498: surface
2
+ // delivery-graph runs in the Lineage tab as a fan-in parent thread). Mirrors app/migration064.test.ts:
3
+ // apply the migration to a real in-memory SQLite DB and assert the VIEW's output over sample rows —
4
+ // so this exercises the real view, not a re-implementation.
5
+ //
6
+ // The extended view adds a third origin arm: a root that matches a `delivery_graph_runs.run_key`
7
+ // derives `kind = 'delivery'`, `title` from the run, and a NULL `issue_url` (a run is keyed by
8
+ // run_key/digest, not a GitHub issue). The epic/feature/pr arms must keep behaving exactly as 064's
9
+ // view did (precedence unchanged).
10
+ import { readFileSync } from "node:fs";
11
+ import { DatabaseSync } from "node:sqlite";
12
+ import { test } from "node:test";
13
+ import { fileURLToPath } from "node:url";
14
+ import { assertEquals } from "#test-assert";
15
+
16
+ const MIGRATION_064 = fileURLToPath(new URL("../db/migrations/064_lineage_thread_view.sql", import.meta.url));
17
+ const MIGRATION_079 = fileURLToPath(
18
+ new URL("../db/migrations/079_lineage_thread_view_delivery.sql", import.meta.url),
19
+ );
20
+
21
+ /** A DB with the base shapes the view reads (`lineage_threads`, `plans`, `feature_runs`,
22
+ * `delivery_graph_runs`) plus 064 then 079 applied in order — so this exercises the real DROP VIEW +
23
+ * re-CREATE the migration performs, not just the final definition. */
24
+ function viewDb(): DatabaseSync {
25
+ const db = new DatabaseSync(":memory:");
26
+ db.exec(
27
+ `CREATE TABLE lineage_threads (
28
+ root_request_key TEXT PRIMARY KEY, title TEXT, stage TEXT,
29
+ stage_label TEXT, process_key TEXT, pr_keys TEXT, pr_count INTEGER, active INTEGER,
30
+ created_at TEXT, updated_at TEXT);
31
+ CREATE TABLE plans (plan_key TEXT PRIMARY KEY, title TEXT, issue_url TEXT);
32
+ CREATE TABLE feature_runs (feature_key TEXT PRIMARY KEY, title TEXT, issue_url TEXT);
33
+ CREATE TABLE delivery_graph_runs (run_key TEXT PRIMARY KEY, title TEXT, phase TEXT, status TEXT);`,
34
+ );
35
+ db.exec(readFileSync(MIGRATION_064, "utf8"));
36
+ db.exec(readFileSync(MIGRATION_079, "utf8"));
37
+ return db;
38
+ }
39
+
40
+ /** Insert a `lineage_threads` row exactly as `pollLineage` denormalises one (post-072 schema — no
41
+ * `kind`/`issue_url` columns; the view derives both from the origin joins). */
42
+ function addThread(
43
+ db: DatabaseSync,
44
+ row: {
45
+ root_request_key: string;
46
+ title: string | null;
47
+ stage: string;
48
+ stage_label: string | null;
49
+ process_key: string | null;
50
+ pr_keys: string | null;
51
+ pr_count: number;
52
+ active: number;
53
+ },
54
+ ): void {
55
+ db.prepare(
56
+ `INSERT INTO lineage_threads (root_request_key, title, stage, stage_label,
57
+ process_key, pr_keys, pr_count, active, created_at, updated_at)
58
+ VALUES (@root_request_key, @title, @stage, @stage_label, @process_key,
59
+ @pr_keys, @pr_count, @active, 't0', 't1')`,
60
+ ).run(row);
61
+ }
62
+
63
+ test("lineage_thread_view derives kind/title/NULL issue_url for a delivery-graph thread from the run origin", () => {
64
+ const db = viewDb();
65
+ db.prepare(
66
+ "INSERT INTO delivery_graph_runs (run_key, title, phase, status) VALUES (?, ?, ?, ?)",
67
+ ).run("dg-abc123", "Ship widget across repos", "Parked on human node: manual OTP publish", "running");
68
+ // pollLineage wrote the fan-in run's procedural frontier onto lineage_threads, keyed on run_key.
69
+ addThread(db, {
70
+ root_request_key: "dg-abc123",
71
+ title: "Ship widget across repos",
72
+ stage: "converging",
73
+ stage_label: "Parked on human node: manual OTP publish",
74
+ process_key: "P-dg",
75
+ pr_keys: '["a/b#1","c/d#9"]',
76
+ pr_count: 2,
77
+ active: 1,
78
+ });
79
+
80
+ const v = db
81
+ .prepare("SELECT * FROM lineage_thread_view WHERE root_request_key = ?")
82
+ .get("dg-abc123") as Record<string, unknown>;
83
+ // Derived from the delivery_graph_runs join.
84
+ assertEquals(v.kind, "delivery");
85
+ assertEquals(v.title, "Ship widget across repos");
86
+ // A run is keyed by run_key/digest, not a GitHub issue — issue_url is always NULL.
87
+ assertEquals(v.issue_url, null);
88
+ // Procedural frontier columns pass through unchanged from lineage_threads (the run's derived phase).
89
+ assertEquals(v.stage, "converging");
90
+ assertEquals(v.stage_label, "Parked on human node: manual OTP publish");
91
+ assertEquals(v.process_key, "P-dg");
92
+ assertEquals(v.pr_keys, '["a/b#1","c/d#9"]');
93
+ assertEquals(v.pr_count, 2);
94
+ assertEquals(v.active, 1);
95
+ });
96
+
97
+ test("lineage_thread_view renders a delivery thread with no PR landed yet (empty member set, run phase)", () => {
98
+ const db = viewDb();
99
+ db.prepare(
100
+ "INSERT INTO delivery_graph_runs (run_key, title, phase, status) VALUES (?, ?, ?, ?)",
101
+ ).run("dg-empty", "Fresh run", "Running", "running");
102
+ addThread(db, {
103
+ root_request_key: "dg-empty",
104
+ title: "Fresh run",
105
+ stage: "implementing",
106
+ stage_label: "Running",
107
+ process_key: "P-e",
108
+ pr_keys: "[]",
109
+ pr_count: 0,
110
+ active: 1,
111
+ });
112
+
113
+ const v = db
114
+ .prepare("SELECT kind, title, issue_url, stage_label, pr_count FROM lineage_thread_view WHERE root_request_key = ?")
115
+ .get("dg-empty") as Record<string, unknown>;
116
+ assertEquals(v.kind, "delivery");
117
+ assertEquals(v.title, "Fresh run");
118
+ assertEquals(v.issue_url, null);
119
+ assertEquals(v.stage_label, "Running");
120
+ assertEquals(v.pr_count, 0);
121
+ });
122
+
123
+ test("lineage_thread_view keeps the epic/feature/pr arms unchanged after the delivery arm is added", () => {
124
+ const db = viewDb();
125
+ db.prepare("INSERT INTO plans (plan_key, title, issue_url) VALUES ('o/r#2', 'Epic', 'u-epic')").run();
126
+ db.prepare("INSERT INTO feature_runs (feature_key, title, issue_url) VALUES ('o/r#1', 'Feat', 'u-feat')").run();
127
+ const rows = [
128
+ { root_request_key: "o/r#2", kind: "epic", title: "Epic", issue_url: "u-epic" },
129
+ { root_request_key: "o/r#1", kind: "feature", title: "Feat", issue_url: "u-feat" },
130
+ { root_request_key: "o/r#30", kind: "pr", title: "PR", issue_url: null },
131
+ ];
132
+ for (const r of rows) {
133
+ addThread(db, {
134
+ root_request_key: r.root_request_key,
135
+ title: r.title,
136
+ stage: "opened",
137
+ stage_label: "Opened",
138
+ process_key: null,
139
+ pr_keys: "[]",
140
+ pr_count: 1,
141
+ active: 1,
142
+ });
143
+ }
144
+
145
+ for (const r of rows) {
146
+ const v = db
147
+ .prepare("SELECT kind, title, issue_url FROM lineage_thread_view WHERE root_request_key = ?")
148
+ .get(r.root_request_key) as Record<string, unknown>;
149
+ assertEquals(v.kind, r.kind);
150
+ assertEquals(v.title, r.title);
151
+ assertEquals(v.issue_url, r.issue_url);
152
+ }
153
+ });
@@ -11,7 +11,9 @@ import { memDataFor } from "../test/worldDb.ts";
11
11
  import { withTrackingViews } from "../test/trackingViews.ts";
12
12
  import { DurableResumeRegistry } from "./durableResume.ts";
13
13
  import { WorldStore } from "./world/index.ts";
14
- import { abandonClosedPr, parsePr, pollCapabilityGatesImpl, pollIncidentsImpl, pollWaveGatesImpl, repoEnvelopeVars, startMerge, submitPr, worldRestoreSha } from "./service.ts";
14
+ import { abandonClosedPr, isPrSettled, parsePr, pollCapabilityGatesImpl, pollIncidentsImpl, pollWaveGatesImpl, repoEnvelopeVars, startMerge, submitPr, worldRestoreSha } from "./service.ts";
15
+ import { trackingTargetFor } from "./instanceTracking.ts";
16
+ import type { DataLayer } from "@nanobpm/urban";
15
17
 
16
18
  function memTable(rows: any[], key: string) {
17
19
  return {
@@ -49,6 +51,31 @@ function withGithubOff(run: () => Promise<void>): Promise<void> {
49
51
  });
50
52
  }
51
53
 
54
+ test("isPrSettled reads the derived tracking view — an out-of-band-abandoned PR (base row still converging) is settled", async () => {
55
+ // The base `pull_requests` row still reads `converging`, but the ADR-0065 derived tracking VIEW
56
+ // folds the reconciler's out-of-band terminal edge into `derived_status: "abandoned"`. Terminal-edge
57
+ // classification must read `derived_status`, not the stale base `status`, or a crash-window RESUME
58
+ // of the delivery-connector enrollment action re-runs `submitPr` against a PR that has actually
59
+ // already settled (and the ledger detail falsely claims it enrolled).
60
+ const PR_KEY = "owner/repo#7";
61
+ const view = trackingTargetFor("pull_requests").view;
62
+ const base = { pr_key: PR_KEY, status: "converging" };
63
+ function make(derived: string) {
64
+ return {
65
+ table(name: string) {
66
+ if (name === "pull_requests") return { get: async (k: string) => (k === PR_KEY ? { ...base } : null) };
67
+ if (name === view) return { get: async (k: string) => (k === PR_KEY ? { ...base, derived_status: derived } : null) };
68
+ throw new Error(`unexpected table ${name}`);
69
+ },
70
+ } as any as DataLayer;
71
+ }
72
+ assertEquals(await isPrSettled(make("abandoned"), PR_KEY), true, "out-of-band-abandoned PR is settled via derived_status");
73
+ assertEquals(await isPrSettled(make("converging"), PR_KEY), false, "a genuinely live PR is not settled");
74
+ const empty = { table: () => ({ get: async () => null }) } as any as DataLayer;
75
+ assertEquals(await isPrSettled(empty, PR_KEY), false, "an absent PR row is not settled");
76
+ });
77
+
78
+
52
79
  test("re-submit of a cancelled PR marks stale open escalations", async () => {
53
80
  await withGithubOff(async () => {
54
81
  const PR_KEY = "owner/repo#42";
package/app/service.ts CHANGED
@@ -482,6 +482,21 @@ export async function worldRestoreSha(data: DataLayer, prKey: string): Promise<s
482
482
  return lastPushedSha(data, prKey);
483
483
  }
484
484
 
485
+ /** Whether the `pull_requests` row for `prKey` already exists AND is in a TERMINAL state
486
+ * (`converged`/`merged`/`abandoned`). Reads the ADR-0065 derived tracking VIEW's `derived_status`
487
+ * (via `prsTracking`), NOT the base `status`, so an out-of-band-terminated PR — whose base row is
488
+ * still `converging` but whose reconciled edge is `abandoned` — is correctly seen as settled. The
489
+ * delivery-connector's converge-enrollment action guards on this so a crash-window RESUME (a
490
+ * `claimed`-but-not-`delivered` ledger row whose first attempt already enrolled the PR and let it
491
+ * settle) never re-runs `submitPr` against a settled PR — `submitPr` deliberately RE-OPENS a terminal
492
+ * row, so an unconditional re-perform would flip the PR back to `converging`, regressing a settled PR.
493
+ * Reuses the canonical `TERMINAL_STATUSES` so the terminal-safety check can't drift from the one the
494
+ * loop/incident logic uses. */
495
+ export async function isPrSettled(data: DataLayer, prKey: string): Promise<boolean> {
496
+ const existing = await prsTracking(data).get(prKey);
497
+ return !!existing && TERMINAL_STATUSES.includes(existing.derived_status);
498
+ }
499
+
485
500
  /** Register a PR row (if new) and start the convergence process. Idempotent on prKey. Optional
486
501
  * `dependsOn` (explicit refs) is unioned with any `Depends-on:` line parsed from the PR body and
487
502
  * recorded as the PR's merge-stage dependency set. */
@@ -0,0 +1,65 @@
1
+ -- Surface delivery-graph runs in the Lineage read model (issue #498: "surface dynamic delivery
2
+ -- graphs in the Lineage tab as a fan-in parent thread").
3
+ --
4
+ -- 064_lineage_thread_view.sql created `lineage_thread_view`, deriving a thread's view-expressible
5
+ -- identity columns (`kind`, `title`, `issue_url`) from the `plans` / `feature_runs` origin joins and
6
+ -- passing the procedural frontier columns through from `lineage_threads`. It matched a root against
7
+ -- exactly two origin tables (else a self-rooted `'pr'`), so a delivery-graph run — a SEPARATE
8
+ -- aggregate (`delivery_graph_runs`, keyed by `run_key`) — was structurally invisible: its thread fell
9
+ -- through to `'pr'` with a NULL title.
10
+ --
11
+ -- `collectThreads` (app/lineage.ts) now enumerates `delivery_graph_runs` as a fan-in parent thread
12
+ -- keyed on `run_key`, so `pollLineage` writes a `lineage_threads` row for each run. Extend the view
13
+ -- with a third origin arm so it derives that row's identity from the run:
14
+ -- • `kind` — 'delivery' when the root matches a `delivery_graph_runs.run_key` (after the
15
+ -- epic/feature arms, mirroring the precedence in `collectThreads` / `deriveLineage`).
16
+ -- • `title` — the run's `title` (its authored delivery-graph title).
17
+ -- • `issue_url` — NULL: a delivery-graph run is keyed by `run_key`/`digest`, not a GitHub issue,
18
+ -- exactly as `deriveLineage` sets it (only feature/epic threads root on an issue).
19
+ -- The procedural frontier columns (`stage`/`stage_label`/`process_key`/`pr_keys`/`pr_count`/`active`)
20
+ -- still pass through from `lineage_threads`, so a delivery thread's frontier reflects the run's
21
+ -- derived phase that `pollLineage` wrote.
22
+ --
23
+ -- A VIEW cannot be `ALTER`ed, so DROP the old definition and CREATE the extended one. This is a NEW
24
+ -- forward-only migration — 064 stays immutable. The view remains a plain `CREATE VIEW <name> AS
25
+ -- SELECT … FROM …` (no CTE, no select-list subquery, every column aliased) so the static
26
+ -- pages↔schema contract guard can still introspect its output columns; the added CASE arm and LEFT
27
+ -- JOIN keep the SAME output column set, so the repointed Lineage page renders identically.
28
+ --
29
+ -- Forward-only, additive: no schema change to any base table, no DROP of `lineage_threads`. The
30
+ -- runner wraps each file in its own transaction, so this file must NOT contain BEGIN/COMMIT.
31
+
32
+ DROP VIEW IF EXISTS lineage_thread_view;
33
+
34
+ CREATE VIEW lineage_thread_view AS
35
+ SELECT
36
+ lt.root_request_key AS root_request_key,
37
+ CASE
38
+ WHEN pl.plan_key IS NOT NULL THEN 'epic'
39
+ WHEN fr.feature_key IS NOT NULL THEN 'feature'
40
+ WHEN dg.run_key IS NOT NULL THEN 'delivery'
41
+ ELSE 'pr'
42
+ END AS kind,
43
+ CASE
44
+ WHEN pl.plan_key IS NOT NULL THEN pl.title
45
+ WHEN fr.feature_key IS NOT NULL THEN fr.title
46
+ WHEN dg.run_key IS NOT NULL THEN dg.title
47
+ ELSE lt.title
48
+ END AS title,
49
+ CASE
50
+ WHEN pl.plan_key IS NOT NULL THEN pl.issue_url
51
+ WHEN fr.feature_key IS NOT NULL THEN fr.issue_url
52
+ ELSE NULL
53
+ END AS issue_url,
54
+ lt.stage AS stage,
55
+ lt.stage_label AS stage_label,
56
+ lt.process_key AS process_key,
57
+ lt.pr_keys AS pr_keys,
58
+ lt.pr_count AS pr_count,
59
+ lt.active AS active,
60
+ lt.created_at AS created_at,
61
+ lt.updated_at AS updated_at
62
+ FROM lineage_threads lt
63
+ LEFT JOIN plans pl ON pl.plan_key = lt.root_request_key
64
+ LEFT JOIN feature_runs fr ON fr.feature_key = lt.root_request_key
65
+ LEFT JOIN delivery_graph_runs dg ON dg.run_key = lt.root_request_key;
@@ -99,6 +99,24 @@ The closed set (extensible only by a deliberate ADR/PR, never by graph authors):
99
99
  - **`human`** — a scheduled user task + form (§4).
100
100
  - **`connector`** — an automated, side-effecting outbound action (the connector I/O surface).
101
101
 
102
+ > **Amendment (issue #500): the connector's first REAL target landed — `converge` / `converge-merge`.**
103
+ > The connector I/O surface shipped in slice S4 with a deliberately forward-declared STUB action
104
+ > (`performConnectorAction`), the real target dispatch deferred to a later slice. That slice is this:
105
+ > a `connector` node whose `target` is **`converge-merge`** (or **`converge`** for converge-only)
106
+ > enrolls its `payload.pr` into the app's shared convergence (+ merge) loop via `submitPr` — the SAME
107
+ > seam the feature cell reuses (`workers/converge-feature`), no duplicated machinery. Enrollment is
108
+ > defined in the worker (it has `app.data`/`app.engine`) but **injected into `dispatchConnector` as the
109
+ > connector's action**, so the existing at-most-once ledger fence wraps the enrollment itself: it fires
110
+ > only on the claim winner (or a resumed crashed claim), and a `deduped` redelivery — a restart / lost
111
+ > ack / graph resume that lands AFTER the PR settled — never re-runs it. That matters because `submitPr`
112
+ > deliberately RE-OPENS a terminal PR; an unfenced re-call would flip a `merged`/`converged`/`abandoned`
113
+ > PR back to `converging`. `submitPr`'s own `prKey` idempotency additionally makes a resumed re-perform
114
+ > double-safe on a still-live row. This retires the manual `land-*` human gate whose only job was "go run convergence
115
+ > yourself" — the canonical shape is now `agent (opens PR) → connector[converge-merge] →
116
+ > wait[pr, merged]` with no human node. The payload is `{ pr, convergeOnly?, dependsOn? }`; the MVP
117
+ > sources `pr` as a literal (auto-emitting it from the `agent` node as a typed `pr` fact is a deferred
118
+ > follow-up). Other connector targets remain the forward-declared stub.
119
+
102
120
  Crucially, **execution stays engine-native**: each node kind is a real, already-deployed
103
121
  sub-process / call activity (`readiness-gate`, a user task, the implementation task, a connector
104
122
  invocation). The graph layer owns **scheduling** (which nodes' edges are satisfied → dispatch), not a
@@ -435,7 +435,7 @@ layer schedules, it does not re-implement execution):
435
435
  | `agent` | `agent: { jobType, prompt? }` | a worker runs an agent job type (the fan-out body). **Side-effecting.** | yes |
436
436
  | `wait` | `wait: <ReadinessProbe>` | a durable, bounded readiness probe — kind ∈ `http`, `command`, `npm`, `github-check`, `capability`, `pr`. Read-only. | yes (binds observed facts) |
437
437
  | `human` | `human?: { formKey?, prompt? }` | a scheduled user task + form (the Tasks inbox, §3). Blocks dependents, SLA-bounded, answerable by a human **or** an agent. | yes |
438
- | `connector` | `connector: { target, dedupeKey?, payload? }` | an automated, side-effecting outbound action. Carries a `dedupeKey` (at-least-once safe). *(payload is a forward-declared stub.)* | yes |
438
+ | `connector` | `connector: { target, dedupeKey?, payload? }` | an automated, side-effecting outbound action. Carries a `dedupeKey` (at-least-once safe). Two **real targets** ship today — **`converge`** and **`converge-merge`** (§9.4); other targets are a forward-declared stub. | yes |
439
439
 
440
440
  A **`wait` node's `wait` is a `ReadinessProbe` verbatim** (the same shape feature-run
441
441
  intake uses): `{ kind, target, onTimeout?, match?, poll? }`. The **`pr` kind** watches an
@@ -559,3 +559,49 @@ To swap the manual PR-#303 path for a **capability** edge instead of a raw `pr`
559
559
  the consumer a `wait` node with `kind: "capability"` (resolving *which published
560
560
  `pkg@version` first carries the change*) fed by the same `manual-publish.publishedVersion`
561
561
  fact — the fact-edge syntax is identical.
562
+
563
+ ### 9.4 Connector targets — drive a PR to convergence + merge (`converge` / `converge-merge`)
564
+
565
+ A `connector` node with **`target: "converge-merge"`** (or **`"converge"`**) enrolls an
566
+ agent-opened PR into the app's **shared convergence loop** — the *same* enrollment §1 (a
567
+ standalone submit) and a feature run use (`submitPr`), no duplicated machinery. This replaces
568
+ the old habit of bridging an `agent`-opened PR to review with a **human `land-*` gate** whose
569
+ only job was "go run convergence yourself".
570
+
571
+ - **`converge-merge`** — drive review convergence **and then the merge loop** (the PR merges
572
+ once converged + green). Equivalent to a submit with `convergeOnly: false`.
573
+ - **`converge`** — **converge-only**: drive review convergence and stop at `converged`, never
574
+ handing off to the merge loop (equivalent to `convergeOnly: true`).
575
+
576
+ **Payload:** `{ pr: "owner/repo#123", convergeOnly?: boolean, dependsOn?: string[] }`. `pr` is
577
+ required (a literal `owner/repo#N`, identical to how a `wait: pr` node targets a known PR).
578
+ `convergeOnly` defaults from the target and may be overridden per-node; `dependsOn` is unioned
579
+ into the PR's merge-stage dependency set. The enrollment is idempotent (the connector's
580
+ at-least-once dedupe fence **plus** `submitPr`'s own `prKey` idempotency), so a graph resume /
581
+ redelivery never double-enrolls.
582
+
583
+ **Canonical shape** — the agent opens the PR, the connector enrolls it, and a `wait[pr, merged]`
584
+ gate binds `mergedSha` when it lands, with **no human node**:
585
+
586
+ ```json
587
+ {
588
+ "name": "open → converge+merge → wait merged",
589
+ "nodes": [
590
+ { "id": "open", "kind": "agent",
591
+ "agent": { "jobType": "senior:feature", "prompt": "Implement the change in acme/repo and open a PR." } },
592
+ { "id": "land", "kind": "connector",
593
+ "connector": { "target": "converge-merge", "payload": { "pr": "acme/repo#123" } } },
594
+ { "id": "merged", "kind": "wait",
595
+ "wait": { "kind": "pr", "target": "acme/repo#123", "match": { "prState": "merged" }, "onTimeout": "escalate" } }
596
+ ],
597
+ "edges": [
598
+ { "from": "open", "to": "land" },
599
+ { "from": "land", "to": "merged" }
600
+ ]
601
+ }
602
+ ```
603
+
604
+ > **Follow-up (not shipped):** the MVP sources the connector's `pr` as a **literal**. Auto-emitting
605
+ > the opened PR from the `agent` node as a typed `pr` fact (so the connector/`wait` bind it instead of
606
+ > a literal) is a later slice — not required for the graph above.
607
+
package/openapi.yaml CHANGED
@@ -148,10 +148,10 @@ components:
148
148
  properties:
149
149
  rootRequestKey:
150
150
  type: string
151
- description: The origin issue key (feature_key/plan_key), or a self-rooted pr_key.
151
+ description: The origin issue key (feature_key/plan_key), a self-rooted pr_key, or a delivery-graph run_key.
152
152
  kind:
153
153
  type: string
154
- enum: [feature, epic, pr]
154
+ enum: [feature, epic, pr, delivery]
155
155
  title:
156
156
  type: string
157
157
  nullable: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.131.1",
3
+ "version": "0.133.0",
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",
@@ -70,7 +70,7 @@
70
70
  "type": "text",
71
71
  "id": "subtitle",
72
72
  "props": {
73
- "text": "One narrative per request, not a card-swap. Each row is a single arc of your intent \u2014 request \u2192 implementing \u2192 PR opened \u2192 converging (round n) \u2192 merged/converged/abandoned \u2014 stitched from the feature/epic that started it, the PR(s) it produced, and their convergence + merge outcome. The Stage column is the active frontier; drill into a row for its member PR(s) and rounds. Epics fan out to N PR sub-threads. A human/webhook PR with no originating request is shown as its own root.",
73
+ "text": "One narrative per request, not a card-swap. Each row is a single arc of your intent \u2014 request \u2192 implementing \u2192 PR opened \u2192 converging (round n) \u2192 merged/converged/abandoned \u2014 stitched from the feature/epic that started it, the PR(s) it produced, and their convergence + merge outcome. The Stage column is the active frontier; drill into a row for its member PR(s) and rounds. Epics fan out to N PR sub-threads. A delivery-graph run is a fan-in parent thread: the heterogeneous downstream PR convergences it spawns (across different repos/issues) nest under the run, whose frontier reflects its derived phase. A human/webhook PR with no originating request is shown as its own root.",
74
74
  "variant": "sub"
75
75
  }
76
76
  },