@nanobpm/nano-workforce 0.140.0 → 0.142.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/openapi.yaml CHANGED
@@ -643,6 +643,11 @@ components:
643
643
  elementId:
644
644
  type: string
645
645
  description: The BPMN element id (activity/task) the job was for, when still known (advisory).
646
+ elementInstanceKey:
647
+ type: string
648
+ description: The engine element-instance key the job's token occupied (#544) — the per-occupancy
649
+ handle, unambiguous across a looping / retried activity where many instances share one elementId.
650
+ Resolved from the job's element-instance wait-state; advisory, present when resolution landed.
646
651
  planKey:
647
652
  type: string
648
653
  description: The plan / epic key this job was part of (e.g. owner/repo#142), when still known (advisory).
@@ -749,6 +754,10 @@ components:
749
754
  elementId:
750
755
  type: string
751
756
  description: The BPMN element id, when still known (advisory).
757
+ elementInstanceKey:
758
+ type: string
759
+ description: The engine element-instance key the job's token occupied (#544) — per-occupancy,
760
+ unambiguous across a looping / retried activity; advisory, present when resolution landed.
752
761
  planKey:
753
762
  type: string
754
763
  description: The plan / epic key, when still known (advisory).
@@ -1352,10 +1361,13 @@ components:
1352
1361
  description: The fact's identifier, referenced downstream as `<nodeId>.<name>`. Must be unique within the node.
1353
1362
  type:
1354
1363
  type: string
1355
- enum: [string, number, boolean, artifact, version, url]
1364
+ enum: [string, number, boolean, artifact, version, url, pr]
1356
1365
  description: >-
1357
1366
  The fact's declared type. `artifact` is a `pkg@version` handle, `version` a bare version
1358
- string, `url` a location — mirrors the values `capability`/`pr` probes late-bind.
1367
+ string, `url` a location — mirrors the values `capability`/`pr` probes late-bind. `pr`
1368
+ (issue #548) is a PR reference (`owner/repo#N`) an `agent` node emits for the PR it opened,
1369
+ so a downstream `connector[converge*]`/`wait[pr]` node LATE-BINDS its target PR from the
1370
+ fact instead of a hardcoded literal.
1359
1371
  description:
1360
1372
  type: string
1361
1373
  maxLength: 512
@@ -2802,6 +2814,13 @@ paths:
2802
2814
  schema:
2803
2815
  type: string
2804
2816
  description: Return only transcripts whose (still-known) correlation names this process instance.
2817
+ - name: elementInstanceKey
2818
+ in: query
2819
+ required: false
2820
+ schema:
2821
+ type: string
2822
+ description: Return only transcripts whose correlation names this engine element-instance key (#544) —
2823
+ resolves a session to one occupancy of a looping / retried activity, unlike the static elementId.
2805
2824
  - name: planKey
2806
2825
  in: query
2807
2826
  required: false
@@ -46,6 +46,7 @@ export default defineOperation("listAgenticTranscripts", async ({ query, req },
46
46
  const filter: TranscriptFilter = {
47
47
  ...(query.jobKey !== undefined ? { jobKey: query.jobKey } : {}),
48
48
  ...(query.processInstanceKey !== undefined ? { processInstanceKey: query.processInstanceKey } : {}),
49
+ ...(query.elementInstanceKey !== undefined ? { elementInstanceKey: query.elementInstanceKey } : {}),
49
50
  ...(query.planKey !== undefined ? { planKey: query.planKey } : {}),
50
51
  ...(query.instance !== undefined ? { instance: query.instance } : {}),
51
52
  ...(query.since !== undefined ? { since: query.since } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.140.0",
3
+ "version": "0.142.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",
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "dependencies": {
61
61
  "@nanobpm/agentic": "^0.4.0",
62
- "@nanobpm/urban": "^0.82.0",
62
+ "@nanobpm/urban": "^0.83.0",
63
63
  "bpmn-auto-layout": "^2.0.0-alpha.2"
64
64
  },
65
65
  "devDependencies": {
@@ -123,34 +123,52 @@ test("readConnectorInput: an array payload is rejected (arrays are not plain obj
123
123
 
124
124
  test("readConvergeInput: parses pr; convergeOnly defaults from the target; dependsOn is optional", () => {
125
125
  // `converge-merge` drives the merge loop → convergeOnly defaults false.
126
- const merge = readConvergeInput("converge-merge", { pr: "owner/repo#7" });
126
+ const merge = readConvergeInput("converge-merge", { pr: "owner/repo#7" }, null);
127
127
  assertEquals(merge.parsed.prKey, "owner/repo#7");
128
128
  assertEquals(merge.convergeOnly, false);
129
129
  assertEquals(merge.dependsOn, []);
130
130
  // `converge` is review-only → convergeOnly defaults true.
131
- const conv = readConvergeInput("converge", { pr: "owner/repo#7" });
131
+ const conv = readConvergeInput("converge", { pr: "owner/repo#7" }, null);
132
132
  assertEquals(conv.convergeOnly, true);
133
133
  });
134
134
 
135
135
  test("readConvergeInput: an explicit payload.convergeOnly overrides the target default; dependsOn threads through", () => {
136
- const r = readConvergeInput("converge-merge", { pr: "owner/repo#7", convergeOnly: true, dependsOn: ["owner/repo#5", 42] as unknown as string[] });
136
+ const r = readConvergeInput("converge-merge", { pr: "owner/repo#7", convergeOnly: true, dependsOn: ["owner/repo#5", 42] as unknown as string[] }, null);
137
137
  assertEquals(r.convergeOnly, true, "the explicit boolean wins over the target default");
138
138
  assertEquals(r.dependsOn, ["owner/repo#5"], "non-string dependsOn entries are dropped");
139
139
  });
140
140
 
141
141
  test("readConvergeInput: a missing / unparseable pr fails CLOSED (a converge connector with no target PR is meaningless)", () => {
142
- assertThrows(() => readConvergeInput("converge-merge", null), Error, "payload.pr");
143
- assertThrows(() => readConvergeInput("converge-merge", {}), Error, "payload.pr");
144
- assertThrows(() => readConvergeInput("converge", { pr: "not-a-pr" }), Error, "payload.pr");
142
+ assertThrows(() => readConvergeInput("converge-merge", null, null), Error, "payload.pr");
143
+ assertThrows(() => readConvergeInput("converge-merge", {}, null), Error, "payload.pr");
144
+ assertThrows(() => readConvergeInput("converge", { pr: "not-a-pr" }, null), Error, "payload.pr");
145
145
  });
146
146
 
147
147
  test("readConvergeInput: an unparseable pr whose value is not JSON-serializable still fails CLOSED with the intended error (not a serializer TypeError)", () => {
148
148
  // `p.pr` is user-controlled payload data; a BigInt (or a circular object) makes JSON.stringify
149
149
  // throw, which must NOT mask the intended "requires payload.pr" error.
150
- assertThrows(() => readConvergeInput("converge", { pr: 10n as unknown as string }), Error, "payload.pr");
150
+ assertThrows(() => readConvergeInput("converge", { pr: 10n as unknown as string }, null), Error, "payload.pr");
151
151
  const circular: Record<string, unknown> = {};
152
152
  circular.self = circular;
153
- assertThrows(() => readConvergeInput("converge", { pr: circular as unknown as string }), Error, "payload.pr");
153
+ assertThrows(() => readConvergeInput("converge", { pr: circular as unknown as string }, null), Error, "payload.pr");
154
+ });
155
+
156
+ test("readConvergeInput: #548 late-binds the PR from an upstream agent's emitted `pr` fact (no literal needed)", () => {
157
+ const bound = [{ from: "open", name: "pr", value: "owner/repo#42" }];
158
+ // OMITTED payload.pr → binds the single incoming `pr` fact.
159
+ assertEquals(readConvergeInput("converge-merge", null, bound).parsed.prKey, "owner/repo#42");
160
+ assertEquals(readConvergeInput("converge-merge", {}, bound).parsed.prKey, "owner/repo#42");
161
+ // EXPLICIT reference form → resolves that specific threaded fact.
162
+ assertEquals(readConvergeInput("converge", { pr: "open.pr" }, bound).parsed.prKey, "owner/repo#42");
163
+ // A LITERAL still wins as-is even when a bound fact is present (never `<node>.<fact>`-shaped).
164
+ assertEquals(readConvergeInput("converge", { pr: "owner/repo#9" }, bound).parsed.prKey, "owner/repo#9");
165
+ });
166
+
167
+ test("readConvergeInput: #548 a dangling reference (no matching bound fact) fails CLOSED", () => {
168
+ // "open.pr" is reference-shaped but nothing threaded it → falls through to a literal parse, which
169
+ // rejects (it is not an `owner/repo#N`), so a mis-wired graph never silently enrolls a junk PR.
170
+ assertThrows(() => readConvergeInput("converge-merge", { pr: "open.pr" }, []), Error, "payload.pr");
171
+ assertThrows(() => readConvergeInput("converge-merge", { pr: "open.pr" }, [{ from: "other", name: "pr", value: "o/r#1" }]), Error, "payload.pr");
154
172
  });
155
173
 
156
174
  test("safeStringify: always returns a string, even for values JSON.stringify serializes to undefined (Symbol/undefined/function)", () => {
@@ -15,6 +15,7 @@ import {
15
15
  convergeOnlyForTarget,
16
16
  dispatchConnector,
17
17
  isConvergeTarget,
18
+ resolveConvergePr,
18
19
  } from "../../app/deliveryConnector.ts";
19
20
  import { isPrSettled, MAX_ROUNDS, type ParsedPr, parsePr, submitPr } from "../../app/service.ts";
20
21
 
@@ -81,19 +82,26 @@ export function safeStringify(value: unknown): string {
81
82
  * `convergeOnly` DEFAULTS from the target (`converge` → review-only `true`; `converge-merge` → drive
82
83
  * the merge loop `false`) and may be overridden per-dispatch by an explicit boolean. `dependsOn` is an
83
84
  * optional list of PR refs unioned into the enrolled PR's merge-stage dependency set (only non-string
84
- * entries are dropped; `submitPr` itself ignores unparseable refs). Exported for unit coverage — the
85
- * MVP sources `pr` as a literal (identical to how the `wait: pr` node targets a known PR), so no new
86
- * fact plumbing is needed to ship. */
85
+ * entries are dropped; `submitPr` itself ignores unparseable refs). Exported for unit coverage.
86
+ *
87
+ * `pr` may be sourced three ways (issue #548), resolved by {@link resolveConvergePr} against the
88
+ * threaded `boundFacts` BEFORE parsing: a LITERAL `owner/repo#N`, an explicit fact REFERENCE
89
+ * (`payload.pr: "<upstreamNode>.pr"`), or OMITTED — late-bound from the single incoming `pr` fact an
90
+ * upstream `agent` node emitted for the PR it opened. This is what lets the canonical
91
+ * `agent → connector[converge-merge] → wait` shape carry no hardcoded PR number. */
87
92
  export function readConvergeInput(
88
93
  target: string,
89
94
  payload: Record<string, unknown> | null,
95
+ boundFacts: readonly BoundFact[] | null,
90
96
  ): { parsed: ParsedPr; convergeOnly: boolean; dependsOn: string[] } {
91
97
  const p = payload ?? {};
92
- const parsed = parsePr(p.pr);
98
+ const prValue = resolveConvergePr(p.pr, boundFacts ?? []);
99
+ const parsed = parsePr(prValue);
93
100
  if (!parsed) {
94
101
  throw new Error(
95
- `delivery-connector: '${target}' target requires payload.pr as a parseable "owner/repo#N" ` +
96
- `(got ${safeStringify(p.pr ?? null)})`,
102
+ `delivery-connector: '${target}' target requires a target PR — a literal "owner/repo#N" in ` +
103
+ `payload.pr, an upstream-fact reference (payload.pr: "<node>.pr"), or a threaded \`pr\` fact ` +
104
+ `bound from an upstream \`agent\` node (got ${safeStringify(prValue ?? null)})`,
97
105
  );
98
106
  }
99
107
  const convergeOnly = typeof p.convergeOnly === "boolean" ? p.convergeOnly : convergeOnlyForTarget(target);
@@ -106,7 +114,7 @@ const handler: AppJobHandler<In, ConnectorDispatchResult> = async (job, app) =>
106
114
  // A `converge`/`converge-merge` target enrolls a PR into the shared convergence (+ merge) loop.
107
115
  // Parse + validate its payload BEFORE claiming a ledger row, so a misconfigured converge node (no
108
116
  // parseable `pr`) fails CLOSED without writing a junk dispatch row it could never act on.
109
- const converge = isConvergeTarget(target) ? readConvergeInput(target, payload) : null;
117
+ const converge = isConvergeTarget(target) ? readConvergeInput(target, payload, boundFacts) : null;
110
118
  const dedupeKey = connectorDedupeKey({
111
119
  dedupeKey: job.variables.dedupeKey ?? null,
112
120
  processInstanceKey: job.processInstanceKey ?? null,