@nanobpm/nano-workforce 0.201.3 → 0.201.5

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.
@@ -58,6 +58,17 @@ jobs:
58
58
  - name: Check agent prompt templates
59
59
  run: npm run check:prompts
60
60
 
61
+ # Agent-definition gate (issue #884): every `senior:*` agent service task the app deploys —
62
+ # hand-authored in resources/processes/*.bpmn OR produced by the delivery-graph compiler — must
63
+ # carry `<zeebe:agentDefinition agentType="external"/>`, or the engine refuses to mint an
64
+ # AgentInstance for it and the run persists no durable agentic transcript (the 269 failed
65
+ # creates on omarchy). Compiled agent cells are deliberately prompt-LESS (the resolved #884
66
+ # "Decide", Option (a)): the compiler emits the marker on EVERY agent cell unconditionally, and
67
+ # the gate keys agent-ness on the `<rank>:<task>` colon-form type (the type-keyed scanner), so a
68
+ # compiled cell that drops the marker fails here, not silently at runtime.
69
+ - name: Check agent task definitions (external marker)
70
+ run: npm run check:agent-definitions
71
+
61
72
  # Merge-safety gate: migrations are applied in numeric-prefix order, but each fan-out branch
62
73
  # picks "the next" prefix independently, so parallel siblings silently collide on one slot
63
74
  # (the historical 004/005/006/007 pairs; recurred on epic #142). This fails the build on any
package/AGENTS.md CHANGED
@@ -393,13 +393,15 @@ npm run typecheck # tsc --noEmit (Node)
393
393
  npm run check # urban check (manifest validation)
394
394
  npm run layout:check # BPMN diagram freshness (no drift)
395
395
  npm run check:prompts # agent-prompt linkedResource resolution
396
+ npm run check:agent-definitions # every senior:* agent task (authored corpus + compiled delivery-graph output) carries the agentDefinition marker
396
397
  npm run check:migrations # migration prefixes + immutability (no rename/delete/edit of a merged migration)
397
398
  npm run check:contracts # contract registry (no synonyms / undeclared env keys)
398
399
  npm test # unit tests (node --test)
