@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 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/` (shallow, one level deep) and deploys
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
- - **Basenames must be unique across the deploy set** the deploy dedupe key is the
135
- filename only, so two files sharing a basename in different dirs collide. `npm run
136
- check:prompts` fails loudly on that.
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="<token>.md" bindingType="latest" resourceType="GenericScript" linkName="prompt"/>`,
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/` (shallow, one level) and
323
- deploys each file as an `application/octet-stream` resource whose deployed **name is
324
- the file's basename** (`resources/prompts/review-round.md` resource `review-round.md`).
325
- Each agent service task links it:
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
 
@@ -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
 
@@ -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
@@ -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.0",
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="=&#34;&#10;&#10;---&#10;&#10;&#34; + task.prompt + (if (baseBranchBrief = null) then &#34;&#34; else baseBranchBrief) + (if (resolvedArtifacts = null or count(resolvedArtifacts[item != null]) = 0) then &#34;&#34; else (&#34;&#10;&#10;---&#10;&#10;**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:&#10;&#10;&#34; + string join(resolvedArtifacts[item != null], &#34;&#10;&#34;))) + (if (customInstructions = null) then &#34;&#34; else &#34;&#10;&#10;---&#10;&#10;## Operator custom instructions&#10;&#10;The operator supplied these instructions for this run — follow them:&#10;&#10;&#34; + 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="=&#34;&#10;&#10;---&#10;&#10;&#34; + task.prompt + (if (is defined(baseBranchBrief) and baseBranchBrief != null) then baseBranchBrief else &#34;&#34;) + (if (is defined(blackboardBrief) and blackboardBrief != null) then blackboardBrief else &#34;&#34;) + (if (is defined(resolvedDepsBrief) and resolvedDepsBrief != null) then resolvedDepsBrief else &#34;&#34;) + (if (is defined(resolvedArtifacts) = false or resolvedArtifacts = null or count(resolvedArtifacts[item != null]) = 0) then &#34;&#34; else (&#34;&#10;&#10;---&#10;&#10;**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:&#10;&#10;&#34; + string join(resolvedArtifacts[item != null], &#34;&#10;&#34;))) + (if (is defined(customInstructions) = false or customInstructions = null) then &#34;&#34; else &#34;&#10;&#10;---&#10;&#10;## Operator custom instructions&#10;&#10;The operator supplied these instructions for this run — follow them:&#10;&#10;&#34; + 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" />