@nanobpm/nano-workforce 0.72.0 → 0.73.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/AGENTS.md CHANGED
@@ -117,6 +117,33 @@ GENERATED, never hand-edited.**
117
117
  that touch a shared process file into one; do **not** paper the collision over
118
118
  with a `dependsOn` edge added purely to serialise otherwise-parallel work.
119
119
 
120
+ ## Deploy by convention: `resources/` (ADR 0062)
121
+
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
124
+ every file: `resources/processes/*.bpmn`, `resources/forms/*.form`, and one prompt per
125
+ task under `resources/prompts/*.md`.
126
+
127
+ - **`resources/` is deploy-only.** Anything under it ships to the engine; anything
128
+ **outside** it never does. Docs therefore live under `docs/` (e.g.
129
+ `docs/agent-guide.md`) — put a `.md` in `resources/` only if you actually want it
130
+ deployed.
131
+ - **No `models` block.** Rely on the convention; add a `models` override *only* for a
132
+ genuinely non-standard layout (nwf doesn't need one). An explicit `models` is used
133
+ 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.
137
+ - **Agent prompts: linkedResource is the blessed *and only* path.** Each agent service
138
+ task links its base prompt with
139
+ `<zeebe:linkedResource resourceId="<token>.md" bindingType="latest" resourceType="GenericScript" linkName="prompt"/>`,
140
+ which the engine resolves to the latest deployed `resources/prompts/<token>.md` at job
141
+ activation, combined at runtime with the per-task `appendPrompt` FEEL. `bindingType="latest"`
142
+ lets a prompt update land mid-epic without a process redeploy.
143
+ - **Deploy-time `{{token}}` templating is removed — no back-compat.** To inject a per-run
144
+ value (URL, flag) into an agent, pass it as a runtime job variable / `appendPrompt`
145
+ FEEL; never bake it into a model at deploy time.
146
+
120
147
  ## Engine capabilities (Zeebe parity — use them, don't work around them)
121
148
 
122
149
  The nanobpmn engine (`~/workspace/nanobpmn` `engine-core`, deployed via the nano
@@ -247,7 +274,7 @@ npm run lint # biome check (incl. ban-`
247
274
  npm run typecheck # tsc --noEmit (Node)
248
275
  npm run check # urban check (manifest validation)
249
276
  npm run layout:check # BPMN diagram freshness (no drift)
250
- npm run check:prompts # agent-prompt template resolution
277
+ npm run check:prompts # agent-prompt linkedResource resolution
251
278
  npm run check:migrations # migration prefixes (no collisions)
252
279
  npm run check:contracts # contract registry (no synonyms / undeclared env keys)
253
280
  npm test # unit tests (node --test)
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [0.73.0](https://github.com/nanobpm/nano-workforce/compare/v0.72.1...v0.73.0) (2026-08-15)
2
+
3
+
4
+ ### Features
5
+
6
+ * **deploy:** resources/ deploy-by-convention; drop models; docs→docs/ (ADR 0062 step 3) ([#241](https://github.com/nanobpm/nano-workforce/issues/241)) ([9f7f85c](https://github.com/nanobpm/nano-workforce/commit/9f7f85c1231ce228c4468ac8f5222dd14d84022b)), closes [nanobpm/nano-ide#244](https://github.com/nanobpm/nano-ide/issues/244) [#244](https://github.com/nanobpm/nano-workforce/issues/244) [#239](https://github.com/nanobpm/nano-workforce/issues/239) [nanobpm/nano-ide#244](https://github.com/nanobpm/nano-ide/issues/244) [nanobpm/nano-workforce#239](https://github.com/nanobpm/nano-workforce/issues/239)
7
+
8
+ ## [0.72.1](https://github.com/nanobpm/nano-workforce/compare/v0.72.0...v0.72.1) (2026-08-15)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * remove question from table ([#240](https://github.com/nanobpm/nano-workforce/issues/240)) ([d672206](https://github.com/nanobpm/nano-workforce/commit/d6722063c3110672f8e2f0ee8c955c7ab0b39577))
14
+
1
15
  # [0.72.0](https://github.com/nanobpm/nano-workforce/compare/v0.71.0...v0.72.0) (2026-08-15)
2
16
 
3
17
 
package/README.md CHANGED
@@ -156,8 +156,8 @@ capability):
156
156
 
157
157
  - `--command 'copilot -p - --allow-all-tools'` starts the Copilot CLI reading its
158
158
  prompt from **stdin** (`-p -`). The harness pipes the whole job JSON (prompt +
159
- `job.variables`) to stdin; the relevant `prompts/*.md` resource (linked into the task
160
- and fetched by the harness at activation) tells the agent how
159
+ `job.variables`) to stdin; the relevant `resources/prompts/*.md` resource (linked into
160
+ the task and fetched by the harness at activation) tells the agent how
161
161
  to read it and where to write its result.
162
162
  - **`--allow-all-tools` is essential** for an unattended worker — without it Copilot
163
163
  pauses for permission before each tool call and the job stalls.
@@ -351,14 +351,15 @@ agent's base prompt is **not** in the job payload — it is delivered as a
351
351
  **linked resource** (likewise `plan.md`, `feature.md`, `fix-ci.md`, …):
352
352
 
353
353
  ```xml
354
- <zeebe:linkedResource resourceId="review-round.md" bindingType="latest" linkName="prompt"/>
354
+ <zeebe:linkedResource resourceId="review-round.md" bindingType="latest" resourceType="GenericScript" linkName="prompt"/>
355
355
  ```
356
356
 
357
- that the engine resolves to the latest deployed `prompts/*.md` at job
358
- activation (the prompts deploy as generic resources via a `models` glob in
359
- `nano.app.json`); per-instance context (e.g. a human's escalation answer) is appended
360
- by the harness. Because the binding is `latest`, redeploying one `prompts/*.md` updates
361
- the prompt for the next activation in a **running** epic — see SPEC §9.
357
+ that the engine resolves to the latest deployed `resources/prompts/*.md` at job
358
+ activation (the prompts deploy as generic resources under the `resources/` deploy-by-
359
+ convention layout — `nano.app.json` declares no `models`; ADR 0062); per-instance context
360
+ (e.g. a human's escalation answer) is appended
361
+ by the harness. Because the binding is `latest`, redeploying one `resources/prompts/*.md`
362
+ updates the prompt for the next activation in a **running** epic — see SPEC §9.
362
363
 
363
364
  ---
364
365
 
package/SPEC.md CHANGED
@@ -65,11 +65,13 @@ nano-workforce/
65
65
  resources/
66
66
  processes/
67
67
  convergence-loop.bpmn # the durable convergence process
68
+ prompts/
69
+ review-round.md # agent instructions asset (deployed by the resources/ convention)
68
70
  db/
69
71
  migrations/
70
72
  001_init.sql # sqlite schema
71
- prompts/
72
- review-round.md # agent instructions asset (injected into job data)
73
+ docs/
74
+ agent-guide.md # operator guide (docs live OUTSIDE resources/ — never deployed)
73
75
  components/
74
76
  review-round.json # Zeebe element template for the senior:pr-review service task
75
77
  SPEC.md # this document
@@ -155,8 +157,8 @@ Notes:
155
157
 
156
158
  The base instructions are **not** a job variable: they are delivered as a
157
159
  **linked resource** on the `senior:pr-review` task —
158
- `<zeebe:linkedResource resourceId="review-round.md" bindingType="latest" linkName="prompt"/>`,
159
- which the engine resolves to the latest deployed `prompts/review-round.md` at job
160
+ `<zeebe:linkedResource resourceId="review-round.md" bindingType="latest" resourceType="GenericScript" linkName="prompt"/>`,
161
+ which the engine resolves to the latest deployed `resources/prompts/review-round.md` at job
160
162
  activation.
161
163
 
162
164
  **Output** (job result variables):
@@ -178,7 +180,7 @@ gives **each job its own `mkdtemp` run-dir + fresh clone**, runs the agent with
178
180
  **reaps that run-dir when the job ends**. So multiple agents on one host do **not**
179
181
  collide even in host mode — the isolation lives below the agent.
180
182
 
181
- Consequences the prompt (`prompts/review-round.md`) encodes:
183
+ Consequences the prompt (`resources/prompts/review-round.md`) encodes:
182
184
  - The agent works only inside its provided `cwd`; it must **not** re-clone or create
183
185
  a separate `git worktree`, and must not touch global/host state.
184
186
  - The agent **cleans up anything it creates outside the commit** before returning
@@ -300,25 +302,26 @@ when the engine data is purged, to keep app state and engine state consistent).
300
302
 
301
303
  ## 9. Prompt delivery — linked resources (`bindingType: latest`)
302
304
 
303
- Each agent task's base prompt lives **only** in its `prompts/*.md` side-car, deployed
304
- as a **generic resource** and **linked** — not baked — into the model (issue #169).
305
- `nano.app.json` lists `prompts/*.md` in a `models` deploy glob, so `@nanobpm/urban`
305
+ Each agent task's base prompt lives **only** in its `resources/prompts/*.md` side-car,
306
+ deployed as a **generic resource** and **linked** — not baked — into the model (issue
307
+ #169). Under the ADR 0062 `resources/` deploy-by-convention layout, `nano.app.json`
308
+ declares **no `models`**, so `@nanobpm/urban` walks `resources/` (shallow, one level) and
306
309
  deploys each file as an `application/octet-stream` resource whose deployed **name is
307
- the file's basename** (`prompts/review-round.md` → resource `review-round.md`). Each
308
- agent service task links it:
310
+ the file's basename** (`resources/prompts/review-round.md` → resource `review-round.md`).
311
+ Each agent service task links it:
309
312
 
310
313
  ```xml
311
314
  <zeebe:linkedResources>
312
- <zeebe:linkedResource resourceId="review-round.md" bindingType="latest" linkName="prompt" />
315
+ <zeebe:linkedResource resourceId="review-round.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
313
316
  </zeebe:linkedResources>
314
317
  ```
315
318
 
316
319
  At **job activation** the engine resolves the *latest deployed* key for that
317
320
  `resourceId` and hands the content to the harness in the `linkedResources` activation
318
321
  header; the harness fetches by key and uses it as the base prompt. Because the binding
319
- is `latest`, **redeploying a single `prompts/*.md` changes the prompt for the next task
320
- activation in a running epic** — no process redeploy, no in-flight epic restart. This
321
- is the live-prompt debugging loop: edit one Markdown file, `urban deploy` (or restart
322
+ is `latest`, **redeploying a single `resources/prompts/*.md` changes the prompt for the
323
+ next task activation in a running epic** — no process redeploy, no in-flight epic restart.
324
+ This is the live-prompt debugging loop: edit one Markdown file, `urban deploy` (or restart
322
325
  the app, which deploys on boot), and the next agent job of that type picks it up.
323
326
 
324
327
  > **Latest-for-now, audited.** The engine currently keeps only the latest version per
@@ -332,7 +335,8 @@ the app, which deploys on boot), and the next agent job of that type picks it up
332
335
  > `resourceId` is dropped from the activation header (no incident) — the agent would
333
336
  > then run prompt-less. `scripts/check-agent-prompts.ts` (CI gate `check:prompts`)
334
337
  > guards against this: every `linkName="prompt"` link's `resourceId` must match a
335
- > prompt file the app actually deploys (a file in a `models` deploy glob), each linked
338
+ > prompt file the app actually deploys (a file under the `resources/` convention walk, or
339
+ > a manifest `models` override glob), each linked
336
340
  > prompt must be non-blank and teach the agent to emit a machine-readable result
337
341
  > (`$AGENT_RESULT_FILE` / `::nano:result::`), and no task may still carry the retired
338
342
  > baked `io.nanobpm.agentTask.task.prompt` header.
@@ -344,7 +348,8 @@ the linked base — the model owns any separator, and a null/empty append leaves
344
348
  untouched. Base prompts can't be composed in FEEL (they are quote-heavy, and XML
345
349
  attribute escaping would corrupt a FEEL string literal), so composition happens via
346
350
  this append seam rather than inline in FEEL. Requires an `@nanobpm/urban` deploy that
347
- emits generic-resource deployments for `prompts/*.md` and a harness that consumes
351
+ deploys the `resources/` convention (generic-resource deployments for
352
+ `resources/prompts/*.md`) and a harness that consumes
348
353
  `linkedResources` and fetches the resource by key.
349
354
 
350
355
  ## 10. Poller
@@ -554,7 +559,7 @@ the loop runs one parallel `implement` MI fan-out per wave:
554
559
  its prerequisites have **landed on the base branch** — not merely opened. This lets
555
560
  a blocking prerequisite (e.g. app scaffolding) fully converge and merge before the
556
561
  next wave builds on it. `gate_wave` lives in `db/migrations/007_wave_gate.sql`.
557
- - **Adopting a decomposed epic** (`prompts/plan.md` Step 0): when adopting existing
562
+ - **Adopting a decomposed epic** (`resources/prompts/plan.md` Step 0): when adopting existing
558
563
  sub-issues, the planner honours an explicit `Depends-on: #N` / `Blocked by #N`
559
564
  directive in a sub-issue body, mapping each prerequisite `#M` to `issue-M` in the
560
565
  adopted task's `dependsOn` — so a human-declared blocking order survives adoption.
package/app/agentGuide.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  // The agent operator guide served by GET /app/api/agent (operationId `getAgentInstructions`).
2
2
  //
3
- // The guide itself is authored as plain markdown in `resources/agent-guide.md` (kept out of
4
- // `prompts/` so it is NOT treated as a deployable agent template) and read from the checkout at
3
+ // The guide itself is authored as plain markdown in `docs/agent-guide.md` (kept OUT of
4
+ // `resources/` so the deploy-by-convention walk does NOT treat it as a deployable model docs
5
+ // live under `docs/`, ADR 0062) and read from the checkout at
5
6
  // module load — same "run the .ts sources directly, inspect the working tree at runtime" approach
6
7
  // as version.ts. Two placeholders are substituted per request/deployment so the embedded examples
7
8
  // are copy-pasteable against THIS instance:
@@ -15,7 +16,7 @@ import { dirname, join } from "node:path";
15
16
  import { fileURLToPath } from "node:url";
16
17
 
17
18
  const REPO_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
18
- const GUIDE_PATH = join(REPO_ROOT, "resources", "agent-guide.md");
19
+ const GUIDE_PATH = join(REPO_ROOT, "docs", "agent-guide.md");
19
20
 
20
21
  // Read the raw guide once, at module load. Frozen for the life of the process.
21
22
  const RAW_GUIDE: string = (() => {
package/app/feature.ts CHANGED
@@ -248,7 +248,7 @@ export function deriveFeatureBlockedPatch(
248
248
  export const featureRuns = (data: DataLayer) => data.table<FeatureRun>("feature_runs", "feature_key");
249
249
 
250
250
  /** The deterministic task id for a single-issue run — the implementation agent branches
251
- * `feat/<task.id>` (see prompts/feature.md), so it MUST be derivable from the issue alone
251
+ * `feat/<task.id>` (see resources/prompts/feature.md), so it MUST be derivable from the issue alone
252
252
  * and stable across a resume. The PR is opened on the target repo, so the issue number
253
253
  * alone is unambiguous within it. */
254
254
  export function featureTaskId(issueNumber: number): string {
@@ -321,7 +321,7 @@ export async function startFeature(
321
321
  issueNumber: parsed.number,
322
322
  issueUrl: parsed.url,
323
323
  // The single slice the implementation agent builds. `task.prompt` is its primary instruction
324
- // (prompts/feature.md); `task.id` fixes its deterministic branch `feat/<task.id>` across a
324
+ // (resources/prompts/feature.md); `task.id` fixes its deterministic branch `feat/<task.id>` across a
325
325
  // resume. Unlike an epic, there is no planner — the whole issue IS the slice.
326
326
  task: {
327
327
  id: featureTaskId(parsed.number),
@@ -337,7 +337,7 @@ export async function startFeature(
337
337
  converge,
338
338
  autoMerge,
339
339
  // A single-issue feature run OWNS its issue (the whole issue is the slice), so the agent may
340
- // claim it with a "starting work" comment on a first run (prompts/feature.md). Epic slices
340
+ // claim it with a "starting work" comment on a first run (resources/prompts/feature.md). Epic slices
341
341
  // (plan-fanout) deliberately DO NOT set this — their `issue` is the shared parent epic, which
342
342
  // must never be claimed per-slice.
343
343
  claimIssue: true,
package/app/plan.ts CHANGED
@@ -31,8 +31,10 @@ export const ESCALATION_SLA_TIMEOUT = escalationSlaTimeout(
31
31
  const now = () => new Date().toISOString();
32
32
 
33
33
  // Agent prompts are no longer read by the host. The `senior:plan`, `senior:plan-review`, and
34
- // `senior:feature` prompts are generic resources (`prompts/plan.md` / `prompts/plan-review.md` /
35
- // `prompts/feature.md`, deployed via a `models` glob in nano.app.json) linked into each task as
34
+ // `senior:feature` prompts are generic resources (`resources/prompts/plan.md` /
35
+ // `resources/prompts/plan-review.md` / `resources/prompts/feature.md`, deployed under the
36
+ // `resources/` deploy-by-convention layout — nano.app.json declares no `models`) linked into each
37
+ // task as
36
38
  // `<zeebe:linkedResource … bindingType="latest" linkName="prompt"/>` and resolved by the engine at
37
39
  // job activation. Per-instance dynamic context (a plan's rejection findings, a task's brief) rides
38
40
  // `appendPrompt`, which the harness concatenates onto the linked base. The host only carries
@@ -244,7 +246,7 @@ export function normalizeBaseBranch(input: string | null | undefined): string {
244
246
 
245
247
  /** The per-instance brief appended to an implementer agent's prompt when the plan pins a base
246
248
  * branch. It is authoritative over the static "branch off the default branch" wording in
247
- * prompts/feature.md, so the agent branches off — and opens its PR against — the integration
249
+ * resources/prompts/feature.md, so the agent branches off — and opens its PR against — the integration
248
250
  * branch, and reads the epic's latest landed state there rather than the repo default branch. */
249
251
  export function renderBaseBranchBrief(baseBranch: string): string {
250
252
  return [
package/app/service.ts CHANGED
@@ -111,7 +111,8 @@ export const MERGE_ADMIN = ["1", "true", "on", "yes"].includes(
111
111
  );
112
112
 
113
113
  // The `senior:pr-review` agent prompt is no longer read by the host: it is a generic resource
114
- // (`prompts/review-round.md`, deployed via a `models` glob in nano.app.json) linked into the task
114
+ // (`resources/prompts/review-round.md`, deployed under the `resources/` deploy-by-convention layout
115
+ // — nano.app.json declares no `models`) linked into the task
115
116
  // as `<zeebe:linkedResource resourceId="review-round.md" bindingType="latest" linkName="prompt"/>`
116
117
  // and resolved by the engine at job activation. The host only carries runtime PR identity + the
117
118
  // round counter now.
package/app/taskDelta.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // nano-workforce — structured scope/impl-change report from implementers (D5, issue #55 / #49).
2
2
  //
3
- // The implementer result contract (prompts/feature.md) can carry an optional `delta`: a machine-
3
+ // The implementer result contract (resources/prompts/feature.md) can carry an optional `delta`: a machine-
4
4
  // readable record of how a slice's implementation diverged from its brief — a changed contract, a
5
5
  // discovered constraint, files it now touches beyond its slice, or other tasks it affects. Before
6
6
  // this, that information lived only in PR prose: invisible to the planner, to sibling agents, and
@@ -307,20 +307,29 @@ both live in the source repo, not in the job payload.
307
307
  `retro.bpmn`. These are the source of truth for routing. To understand *why* an
308
308
  instance went where it did, read the gateway conditions (FEEL expressions on the
309
309
  sequence flows) for the element it is parked on (§5).
310
- - **Prompts (agent base instructions):** `prompts/*.md` — `review-round.md`,
311
- `plan.md`, `feature.md`, `fix-ci.md`, `rebase.md`, `trial-merge.md`, etc. An
312
- agent's base prompt is **not** a job variable: it is delivered as a
313
- **linked resource** (likewise `plan.md`, …):
310
+ - **Prompts (agent base instructions):** `resources/prompts/*.md` — `review-round.md`,
311
+ `plan.md`, `feature.md`, `fix-ci.md`, `rebase.md`, `trial-merge.md`, etc. They live
312
+ under `resources/` so they deploy by convention (ADR 0062 the app declares no
313
+ `models`; every file under `resources/` is deployed, one `.md` per task under
314
+ `resources/prompts/`). An agent's base prompt is **not** a job variable and is **not**
315
+ a deploy-time `{{token}}` substitution (that templating is removed — there is no
316
+ back-compat): it is delivered as a **linked resource**, the blessed **and only**
317
+ prompt-modularity path:
314
318
 
315
319
  ```xml
316
- <zeebe:linkedResource resourceId="review-round.md" bindingType="latest" linkName="prompt"/>
320
+ <zeebe:linkedResource resourceId="review-round.md" bindingType="latest" resourceType="GenericScript" linkName="prompt"/>
317
321
  ```
318
322
 
319
323
  that the engine
320
- resolves to the latest deployed prompt at job activation. Because the binding is
321
- `latest`, redeploying one of these files updates the prompt mid-epic without a process
322
- redeploy. If an agent misbehaves systematically, the prompt is the first thing
323
- to inspect/fix.
324
+ resolves to the latest deployed prompt at job activation, combined at runtime with the
325
+ per-task `appendPrompt` FEEL (the task-specific slice appended to this base). Because
326
+ the binding is `latest`, redeploying one of these files updates the prompt mid-epic
327
+ without a process redeploy. If an agent misbehaves systematically, the prompt is the
328
+ first thing to inspect/fix.
329
+
330
+ > **Value-injection caveat:** never bake a per-run value (a URL, a flag) into a prompt
331
+ > at deploy time — that path is gone. Pass it as a runtime job variable / `appendPrompt`
332
+ > FEEL instead; deploy-time string substitution is not available.
324
333
  - **Job contract:** `senior:pr-review` receives `{ prUrl, repo, prNumber, round,
325
334
  answer? }` and must return a flat result `{ status, summary, question? }` with
326
335
  `status ∈ { converged, addressed, waiting, needs_input, blocked }`. A round that
@@ -329,7 +338,7 @@ both live in the source repo, not in the job payload.
329
338
  rather than escalating.
330
339
 
331
340
  To validate a model/prompt change locally: `npm run layout:check` (BPMN diagram
332
- freshness), `npm run check:prompts` (every template resolves), `npm run check`
341
+ freshness), `npm run check:prompts` (every prompt link resolves), `npm run check`
333
342
  (manifest), `npm run typecheck`, `npm run lint`, `npm test`.
334
343
 
335
344
  ---
@@ -25,6 +25,7 @@ import { fileURLToPath } from "node:url";
25
25
  import type { EngineJob } from "@nanobpm/urban/runtime";
26
26
  import { bootTestApp, type TestApp } from "@nanobpm/urban-testkit";
27
27
  import { admitGithubState, installAdmitGithub } from "./support/github-admit.ts";
28
+ import { asEngineClient } from "./support/engine-client.ts";
28
29
  import {
29
30
  completeEscalationAsAgent,
30
31
  latestCompletion,
@@ -138,7 +139,7 @@ describe("agent-answerable escalations (U6 — same form, agent completer, attri
138
139
 
139
140
  // Complete AS AN AGENT through the host-side completer — the same typed `{resolution, answer}`
140
141
  // a human submits through the inbox, only the caller differs.
141
- const r = await completeEscalationAsAgent(app.db, app.engine, {
142
+ const r = await completeEscalationAsAgent(app.db, asEngineClient(app.engine), {
142
143
  userTaskKey: task.userTaskKey,
143
144
  agentId: "senior:answer-bot",
144
145
  variables: { resolution: "answer", answer: "use v2" },
@@ -22,6 +22,7 @@ import { fileURLToPath } from "node:url";
22
22
  import type { EngineJob } from "@nanobpm/urban/runtime";
23
23
  import { bootTestApp, type TestApp } from "@nanobpm/urban-testkit";
24
24
  import { admitGithubState, installAdmitGithub } from "./support/github-admit.ts";
25
+ import { asEngineClient } from "./support/engine-client.ts";
25
26
  import { pollFeatureBlocked, pollFeatureEscalations } from "../app/service.ts";
26
27
 
27
28
  const APP_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -192,7 +193,7 @@ describe("single-issue feature run (#172 — feature.bpmn)", () => {
192
193
 
193
194
  // The poller fills in the completable user-task key (which no service task can know — the task
194
195
  // doesn't exist yet when record-feature runs) so the pages can drive an attributed acknowledge.
195
- await pollFeatureBlocked(app.db, app.engine);
196
+ await pollFeatureBlocked(app.db, asEngineClient(app.engine));
196
197
  const denorm = await featureRow(app, featureKey);
197
198
  assert.ok(denorm.blocked_user_task_key, "the poller denormalised the completable blocked user-task key");
198
199
  assert.equal(denorm.status, "awaiting_operator", "the run stays awaiting_operator while parked");
@@ -214,7 +215,7 @@ describe("single-issue feature run (#172 — feature.bpmn)", () => {
214
215
  assert.equal(settled.blocked_user_task_key, null, "the completable-task pointer was cleared on ack");
215
216
 
216
217
  // A further poll pass is an idempotent no-op — a terminal run is not a candidate.
217
- await pollFeatureBlocked(app.db, app.engine);
218
+ await pollFeatureBlocked(app.db, asEngineClient(app.engine));
218
219
  assert.equal((await featureRow(app, featureKey)).status, "blocked");
219
220
  },
220
221
  );
@@ -297,7 +298,7 @@ describe("single-issue feature run (#172 — feature.bpmn)", () => {
297
298
 
298
299
  // The poller fills in the completable user-task key (which the service task can't know — the
299
300
  // task doesn't exist yet when it runs) so the UI can drive an attributed answer.
300
- await pollFeatureEscalations(app.db, app.engine);
301
+ await pollFeatureEscalations(app.db, asEngineClient(app.engine));
301
302
  const escalated = await featureRow(app, featureKey);
302
303
  assert.ok(escalated.escalation_user_task_key, "the poller denormalised the completable user-task key");
303
304
  assert.equal(escalated.status, "escalated", "the run stays escalated while parked");
@@ -324,7 +325,7 @@ describe("single-issue feature run (#172 — feature.bpmn)", () => {
324
325
  assert.equal(settled.escalation_question, null, "the surfaced question was cleared once resolved");
325
326
 
326
327
  // A further poll pass is an idempotent no-op — a terminal run is not a candidate.
327
- await pollFeatureEscalations(app.db, app.engine);
328
+ await pollFeatureEscalations(app.db, asEngineClient(app.engine));
328
329
  assert.equal((await featureRow(app, featureKey)).status, "opened");
329
330
  },
330
331
  );
@@ -0,0 +1,17 @@
1
+ import type { EngineClient } from "@nanobpm/urban";
2
+ import type { TestApp } from "@nanobpm/urban-testkit";
3
+
4
+ // urban 0.49.0 (ADR 0062) added `EngineClient.getForm`, but the published
5
+ // @nanobpm/urban-testkit (0.4.0) predates it, so its `WasmEngineClient` neither
6
+ // declares nor implements the method. These hermetic e2e flows drive user-task
7
+ // completion directly (`completeUserTask`) and never resolve a form schema, so we
8
+ // complete the contract with a null-returning `getForm` — the documented "no
9
+ // matching form" path — until a testkit release catches up with urban's engine
10
+ // seam. Scoped to the test harness; production adapters implement `getForm` for real.
11
+ export function asEngineClient(engine: TestApp["engine"]): EngineClient {
12
+ const e = engine as unknown as EngineClient & { getForm?: EngineClient["getForm"] };
13
+ if (typeof e.getForm !== "function") {
14
+ e.getForm = async () => null;
15
+ }
16
+ return e;
17
+ }
package/nano.app.json CHANGED
@@ -4,15 +4,6 @@
4
4
  "id": "nano-workforce",
5
5
  "name": "Nano Workforce",
6
6
  "codename": "nano-workforce",
7
- "models": {
8
- "processes": [
9
- "resources/processes/*.bpmn",
10
- "prompts/*.md"
11
- ],
12
- "forms": [
13
- "resources/forms/*.form"
14
- ]
15
- },
16
7
  "data": {
17
8
  "default": "app",
18
9
  "sources": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.72.0",
3
+ "version": "0.73.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",
@@ -53,11 +53,11 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@nanobpm/agentic": "^0.1.0",
56
- "@nanobpm/urban": "^0.48.0"
56
+ "@nanobpm/urban": "^0.50.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@biomejs/biome": "^2.4.11",
60
- "@nanobpm/urban-testkit": "^0.4.0",
60
+ "@nanobpm/urban-testkit": "^0.5.0",
61
61
  "@semantic-release/changelog": "^6.0.3",
62
62
  "@semantic-release/git": "^10.0.1",
63
63
  "@semantic-release/npm": "^13.1.5",