399
400
  ```
400
401
 
401
402
  CI (`.github/workflows/ci.yml`) gates lint, typecheck, `urban check`, `layout:check`,
402
- the prompt check, the migration-prefix check, the contract-registry check, and the Node test suite. Run `npm run layout <file.bpmn>` after
403
+ the prompt check, the agent-definition check, the migration-prefix check, the
404
+ contract-registry check, and the Node test suite. Run `npm run layout <file.bpmn>` after
403
405
  any BPMN flow change and commit the regenerated diagram — the `layout:check`
404
406
  gate fails the build otherwise.
405
407
 
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [0.201.5](https://github.com/nanobpm/nano-workforce/compare/v0.201.4...v0.201.5) (2026-10-11)
2
+
3
+ ### Bug Fixes
4
+
5
+ * protect engine-owned process vars from agent-completion clobber (Closes [#887](https://github.com/nanobpm/nano-workforce/issues/887)) ([#888](https://github.com/nanobpm/nano-workforce/issues/888)) ([4245287](https://github.com/nanobpm/nano-workforce/commit/4245287e3d7553881f29bdda50cbd6d2b60b6268))
6
+
7
+ ## [0.201.4](https://github.com/nanobpm/nano-workforce/compare/v0.201.3...v0.201.4) (2026-10-11)
8
+
9
+ ### Bug Fixes
10
+
11
+ * **delivery-graph:** emit zeebe:agentDefinition marker on every compiled agent cell (prompt-less, Option (a)) ([#886](https://github.com/nanobpm/nano-workforce/issues/886)) ([1e4fa4f](https://github.com/nanobpm/nano-workforce/commit/1e4fa4f46aad542b2e6fc175a3af0b46d0980f50)), closes [#464](https://github.com/nanobpm/nano-workforce/issues/464) [#745](https://github.com/nanobpm/nano-workforce/issues/745) [#884](https://github.com/nanobpm/nano-workforce/issues/884) [#884](https://github.com/nanobpm/nano-workforce/issues/884) [#776](https://github.com/nanobpm/nano-workforce/issues/776) [#884](https://github.com/nanobpm/nano-workforce/issues/884) [#884](https://github.com/nanobpm/nano-workforce/issues/884) [#884](https://github.com/nanobpm/nano-workforce/issues/884)
12
+
1
13
  ## [0.201.3](https://github.com/nanobpm/nano-workforce/compare/v0.201.2...v0.201.3) (2026-10-10)
2
14
 
3
15
  ### Bug Fixes
package/SPEC.md CHANGED
@@ -428,6 +428,79 @@ Notes:
428
428
  `answer`/`scopePending` (inputs the agent consumes) stay cleared on the
429
429
  review-round output.
430
430
 
431
+ - **Engine-owned process variables are restored around every external-agent task
432
+ (issue #887).** The engine merges **every** variable an external agent returns
433
+ into process scope **unfiltered** — the `io.nanobpm.dataEnvelope.out` shape is
434
+ codegen/typing-only and applies **no** runtime filtering or coercion. An agent
435
+ that echoes an engine-owned variable with a colliding name or drifted type (a
436
+ `senior:pr-review` job that returned `round: "4"` — a string) therefore
437
+ overwrites the real process value, and the next FEEL guard that reads it
438
+ (`round >= maxRounds` at `gw-guard`) raises `comparison not defined for string
439
+ and number` and wedges the loop on an unrecoverable incident. The categorical
440
+ guard is a **snapshot/restore** on each external-agent task (`review-round`,
441
+ `adversarial-review`, `classify-scope`): before the agent runs, each
442
+ engine-owned variable is copied into a task-**local** `guard<Name>` input
443
+ (`<zeebe:input source="=round" target="guardRound"/>` …), and the task's output
444
+ mapping **restores** the process variable from that snapshot
445
+ (`<zeebe:output source="=guardRound" target="round"/>`). Because the snapshot
446
+ holds the pre-merge value, the restore overwrites whatever the completion
447
+ merged in, so the agent's real outputs (`status`/`summary`/`question`,
448
+ `adversarialFindings`, the scope verdict) flow through but it can never alter
449
+ the protected set. The protected set is **every engine-owned variable the loop
450
+ relies on that the agent does not own as an output**: the control counters and
451
+ budgets (`round`, `maxRounds`, `advPass`, `advMax`, `ackRetryRound`,
452
+ `ackRetryMax`, `huskRetries`, `reviewWaitTimeout`), the `prKey` correlation
453
+ key, **and** the PR identity inputs (`repo`, `prNumber`, `prUrl`) — the last
454
+ three because the unfiltered merge lets an input-echo replace the identity that
455
+ `progress-check`, `converge-gate`, and `finalize` go on to read (a wrong
456
+ `prNumber` would make `finalize` start the merge loop for the wrong PR) — **and**
457
+ the gate controls `humanApproval` and `convergeOnly`. These last two are
458
+ decision flags the loop acts on after the agent returns: `humanApproval = true`
459
+ routes a converged PR through the `gw-approval` human gate instead of
460
+ auto-merging, and `convergeOnly = true` ends a review-only run without ever
461
+ handing the PR to the merge loop. Left unguarded, a converged completion that
462
+ echoed `humanApproval: false` would **bypass** the approval gate, and one that
463
+ echoed `convergeOnly: false` would hand a review-only PR to the merge loop.
464
+ `convergeOnly` is a plain seeded constant, so the usual `=guardConvergeOnly`
465
+ restore applies. `humanApproval` is the one engine-owned flag that is **also
466
+ legitimately mutated mid-flight** — a gated caller that adopts a live loop
467
+ raises it `false → true` via `engine.setVariables` (`submitPr` in
468
+ `app/service.ts`); it is **monotonic** (only ever rises). A plain
469
+ `=guardHumanApproval` restore would clobber that concurrent host-set true, so
470
+ the restore is the monotonic join
471
+ `=(guardHumanApproval = true) or (humanApproval = true)`: it recovers the
472
+ snapshot's `true`, honours a concurrent adopter's `true`, and ignores an agent
473
+ echo of `false`. The direction is **fail-safe** — the only state it can force
474
+ is *more* human review, never a merge the loop would otherwise have gated. The
475
+ protected set also covers the two **progress bookkeeping** values the loop
476
+ reads back after the agent returns: `reviewStale` (the converge-gate's
477
+ stale-body signal — `f_guardMax` escalates only when `round >= maxRounds and
478
+ reviewStale != true`, so a forged `reviewStale: true` would suppress the
479
+ max-round escalation) and `roundEntryHead` (the within-round baseline
480
+ `pr.capture-head` captures and `pr.progress-check` consumes — a forged head
481
+ would falsify the no-progress decision). Both are plain process-owned reads, so
482
+ the usual `=guardReviewStale` / `=guardRoundEntryHead` restore pins them. The
483
+ protected set further covers the two **seeded cooperative-abandon** reads:
484
+ `abandonUrl` (the capability `pr.persist-round`/`pr.persist-escalation`/
485
+ `pr.finalize` record for a human — a forged echo would redirect the abandon
486
+ token) and `abandonBrief` (which feeds the NEXT agent task's `appendPrompt`
487
+ input `=(if abandonBrief = null then "" else abandonBrief)` — a non-string echo
488
+ would carry a wrong/non-string brief into that later prompt). Both are plain
489
+ process-owned reads, pinned by the usual `=guardAbandonUrl` / `=guardAbandonBrief`
490
+ restore. The reserved **`io.nanobpm.agentTask`** repository envelope is
491
+ *deliberately not* in this list: it is a flat dotted framework key with no
492
+ FEEL-path access, so snapshotting it would need backtick-escaped FEEL
493
+ (`` =`io.nanobpm.agentTask` ``) that has no precedent on this engine and would,
494
+ if evaluated as the default dotted path, resolve to `null` and **null the
495
+ fleet-critical envelope on restore** — a strictly worse failure than the
496
+ hypothetical clobber (an agent would have to emit a result-JSON key literally
497
+ named `io.nanobpm.agentTask`). A counter the task itself advances is incremented
498
+ **from the snapshot** (`adversarial-review` computes `advPass` as
499
+ `(if guardAdvPass = null then 0
500
+ else guardAdvPass) + 1`), never from the mergeable process variable. Any new
501
+ external-agent task added to this loop must apply the same guard to every
502
+ engine-owned variable in scope.
503
+
431
504
 
432
505
  ## 5. Agent job contract (`senior:pr-review`)
433
506
 
@@ -646,11 +719,25 @@ the app, which deploys on boot), and the next agent job of that type picks it up
646
719
  > marker is CI-enforced over the deployed `resources/` process models
647
720
  > (`agentTaskTypesMissingExternalMarker`,
648
721
  > `agent-marker.test.ts`), so no prompt-bearing agent task in those models relies on
649
- > prompt-link-only discovery. (The delivery-graph compiler's GENERATED agent BPMN —
650
- > deployed at run time by `runDeliveryGraph`, not authored under `resources/` — is a
651
- > separate deployed path NOT covered by this static guard; whether its generated cells
652
- > should also carry the marker/opt-out convention is tracked separately under issue #745,
653
- > not #779.) To **exclude** a task from `--auto` — one that must be served only by a
722
+ > prompt-link-only discovery. The delivery-graph compiler's GENERATED agent BPMN —
723
+ > deployed at run time by `runDeliveryGraph`, not authored under `resources/` — is the
724
+ > second producer of agent service tasks, and it is covered too (issue #884): the
725
+ > compiler emits the marker on **every** `agent` cell it compiles. Per the resolved
726
+ > "Decide" half of #884 (Option (a), PR #886 review) a compiled cell carries **no**
727
+ > base-prompt link — the hand-authored process prompts (`prompts/feature.md`, …) are
728
+ > process-specific contracts that require process variables (`variables.task`/
729
+ > `variables.issue`) and a `feat/<task.id>` branch the generic compiled cell never
730
+ > supplies (it seeds only `{jobType, appendPrompt, timeout}` and the harness provisions
731
+ > `feat/<node.id>`, #776), so linking one would hand the cell instructions referencing
732
+ > missing inputs and a conflicting branch. The cell is deliberately prompt-less, steered
733
+ > by its per-node `appendPrompt` alone; the marker alone fixes the #884
734
+ > `createAgentInstance` failure. `npm run check:agent-definitions`
735
+ > (CI gate `check:agent-definitions`, `scripts/check-agent-definitions.ts`) compiles a
736
+ > representative graph — including a prompt-less probe cell — and scans the
737
+ > output with both the prompt-keyed scanner and the prompt-link-independent
738
+ > `externalAgentTaskTypesMissingMarker` (a task is an agent by its `<rank>:<task>`
739
+ > colon-form type alone), so a compiler regression that drops the marker fails the
740
+ > build rather than silently at runtime. To **exclude** a task from `--auto` — one that must be served only by a
654
741
  > worker that explicitly subscribes (`--job-type <type>` / a profile capability) — add
655
742
  > the inert opt-out property inside its `extensionElements`, nested in the
656
743
  > `<zeebe:properties>` wrapper the models and engine expect (as
@@ -803,6 +890,37 @@ start ─► wait: deps merged ─► arm merge ─► wait: mergeable ─┬─
803
890
  `pr.persist-escalation` worker as the review stage (status `escalated`), answered
804
891
  via the native `wait-merge-answer` `userTask` (the same `completeUserTask` door as
805
892
  the review escalation, #256); answering re-arms and retries.
893
+ - **Engine-owned variable guard (issue #887)** — the merge-loop applies the same
894
+ snapshot/restore invariant as the convergence loop (§4) around its external-agent
895
+ tasks (`fix-ci`, `rebase`). Each guards the loop counters and budgets
896
+ (`ciFixRound`, `ciFixMax`, `rebaseRound`, `rebaseMax`, `mergeRetryRound`,
897
+ `mergeRetryMax`, `mergeStallRounds`, `mergeStallMax`), the `prKey` correlation
898
+ key, and the PR identity inputs (`repo`, `prNumber`, `prUrl`) — the last three
899
+ because `pr.merge` reads them directly for GitHub operations after the agent
900
+ re-arms the poller, so an input-echo that replaced them would merge the wrong
901
+ PR. It further guards the **lifecycle marker and timeout config** the loop
902
+ re-arms on every pass: `ciBlockedReconciled` (arm-merge seeds it `null`;
903
+ `ci-reconcile` sets it `true`; `gw-ci-blocked` runs the one-shot ground-truth
904
+ reconcile exactly once per episode while it is `!= true`) and the seeded timer
905
+ durations (`agentSlaTimeout`, `landedWaitTimeout`, `mergeableWaitTimeout`,
906
+ `mergeableRepollInterval`). Left unguarded, a `{status:"blocked", pushed:false,
907
+ ciBlockedReconciled:true}` completion would **skip** the mandatory first
908
+ reconcile and page a human immediately, and a clobbered timeout would silently
909
+ reshape a wait a later loop pass re-arms. The guard also pins the two values
910
+ `pr.merge` (attempt-merge) consumes directly after a successful fix/rebase
911
+ re-arms the poller: `round` (the converged round `ensurePr` records on the
912
+ merge audit row) and `abandonUrl` (the cooperative-abandon capability URL whose
913
+ token the merge worker extracts). A completion echoing either would corrupt the
914
+ recorded round or replace the abandon token, so `=guardRound` /
915
+ `=guardAbandonUrl` restores pin both. It likewise guards `abandonBrief`, which
916
+ feeds the fix-ci/rebase `appendPrompt` input (`=(if abandonBrief = null then ""
917
+ else abandonBrief)`) on the next re-arm — a non-string echo would carry a
918
+ non-string brief into that later prompt. (As in the convergence loop (§4), the
919
+ reserved dotted `io.nanobpm.agentTask` repository envelope is *not* guarded: it
920
+ has no FEEL-path access and a backtick-escaped restore, untested on this engine,
921
+ would risk nulling the envelope used for later agent provisioning.) The counter
922
+ the task advances is incremented from the snapshot (`ciFixRound`/`rebaseRound`
923
+ become `guard<Name> + 1`).
806
924
  - **Terminal** — `merged` (with `merged_at`), or `converged` when
807
925
  `NANO_PR_AUTO_MERGE=0` (review-only), or `abandoned` on cancel.
808
926