@nanobpm/nano-workforce 0.162.0 → 0.162.2
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/AGENTS.md +12 -6
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/SPEC.md +8 -6
- package/app/convergeGate.test.ts +1 -1
- package/app/service.ts +1 -1
- package/docs/agent-guide.md +1 -1
- package/e2e/plan-fanout.e2e.ts +37 -0
- package/package.json +1 -1
- package/resources/processes/convergence-loop.bpmn +2 -2
- package/resources/processes/feature.bpmn +1 -1
- package/resources/processes/implement-cell.bpmn +1 -1
- package/resources/processes/merge-cell.bpmn +1 -1
- package/resources/processes/merge-loop.bpmn +2 -2
- package/resources/processes/plan-fanout.bpmn +250 -211
- package/resources/processes/retro.bpmn +2 -2
- package/scripts/check-agent-prompts.test.ts +21 -2
- package/scripts/check-agent-prompts.ts +46 -27
- package/test/derivation-parity/flows.ts +2 -2
- package/workers/record-plan/worker.test.ts +8 -2
- package/workers/record-plan/worker.ts +8 -2
- package/workers/record-plan-review/worker.test.ts +18 -0
- package/workers/record-plan-review/worker.ts +27 -3
package/AGENTS.md
CHANGED
|
@@ -120,7 +120,8 @@ GENERATED, never hand-edited.**
|
|
|
120
120
|
## Deploy by convention: `resources/` (ADR 0062)
|
|
121
121
|
|
|
122
122
|
**Deployables live under `resources/` and deploy by convention — `nano.app.json`
|
|
123
|
-
declares no `models`.** urban walks `resources/` (
|
|
123
|
+
declares no `models`.** urban walks `resources/` **recursively (every file at any depth)** and
|
|
124
|
+
deploys
|
|
124
125
|
every file: `resources/processes/*.bpmn`, `resources/forms/*.form`, and one prompt per
|
|
125
126
|
task under `resources/prompts/*.md`.
|
|
126
127
|
|
|
@@ -131,15 +132,20 @@ task under `resources/prompts/*.md`.
|
|
|
131
132
|
- **No `models` block.** Rely on the convention; add a `models` override *only* for a
|
|
132
133
|
genuinely non-standard layout (nwf doesn't need one). An explicit `models` is used
|
|
133
134
|
verbatim and skips the convention walk.
|
|
134
|
-
- **
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
- **Prompt ids are `resources/`-relative; basenames must be unique only within a
|
|
136
|
+
`models` override.** A convention resource's deploy id (`resourceId`) is its path
|
|
137
|
+
**relative to `resources/`** — `resources/prompts/plan.md` → `prompts/plan.md` — so files
|
|
138
|
+
sharing a basename in different sub-dirs deploy as distinct resources. Only a `models`
|
|
139
|
+
override keys by basename (and so must avoid basename collisions). `npm run check:prompts`
|
|
140
|
+
validates each prompt link against the actual deployed id and fails loudly on a mismatch.
|
|
137
141
|
- **Agent prompts: linkedResource is the blessed *and only* path.** Each agent service
|
|
138
142
|
task links its base prompt with
|
|
139
|
-
`<zeebe:linkedResource resourceId="
|
|
143
|
+
`<zeebe:linkedResource resourceId="prompts/<token>.md" bindingType="latest" resourceType="GenericScript" linkName="prompt"/>`,
|
|
140
144
|
which the engine resolves to the latest deployed `resources/prompts/<token>.md` at job
|
|
141
145
|
activation, combined at runtime with the per-task `appendPrompt` FEEL. `bindingType="latest"`
|
|
142
|
-
lets a prompt update land mid-epic without a process redeploy.
|
|
146
|
+
lets a prompt update land mid-epic without a process redeploy. **The id is the
|
|
147
|
+
`resources/`-relative path (`prompts/<token>.md`), NOT the bare basename** — a bare id
|
|
148
|
+
resolves to nothing and the engine silently omits the link, so the agent runs prompt-less.
|
|
143
149
|
- **Deploy-time `{{token}}` templating is removed — no back-compat.** To inject a per-run
|
|
144
150
|
value (URL, flag) into an agent, pass it as a runtime job variable / `appendPrompt`
|
|
145
151
|
FEEL; never bake it into a model at deploy time.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [0.162.2](https://github.com/nanobpm/nano-workforce/compare/v0.162.1...v0.162.2) (2026-08-30)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* **plan-fanout:** terminate empty plans before the review gate ([#623](https://github.com/nanobpm/nano-workforce/issues/623)) ([#625](https://github.com/nanobpm/nano-workforce/issues/625)) ([02dca9f](https://github.com/nanobpm/nano-workforce/commit/02dca9f02f817bc0c178255a50ef95308318502b)), closes [Magikcraft/nano-bpm#1067](https://github.com/Magikcraft/nano-bpm/issues/1067)
|
|
6
|
+
|
|
7
|
+
## [0.162.1](https://github.com/nanobpm/nano-workforce/compare/v0.162.0...v0.162.1) (2026-08-30)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **prompts:** reference prompts by resources-relative id so agents get their base prompt ([#628](https://github.com/nanobpm/nano-workforce/issues/628)) ([084e8f8](https://github.com/nanobpm/nano-workforce/commit/084e8f8f4ad842cc9f9e92c25b4ef80d0b0b66e1)), closes [#241](https://github.com/nanobpm/nano-workforce/issues/241)
|
|
12
|
+
|
|
1
13
|
## [0.162.0](https://github.com/nanobpm/nano-workforce/compare/v0.161.0...v0.162.0) (2026-08-29)
|
|
2
14
|
|
|
3
15
|
### Features
|
package/README.md
CHANGED
|
@@ -499,7 +499,7 @@ agent's base prompt is **not** in the job payload — it is delivered as a
|
|
|
499
499
|
**linked resource** (likewise `plan.md`, `feature.md`, `fix-ci.md`, …):
|
|
500
500
|
|
|
501
501
|
```xml
|
|
502
|
-
<zeebe:linkedResource resourceId="review-round.md" bindingType="latest" resourceType="GenericScript" linkName="prompt"/>
|
|
502
|
+
<zeebe:linkedResource resourceId="prompts/review-round.md" bindingType="latest" resourceType="GenericScript" linkName="prompt"/>
|
|
503
503
|
```
|
|
504
504
|
|
|
505
505
|
that the engine resolves to the latest deployed `resources/prompts/*.md` at job
|
package/SPEC.md
CHANGED
|
@@ -164,7 +164,7 @@ Notes:
|
|
|
164
164
|
|
|
165
165
|
The base instructions are **not** a job variable: they are delivered as a
|
|
166
166
|
**linked resource** on the `senior:pr-review` task —
|
|
167
|
-
`<zeebe:linkedResource resourceId="review-round.md" bindingType="latest" resourceType="GenericScript" linkName="prompt"/>`,
|
|
167
|
+
`<zeebe:linkedResource resourceId="prompts/review-round.md" bindingType="latest" resourceType="GenericScript" linkName="prompt"/>`,
|
|
168
168
|
which the engine resolves to the latest deployed `resources/prompts/review-round.md` at job
|
|
169
169
|
activation.
|
|
170
170
|
|
|
@@ -319,14 +319,16 @@ when the engine data is purged, to keep app state and engine state consistent).
|
|
|
319
319
|
Each agent task's base prompt lives **only** in its `resources/prompts/*.md` side-car,
|
|
320
320
|
deployed as a **generic resource** and **linked** — not baked — into the model (issue
|
|
321
321
|
#169). Under the ADR 0062 `resources/` deploy-by-convention layout, `nano.app.json`
|
|
322
|
-
declares **no `models`**, so `@nanobpm/urban` walks `resources/` (
|
|
323
|
-
deploys each file as an
|
|
324
|
-
|
|
325
|
-
Each agent service
|
|
322
|
+
declares **no `models`**, so `@nanobpm/urban` walks `resources/` **recursively (every file at any
|
|
323
|
+
depth)** and deploys each file as an
|
|
324
|
+
`application/octet-stream` resource whose deployed **id is its path relative to `resources/`**
|
|
325
|
+
(`resources/prompts/review-round.md` → resource `prompts/review-round.md`). Each agent service
|
|
326
|
+
task links it by that `resources/`-relative id (a bare basename resolves to nothing and the
|
|
327
|
+
engine silently omits the link, leaving the agent prompt-less):
|
|
326
328
|
|
|
327
329
|
```xml
|
|
328
330
|
<zeebe:linkedResources>
|
|
329
|
-
<zeebe:linkedResource resourceId="review-round.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
331
|
+
<zeebe:linkedResource resourceId="prompts/review-round.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
330
332
|
</zeebe:linkedResources>
|
|
331
333
|
```
|
|
332
334
|
|
package/app/convergeGate.test.ts
CHANGED
|
@@ -394,7 +394,7 @@ test("classify-scope is an agent task servicing senior:scope-classify with a lin
|
|
|
394
394
|
const task = flat.match(/<bpmn:serviceTask\b[^>]*\bid="classify-scope"[^>]*>.*?<\/bpmn:serviceTask>/);
|
|
395
395
|
assert(task, "classify-scope service task missing");
|
|
396
396
|
assertStringIncludes(task[0], 'type="senior:scope-classify"');
|
|
397
|
-
assertStringIncludes(task[0], 'resourceId="scope-classify.md"');
|
|
397
|
+
assertStringIncludes(task[0], 'resourceId="prompts/scope-classify.md"');
|
|
398
398
|
assertStringIncludes(task[0], 'linkName="prompt"');
|
|
399
399
|
// After honouring (or ignoring) the human's scope answer, it clears the one-shot
|
|
400
400
|
// scopeAnswer so a later round does not re-honour a stale decision.
|
package/app/service.ts
CHANGED
|
@@ -193,7 +193,7 @@ export const MERGE_ADMIN = ["1", "true", "on", "yes"].includes(
|
|
|
193
193
|
// The `senior:pr-review` agent prompt is no longer read by the host: it is a generic resource
|
|
194
194
|
// (`resources/prompts/review-round.md`, deployed under the `resources/` deploy-by-convention layout
|
|
195
195
|
// — nano.app.json declares no `models`) linked into the task
|
|
196
|
-
// as `<zeebe:linkedResource resourceId="review-round.md" bindingType="latest" linkName="prompt"/>`
|
|
196
|
+
// as `<zeebe:linkedResource resourceId="prompts/review-round.md" bindingType="latest" linkName="prompt"/>`
|
|
197
197
|
// and resolved by the engine at job activation. The host only carries runtime PR identity + the
|
|
198
198
|
// round counter now.
|
|
199
199
|
|
package/docs/agent-guide.md
CHANGED
|
@@ -326,7 +326,7 @@ both live in the source repo, not in the job payload.
|
|
|
326
326
|
prompt-modularity path:
|
|
327
327
|
|
|
328
328
|
```xml
|
|
329
|
-
<zeebe:linkedResource resourceId="review-round.md" bindingType="latest" resourceType="GenericScript" linkName="prompt"/>
|
|
329
|
+
<zeebe:linkedResource resourceId="prompts/review-round.md" bindingType="latest" resourceType="GenericScript" linkName="prompt"/>
|
|
330
330
|
```
|
|
331
331
|
|
|
332
332
|
that the engine
|
package/e2e/plan-fanout.e2e.ts
CHANGED
|
@@ -241,6 +241,43 @@ describe("plan-fanout escalations (U2 — task + plan-review + trial-merge → u
|
|
|
241
241
|
);
|
|
242
242
|
});
|
|
243
243
|
|
|
244
|
+
// Empty-plan short-circuit (issue #623 — regression for Merlin instance-46 / epic #1067). A
|
|
245
|
+
// planner that legitimately emits `{tasks:[]}` (meta/tracking epic, or all sub-issues closed) must
|
|
246
|
+
// reach a terminal state WITHOUT entering the adversarial plan-review loop — feeding an empty plan
|
|
247
|
+
// into review caused a plan↔plan-review livelock (it can neither be approved nor produce findings).
|
|
248
|
+
test("empty plan short-circuits to the taskless-done end, never entering plan-review (issue #623)", async () => {
|
|
249
|
+
let reviewCalls = 0;
|
|
250
|
+
await withApp(
|
|
251
|
+
{
|
|
252
|
+
"senior:plan": () => ({ tasks: [], note: "all sub-issues closed" }),
|
|
253
|
+
// If this ever fires, the short-circuit failed and the empty plan entered the review loop.
|
|
254
|
+
"senior:plan-review": () => {
|
|
255
|
+
reviewCalls += 1;
|
|
256
|
+
return { approved: false, findings: "" };
|
|
257
|
+
},
|
|
258
|
+
"senior:feature": () => ({ status: "blocked", summary: "n/a" }),
|
|
259
|
+
},
|
|
260
|
+
async ({ app, planKey }) => {
|
|
261
|
+
const flows = takenFlows(app);
|
|
262
|
+
assert.ok(
|
|
263
|
+
flows.includes("gw-plan-empty->EndTasklessDone"),
|
|
264
|
+
`empty plan routed to the taskless-done end (flows: ${flows.join(", ")})`,
|
|
265
|
+
);
|
|
266
|
+
assert.ok(
|
|
267
|
+
!flows.includes("gw-plan-empty->review-plan"),
|
|
268
|
+
"the has-tasks flow into plan-review was NOT taken",
|
|
269
|
+
);
|
|
270
|
+
assert.equal(reviewCalls, 0, "the plan-review agent must never run for an empty plan");
|
|
271
|
+
|
|
272
|
+
const plan = await app.db
|
|
273
|
+
.table<{ plan_key: string; status: string; outcome: string | null }>("plans", "plan_key")
|
|
274
|
+
.findOne({ plan_key: planKey });
|
|
275
|
+
assert.equal(plan?.status, "done", "the empty plan reached a terminal done state");
|
|
276
|
+
assert.equal(plan?.outcome, "all sub-issues closed", "the planner note was recorded as the outcome");
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
244
281
|
// A two-task wave whose PRs both open triggers the D3 trial-merge gate; a suite-failed trial
|
|
245
282
|
// parks on the trial-merge decision user task.
|
|
246
283
|
const twoTaskPlan: Stub = () => ({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.162.
|
|
3
|
+
"version": "0.162.2",
|
|
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",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
<bpmn:extensionElements>
|
|
102
102
|
<zeebe:taskDefinition type="senior:pr-review" />
|
|
103
103
|
<zeebe:linkedResources>
|
|
104
|
-
<zeebe:linkedResource resourceId="review-round.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
104
|
+
<zeebe:linkedResource resourceId="prompts/review-round.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
105
105
|
</zeebe:linkedResources>
|
|
106
106
|
<zeebe:properties>
|
|
107
107
|
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="PrReviewRoundIn" />
|
|
@@ -321,7 +321,7 @@
|
|
|
321
321
|
<bpmn:extensionElements>
|
|
322
322
|
<zeebe:taskDefinition type="senior:scope-classify" />
|
|
323
323
|
<zeebe:linkedResources>
|
|
324
|
-
<zeebe:linkedResource resourceId="scope-classify.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
324
|
+
<zeebe:linkedResource resourceId="prompts/scope-classify.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
325
325
|
</zeebe:linkedResources>
|
|
326
326
|
<zeebe:properties>
|
|
327
327
|
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="PrScopeClassifyIn" />
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
<bpmn:extensionElements>
|
|
174
174
|
<zeebe:taskDefinition type="senior:feature" />
|
|
175
175
|
<zeebe:linkedResources>
|
|
176
|
-
<zeebe:linkedResource resourceId="feature.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
176
|
+
<zeebe:linkedResource resourceId="prompts/feature.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
177
177
|
</zeebe:linkedResources>
|
|
178
178
|
<zeebe:ioMapping>
|
|
179
179
|
<zeebe:input source="=" --- " + task.prompt + (if (baseBranchBrief = null) then "" else baseBranchBrief) + (if (resolvedArtifacts = null or count(resolvedArtifacts[item != null]) = 0) then "" else (" --- **Bound upstream readiness (intake gate).** This feature was scheduled to wait until an upstream landed and published. Build/install/clone against EXACTLY these resolved published versions (the ones first carrying the awaited capability), and bump the consumer dependency to them — never merely the newest: " + string join(resolvedArtifacts[item != null], " "))) + (if (customInstructions = null) then "" else " --- ## Operator custom instructions The operator supplied these instructions for this run — follow them: " + customInstructions)" target="appendPrompt" />
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<bpmn:extensionElements>
|
|
9
9
|
<zeebe:taskDefinition type="senior:feature" />
|
|
10
10
|
<zeebe:linkedResources>
|
|
11
|
-
<zeebe:linkedResource resourceId="feature.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
11
|
+
<zeebe:linkedResource resourceId="prompts/feature.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
12
12
|
</zeebe:linkedResources>
|
|
13
13
|
<zeebe:ioMapping>
|
|
14
14
|
<zeebe:input source="=" --- " + task.prompt + (if (is defined(baseBranchBrief) and baseBranchBrief != null) then baseBranchBrief else "") + (if (is defined(blackboardBrief) and blackboardBrief != null) then blackboardBrief else "") + (if (is defined(resolvedDepsBrief) and resolvedDepsBrief != null) then resolvedDepsBrief else "") + (if (is defined(resolvedArtifacts) = false or resolvedArtifacts = null or count(resolvedArtifacts[item != null]) = 0) then "" else (" --- **Bound upstream readiness.** Build/install/clone against EXACTLY these resolved published versions (the ones first carrying the awaited capability), and bump the consumer dependency to them — never merely the newest: " + string join(resolvedArtifacts[item != null], " "))) + (if (is defined(customInstructions) = false or customInstructions = null) then "" else " --- ## Operator custom instructions The operator supplied these instructions for this run — follow them: " + customInstructions)" target="appendPrompt" />
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<bpmn:extensionElements>
|
|
9
9
|
<zeebe:taskDefinition type="senior:trial-merge" />
|
|
10
10
|
<zeebe:linkedResources>
|
|
11
|
-
<zeebe:linkedResource resourceId="trial-merge.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
11
|
+
<zeebe:linkedResource resourceId="prompts/trial-merge.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
12
12
|
</zeebe:linkedResources>
|
|
13
13
|
<zeebe:ioMapping>
|
|
14
14
|
<zeebe:input source="=null" target="result" />
|
|
@@ -293,7 +293,7 @@
|
|
|
293
293
|
<bpmn:extensionElements>
|
|
294
294
|
<zeebe:taskDefinition type="senior:fix-ci" />
|
|
295
295
|
<zeebe:linkedResources>
|
|
296
|
-
<zeebe:linkedResource resourceId="fix-ci.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
296
|
+
<zeebe:linkedResource resourceId="prompts/fix-ci.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
297
297
|
</zeebe:linkedResources>
|
|
298
298
|
<zeebe:properties>
|
|
299
299
|
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="FixCiIn" />
|
|
@@ -450,7 +450,7 @@
|
|
|
450
450
|
<bpmn:extensionElements>
|
|
451
451
|
<zeebe:taskDefinition type="senior:rebase" />
|
|
452
452
|
<zeebe:linkedResources>
|
|
453
|
-
<zeebe:linkedResource resourceId="rebase.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
453
|
+
<zeebe:linkedResource resourceId="prompts/rebase.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
454
454
|
</zeebe:linkedResources>
|
|
455
455
|
<zeebe:properties>
|
|
456
456
|
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="RebaseIn" />
|