@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
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// check-agent-prompts — deploy-safety gate for the agent prompts, now authored as *linked
|
|
2
2
|
// resources* (issue #169) rather than baked `{{token}}` templates.
|
|
3
3
|
//
|
|
4
|
-
// Since #169 each agent's base prompt is a generic resource: `prompts/<token>.md` is
|
|
5
|
-
// an `application/octet-stream` resource (
|
|
6
|
-
//
|
|
4
|
+
// Since #169 each agent's base prompt is a generic resource: `resources/prompts/<token>.md` is
|
|
5
|
+
// deployed as an `application/octet-stream` resource (under the ADR 0062 `resources/` deploy-by-
|
|
6
|
+
// convention layout — see nano.app.json, which declares no `models`) and each agent service task
|
|
7
|
+
// links it at job-activation time:
|
|
7
8
|
//
|
|
8
9
|
// <zeebe:linkedResources>
|
|
9
10
|
// <zeebe:linkedResource resourceId="review-round.md" bindingType="latest" linkName="prompt" />
|
|
@@ -17,9 +18,9 @@
|
|
|
17
18
|
// #597/#599). This guard turns that silent runtime failure into a hard build failure:
|
|
18
19
|
//
|
|
19
20
|
// 1. Every `linkName="prompt"` link's `resourceId` MUST match a prompt file that the app actually
|
|
20
|
-
// deploys (a file
|
|
21
|
-
// and a prompt that exists on disk but is not
|
|
22
|
-
// engine — the link would resolve to nothing).
|
|
21
|
+
// deploys (a file under the `resources/` convention walk, or a manifest `models` override
|
|
22
|
+
// glob). This catches both a typo'd `resourceId` and a prompt that exists on disk but is not
|
|
23
|
+
// wired into the deploy set (so never reaches the engine — the link would resolve to nothing).
|
|
23
24
|
// 2. Each linked prompt file must be non-blank and must teach the agent to emit a machine-readable
|
|
24
25
|
// result (`$AGENT_RESULT_FILE`, or the `::nano:result::` stdout fallback) — a prose-only agent
|
|
25
26
|
// leaves `status` blank and the status gateway escalates/stalls (the fix-ci/rebase gap behind
|
|
@@ -68,6 +69,33 @@ function expandGlob(root: string, pattern: string): string[] {
|
|
|
68
69
|
.map((f) => join(dir, f));
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
// The convention directory (ADR 0062): deploy-only, walked one level deep when the manifest declares
|
|
73
|
+
// no `models`. Must stay in lock-step with urban's `RESOURCES_DIR`/`deployModels`.
|
|
74
|
+
const RESOURCES_DIR = "resources";
|
|
75
|
+
|
|
76
|
+
// Mirror urban's deploy-by-convention walk (ADR 0062): when the manifest declares no `models`, the
|
|
77
|
+
// deployables are every file directly under `resources/` PLUS every file one directory deeper
|
|
78
|
+
// (`resources/<subdir>/*`) — shallow, one level only. Deeper nesting is intentionally NOT swept in:
|
|
79
|
+
// the deploy dedupe key is the basename, so a deep walk would reintroduce cross-directory basename
|
|
80
|
+
// collision risk. Paths come back repo-relative (with `/`), matching `expandGlob`'s output so the
|
|
81
|
+
// two discovery modes are interchangeable downstream.
|
|
82
|
+
function discoverResources(root: string): string[] {
|
|
83
|
+
const base = join(root, RESOURCES_DIR);
|
|
84
|
+
if (!existsSync(base)) return [];
|
|
85
|
+
const out: string[] = [];
|
|
86
|
+
for (const entry of readdirSync(base, { withFileTypes: true })) {
|
|
87
|
+
if (entry.isFile()) {
|
|
88
|
+
out.push(join(RESOURCES_DIR, entry.name));
|
|
89
|
+
} else if (entry.isDirectory()) {
|
|
90
|
+
const sub = join(base, entry.name);
|
|
91
|
+
for (const f of readdirSync(sub, { withFileTypes: true })) {
|
|
92
|
+
if (f.isFile()) out.push(join(RESOURCES_DIR, entry.name, f.name));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out.sort();
|
|
97
|
+
}
|
|
98
|
+
|
|
71
99
|
interface PromptLink {
|
|
72
100
|
resourceId: string;
|
|
73
101
|
bindingType: string | null;
|
|
@@ -130,24 +158,33 @@ export function checkAgentPrompts(root: string): CheckResult {
|
|
|
130
158
|
}
|
|
131
159
|
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
132
160
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as AppManifest;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
161
|
+
|
|
162
|
+
// The resources the app actually DEPLOYS. Under ADR 0062 deploy-by-convention this is derived the
|
|
163
|
+
// SAME way urban's deployModels derives it, so this gate reasons about exactly the file set that
|
|
164
|
+
// ships to the engine:
|
|
165
|
+
// • no `models` block → discover by convention: every file under `resources/` (shallow, one
|
|
166
|
+
// level deep). This is nwf's blessed layout — prompts live at `resources/prompts/*.md`.
|
|
167
|
+
// • `models` globs present → explicit override, used verbatim (the escape hatch for a
|
|
168
|
+
// non-convention layout). A declared-but-empty `models` is still an override, NOT a fallback
|
|
169
|
+
// to the convention walk — mirror deployModels, which keys convention off the block's absence.
|
|
170
|
+
// Either way a deployed resource's name is the file's basename — the same string a
|
|
171
|
+
// `linkedResource resourceId` must reference — which is what catches a prompt that exists on disk
|
|
172
|
+
// but is not actually deployed (so it never reaches the engine and the link resolves to nothing).
|
|
173
|
+
const byConvention = manifest.models === undefined;
|
|
174
|
+
const deployedRels = byConvention
|
|
175
|
+
? discoverResources(root)
|
|
176
|
+
: [
|
|
177
|
+
...(manifest.models?.processes ?? []),
|
|
178
|
+
...(manifest.models?.decisions ?? []),
|
|
179
|
+
...(manifest.models?.forms ?? []),
|
|
180
|
+
].flatMap((p) => expandGlob(root, p));
|
|
181
|
+
|
|
182
|
+
// Keyed by basename (the deployed resource name a `resourceId` references). Two deployables sharing
|
|
183
|
+
// a basename would silently overwrite here — and clobber each other at the engine — so a
|
|
184
|
+
// `resourceId` lookup could resolve to the wrong file (or mask a misconfiguration). Fail fast on
|
|
185
|
+
// the collision so the lookup stays unambiguous.
|
|
149
186
|
const deployedFiles = new Map<string, string>();
|
|
150
|
-
for (const rel of
|
|
187
|
+
for (const rel of deployedRels) {
|
|
151
188
|
const name = basename(rel);
|
|
152
189
|
const prior = deployedFiles.get(name);
|
|
153
190
|
if (prior != null && prior !== rel) {
|
|
@@ -162,11 +199,13 @@ export function checkAgentPrompts(root: string): CheckResult {
|
|
|
162
199
|
}
|
|
163
200
|
|
|
164
201
|
// The model files whose XML we scan for `<zeebe:linkedResource>` links.
|
|
165
|
-
const xmlModelFiles =
|
|
166
|
-
.flatMap((p) => expandGlob(root, p))
|
|
167
|
-
.filter((rel) => contentTypeFor(rel) === "text/xml");
|
|
202
|
+
const xmlModelFiles = deployedRels.filter((rel) => contentTypeFor(rel) === "text/xml");
|
|
168
203
|
if (xmlModelFiles.length === 0) {
|
|
169
|
-
errors.push(
|
|
204
|
+
errors.push(
|
|
205
|
+
byConvention
|
|
206
|
+
? `no BPMN/DMN model files found under ${RESOURCES_DIR}/ by convention`
|
|
207
|
+
: "no BPMN/DMN model files matched the manifest's models globs",
|
|
208
|
+
);
|
|
170
209
|
}
|
|
171
210
|
|
|
172
211
|
let linkCount = 0;
|
|
@@ -199,8 +238,9 @@ export function checkAgentPrompts(root: string): CheckResult {
|
|
|
199
238
|
if (deployedRel == null) {
|
|
200
239
|
errors.push(
|
|
201
240
|
`${rel}: linkName="prompt" resourceId="${link.resourceId}" has no deployed resource — ` +
|
|
202
|
-
`no
|
|
203
|
-
`
|
|
241
|
+
`no deployed resource has that name — no file under the app's deploy set (the ` +
|
|
242
|
+
`resources/ convention walk, or the manifest's models globs) matches it, so the engine ` +
|
|
243
|
+
`would omit the link and the agent would run prompt-less`,
|
|
204
244
|
);
|
|
205
245
|
continue;
|
|
206
246
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// merged. The process re-enters its existing `wait-deps` catch, so no human has to babysit an
|
|
7
7
|
// ordering constraint the machinery already knows how to satisfy.
|
|
8
8
|
//
|
|
9
|
-
// `dependsOn` is whatever the agent returned (see prompts/fix-ci.md, prompts/rebase.md): a
|
|
9
|
+
// `dependsOn` is whatever the agent returned (see resources/prompts/fix-ci.md, resources/prompts/rebase.md): a
|
|
10
10
|
// string of one or more `owner/repo#N` refs (or PR URLs) separated by commas/whitespace/newlines,
|
|
11
11
|
// or an array of such tokens. We parse each robustly (reusing `parsePr`), drop self-references and
|
|
12
12
|
// duplicates, and insert missing edges idempotently — a worker retry never double-inserts, and an
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// pr.record-feature — the single-issue `implement` block has finished (issue #172).
|
|
2
2
|
//
|
|
3
|
-
// The `senior:feature` agent reported one of `opened` / `blocked` / `skipped` (prompts/feature.md);
|
|
3
|
+
// The `senior:feature` agent reported one of `opened` / `blocked` / `skipped` (resources/prompts/feature.md);
|
|
4
4
|
// anything else — including a missing status, or an `escalated` status that fell through to abandon
|
|
5
5
|
// (the human abandoned or the SLA fired) — is treated as `blocked`: we must not assume a PR was
|
|
6
6
|
// opened. This worker:
|
|
@@ -47,7 +47,7 @@ interface Out extends Record<string, unknown> {
|
|
|
47
47
|
const str = (v: unknown): string | undefined =>
|
|
48
48
|
typeof v === "string" && v.trim().length > 0 ? v.trim() : undefined;
|
|
49
49
|
|
|
50
|
-
// The implementation agent reports one of these (see prompts/feature.md). Anything else —
|
|
50
|
+
// The implementation agent reports one of these (see resources/prompts/feature.md). Anything else —
|
|
51
51
|
// including a missing status — is treated as `blocked`: we must not assume a PR was opened,
|
|
52
52
|
// and we only hand off / persist a PR when the status is `opened`.
|
|
53
53
|
type WaveResultStatus = Extract<PlanTaskStatus, "opened" | "blocked" | "skipped">;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|