@nanobpm/nano-workforce 0.206.3 → 0.206.4

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.206.4](https://github.com/nanobpm/nano-workforce/compare/v0.206.3...v0.206.4) (2026-11-09)
2
+
3
+ ### Bug Fixes
4
+
5
+ * **delivery:** accept affirmative-completion aliases as producer terminal-success statuses ([#934](https://github.com/nanobpm/nano-workforce/issues/934)) ([b8c5e4e](https://github.com/nanobpm/nano-workforce/commit/b8c5e4e5cb59ce42bb2d6573e107d1865393fa27)), closes [731/#760](https://github.com/731/nano-workforce/issues/760) [nano-bpm#1337](https://github.com/nanobpm/nano-bpm/issues/1337) [#1464](https://github.com/nanobpm/nano-workforce/issues/1464) [nano-bpm#1463](https://github.com/nanobpm/nano-bpm/issues/1463) [#1476](https://github.com/nanobpm/nano-workforce/issues/1476) [#731](https://github.com/nanobpm/nano-workforce/issues/731) [#760](https://github.com/nanobpm/nano-workforce/issues/760) [#761](https://github.com/nanobpm/nano-workforce/issues/761)
6
+
1
7
  ## [0.206.3](https://github.com/nanobpm/nano-workforce/compare/v0.206.2...v0.206.3) (2026-11-09)
2
8
 
3
9
  ### Bug Fixes
package/SPEC.md CHANGED
@@ -1289,7 +1289,9 @@ reserved — with **one exception**: an agent emitting `pr` (the canonical
1289
1289
  result field holds, and the retry reset correctly clears both — reserving them would
1290
1290
  forbid that flagship pattern. The exception is the agent completion-control **`status`**:
1291
1291
  the producer status gate reads that same node-local field as the node's completion status
1292
- and accepts only `done`/`opened`/`skipped`, so an agent emit named `status` carrying any
1292
+ and accepts only the terminal-success vocabulary (`AGENT_TERMINAL_SUCCESS_STATUSES` — the
1293
+ canonical `done`/`opened`/`skipped` plus the issue-#932 affirmative-completion aliases
1294
+ `completed`/`succeeded`/`success`), so an agent emit named `status` carrying any
1293
1295
  other routing value (`approved`, …) always escalates, and `status="done"` can never take an
1294
1296
  `approved` branch — the emit contract and the gate conflict. `status` is therefore reserved
1295
1297
  for an **`agent`** (a connector has no producer status gate, so a connector `status` emit
@@ -538,7 +538,7 @@ test("invalid-fact-name: an AGENT emit named `status` is reserved — the produc
538
538
  // Regression guard (PR #863 Copilot Medium "Previously missed", deliveryGraph.ts:418): the result-set
539
539
  // exemption was too broad for an agent emit named `status`. The producer status gate
540
540
  // (`agentContractProceedCondition`) interprets that same node-local field as the completion status and
541
- // accepts only `done`/`opened`/`skipped`, while the emit contract may require a different routing
541
+ // accepts only the terminal-success vocabulary (`AGENT_TERMINAL_SUCCESS_STATUSES`), while the emit contract may require a different routing
542
542
  // value — `status="approved"` always escalates, and `status="done"` can never take an `approved`
543
543
  // branch. Unlike an aligned result fact such as `pr` (whose emit value and result field agree),
544
544
  // `status` is CONSTRAINED by the gate, so the two uses conflict. Reserve the agent completion-control
@@ -458,7 +458,9 @@ export const DELIVERY_SCAFFOLDING_VARS = ["boundFacts", "nodeInputs", "nodeInput
458
458
  * correctly clears both. Reserving the whole set would forbid the flagship
459
459
  * `agent → connector[converge] → wait[pr]` pattern. The exception is the agent completion-control
460
460
  * `status`: the producer status gate reads that SAME node-local field as the node's completion status
461
- * and accepts only `done`/`opened`/`skipped`, so an agent emit named `status` whose routing value is
461
+ * and accepts only the terminal-success vocabulary (`AGENT_TERMINAL_SUCCESS_STATUSES` — the canonical
462
+ * `done`/`opened`/`skipped` plus the #932 affirmative-completion aliases), so an agent emit named
463
+ * `status` whose routing value is
462
464
  * anything else (`approved`, …) ALWAYS escalates, and `status="done"` can never take an `approved`
463
465
  * branch — the emit contract and the gate conflict. `status` is therefore reserved for an `agent`
464
466
  * (a connector has no producer status gate, so its `status` emit collides with nothing and stays
@@ -15,7 +15,7 @@ import { test } from "node:test";
15
15
  import { assert, assertEquals } from "#test-assert";
16
16
  import type { DeliveryGraph } from "../nano-generated/api-io.d.ts";
17
17
  import { missingExternalMarkerAgentTaskTypes } from "./agentic/vocab/job-types.ts";
18
- import { compileDeliveryGraph, digestInvisibleRawValues, graphCarriesRedactedSecrets, nodeDisplay, redactFreeText, resumeValidVar } from "./deliveryGraphCompiler.ts";
18
+ import { AGENT_TERMINAL_SUCCESS_STATUSES, compileDeliveryGraph, digestInvisibleRawValues, graphCarriesRedactedSecrets, nodeDisplay, redactFreeText, resumeValidVar } from "./deliveryGraphCompiler.ts";
19
19
  import { isDeliveryEscalationTwin } from "./deliveryHuman.ts";
20
20
 
21
21
  /** Compile and assert success, returning the narrowed ok-result. */
@@ -2131,7 +2131,11 @@ test("#731 producer status gate: an agent node inserts a post-completion contrac
2131
2131
  assert(g0, "the contract-met success flow exists");
2132
2132
  assert(g0![1].includes(`=${el}_contractMet = true`), "the success flow reads the node-unique contract decision");
2133
2133
  const cond = contractMetExpr(r.bpmn, el);
2134
- assert(cond.includes('list contains(["done", "opened", "skipped"], status)'), "the success flow gates on the terminal-success status allowlist");
2134
+ // The gate's FEEL allowlist is DERIVED from the single source of truth (AGENT_TERMINAL_SUCCESS_STATUSES)
2135
+ // — assert against the derived rendering, not a hand-copied literal, so this test can never drift from
2136
+ // the vocabulary it guards (issue #932).
2137
+ const derivedList = `[${AGENT_TERMINAL_SUCCESS_STATUSES.map((s) => `"${s}"`).join(", ")}]`;
2138
+ assert(cond.includes(`list contains(${derivedList}, status)`), `the success flow gates on the derived terminal-success status allowlist, got: ${cond}`);
2135
2139
  assert(cond.includes("not(is defined(status)) or status = null"), "an absent/null status is not itself the failure mode — it still proceeds");
2136
2140
  // The contract escalation's read-only context names the node and its reported status (#731).
2137
2141
  const esc = escBlockForNodeSuffix(r.bpmn, "open", "contract");
@@ -2139,6 +2143,49 @@ test("#731 producer status gate: an agent node inserts a post-completion contrac
2139
2143
  assert(esc.includes("Reported status="), "the context surfaces the actual reported status");
2140
2144
  });
2141
2145
 
2146
+ test("#932 producer status gate: the affirmative-completion synonyms agents actually report are allowlisted terminal-success statuses, derived from one source", () => {
2147
+ // Issue #932: agents keep self-reporting `status: "completed"` (and other unambiguous success
2148
+ // synonyms) after FULLY delivering — every required emit present — yet the #731 gate accepted only
2149
+ // `done`/`opened`/`skipped`, so a fully-delivered node parked on a human "Continue" escalation as a
2150
+ // contract violation (nano-bpm#1337 → PR #1464 escalation 584376; nano-bpm#1463 → PR #1476
2151
+ // escalation 599960). The fix is the CLASS fix: the affirmative-completion aliases are
2152
+ // terminal-success statuses in the ONE canonical vocabulary (AGENT_TERMINAL_SUCCESS_STATUSES), which
2153
+ // the gate's FEEL AND the #760 prompt contract both derive from. Pin the vocabulary content so a
2154
+ // future "tidy-up" cannot silently re-narrow it (a removed synonym re-introduces the escalation).
2155
+ for (const synonym of ["completed", "success", "succeeded"]) {
2156
+ assert(
2157
+ AGENT_TERMINAL_SUCCESS_STATUSES.includes(synonym),
2158
+ `"${synonym}" — an unambiguous affirmative-completion alias agents report — must be a terminal-success status (issue #932)`,
2159
+ );
2160
+ }
2161
+ // The canonical statuses stay.
2162
+ for (const canonical of ["done", "opened", "skipped"]) {
2163
+ assert(AGENT_TERMINAL_SUCCESS_STATUSES.includes(canonical), `the canonical "${canonical}" status stays allowlisted`);
2164
+ }
2165
+ // And the vocabulary remains CLOSED to ambiguous non-success reports: a failure/give-up/in-progress
2166
+ // self-report must NEVER be normalised to success (the gate stays fail-closed for those).
2167
+ for (const nonSuccess of ["in_progress", "blocked", "failed", "escalated", "needs_input"]) {
2168
+ assert(
2169
+ !AGENT_TERMINAL_SUCCESS_STATUSES.includes(nonSuccess),
2170
+ `"${nonSuccess}" is not a success claim — it must stay OFF the terminal-success allowlist`,
2171
+ );
2172
+ }
2173
+ });
2174
+
2175
+ test("#932 producer status gate: the compiled contract gate accepts a `completed` report (the observed failure) as terminal-success", async () => {
2176
+ // The compile-level half of the #932 regression guard: the gate's FEEL allowlist (derived from
2177
+ // AGENT_TERMINAL_SUCCESS_STATUSES) must literally contain `completed`, so a fully-delivered node
2178
+ // reporting it passes the producer gate instead of escalating (the e2e half, in
2179
+ // e2e/delivery-graph.e2e.ts, proves the routing end-to-end).
2180
+ const r = await compileOk(PRODUCER_GATE);
2181
+ const el = elementForNode(r.bpmn, "open");
2182
+ const cond = contractMetExpr(r.bpmn, el);
2183
+ assert(cond.includes('"completed"'), `the gate's status allowlist contains "completed", got: ${cond}`);
2184
+ // The escalation context line names the SAME derived vocabulary (never a stale hand-typed list).
2185
+ const esc = escBlockForNodeSuffix(r.bpmn, "open", "contract");
2186
+ assert(esc.includes("completed"), "the escalation context names the widened vocabulary");
2187
+ });
2188
+
2142
2189
  test("#731 required-emit gate: a producer's declared emit consumed as a required data dependency adds a non-null gate clause and a resumable escalation NAMING the fact", async () => {
2143
2190
  const r = await compileOk(PRODUCER_GATE);
2144
2191
  const el = elementForNode(r.bpmn, "open");
@@ -254,11 +254,22 @@ export function resumeValidVar(esc: string): string {
254
254
  * and escalates AT the node instead of threading an incomplete result into a downstream consumer. An
255
255
  * ABSENT/null status passes the gate (a status-less completion — an older fleet worker or a bare test
256
256
  * stub — is not itself the failure mode; the required-emit gate still catches a missing data fact).
257
+ *
258
+ * The list deliberately includes the unambiguous AFFIRMATIVE-COMPLETION aliases agents actually report
259
+ * — `completed`/`success`/`succeeded` (issue #932: agents kept reporting `status: "completed"` after
260
+ * FULLY delivering — every required emit present — and the narrow list parked that good work on a
261
+ * human "Continue" escalation as a contract violation; nano-bpm#1337 → PR #1464 escalation 584376,
262
+ * nano-bpm#1463 → PR #1476 escalation 599960). An affirmative alias is normalised to terminal-success
263
+ * AT THE GATE: it routes onward exactly like `done` — the required-emit clauses still gate it, so a
264
+ * `completed` WITHOUT its required emits still escalates. Only UNAMBIGUOUS success claims belong here:
265
+ * a failure / input-required / in-progress report (`failed`, `needs_input`, `in_progress`, …) is NOT a
266
+ * completion claim and must NEVER be added — the gate stays fail-closed for those.
267
+ *
257
268
  * Sorted for the compiler's byte-identical-output determinism. Exported as the SINGLE SOURCE OF TRUTH:
258
269
  * the compiler's contract gate reads it here, and the runner's `renderProducerContract` (#760) derives
259
270
  * the agent-facing status vocabulary from the SAME list — changing it changes both the gate and the
260
271
  * prompt at once, so the two representations of the producer contract can never drift. */
261
- export const AGENT_TERMINAL_SUCCESS_STATUSES: readonly string[] = ["done", "opened", "skipped"];
272
+ export const AGENT_TERMINAL_SUCCESS_STATUSES: readonly string[] = ["completed", "done", "opened", "skipped", "succeeded", "success"];
262
273
 
263
274
  /** A never-reached exhaustiveness guard: `compileNode`'s `switch` covers every allowlisted kind, so
264
275
  * the closed union narrows to `never` here. If a future kind is added to the vocabulary without a
@@ -279,6 +279,25 @@ test("producer contract semantics coverage (#761 follow-up): renderProducerContr
279
279
  });
280
280
 
281
281
 
282
+ test("#932 producer contract prompt: the affirmative-completion synonyms agents actually report are listed AND documented, so the prompt and the gate accept the same vocabulary", () => {
283
+ // Issue #932: agents kept reporting `status: "completed"` (and synonyms) after fully delivering and
284
+ // were parked on a human escalation, because the #731 gate accepted only `done`/`opened`/`skipped`
285
+ // and the #760 prompt told agents the same narrow list. The class fix puts the synonyms in the ONE
286
+ // canonical vocabulary (AGENT_TERMINAL_SUCCESS_STATUSES); the prompt contract is DERIVED from it, so
287
+ // the agent is now TOLD these are valid — pin that the observed aliases surface with semantics.
288
+ const rendered = renderProducerContract([]);
289
+ for (const synonym of ["completed", "success", "succeeded"]) {
290
+ assert(
291
+ rendered.includes(`- \`${synonym}\` — `),
292
+ `the prompt documents the "${synonym}" alias with its semantics (issue #932), got: ${rendered}`,
293
+ );
294
+ }
295
+ // The "do not invent a status" warning must name the widened list too (derived, never hand-typed).
296
+ const list = AGENT_TERMINAL_SUCCESS_STATUSES.map((s) => `\`${s}\``).join(", ");
297
+ assert(rendered.includes(`Any \`status\` OUTSIDE ${list}`), `the fail-closed warning names the derived allowlist, got: ${rendered}`);
298
+ });
299
+
300
+
282
301
  test("agent node idempotency preflight (#551): every agent prompt leads with adopt-and-report guidance; non-agent nodes are untouched", async () => {
283
302
  // #551: a delivery agent node dispatches a raw retry-carrying `senior:feature` job with no
284
303
  // PR-existence guard, so a re-dispatch opened a DUPLICATE PR (instance 43077 n0 → #979/#980). The
@@ -754,3 +773,15 @@ test("the canonical `agent → converge-merge → wait[pr merged]` graph DISPATC
754
773
  assert(r.ok, `expected the canonical fact-bound wait[pr] graph to launch, got ${JSON.stringify(r)}`);
755
774
  assertEquals(r.handle.processInstanceKey, "555");
756
775
  });
776
+ test("PR #934 producer contract prompt: no accepted status is ever cited as a forbidden example", () => {
777
+ // Class: the rejection sentence named `success` as invalid after it became an accepted alias, so every
778
+ // agent got contradictory instructions. Every accepted status must be absent from that sentence.
779
+ const rendered = renderProducerContract([]);
780
+ const rejectLine = rendered.split("\n").find((l) => l.includes("including a free-form"));
781
+ assert(rejectLine !== undefined, `the fail-closed warning carries rejection examples, got: ${rendered}`);
782
+ const examples = rejectLine.split("including a free-form")[1] ?? "";
783
+ for (const s of AGENT_TERMINAL_SUCCESS_STATUSES) {
784
+ assert(!examples.includes(`\`${s}\``), `accepted status "${s}" must not be cited as a rejected example: ${rejectLine}`);
785
+ }
786
+ assert(examples.includes("`in_progress`"), `a genuine out-of-vocabulary example remains: ${rejectLine}`);
787
+ });
@@ -495,6 +495,13 @@ const PRODUCER_STATUS_SEMANTICS: Readonly<Record<string, string>> = {
495
495
  opened: "you opened OR adopted a PR (return it in your `pr` emit if this node declares one)",
496
496
  done: "the work completed with no PR to open",
497
497
  skipped: "there was genuinely nothing to do",
498
+ // Issue #932: the unambiguous AFFIRMATIVE-COMPLETION aliases agents actually self-report. They are
499
+ // full terminal-success statuses (normalised at the gate), documented here so the prompt and the
500
+ // gate accept the SAME vocabulary — an agent told only `done`/`opened`/`skipped` keeps inventing
501
+ // `completed` and parking good work on a human escalation.
502
+ completed: "an alias of `done`/`opened` — the work fully completed (with its required emits populated)",
503
+ succeeded: "an alias of `done`/`opened` — the work fully succeeded (with its required emits populated)",
504
+ success: "an alias of `done`/`opened` — the work fully succeeded (with its required emits populated)",
498
505
  };
499
506
 
500
507
  /** Render the producer-completion contract auto-injected into EVERY `agent` node's `appendPrompt`
@@ -518,6 +525,11 @@ const PRODUCER_STATUS_SEMANTICS: Readonly<Record<string, string>> = {
518
525
  * contract already tells the agent to omit an undecidable routing fact. Listing every DECLARED emit
519
526
  * here instead would contradict that guidance and push agents to guess values that should stay
520
527
  * optional (#761). */
528
+ /** Illustrative out-of-vocabulary statuses named in the producer contract's rejection sentence. Always
529
+ * filtered against `AGENT_TERMINAL_SUCCESS_STATUSES` at render time, so an example that later becomes an
530
+ * accepted alias silently drops out instead of contradicting the accepted list. */
531
+ const PRODUCER_REJECTED_STATUS_EXAMPLES: readonly string[] = ["in_progress", "failed", "partial", "success"];
532
+
521
533
  export function renderProducerContract(requiredEmits: readonly DeliveryFact[]): string {
522
534
  const list = AGENT_TERMINAL_SUCCESS_STATUSES.map((s) => `\`${s}\``).join(", ");
523
535
  const semantics = AGENT_TERMINAL_SUCCESS_STATUSES.map((s) => {
@@ -530,6 +542,11 @@ export function renderProducerContract(requiredEmits: readonly DeliveryFact[]):
530
542
  }
531
543
  return `- \`${s}\` — ${doc}.`;
532
544
  });
545
+ // Rejection examples are FILTERED against the allowlist so the prompt can never present one status as
546
+ // both accepted and forbidden (PR #934 review: `success` became an alias but stayed an example).
547
+ const rejected = PRODUCER_REJECTED_STATUS_EXAMPLES.filter((s) => !AGENT_TERMINAL_SUCCESS_STATUSES.includes(s))
548
+ .map((s) => `\`${s}\``)
549
+ .join("/");
533
550
  const lines = [
534
551
  "",
535
552
  "",
@@ -543,7 +560,7 @@ export function renderProducerContract(requiredEmits: readonly DeliveryFact[]):
543
560
  "",
544
561
  ...semantics,
545
562
  "",
546
- `Any \`status\` OUTSIDE ${list} — including a free-form \`success\`/\`in_progress\`/\`failed\` — parks the`,
563
+ `Any \`status\` OUTSIDE ${list} — including a free-form ${rejected} — parks the`,
547
564
  "run on a human escalation (the gate is fail-closed), EVEN when your underlying work was correct. So",
548
565
  "do not invent a status: report exactly one of the allowlisted values above.",
549
566
  ];
package/build-info.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.206.3",
3
- "gitSha": "f731dc60dc0cb7480b425e88d1d216b95d63a5c1",
4
- "gitTag": "v0.206.3",
5
- "builtAt": "2026-11-09T21:25:41.376Z"
2
+ "version": "0.206.4",
3
+ "gitSha": "b8c5e4e5cb59ce42bb2d6573e107d1865393fa27",
4
+ "gitTag": "v0.206.4",
5
+ "builtAt": "2026-11-09T21:50:28.782Z"
6
6
  }
@@ -359,7 +359,10 @@ instruction to the agent's prompt automatically when the node declares `emits`.
359
359
  omits the fact, the `default` branch is taken.
360
360
 
361
361
  **Result statuses are added automatically. Don't write them into your prompt.** Every agent
362
- node's prompt gets the allowed terminal statuses (`done` / `opened` / `skipped`). A completion
362
+ node's prompt gets the allowed terminal statuses (`done` / `opened` / `skipped`, plus the
363
+ unambiguous affirmative-completion aliases `completed` / `succeeded` / `success` — issue #932:
364
+ a fully-delivered node reporting one of those is a terminal success, never a contract
365
+ violation). A completion
363
366
  moves on only with one of those statuses and every required emit present. Anything else parks
364
367
  the run on a human `__contract` escalation — **except** an absent/null `status`, which a legacy
365
368
  worker that populates its required emits may still advance with (the status contract binds only
@@ -373,6 +373,66 @@ describe("delivery-graph runner — engine-native execution (S4)", () => {
373
373
  assert.ok(takenFlows(app).some((f) => f.endsWith("->End")), "the satisfied producer's result reaches End");
374
374
  });
375
375
 
376
+ test("#932 producer contract: an agent reporting status=completed with all required emits routes onward and NEVER reaches a human escalation", async () => {
377
+ const app = track(await boot(freshDir()));
378
+
379
+ // Issue #932's observed failure (nano-bpm#1337 → PR #1464 escalation 584376; nano-bpm#1463 →
380
+ // PR #1476 escalation 599960): the agent FULLY delivered — required `pr` emit present, work done,
381
+ // PR later merged — but self-reported the affirmative-completion alias `status: "completed"`,
382
+ // which the #731 gate (accepting only `done`/`opened`/`skipped`) treated as a contract violation
383
+ // and parked on a human "Continue" escalation. With the synonym allowlisted in the ONE canonical
384
+ // vocabulary, the same result must sail through the gate: no __contract task, the downstream
385
+ // connector fires, the graph reaches End.
386
+ let agentFired = 0;
387
+ await app.engine.registerWorker("senior:demo", async () => {
388
+ agentFired++;
389
+ return { status: "completed", pr: "owner/repo#100", summary: "Fully delivered; PR opened and green." };
390
+ });
391
+ let connectorFired = 0;
392
+ await app.engine.registerWorker(
393
+ "pr.delivery-connector",
394
+ async (job) => {
395
+ connectorFired++;
396
+ const vars = job.variables as Record<string, unknown>;
397
+ const { target, payload, boundFacts } = readConnectorInput(vars as Parameters<typeof readConnectorInput>[0]);
398
+ const dedupeKey = connectorDedupeKey({
399
+ dedupeKey: (vars.dedupeKey as string | null | undefined) ?? null,
400
+ processInstanceKey: job.processInstanceKey ?? null,
401
+ elementId: job.elementId ?? null,
402
+ });
403
+ if (!dedupeKey) throw new Error("connector stub: no dedupe key (author-supplied or graph-derived) available");
404
+ return await dispatchConnector(app.db, { dedupeKey, target, payload, boundFacts }, new Date().toISOString());
405
+ },
406
+ { fetchVariables: ["boundFacts", "target", "dedupeKey", "payload"] },
407
+ );
408
+
409
+ const graph: DeliveryGraph = {
410
+ name: "e2e producer gate completed synonym",
411
+ nodes: [
412
+ { id: "open", kind: "agent", agent: { jobType: "senior:demo" }, emits: [{ name: "pr", type: "pr" }] },
413
+ { id: "land", kind: "connector", connector: { target: "slack", payload: { pr: "open.pr" }, dedupeKey: "land-932" } },
414
+ ],
415
+ edges: [{ from: "open.pr", to: "land" }],
416
+ };
417
+
418
+ const run = await runDeliveryGraph(app.engine, graph, { escalationSlaTimeout: "PT1H", repoless: true });
419
+ assert.ok(run.ok, `graph should deploy + run, got ${JSON.stringify(run)}`);
420
+ await app.settle();
421
+
422
+ // The `completed` report + all required emits is a terminal success: it routes ONWARD — no
423
+ // producer-contract escalation (and no escalation of any kind), the connector fired once on the
424
+ // real PR, and the graph reached End. A human is never in the loop.
425
+ assert.equal(agentFired, 1, "the agent node's job fired and completed");
426
+ const createdTasks = await app.engine.searchUserTasks({ state: "CREATED" });
427
+ assert.deepEqual(
428
+ createdTasks.map((t) => t.elementId),
429
+ [],
430
+ `a fully-delivered "completed" result must NEVER park a human task, got ${JSON.stringify(createdTasks.map((t) => t.elementId))}`,
431
+ );
432
+ assert.equal(connectorFired, 1, "the satisfied producer threads its result to the downstream connector");
433
+ assert.ok(takenFlows(app).some((f) => f.endsWith("->End")), "the completed producer's result reaches End");
434
+ });
435
+
376
436
  test("resume never double-fires: an at-least-once redelivery of the connector dedupes", async () => {
377
437
  const app = track(await boot(freshDir()));
378
438
  // The connector fired once above's-style; here prove the idempotency directly against the ledger a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.206.3",
3
+ "version": "0.206.4",
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",