@nanobpm/nano-workforce 0.107.2 → 0.108.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [0.108.0](https://github.com/nanobpm/nano-workforce/compare/v0.107.2...v0.108.0) (2026-08-20)
2
+
3
+
4
+ ### Features
5
+
6
+ * **derivation-parity:** un-park retro to full whole-model parity ([#372](https://github.com/nanobpm/nano-workforce/issues/372)) ([1320838](https://github.com/nanobpm/nano-workforce/commit/1320838251646258ec2151a547ed2b5573ce1168)), closes [nano-ide#405](https://github.com/nano-ide/issues/405) [355/#356](https://github.com/nanobpm/nano-workforce/issues/356) [#371](https://github.com/nanobpm/nano-workforce/issues/371)
7
+
1
8
  ## [0.107.2](https://github.com/nanobpm/nano-workforce/compare/v0.107.1...v0.107.2) (2026-08-20)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.107.2",
3
+ "version": "0.108.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",
@@ -64,7 +64,7 @@
64
64
  "devDependencies": {
65
65
  "@biomejs/biome": "^2.4.11",
66
66
  "@nanobpm/urban-testkit": "^0.5.0",
67
- "@nanobpm/workflow": "^0.12.0",
67
+ "@nanobpm/workflow": "^0.13.0",
68
68
  "@semantic-release/changelog": "^6.0.3",
69
69
  "@semantic-release/git": "^10.0.1",
70
70
  "@semantic-release/npm": "^13.1.5",
@@ -15,19 +15,20 @@
15
15
  //
16
16
  // Any structural drift (b) OR deploy rejection (c) fails the build.
17
17
  //
18
- // PARKED MODELS ARE ACCOUNTED FOR, NOT IGNORED. The corpus is currently fully parked behind
19
- // upstream `@nanobpm/workflow` constructs (see test/derivation-parity/flows.ts for the three blocker
20
- // classes). Each parked model must carry a documented `blockedReason`; a model that is neither
21
- // ported nor documented fails this gate, so the corpus can never silently lose coverage. As each
22
- // parked model flips to a real `flow` upstream, it is automatically pulled into the full
23
- // derive → diff → deploy loop here with NO change to this script.
18
+ // PARKED MODELS ARE ACCOUNTED FOR, NOT IGNORED. `retro` is a green whole-model parity port; the
19
+ // remaining corpus is parked behind upstream `@nanobpm/workflow` constructs (see
20
+ // test/derivation-parity/flows.ts for the two blocker classes). Each parked model must carry a
21
+ // documented `blockedReason`; a model that is neither ported nor documented fails this gate, so the
22
+ // corpus can never silently lose coverage. As each parked model flips to a real `flow` upstream, it
23
+ // is automatically pulled into the full derive → diff → deploy loop here with NO change to this
24
+ // script.
24
25
  //
25
- // SELF-PROVING CANARY. Because the corpus is (today) all-parked, a gate that merely iterated it
26
- // would be a vacuous green — it could rot without anyone noticing. So before touching the corpus we
27
- // run a CANARY that proves the oracle's RED path genuinely fires: a faithful derived flow deploys
28
- // green and diffs green, a drifted derivation is CAUGHT by the diff, and a corrupted model is
29
- // REJECTED by the engine. If any red path fails to fire (the diff misses drift, or the engine
30
- // accepts garbage), the gate fails — the oracle must be able to say no.
26
+ // SELF-PROVING CANARY. To keep the gate honest even while most of the corpus is parked, a gate that
27
+ // merely iterated it could be a near-vacuous green — it could rot without anyone noticing. So before
28
+ // touching the corpus we run a CANARY that proves the oracle's RED path genuinely fires: a faithful
29
+ // derived flow deploys green and diffs green, a drifted derivation is CAUGHT by the diff, and a
30
+ // corrupted model is REJECTED by the engine. If any red path fails to fire (the diff misses drift,
31
+ // or the engine accepts garbage), the gate fails — the oracle must be able to say no.
31
32
 
32
33
  import { mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
33
34
  import { tmpdir } from "node:os";
@@ -26,7 +26,7 @@ behind an upstream construct, and do **not** relax to node-surface parity_:
26
26
 
27
27
  | Model | Top-level start/end | Status |
28
28
  | ------------------ | ------------------- | ------ |
29
- | `retro` | 1 / 1 | parkedclass 3 (general service-task ioMapping) |
29
+ | `retro` | 1 / 1 | greenwhole-model parity |
30
30
  | `convergence-loop` | 1 / 1 | ⛔ parked — class 2 (arbitrary graph) |
31
31
  | `spine-demo` | 1 / 2 | ⛔ parked — class 1 (multi start/end) |
32
32
  | `readiness-gate` | 1 / 5 | ⛔ parked — class 1 (multi start/end) |
@@ -34,13 +34,17 @@ behind an upstream construct, and do **not** relax to node-surface parity_:
34
34
  | `merge-loop` | 1 / 2 | ⛔ parked — class 1 (multi start/end) |
35
35
  | `plan-fanout` | 3 / 3 | ⛔ parked — class 1 (multi start/end) |
36
36
 
37
- `retro` was a green full-parity port — a linear single-start/single-end agent
38
- pipeline until the conformance work (#355/#356) added a conformance-escalation
39
- subgraph to its golden, which introduced a service task carrying a general
40
- `<zeebe:ioMapping>` the stock `task` builder cannot emit (class 3). All seven
41
- goldens are now parked, across **three** distinct blocker classes, each awaiting
42
- an upstream `@nanobpm/workflow` (nano-ide) construct + re-release (never a golden
43
- edit, never relaxed acceptance).
37
+ `retro` is a green whole-model parity port — a single-start/single-end agent
38
+ pipeline (gather conformance record-conformance) with a `deviations?`
39
+ conformance-escalation subgraph (#355/#356) and a shared synthesize record
40
+ tail. Its `record-conformance-ack` service task carries a general
41
+ `<zeebe:ioMapping>` that the stock `task` builder could not emit until
42
+ `@nanobpm/workflow@0.13.0` landed the general service-task `io` construct
43
+ (nano-ide#405); every other element was always expressible (`w.task`+prompt,
44
+ `w.branch`, `w.human`, envelopes). The remaining six goldens are parked across
45
+ **two** distinct blocker classes, each awaiting an upstream `@nanobpm/workflow`
46
+ (nano-ide) construct + re-release (never a golden edit, never relaxed
47
+ acceptance).
44
48
 
45
49
  ## The blockers
46
50
 
@@ -83,24 +87,6 @@ pinned by a diagnostic in `derivation-parity.test.ts`:
83
87
  The fix is an **arbitrary-graph / explicit-join (named-target)** builder upstream
84
88
  in `@nanobpm/workflow` — a **superset** of the class-1 gap.
85
89
 
86
- ### Class 3 — general service-task ioMapping (`retro`)
87
-
88
- `retro` clears classes 1 and 2 (single start/end, structured topology), but its
89
- golden's `record-conformance-ack` service task carries a **general**
90
- `<zeebe:ioMapping>` — inputs `=planKey`→`planKey` and
91
- `=if (is defined(note)) then note else null`→`note`. `@nanobpm/workflow@0.12.0`'s
92
- `task` builder only emits an ioMapping as a side effect of a `prompt.append` (a
93
- single `appendPrompt` input); there is no way to declare arbitrary input/output
94
- mappings on a service task. Every other element of the golden IS expressible
95
- (`w.task`+prompt, `w.branch` for the `deviations?` gateway, `w.human` for the
96
- `conformance-escalation` userTask, envelopes) — this one service-task ioMapping
97
- is the sole gap. The `retro golden needs a general service-task ioMapping the
98
- stock builder cannot emit` diagnostic pins both halves (the golden needs it; the
99
- stock builder cannot produce it).
100
-
101
- The fix is a general `io: { input?, output? }` on the external `task`/`run`
102
- builder upstream in `@nanobpm/workflow` (**nano-ide#405**).
103
-
104
90
  ## Resuming this slice
105
91
 
106
92
  The follow-up upstream slices (opened in **nanobpm/nano-ide** per decision path
@@ -108,8 +94,11 @@ The follow-up upstream slices (opened in **nanobpm/nano-ide** per decision path
108
94
 
109
95
  - a terminal / explicit-end (+ multi-start) construct (unblocks class 1), **and**
110
96
  - an arbitrary-graph / explicit-join (named-target) builder (unblocks class 2 —
111
- `convergence-loop`), **and**
112
- - a general service-task `io` mapping (unblocks class 3 — `retro`; nano-ide#405).
97
+ `convergence-loop`).
98
+
99
+ (The class-3 general service-task `io` mapping has already landed —
100
+ `@nanobpm/workflow@0.13.0`, nano-ide#405 — unparking `retro` to whole-model
101
+ parity.)
113
102
 
114
103
  Then, on a resumed run here:
115
104
 
@@ -140,7 +129,7 @@ documented `blockedReason`, so the corpus can never silently lose coverage; a
140
129
  parked model flips into the full derive → diff → deploy loop automatically the
141
130
  moment its `flow` lands — no change to the gate.
142
131
 
143
- Because the corpus is (currently) fully parked, the gate runs a **self-proving
132
+ Because most of the corpus is (currently) parked, the gate runs a **self-proving
144
133
  canary** first: it proves a faithful derivation deploys and diffs green, a
145
134
  drifted derivation is caught by the diff, and a corrupted model is rejected by
146
135
  the engine — so the oracle's red path can never rot into a vacuous green. The
@@ -85,8 +85,8 @@ test("class-1 blocked goldens genuinely have multiple top-level start/end events
85
85
  }
86
86
 
87
87
  // The two single-start/single-end goldens (retro, convergence-loop) clear
88
- // class 1; retro is class-3 blocked (a general service-task ioMapping),
89
- // convergence-loop is class-2 blocked — both below.
88
+ // class 1; retro is a GREEN whole-model parity port (it runs through
89
+ // `assertDerivationParity` above), convergence-loop is class-2 blocked (below).
90
90
  for (const model of ["retro", "convergence-loop"]) {
91
91
  const xml = readFileSync(goldenPath(model), "utf8");
92
92
  assertEquals(countTag(xml, "startEvent"), 1, `${model} should have one start event`);
@@ -94,37 +94,6 @@ test("class-1 blocked goldens genuinely have multiple top-level start/end events
94
94
  }
95
95
  });
96
96
 
97
- // CLASS 3 — retro clears classes 1 & 2 (single start/end, structured topology)
98
- // but its golden carries a service task with a GENERAL <zeebe:ioMapping> — inputs
99
- // whose target is NOT `appendPrompt` — which @nanobpm/workflow@0.12.0's `task`
100
- // builder cannot emit (it only produces an ioMapping via a `prompt.append`, i.e.
101
- // a lone `appendPrompt` input). Prove both halves: the golden needs it, and the
102
- // stock builder cannot produce it (awaits nano-ide#405).
103
- test("retro golden needs a general service-task ioMapping the stock builder cannot emit", () => {
104
- const xml = readFileSync(goldenPath("retro"), "utf8");
105
- // (a) The golden has a service task carrying an ioMapping input to a non-prompt
106
- // target (`record-conformance-ack`: =planKey→planKey, note→note).
107
- assert(
108
- /target="planKey"/.test(xml) && /target="note"/.test(xml),
109
- "retro golden should carry general ioMapping inputs (planKey, note) on record-conformance-ack",
110
- );
111
- // (b) The stock `task` builder only ever emits `appendPrompt` as an ioMapping
112
- // target — never a general input like `note` — so the golden is not
113
- // derivable until the upstream `io` construct lands.
114
- const probe = defineFlow("io-probe", (w) => {
115
- w.task("agent", {
116
- jobType: "senior:retro",
117
- prompt: { resourceId: "retro.md", bindingType: "latest", append: "=retroDigest" },
118
- });
119
- });
120
- const derived = declarativeToBpmn(probe);
121
- assert(/target="appendPrompt"/.test(derived), "prompt.append should emit an appendPrompt ioMapping input");
122
- assert(
123
- !/target="note"/.test(derived) && !/target="planKey"/.test(derived),
124
- "the stock task builder cannot emit a general (non-appendPrompt) ioMapping input",
125
- );
126
- });
127
-
128
97
  // CLASS 2 — convergence-loop has a single start/end (clears class 1) but an
129
98
  // ARBITRARY control-flow graph the structured-only builder cannot emit. Prove
130
99
  // the three specific features against the golden itself.
@@ -9,12 +9,13 @@
9
9
  // the structurally-derivable goldens at full whole-model parity, park the rest
10
10
  // pending an upstream construct, and do NOT relax to node-surface parity):
11
11
  //
12
- // • All seven goldens are currently `blockedReason`-parked, in THREE distinct
13
- // classes, each awaiting an upstream `@nanobpm/workflow` (nano-ide) construct
14
- // + re-release — never a golden edit and never relaxed acceptance:
12
+ // • `retro` is a GREEN whole-model parity port (see below). The remaining six
13
+ // goldens are `blockedReason`-parked, in TWO distinct classes, each awaiting
14
+ // an upstream `@nanobpm/workflow` (nano-ide) construct + re-release — never a
15
+ // golden edit and never relaxed acceptance:
15
16
  //
16
17
  // (1) MULTI top-level start/end (spine-demo, readiness-gate, feature,
17
- // merge-loop, plan-fanout). `@nanobpm/workflow@0.12.0` derives EXACTLY
18
+ // merge-loop, plan-fanout). `@nanobpm/workflow` derives EXACTLY
18
19
  // ONE `<bpmn:startEvent id="Start">` + ONE `<bpmn:endEvent id="End">`,
19
20
  // converging every dangler into that single end (see `Compiler.compile`
20
21
  // in the package's `declarative.ts`). Needs a terminal/explicit-end
@@ -34,23 +35,10 @@
34
35
  // arbitrary-graph / explicit-join (named-target) builder — a SUPERSET of
35
36
  // the class-(1) gap.
36
37
  //
37
- // (3) GENERAL service-task ioMapping (retro). retro WAS a green full-parity
38
- // port (a linear gather → synthesize → record agent pipeline) until the
39
- // conformance work (nano-workforce #355/#356) added a conformance-
40
- // escalation subgraph to its golden. Every new element ports with the
41
- // stock builder (`w.branch` for the `deviations?` gateway, `w.human` for
42
- // the `conformance-escalation` userTask, `w.task`+prompt/envelopes for
43
- // the service tasks) EXCEPT `record-conformance-ack`: a service task with
44
- // a general <zeebe:ioMapping> (inputs `=planKey`→planKey and
45
- // `=if (is defined(note)) then note else null`→note). @nanobpm/workflow@
46
- // 0.12.0's `task` builder only emits an ioMapping via a `prompt.append`
47
- // (a single `appendPrompt` input), so this task is not derivable. Needs a
48
- // general `io` on the task/run builder upstream (nano-ide#405).
49
- //
50
38
  // A resumed run flips any parked model to a real `flow` once the corresponding
51
- // upstream construct lands and `@nanobpm/workflow` is bumped past 0.12.0.
39
+ // upstream construct lands and `@nanobpm/workflow` is bumped to carry it.
52
40
 
53
- import type { DeclarativeFlow } from "@nanobpm/workflow";
41
+ import { type DeclarativeFlow, defineFlow, envelope } from "@nanobpm/workflow";
54
42
 
55
43
  /** One model's port entry: the golden basename plus EITHER the derived flow
56
44
  * (when it can be reproduced) OR the reason it is blocked — never both and never
@@ -72,38 +60,87 @@ export type PortEntry =
72
60
  readonly blockedReason: string;
73
61
  };
74
62
 
75
- // ── retro (PARKEDclass 3) ─────────────────────────────────────────────────
76
- // retro WAS a green full-parity port a linear gather → synthesize record
77
- // agent pipeline. The conformance work (nano-workforce #355/#356) then added a
78
- // conformance-escalation subgraph to the golden: a `deviations?` exclusive
79
- // gateway, a `conformance-escalation` userTask, and `senior:conformance` /
80
- // `pr.conformance-record` / `pr.conformance-ack` service tasks. All of those ARE
81
- // expressible with the stock builder (`w.branch`, `w.human`, `w.task`+prompt,
82
- // envelopes) EXCEPT `record-conformance-ack`: it carries a general
63
+ // ── retro (GREENwhole-model parity) ───────────────────────────────────────
64
+ // retro is a single-start/single-end model: a linear gather → conformance
65
+ // record-conformance agent pipeline, a `deviations?` exclusive gateway guarding a
66
+ // conformance-escalation subgraph (nano-workforce #355/#356), then a shared
67
+ // synthesize record tail. Every element ports with the stock builder:
68
+ // `w.task`+envelopes for the data-envelope service tasks, `w.task`+prompt for the
69
+ // two agent tasks (`senior:conformance`, `senior:retro`), `w.branch` for the
70
+ // `deviations?` gateway, `w.human` for the `conformance-escalation` userTask, and
71
+ // — since @nanobpm/workflow@0.13.0 landed the general service-task `io` construct
72
+ // (nano-ide#405) — `w.task`+`io` for `record-conformance-ack`'s general
83
73
  // <zeebe:ioMapping> (inputs `=planKey`→planKey and
84
- // `=if (is defined(note)) then note else null`→note), which
85
- // @nanobpm/workflow@0.12.0's `task` builder cannot emit — it only produces an
86
- // ioMapping via a `prompt.append` (a single `appendPrompt` input). So retro
87
- // regresses to a parked model pending the upstream construct: a general `io` on
88
- // the external `task`/`run` builder (nano-ide#405). It flips back to a green
89
- // port once that lands and @nanobpm/workflow is bumped past 0.12.0.
74
+ // `=if (is defined(note)) then note else null`→note).
75
+
76
+ /** The typed data envelopes retro's non-agent service tasks lift into the model
77
+ * (`nano:shape` + `io.nanobpm.dataEnvelope.in`), matching the golden's shapes. */
78
+ const RetroGatherIn = envelope("RetroGatherIn", { planKey: "string" });
79
+ const RetroRecordIn = envelope("RetroRecordIn", {
80
+ planKey: "string",
81
+ retroLearnings: { type: "integer", optional: true },
82
+ status: { type: "string", optional: true },
83
+ pr: { type: "string", optional: true },
84
+ summary: { type: "string", optional: true },
85
+ });
86
+ const ConformanceRecordIn = envelope("ConformanceRecordIn", {
87
+ planKey: "string",
88
+ status: { type: "string", optional: true },
89
+ commentUrl: { type: "string", optional: true },
90
+ slicesMet: { type: "integer", optional: true },
91
+ slicesReduced: { type: "integer", optional: true },
92
+ slicesNotVerified: { type: "integer", optional: true },
93
+ deviationsRaised: { type: "integer", optional: true },
94
+ deviationsUnraised: { type: "integer", optional: true },
95
+ hasDeviations: { type: "boolean", optional: true },
96
+ summary: { type: "string", optional: true },
97
+ });
90
98
 
91
- const RETRO_IO_BLOCK =
92
- "blocked (general service-task ioMapping): retro's golden gained a " +
93
- "conformance-escalation subgraph whose `record-conformance-ack` service task " +
94
- "carries a general <zeebe:ioMapping> (inputs =planKey→planKey and " +
95
- "=if (is defined(note)) then note else null→note). @nanobpm/workflow@0.12.0's " +
96
- "`task` builder only emits an ioMapping via a `prompt.append` (a single " +
97
- "appendPrompt input), so this task is not derivable. Every other element of " +
98
- "the golden IS expressible (w.task+prompt, w.branch, w.human, envelopes). " +
99
- "Awaits a general `io` on the task/run builder upstream in @nanobpm/workflow " +
100
- "(nano-ide#405).";
99
+ /** The code-first port of `resources/processes/retro.bpmn`. */
100
+ const retroFlow: DeclarativeFlow = defineFlow(
101
+ "retro",
102
+ {
103
+ gather: { in: RetroGatherIn },
104
+ "record-conformance": { in: ConformanceRecordIn },
105
+ record: { in: RetroRecordIn },
106
+ },
107
+ (w) => {
108
+ w.task("gather", { jobType: "pr.retro-gather" });
109
+ w.task("conformance", {
110
+ jobType: "senior:conformance",
111
+ prompt: { resourceId: "conformance.md", bindingType: "latest", append: "=conformanceDigest" },
112
+ });
113
+ w.task("record-conformance", { jobType: "pr.conformance-record" });
114
+ w.branch("hasDeviations = true", {
115
+ then: (b) => {
116
+ b.human("conformance-escalation", {
117
+ form: "conformance-escalation",
118
+ candidateGroups: "operators",
119
+ });
120
+ b.task("record-conformance-ack", {
121
+ jobType: "pr.conformance-ack",
122
+ io: {
123
+ input: [
124
+ { source: "=planKey", target: "planKey" },
125
+ { source: "=if (is defined(note)) then note else null", target: "note" },
126
+ ],
127
+ },
128
+ });
129
+ },
130
+ });
131
+ w.task("synthesize", {
132
+ jobType: "senior:retro",
133
+ prompt: { resourceId: "retro.md", bindingType: "latest", append: "=retroDigest" },
134
+ });
135
+ w.task("record", { jobType: "pr.retro-record" });
136
+ },
137
+ );
101
138
 
102
139
  /** The single-top-level-end/start compiler limitation, reused as the
103
140
  * `blockedReason` for every golden that has more than one top-level start
104
141
  * and/or end event. */
105
142
  const MULTI_START_END_BLOCK =
106
- "blocked: @nanobpm/workflow@0.12.0 derives a single top-level start/end and " +
143
+ "blocked: the published @nanobpm/workflow compiler derives a single top-level start/end and " +
107
144
  "converges all danglers into one <endEvent id=\"End\">; this golden has " +
108
145
  "multiple top-level start and/or end events, which the published compiler " +
109
146
  "cannot reproduce. Awaits an upstream terminal/explicit-end (+ multi-start) " +
@@ -111,7 +148,7 @@ const MULTI_START_END_BLOCK =
111
148
 
112
149
  /** All seven ports, keyed by model, in the epic's stated authoring order. */
113
150
  export const PORTS: readonly PortEntry[] = [
114
- { model: "retro", blockedReason: RETRO_IO_BLOCK },
151
+ { model: "retro", flow: retroFlow },
115
152
  {
116
153
  model: "spine-demo",
117
154
  blockedReason: `${MULTI_START_END_BLOCK} (spine-demo: 1 start, 2 ends)`,
@@ -128,7 +165,7 @@ export const PORTS: readonly PortEntry[] = [
128
165
  model: "convergence-loop",
129
166
  blockedReason:
130
167
  "blocked (arbitrary control-flow graph): single top-level start/end, but " +
131
- "its topology is not expressible with @nanobpm/workflow@0.12.0's " +
168
+ "its topology is not expressible with the published @nanobpm/workflow's " +
132
169
  "structured-only builder (loop/switch/branch). Proven in the test suite: " +
133
170
  "the loop head `review-round` is a serviceTask that merges 3 back-edges " +
134
171
  "directly (in=3), but loop() always inserts an exclusive-gateway head " +