@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 +28 -1
- package/CHANGELOG.md +14 -0
- package/README.md +9 -8
- package/SPEC.md +22 -17
- package/app/agentGuide.ts +4 -3
- package/app/feature.ts +3 -3
- package/app/plan.ts +5 -3
- package/app/service.ts +2 -1
- package/app/taskDelta.ts +1 -1
- package/{resources → docs}/agent-guide.md +19 -10
- package/e2e/agent-answerable.e2e.ts +2 -1
- package/e2e/feature-run.e2e.ts +5 -4
- package/e2e/support/engine-client.ts +17 -0
- package/nano.app.json +0 -9
- package/package.json +3 -3
- package/pages/tasks.page.json +228 -172
- package/scripts/check-agent-prompts.test.ts +38 -5
- package/scripts/check-agent-prompts.ts +69 -29
- package/workers/record-dependency/worker.ts +1 -1
- package/workers/record-feature/worker.ts +1 -1
- package/workers/record-wave/worker.ts +1 -1
- /package/{prompts → resources/prompts}/feature.md +0 -0
- /package/{prompts → resources/prompts}/fix-ci.md +0 -0
- /package/{prompts → resources/prompts}/plan-review.md +0 -0
- /package/{prompts → resources/prompts}/plan.md +0 -0
- /package/{prompts → resources/prompts}/rebase.md +0 -0
- /package/{prompts → resources/prompts}/retro.md +0 -0
- /package/{prompts → resources/prompts}/review-round.md +0 -0
- /package/{prompts → resources/prompts}/trial-merge.md +0 -0
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
|
|
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
|
|
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
|
|
359
|
-
`nano.app.json`); per-instance context
|
|
360
|
-
|
|
361
|
-
the
|
|
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
|
-
|
|
72
|
-
|
|
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,
|
|
304
|
-
as a **generic resource** and **linked** — not baked — into the model (issue
|
|
305
|
-
|
|
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`).
|
|
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
|
|
320
|
-
activation in a running epic** — no process redeploy, no in-flight epic restart.
|
|
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
|
|
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
|
-
|
|
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 `
|
|
4
|
-
// `
|
|
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, "
|
|
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 (`
|
|
35
|
-
// `prompts/
|
|
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
|
|
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.
|
|
312
|
-
|
|
313
|
-
|
|
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
|
|
321
|
-
`
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
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" },
|
package/e2e/feature-run.e2e.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
56
|
+
"@nanobpm/urban": "^0.50.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@biomejs/biome": "^2.4.11",
|
|
60
|
-
"@nanobpm/urban-testkit": "^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",
|