@nanobpm/nano-workforce 0.162.0 → 0.162.1
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 +6 -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/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 +4 -4
- 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/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,9 @@
|
|
|
1
|
+
## [0.162.1](https://github.com/nanobpm/nano-workforce/compare/v0.162.0...v0.162.1) (2026-08-30)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* **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)
|
|
6
|
+
|
|
1
7
|
## [0.162.0](https://github.com/nanobpm/nano-workforce/compare/v0.161.0...v0.162.0) (2026-08-29)
|
|
2
8
|
|
|
3
9
|
### 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.162.
|
|
3
|
+
"version": "0.162.1",
|
|
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" />
|
|
@@ -266,7 +266,7 @@
|
|
|
266
266
|
<bpmn:extensionElements>
|
|
267
267
|
<zeebe:taskDefinition type="senior:plan" />
|
|
268
268
|
<zeebe:linkedResources>
|
|
269
|
-
<zeebe:linkedResource resourceId="plan.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
269
|
+
<zeebe:linkedResource resourceId="prompts/plan.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
270
270
|
</zeebe:linkedResources>
|
|
271
271
|
<zeebe:ioMapping>
|
|
272
272
|
<zeebe:input source="=(if (planFindings = null or planFindings = "") then "" else " --- A prior review REJECTED your last plan. Address every point, then re-emit the full plan: " + planFindings)" target="appendPrompt" />
|
|
@@ -292,7 +292,7 @@
|
|
|
292
292
|
<bpmn:extensionElements>
|
|
293
293
|
<zeebe:taskDefinition type="senior:plan-review" />
|
|
294
294
|
<zeebe:linkedResources>
|
|
295
|
-
<zeebe:linkedResource resourceId="plan-review.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
295
|
+
<zeebe:linkedResource resourceId="prompts/plan-review.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
296
296
|
</zeebe:linkedResources>
|
|
297
297
|
</bpmn:extensionElements>
|
|
298
298
|
<bpmn:incoming>f_toReviewPlan</bpmn:incoming>
|
|
@@ -419,7 +419,7 @@
|
|
|
419
419
|
<bpmn:extensionElements>
|
|
420
420
|
<zeebe:taskDefinition type="senior:feature" />
|
|
421
421
|
<zeebe:linkedResources>
|
|
422
|
-
<zeebe:linkedResource resourceId="feature.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
422
|
+
<zeebe:linkedResource resourceId="prompts/feature.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
423
423
|
</zeebe:linkedResources>
|
|
424
424
|
<zeebe:ioMapping>
|
|
425
425
|
<zeebe:input source="=" --- " + task.prompt + (if (blackboardBrief = null) then "" else blackboardBrief) + (if (baseBranchBrief = null) then "" else baseBranchBrief) + (if (resolvedDepsBrief = null) then "" else resolvedDepsBrief) + (if (resolvedArtifacts = null or count(resolvedArtifacts[item != null]) = 0) then "" else (" --- **Bound producer capabilities (inter-epic gate).** This epic depends on capabilities another epic in the set just published. Build/install/clone against EXACTLY these published versions (the ones first carrying the awaited capability), never merely the newest: " + string join(resolvedArtifacts[item != null], " ")))" target="appendPrompt" />
|
|
@@ -518,7 +518,7 @@
|
|
|
518
518
|
<bpmn:extensionElements>
|
|
519
519
|
<zeebe:taskDefinition type="senior:trial-merge" />
|
|
520
520
|
<zeebe:linkedResources>
|
|
521
|
-
<zeebe:linkedResource resourceId="trial-merge.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
521
|
+
<zeebe:linkedResource resourceId="prompts/trial-merge.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
522
522
|
</zeebe:linkedResources>
|
|
523
523
|
<zeebe:ioMapping>
|
|
524
524
|
<zeebe:input source="=null" target="result" />
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
<bpmn:extensionElements>
|
|
45
45
|
<zeebe:taskDefinition type="senior:conformance" />
|
|
46
46
|
<zeebe:linkedResources>
|
|
47
|
-
<zeebe:linkedResource resourceId="conformance.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
47
|
+
<zeebe:linkedResource resourceId="prompts/conformance.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
48
48
|
</zeebe:linkedResources>
|
|
49
49
|
<zeebe:ioMapping>
|
|
50
50
|
<zeebe:input source="=conformanceDigest" target="appendPrompt" />
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
<bpmn:extensionElements>
|
|
93
93
|
<zeebe:taskDefinition type="senior:retro" />
|
|
94
94
|
<zeebe:linkedResources>
|
|
95
|
-
<zeebe:linkedResource resourceId="retro.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
95
|
+
<zeebe:linkedResource resourceId="prompts/retro.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
96
96
|
</zeebe:linkedResources>
|
|
97
97
|
<zeebe:ioMapping>
|
|
98
98
|
<zeebe:input source="=retroDigest" target="appendPrompt" />
|
|
@@ -60,7 +60,9 @@ test("passes when every prompt link resolves to a deployed, non-blank, result-em
|
|
|
60
60
|
test("discovers prompts by the resources/ convention when the manifest declares no models", () => {
|
|
61
61
|
const root = fixture({
|
|
62
62
|
"nano.app.json": MANIFEST_CONVENTION,
|
|
63
|
-
|
|
63
|
+
// Under ADR 0062 deploy-by-convention the resource id is the path relative to resources/, so the
|
|
64
|
+
// link must reference `prompts/review-round.md` — the exact id urban deploys and the engine resolves.
|
|
65
|
+
"resources/processes/loop.bpmn": serviceTask(link("prompts/review-round.md")),
|
|
64
66
|
"resources/prompts/review-round.md": "# Round\nDo the thing, then write `$AGENT_RESULT_FILE`.",
|
|
65
67
|
// Docs live OUTSIDE resources/ and must never be swept into the deploy set.
|
|
66
68
|
"docs/agent-guide.md": "# Guide\nnot a deployable",
|
|
@@ -71,6 +73,23 @@ test("discovers prompts by the resources/ convention when the manifest declares
|
|
|
71
73
|
assertEquals(res.resolved, ["review-round"]);
|
|
72
74
|
});
|
|
73
75
|
|
|
76
|
+
test("convention: a bare basename resourceId (pre-ADR-0062) fails — the id must be resources/-relative", () => {
|
|
77
|
+
// Regression guard for the #241 migration miss: the prompt WAS moved under resources/prompts/, so it
|
|
78
|
+
// deploys as `prompts/review-round.md`, but the link still names the bare `review-round.md`. The
|
|
79
|
+
// engine resolves ids exactly, omits the unresolvable link, and the agent runs prompt-less. The gate
|
|
80
|
+
// MUST catch this, not silently pass as it did before.
|
|
81
|
+
const root = fixture({
|
|
82
|
+
"nano.app.json": MANIFEST_CONVENTION,
|
|
83
|
+
"resources/processes/loop.bpmn": serviceTask(link("review-round.md")),
|
|
84
|
+
"resources/prompts/review-round.md": "# Round\nWrite `$AGENT_RESULT_FILE`.",
|
|
85
|
+
});
|
|
86
|
+
const res = checkAgentPrompts(root);
|
|
87
|
+
assert(!res.ok);
|
|
88
|
+
assert(
|
|
89
|
+
res.errors.some((e) => e.includes('resourceId="review-round.md"') && e.includes("no deployed resource")),
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
74
93
|
test("convention walk flags a prompt link that has no deployed resource under resources/", () => {
|
|
75
94
|
const root = fixture({
|
|
76
95
|
"nano.app.json": MANIFEST_CONVENTION,
|
|
@@ -236,7 +255,7 @@ test("fails when two deploy globs match files sharing a basename (ambiguous reso
|
|
|
236
255
|
});
|
|
237
256
|
const res = checkAgentPrompts(root);
|
|
238
257
|
assert(!res.ok);
|
|
239
|
-
assert(res.errors.some((e) => e.includes("duplicate deployed resource
|
|
258
|
+
assert(res.errors.some((e) => e.includes("duplicate deployed resource id") && e.includes("review-round.md")));
|
|
240
259
|
});
|
|
241
260
|
|
|
242
261
|
test("checks the real repo: all committed agent prompts link to deployed resources", () => {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// the deploy no longer substitutes `{{token}}` templates, so such a header would ship a literal
|
|
30
30
|
// `{{token}}` (or stale frozen text) as the prompt.
|
|
31
31
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
32
|
-
import { basename, join } from "node:path";
|
|
32
|
+
import { basename, join, relative, sep } from "node:path";
|
|
33
33
|
|
|
34
34
|
// The retired header that used to carry an agent's baked base prompt. Its continued presence is a
|
|
35
35
|
// migration regression (the deploy no longer substitutes templates), so we flag it.
|
|
@@ -69,33 +69,49 @@ function expandGlob(root: string, pattern: string): string[] {
|
|
|
69
69
|
.map((f) => join(dir, f));
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
// The convention directory (ADR 0062): deploy-only, walked
|
|
73
|
-
// no `models`. Must stay in lock-step with urban's
|
|
72
|
+
// The convention directory (ADR 0062): deploy-only, walked recursively (every file at any depth)
|
|
73
|
+
// when the manifest declares no `models`. Must stay in lock-step with urban's
|
|
74
|
+
// `RESOURCES_DIR`/`deployModels`.
|
|
74
75
|
const RESOURCES_DIR = "resources";
|
|
75
76
|
|
|
76
77
|
// Mirror urban's deploy-by-convention walk (ADR 0062): when the manifest declares no `models`, the
|
|
77
|
-
// deployables are every file
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
// two discovery modes are interchangeable downstream.
|
|
78
|
+
// deployables are every file under `resources/` at ANY depth (urban's deploy.js walks it
|
|
79
|
+
// recursively). A convention resource is keyed by its path relative to `resources/`, so files
|
|
80
|
+
// sharing a basename in different sub-directories deploy as distinct resources — no collision — and
|
|
81
|
+
// a deep walk is safe. Paths come back repo-relative (with `/`), matching `expandGlob`'s output so
|
|
82
|
+
// the two discovery modes are interchangeable downstream.
|
|
82
83
|
function discoverResources(root: string): string[] {
|
|
83
84
|
const base = join(root, RESOURCES_DIR);
|
|
84
85
|
if (!existsSync(base)) return [];
|
|
85
86
|
const out: string[] = [];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
const walk = (dir: string): void => {
|
|
88
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
89
|
+
const abs = join(dir, entry.name);
|
|
90
|
+
if (entry.isFile()) {
|
|
91
|
+
out.push(join(RESOURCES_DIR, relative(base, abs)));
|
|
92
|
+
} else if (entry.isDirectory()) {
|
|
93
|
+
walk(abs);
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
|
-
}
|
|
96
|
+
};
|
|
97
|
+
walk(base);
|
|
96
98
|
return out.sort();
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
// The deployed `resourceId` a `linkName="prompt"` link must reference. Kept in lock-step with
|
|
102
|
+
// urban's deploy.js: a CONVENTION resource (no `models` block) is keyed by its path relative to
|
|
103
|
+
// `resources/` (POSIX) — `resources/prompts/plan.md` → `prompts/plan.md`; a `models` OVERRIDE
|
|
104
|
+
// resource is keyed by its basename. This is the exact string the engine matches a linkedResource
|
|
105
|
+
// `resourceId` against at activation, so the gate must reason about it (not the bare basename) —
|
|
106
|
+
// otherwise a prompt moved into a sub-directory deploys as `prompts/plan.md` while a stale bare
|
|
107
|
+
// `resourceId="plan.md"` link resolves to nothing and the agent runs prompt-less.
|
|
108
|
+
function deployResourceId(rel: string, byConvention: boolean): string {
|
|
109
|
+
if (!byConvention) return basename(rel);
|
|
110
|
+
const posix = rel.split(sep).join("/");
|
|
111
|
+
const prefix = `${RESOURCES_DIR}/`;
|
|
112
|
+
return posix.startsWith(prefix) ? posix.slice(prefix.length) : basename(rel);
|
|
113
|
+
}
|
|
114
|
+
|
|
99
115
|
interface PromptLink {
|
|
100
116
|
resourceId: string;
|
|
101
117
|
bindingType: string | null;
|
|
@@ -162,14 +178,16 @@ export function checkAgentPrompts(root: string): CheckResult {
|
|
|
162
178
|
// The resources the app actually DEPLOYS. Under ADR 0062 deploy-by-convention this is derived the
|
|
163
179
|
// SAME way urban's deployModels derives it, so this gate reasons about exactly the file set that
|
|
164
180
|
// ships to the engine:
|
|
165
|
-
// • no `models` block → discover by convention: every file under `resources/`
|
|
166
|
-
//
|
|
181
|
+
// • no `models` block → discover by convention: every file under `resources/` at any depth
|
|
182
|
+
// (recursive, mirroring urban). This is nwf's blessed layout — prompts live at
|
|
183
|
+
// `resources/prompts/*.md`.
|
|
167
184
|
// • `models` globs present → explicit override, used verbatim (the escape hatch for a
|
|
168
185
|
// non-convention layout). A declared-but-empty `models` is still an override, NOT a fallback
|
|
169
186
|
// to the convention walk — mirror deployModels, which keys convention off the block's absence.
|
|
170
|
-
// Either way a deployed resource's
|
|
171
|
-
//
|
|
172
|
-
//
|
|
187
|
+
// Either way a deployed resource's id is what a `linkedResource resourceId` must reference — for a
|
|
188
|
+
// convention resource its path relative to `resources/` (`prompts/plan.md`), for a `models`
|
|
189
|
+
// override its basename (see deployResourceId) — which is what catches a prompt that exists on
|
|
190
|
+
// disk but is not actually deployed under the id the link names (so the link resolves to nothing).
|
|
173
191
|
const byConvention = manifest.models === undefined;
|
|
174
192
|
const deployedRels = byConvention
|
|
175
193
|
? discoverResources(root)
|
|
@@ -179,19 +197,20 @@ export function checkAgentPrompts(root: string): CheckResult {
|
|
|
179
197
|
...(manifest.models?.forms ?? []),
|
|
180
198
|
].flatMap((p) => expandGlob(root, p));
|
|
181
199
|
|
|
182
|
-
// Keyed by
|
|
183
|
-
//
|
|
200
|
+
// Keyed by the DEPLOYED resourceId a link references: the path relative to `resources/` for a
|
|
201
|
+
// convention resource, or the basename for a `models` override (see deployResourceId). Two
|
|
202
|
+
// deployables that would deploy under the same id clobber each other at the engine, so a
|
|
184
203
|
// `resourceId` lookup could resolve to the wrong file (or mask a misconfiguration). Fail fast on
|
|
185
204
|
// the collision so the lookup stays unambiguous.
|
|
186
205
|
const deployedFiles = new Map<string, string>();
|
|
187
206
|
for (const rel of deployedRels) {
|
|
188
|
-
const name =
|
|
207
|
+
const name = deployResourceId(rel, byConvention);
|
|
189
208
|
const prior = deployedFiles.get(name);
|
|
190
209
|
if (prior != null && prior !== rel) {
|
|
191
210
|
errors.push(
|
|
192
|
-
`duplicate deployed resource
|
|
193
|
-
`same
|
|
194
|
-
`rename one so deployed resource
|
|
211
|
+
`duplicate deployed resource id "${name}": both "${prior}" and "${rel}" deploy under the ` +
|
|
212
|
+
`same id, so a linkName="prompt" resourceId="${name}" would resolve ambiguously — ` +
|
|
213
|
+
`rename one so deployed resource ids stay unique`,
|
|
195
214
|
);
|
|
196
215
|
continue;
|
|
197
216
|
}
|
|
@@ -108,7 +108,7 @@ const retroFlow: DeclarativeFlow = defineFlow(
|
|
|
108
108
|
w.task("gather", { jobType: "pr.retro-gather" });
|
|
109
109
|
w.task("conformance", {
|
|
110
110
|
jobType: "senior:conformance",
|
|
111
|
-
prompt: { resourceId: "conformance.md", bindingType: "latest", append: "=conformanceDigest" },
|
|
111
|
+
prompt: { resourceId: "prompts/conformance.md", bindingType: "latest", append: "=conformanceDigest" },
|
|
112
112
|
});
|
|
113
113
|
w.task("record-conformance", { jobType: "pr.conformance-record" });
|
|
114
114
|
w.branch("hasDeviations = true", {
|
|
@@ -130,7 +130,7 @@ const retroFlow: DeclarativeFlow = defineFlow(
|
|
|
130
130
|
});
|
|
131
131
|
w.task("synthesize", {
|
|
132
132
|
jobType: "senior:retro",
|
|
133
|
-
prompt: { resourceId: "retro.md", bindingType: "latest", append: "=retroDigest" },
|
|
133
|
+
prompt: { resourceId: "prompts/retro.md", bindingType: "latest", append: "=retroDigest" },
|
|
134
134
|
});
|
|
135
135
|
w.task("record", { jobType: "pr.retro-record" });
|
|
136
136
|
},
|