@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.
- package/.github/workflows/ci.yml +11 -0
- package/AGENTS.md +3 -1
- package/CHANGELOG.md +12 -0
- package/SPEC.md +123 -5
- package/app/agentOutputEnvelope.test.ts +464 -0
- package/app/agentic/vocab/agent-marker.test.ts +30 -1
- package/app/agentic/vocab/job-types.test.ts +133 -1
- package/app/agentic/vocab/job-types.ts +57 -0
- package/app/contracts.ts +2 -2
- package/app/deliveryGraphCompiler.test.ts +81 -0
- package/app/deliveryGraphCompiler.ts +22 -1
- package/app/mergeLoopBehaviour.test.ts +208 -1
- package/package.json +2 -1
- package/resources/processes/convergence-loop.bpmn +108 -1
- package/resources/processes/merge-loop.bpmn +80 -2
- package/scripts/check-agent-definitions.test.ts +126 -0
- package/scripts/check-agent-definitions.ts +191 -0
package/.github/workflows/ci.yml
CHANGED
|
@@ -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
|
|
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.
|
|
650
|
-
> deployed at run time by `runDeliveryGraph`, not authored under `resources/` — is
|
|
651
|
-
>
|
|
652
|
-
>
|
|
653
|
-
>
|
|
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
|
|