@nanobpm/nano-workforce 0.175.1 → 0.176.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,15 @@
1
+ ## [0.176.0](https://github.com/nanobpm/nano-workforce/compare/v0.175.2...v0.176.0) (2026-09-02)
2
+
3
+ ### Features
4
+
5
+ * **agentic:** flush durable transcript on a worker phase:close event (fixes truncated tails) ([#711](https://github.com/nanobpm/nano-workforce/issues/711)) ([e02ff9b](https://github.com/nanobpm/nano-workforce/commit/e02ff9b4f8c2b7452dc40b0b8532736c5d418cc0)), closes [#710](https://github.com/nanobpm/nano-workforce/issues/710)
6
+
7
+ ## [0.175.2](https://github.com/nanobpm/nano-workforce/compare/v0.175.1...v0.175.2) (2026-09-02)
8
+
9
+ ### Bug Fixes
10
+
11
+ * **abandon:** don't let the cancel-check deadlock a shell-less agent ([#678](https://github.com/nanobpm/nano-workforce/issues/678)) ([#709](https://github.com/nanobpm/nano-workforce/issues/709)) ([f5fed93](https://github.com/nanobpm/nano-workforce/commit/f5fed93bdd0e98cbb24f7dff4b0f9a650c235a69)), closes [#76](https://github.com/nanobpm/nano-workforce/issues/76) [#76](https://github.com/nanobpm/nano-workforce/issues/76)
12
+
1
13
  ## [0.175.1](https://github.com/nanobpm/nano-workforce/compare/v0.175.0...v0.175.1) (2026-09-02)
2
14
 
3
15
  ### Bug Fixes
@@ -83,6 +83,32 @@ test("renderAbandonBrief embeds the concrete URL and the stop contract", () => {
83
83
  assertEquals(brief.includes("curl -fsS"), true);
84
84
  });
85
85
 
86
+ test("renderAbandonBrief forbids delegating the check to a sub-agent (issue #678)", () => {
87
+ // A non-Claude agent (e.g. qwen) that hands this shell command to a shell-less
88
+ // `general-purpose` sub-agent deadlocks: the sub-agent has no shell, produces no
89
+ // output, and the worker's idle timeout kills the round. The brief must steer the
90
+ // agent to run the check inline with its OWN shell and never delegate it.
91
+ const brief = renderAbandonBrief("https://host/app/api/hooks/abandon?token=tok");
92
+ // Not merely that it *mentions* sub-agents/inline — it must explicitly PROHIBIT delegation, so a
93
+ // future edit that softened the brief into *encouraging* delegation would fail this test.
94
+ assertEquals(/do \*\*not\*\* delegate it to a\s+sub-?agent/i.test(brief), true, "explicitly forbids delegating to a sub-agent");
95
+ assertEquals(/inline/i.test(brief), true, "tells the agent to run it inline");
96
+ });
97
+
98
+ test("renderAbandonBrief degrades gracefully when the agent has no shell (issue #678)", () => {
99
+ // The check is ADVISORY over an airtight harness job-fence (issue #76 layer 2). An
100
+ // agent that cannot run a shell command at all must be told to SKIP and proceed
101
+ // rather than thrash/hang — the harness job-fence (issue #76 layer 2) still fences
102
+ // a cancelled run, so a skipped check degrades safely rather than promising full
103
+ // independent enforcement.
104
+ const brief = renderAbandonBrief("https://host/app/api/hooks/abandon?token=tok");
105
+ assertEquals(/skip this check/i.test(brief), true, "offers a skip path");
106
+ // Assert the actual anti-deadlock contract, not an explanatory word: a shell-less agent must be
107
+ // told its lack of a shell is NOT a failed check, and must be forbidden from stalling/hanging.
108
+ assertEquals(/not (a )?failed check/i.test(brief), true, "no shell is not treated as a failed check");
109
+ assertEquals(/never stall,.*hang/i.test(brief), true, "forbids stalling/hanging when it can't run curl");
110
+ });
111
+
86
112
  test("prKeyForAbandonToken resolves a known token and rejects unknowns", async () => {
87
113
  const data = memData();
88
114
  await seedPr(data, "o/r#1", "tok", "converging");
package/app/abandon.ts CHANGED
@@ -148,5 +148,18 @@ down), instead of silently printing an error body with exit 0.
148
148
  try to complete the job is EXPECTED after a cancel — do not treat it as an error to retry.
149
149
  - **Proceed** only when the command **succeeds** AND reports \`"abandoned": false\` — and re-check
150
150
  right before the push, since a cancel can land at any moment. Checking as late as possible keeps
151
- the window tiny.`;
151
+ the window tiny.
152
+
153
+ **Run this check yourself, inline, with your own shell tool.** Do **not** delegate it to a
154
+ sub-agent, helper, or a separate "run this command" task: a sub-agent may have **no shell**, in which
155
+ case it cannot run \`curl\`, produces no output, and the worker's idle timeout will kill the round.
156
+ One inline \`curl\` in your own turn is all this needs.
157
+
158
+ **Having no way to run a shell command at all is NOT a failed check — SKIP this check and proceed.**
159
+ The "abort on non-zero exit" rule above is about \`curl\` *running and reporting* a torn-down run; it
160
+ does **not** mean "abort because you have no shell to run \`curl\` with". This check is a best-effort
161
+ optimisation, not a hard gate: the harness that runs your job also fences a cancelled run's
162
+ completion, so a skipped check only widens a tiny race — it does not by itself create an orphaned
163
+ run. Never stall, thrash, hang, or fail the round trying to run \`curl\` through a tool you do not
164
+ have; a skipped check is strictly better than a hung one.`;
152
165
  }
@@ -16,7 +16,13 @@ import { fileURLToPath } from "node:url";
16
16
  import { ConnectionRegistry } from "@nanobpm/agentic/channel";
17
17
  import type { Frame } from "@nanobpm/agentic/protocol";
18
18
  import { RELAY_FAMILY } from "@nanobpm/agentic/relay";
19
- import { encodeTranscriptEvent, type SqliteDb, TRANSCRIPT_SCHEMA_SQL } from "@nanobpm/agentic/transcript";
19
+ import {
20
+ encodeTranscriptEvent,
21
+ type SqliteDb,
22
+ TRANSCRIPT_EVENT_MARKER,
23
+ TRANSCRIPT_EVENT_VERSION,
24
+ TRANSCRIPT_SCHEMA_SQL,
25
+ } from "@nanobpm/agentic/transcript";
20
26
  import { assert, assertEquals, assertThrows } from "#test-assert";
21
27
  import { noopLog } from "../../../test/log.ts";
22
28
  import { CorrelationRegistry, jobStream } from "../correlation.ts";
@@ -551,6 +557,21 @@ test("#544 element-instance enrichment: an unresolved job (never parked) leaves
551
557
  const lifecycle = (stream: string, incarnation: number, phase: "open" | "completed" | "exited"): Frame =>
552
558
  produce(stream, incarnation, encodeTranscriptEvent({ kind: "lifecycle", phase, offset: 0 }));
553
559
 
560
+ /**
561
+ * A `produce` frame carrying the harness's job-end `phase:"close"` transcript lifecycle marker
562
+ * (#710 / jwulf/c8ctl-plugin-nano#150) — the closing twin of the `phase:"open"` RELAY_OPEN_CHUNK the
563
+ * harness emits at relay-session open. The typed `encodeTranscriptEvent` cannot express the `close`
564
+ * phase (the agentic `LifecycleEvent` union is `open|completed|exited`), so the on-wire envelope is
565
+ * built through the SAME marker/version constants the encoder stamps — mirroring exactly what the
566
+ * harness's untyped `encodeTranscriptEvent({ kind: "lifecycle", phase: "close" })` puts on the wire,
567
+ * without a forked wire shape. */
568
+ const closeMarker = JSON.stringify({
569
+ [TRANSCRIPT_EVENT_MARKER]: TRANSCRIPT_EVENT_VERSION,
570
+ kind: "lifecycle",
571
+ phase: "close",
572
+ });
573
+ const close = (stream: string, incarnation: number): Frame => produce(stream, incarnation, closeMarker);
574
+
554
575
  test("#661 primary release: a terminal lifecycle event clears an idle-but-connected worker's finished job", () => {
555
576
  const registry = new ConnectionRegistry();
556
577
  const correlation = new CorrelationRegistry();
@@ -607,6 +628,79 @@ test("#661 primary release: an `exited` lifecycle also releases; a non-terminal
607
628
  service.teardown();
608
629
  });
609
630
 
631
+ test("#710 close release: a job-end `phase:close` marker flushes the durable transcript with ALL bytes (no truncated tail)", () => {
632
+ const registry = new ConnectionRegistry();
633
+ const correlation = new CorrelationRegistry();
634
+ const byConnection = new Map([["prod", "worker-A"]]);
635
+ const { service, hub } = mkCorrelatedService(registry, memoryDb(), correlation, byConnection);
636
+ const p = connect("prod", registry);
637
+
638
+ // The worker relays N job frames on a live connection → linked as active. It then goes idle after the
639
+ // job WITHOUT a supersede (no next job) and WITHOUT a disconnect (the channel persists across jobs),
640
+ // so the ONLY job-end signal is the harness's drained `phase:close` marker.
641
+ const N = 4;
642
+ for (let i = 0; i < N; i++) hub.handler?.(produce(jobStream("k1"), 1, `job-1 line ${i}`), p.conn);
643
+ assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "the job is active while it runs");
644
+ assertEquals(service.transcriptOf(jobStream("k1")), undefined, "not flushed while the job runs");
645
+
646
+ // The job completes: the harness drains its outbound relay buffer and emits `phase:close` on the SAME
647
+ // live connection, then goes idle. Before #710 this was NOT a flush trigger — the durable transcript
648
+ // was snapshotted late (or not at all) by a later supersede/disconnect, truncating the tail. Now the
649
+ // close event flushes it deterministically at job-completion time.
650
+ hub.handler?.(close(jobStream("k1"), 1), p.conn);
651
+
652
+ assertEquals(correlation.jobKeysFor("worker-A"), [], "the finished job is released on the close marker");
653
+ assertEquals(correlation.count(), 0, "no phantom active job after close");
654
+
655
+ const meta = service.transcriptOf(jobStream("k1"));
656
+ assertEquals(meta?.status, "completed", "the job becomes a completed past session at close time");
657
+ // ALL N produced frames PLUS the close marker itself are in the durable transcript — no truncated
658
+ // tail (the close event rides the ring append before the release, like the #661 terminal path).
659
+ assertEquals(
660
+ service.reattach(jobStream("k1"), 0)?.entries.length,
661
+ N + 1,
662
+ "every relayed byte (and the close marker) is flushed — the tail is not truncated",
663
+ );
664
+ service.teardown();
665
+ });
666
+
667
+ test("#710 idempotent: a close then a duplicate close then a producer disconnect neither double-flushes nor throws", () => {
668
+ const registry = new ConnectionRegistry();
669
+ const correlation = new CorrelationRegistry();
670
+ const byConnection = new Map([["prod", "worker-A"]]);
671
+ const { service, hub } = mkCorrelatedService(registry, memoryDb(), correlation, byConnection);
672
+ const p = connect("prod", registry);
673
+
674
+ hub.handler?.(produce(jobStream("k1"), 1, "job-1 line"), p.conn);
675
+ hub.handler?.(close(jobStream("k1"), 1), p.conn);
676
+ const afterClose = service.reattach(jobStream("k1"), 0)?.entries.length;
677
+ assertEquals(service.transcriptOf(jobStream("k1"))?.status, "completed", "flushed on the first close");
678
+ assertEquals(correlation.count(), 0, "correlation released on the first close");
679
+
680
+ // A late DUPLICATE close (a harness retry, or a redelivered frame) is a no-op — the completed stream
681
+ // is terminal, so it neither re-flushes nor throws.
682
+ hub.handler?.(close(jobStream("k1"), 1), p.conn);
683
+ assertEquals(
684
+ service.reattach(jobStream("k1"), 0)?.entries.length,
685
+ afterClose,
686
+ "a duplicate close does not append or re-flush",
687
+ );
688
+
689
+ // The producer later disconnects (the belt-and-suspenders fallback). #reconcile runs on the next
690
+ // inbound frame; the already-completed stream is skipped, so the fallback neither double-flushes nor
691
+ // throws — the additive close trigger composes cleanly with the disconnect fallback.
692
+ registry.remove("prod");
693
+ const q = connect("other", registry);
694
+ hub.handler?.(produce(jobStream("k2"), 1, "unrelated"), q.conn);
695
+ assertEquals(
696
+ service.reattach(jobStream("k1"), 0)?.entries.length,
697
+ afterClose,
698
+ "the disconnect fallback does not re-flush the already-closed stream",
699
+ );
700
+ assertEquals(service.transcriptOf(jobStream("k1"))?.status, "completed", "still exactly one completed past session");
701
+ service.teardown();
702
+ });
703
+
610
704
  test("#661 no-regression: an ordinary (non-lifecycle) chunk never clears a genuinely active job", () => {
611
705
  const registry = new ConnectionRegistry();
612
706
  const correlation = new CorrelationRegistry();
@@ -24,6 +24,8 @@ import type { ConnectionRegistry } from "@nanobpm/agentic/channel";
24
24
  import type { Frame } from "@nanobpm/agentic/protocol";
25
25
  import { RELAY_FAMILY, RelayHub, type RelayHubOptions } from "@nanobpm/agentic/relay";
26
26
  import {
27
+ CORE_TRANSCRIPT_VOCAB,
28
+ mergeTranscriptVocab,
27
29
  parseTranscriptEvent,
28
30
  type SqliteDb,
29
31
  type TranscriptLifecycle,
@@ -32,6 +34,7 @@ import {
32
34
  TranscriptStore,
33
35
  type TranscriptStoreOptions,
34
36
  type TranscriptStream,
37
+ type TranscriptVocab,
35
38
  } from "@nanobpm/agentic/transcript";
36
39
  import type { Logger } from "@nanobpm/urban";
37
40
  import { currentCorrelation, type JobContext, type JobCorrelation, jobKeyOfStream } from "../correlation.ts";
@@ -128,17 +131,50 @@ function readProp(value: unknown, key: string): unknown {
128
131
  return Object.hasOwn(value, key) ? Object.getOwnPropertyDescriptor(value, key)?.value : undefined;
129
132
  }
130
133
 
134
+ /**
135
+ * The harness's job-end lifecycle phase (#710 / jwulf/c8ctl-plugin-nano#150): the closing twin of the
136
+ * `phase:"open"` `RELAY_OPEN_CHUNK` the harness emits at relay-session open. The harness emits it —
137
+ * through agentic's own `encodeTranscriptEvent`, never hand-rolled — right before `job.complete`/
138
+ * `job.fail`, once it has DRAINED its outbound relay buffer, so it is the deterministic "this job's
139
+ * bytes are all here, it is done" signal that supersede/disconnect only ever approximated.
140
+ */
141
+ const HARNESS_CLOSE_PHASE = "close";
142
+
143
+ /**
144
+ * The relay family's terminal-detection vocabulary: the ONE canonical transcript vocab, additively
145
+ * EXTENDED (via the sanctioned {@link mergeTranscriptVocab} extension point, never a forked parser) to
146
+ * ALSO classify the harness's job-end `phase:"close"` marker (#710). The agentic `LifecycleEvent`
147
+ * contract's phases are `open|completed|exited` — `close` is a fourth, job-scoped termination marker
148
+ * agentic 0.10.0 does not yet know, so the core `lifecycle` decoder drops it to a raw `stream-chunk`.
149
+ * This override recognizes it and maps it onto the contract's terminal `completed` phase, so
150
+ * {@link parseTranscriptEvent} classifies a close envelope as a terminal lifecycle WITHOUT a second
151
+ * wire shape. It is scoped to {@link isTerminalLifecycleChunk} (the correlation-release detector); the
152
+ * cockpit derive/read fold keeps using the CORE vocab, so the close chunk stays byte-faithful in the
153
+ * durable transcript and this remap never leaks into the derived view.
154
+ */
155
+ const RELAY_TERMINAL_VOCAB: TranscriptVocab = mergeTranscriptVocab(CORE_TRANSCRIPT_VOCAB, {
156
+ lifecycle: (body, offset) => {
157
+ const phase = typeof body.phase === "string" ? body.phase : undefined;
158
+ if (phase === "open" || phase === "completed" || phase === "exited") return { kind: "lifecycle", offset, phase };
159
+ if (phase === HARNESS_CLOSE_PHASE) return { kind: "lifecycle", offset, phase: "completed" };
160
+ return undefined;
161
+ },
162
+ });
163
+
131
164
  /**
132
165
  * Decode a single relay chunk through the ONE canonical transcript parser and report whether it is a
133
- * TERMINAL `lifecycle` event (`phase` `completed`/`exited`) — the authoritative "job end" signal a
134
- * clean agent run emits on its transcript stream (#661). Anything else raw terminal bytes, a
135
- * non-envelope JSON value, a `phase: "open"` lifecycle, any other event kind — is not terminal.
136
- * Reusing {@link parseTranscriptEvent} keeps transcript-vocab knowledge out of the content-agnostic
137
- * relay ring and off any forked decoder (Derivation Over Duplication): the ring still sees opaque
138
- * bytes; only this narrow seam classifies them.
166
+ * TERMINAL `lifecycle` event — the authoritative "job end" signal that flushes the durable transcript
167
+ * and releases correlation. Two shapes are terminal: the agent run's own `phase` `completed`/`exited`
168
+ * (#661), and the harness's drained job-end `phase:"close"` marker (#710), which
169
+ * {@link RELAY_TERMINAL_VOCAB} decodes onto the contract's `completed` phase. Anything else — raw
170
+ * terminal bytes, a non-envelope JSON value, a `phase: "open"` lifecycle, any other event kind — is
171
+ * not terminal. Reusing {@link parseTranscriptEvent} (with the additively-extended vocab) keeps
172
+ * transcript-vocab knowledge out of the content-agnostic relay ring and off any forked decoder
173
+ * (Derivation Over Duplication): the ring still sees opaque bytes; only this narrow seam classifies
174
+ * them.
139
175
  */
140
176
  function isTerminalLifecycleChunk(chunk: string): boolean {
141
- const event = parseTranscriptEvent({ offset: 0, chunk });
177
+ const event = parseTranscriptEvent({ offset: 0, chunk }, RELAY_TERMINAL_VOCAB);
142
178
  return event.kind === "lifecycle" && (event.phase === "completed" || event.phase === "exited");
143
179
  }
144
180
 
@@ -516,8 +552,8 @@ export class RelayTranscriptService {
516
552
  }
517
553
 
518
554
  /** Handle one inbound `relay` frame: reconcile dead producers, observe ownership, delegate, then
519
- * release on a terminal `lifecycle` event (#661 — AFTER the hub appended the chunk, so the terminal
520
- * event itself is captured in the flushed transcript). */
555
+ * release on a terminal `lifecycle` event (#661/#710 — AFTER the hub appended the chunk, so the
556
+ * terminal event itself is captured in the flushed transcript). */
521
557
  #onFrame(frame: Frame, conn: RelayConnectionCtx): void {
522
558
  this.#reconcile();
523
559
  this.#observe(frame, conn);
@@ -526,17 +562,23 @@ export class RelayTranscriptService {
526
562
  }
527
563
 
528
564
  /**
529
- * Primary job-end release (#661): when a `produce` frame carries the terminal `lifecycle` event
530
- * (`phase` `completed`/`exited`), complete its stream so the worker's job⇄instance correlation is
531
- * released the moment the job ends even though the worker's relay connection stays open across jobs
532
- * (the disconnect/supersede release paths miss that idle-after-last-job tail, so an idle worker's
533
- * finished job would otherwise linger as a phantom active job on its supply row). Runs AFTER the hub
534
- * appends the chunk to the ring, so the terminal event is part of the flushed transcript. A narrow,
535
- * self-contained decode at the correlation seam that reuses the ONE canonical
536
- * {@link parseTranscriptEvent} the content-agnostic relay ring keeps treating chunks as opaque
537
- * bytes, and no transcript-vocab knowledge is forked into it. Non-terminal chunks (raw bytes, a
538
- * `phase: "open"` lifecycle, any other event kind) never complete a live stream, so a genuinely
539
- * active job is never cleared. Advisory never throws into the frame handler.
565
+ * Primary job-end release (#661, #710): when a `produce` frame carries a terminal `lifecycle` event
566
+ * the agent run's own `phase` `completed`/`exited` (#661), OR the harness's drained job-end
567
+ * `phase:"close"` marker (#710) complete its stream so the worker's job⇄instance correlation is
568
+ * released, and the durable "past session" transcript is flushed, the moment the job ends. This runs
569
+ * even though the worker's relay connection stays open across jobs (the disconnect/supersede release
570
+ * paths miss that idle-after-last-job tail, so an idle worker's finished job would otherwise linger as
571
+ * a phantom active job on its supply row, and its transcript would be snapshotted late → truncated
572
+ * tail). The `close` trigger is ADDITIVE supersede + disconnect remain as belt-and-suspenders
573
+ * fallbacks for a harness that predates the close event or a crash that never sends one — and the
574
+ * `state.completed` guard below makes a close-then-later-disconnect (or a duplicate close) an
575
+ * idempotent no-op. Runs AFTER the hub appends the chunk to the ring, so the terminal event is part
576
+ * of the flushed transcript. A narrow, self-contained decode at the correlation seam that reuses the
577
+ * ONE canonical {@link parseTranscriptEvent} ({@link isTerminalLifecycleChunk}) — the content-agnostic
578
+ * relay ring keeps treating chunks as opaque bytes, and no transcript-vocab knowledge is forked into
579
+ * it. Non-terminal chunks (raw bytes, a `phase: "open"` lifecycle, any other event kind) never
580
+ * complete a live stream, so a genuinely active job is never cleared. Advisory — never throws into
581
+ * the frame handler.
540
582
  */
541
583
  #observeTerminalLifecycle(frame: Frame): void {
542
584
  if (readProp(frame.payload, "op") !== "produce") return;
package/app/contracts.ts CHANGED
@@ -463,6 +463,14 @@ export const WIRE_CONTRACTS = {
463
463
  shape:
464
464
  '{ nwfTranscriptEvent: 1, kind: "permission", phase: "request", callId: string, policy: "escalate"|"yolo", options: [{ optionId: string, name: string, kind: "allow-once"|"allow-always"|"reject-once"|"reject-always" }, ...Array<{ optionId: string, name: string, kind: "allow-once"|"allow-always"|"reject-once"|"reject-always" }>], toolName?: string, title?: string, reason?: string } | { nwfTranscriptEvent: 1, kind: "permission", phase: "resolution", callId: string, optionId: string, allowed: boolean, by?: "operator"|"auto" }',
465
465
  },
466
+ "transcript.lifecycleClose": {
467
+ category: "wire",
468
+ name: "transcript.lifecycleClose",
469
+ owner: "app/agentic/families/relay.family.ts",
470
+ semantics:
471
+ "The harness's job-end `phase:\"close\"` transcript `lifecycle` event (issue #710, harness half jwulf/c8ctl-plugin-nano#150) — the closing twin of the `phase:\"open\"` RELAY_OPEN_CHUNK the harness emits at relay-session open. The harness emits it on the `job:<jobKey>` relay stream through agentic's own `encodeTranscriptEvent` (never hand-rolled), right before `job.complete`/`job.fail` and AFTER draining its outbound relay buffer, so it is the deterministic \"all this job's bytes are here, it is done\" signal. The app recognizes it in `isTerminalLifecycleChunk` (relay.family.ts) to `completeStream()` — flush the durable past-session transcript and release job⇄instance correlation — at job-completion time, fixing the truncated-tail defect where the flush waited for a supersede/disconnect. `close` is NOT a core agentic `LifecycleEvent` phase (the contract's phases are open|completed|exited); the app decodes it via an ADDITIVE `mergeTranscriptVocab` extension (`RELAY_TERMINAL_VOCAB`) that maps it onto the terminal `completed` phase — never a forked wire shape, and scoped to the relay job-end detector so the cockpit derive (CORE vocab) keeps the close chunk byte-faithful. The `close` trigger is ADDITIVE: supersede + disconnect remain as fallbacks and the `state.completed` guard keeps a close-then-disconnect (or duplicate close) idempotent. Consume this ONE marker — do not re-declare a synonym or a second job-end signal.",
472
+ shape: '{ nwfTranscriptEvent: 1, kind: "lifecycle", phase: "close" }',
473
+ },
466
474
  } as const satisfies Record<string, WireContract>;
467
475
 
468
476
  export const TYPE_CONTRACTS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.175.1",
3
+ "version": "0.176.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",