@nanobpm/nano-workforce 0.46.1 → 0.47.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/CHANGELOG.md +14 -0
- package/e2e/convergence-loop.e2e.ts +49 -2
- package/package.json +2 -2
- package/pages/home.page.json +14 -0
- package/prompts/fix-ci.md +45 -0
- package/prompts/rebase.md +45 -0
- package/prompts/review-round.md +1 -1
- package/scripts/check-agent-prompts.test.ts +28 -3
- package/scripts/check-agent-prompts.ts +42 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [0.47.0](https://github.com/nanobpm/nano-workforce/compare/v0.46.2...v0.47.0) (2026-08-12)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **ui:** Agent Instructions button (copy-paste agent prompt) ([#126](https://github.com/nanobpm/nano-workforce/issues/126)) ([4c022ab](https://github.com/nanobpm/nano-workforce/commit/4c022abcbaa90501cacc27ac0b9c8d77b3f81df1)), closes [nanobpm/nano-ide#196](https://github.com/nanobpm/nano-ide/issues/196)
|
|
7
|
+
|
|
8
|
+
## [0.46.2](https://github.com/nanobpm/nano-workforce/compare/v0.46.1...v0.46.2) (2026-08-12)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **prompts:** make merge-phase agents emit a machine-readable result ([#133](https://github.com/nanobpm/nano-workforce/issues/133)) ([6584092](https://github.com/nanobpm/nano-workforce/commit/6584092b3ef8f15159c024c080b312ead7484076)), closes [Magikcraft/nano-bpm#746](https://github.com/Magikcraft/nano-bpm/issues/746)
|
|
14
|
+
|
|
1
15
|
## [0.46.1](https://github.com/nanobpm/nano-workforce/compare/v0.46.0...v0.46.1) (2026-08-12)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
// End-to-end pilot for @nanobpm/urban-testkit (nano-ide issue #157,
|
|
1
|
+
// End-to-end pilot for @nanobpm/urban-testkit (nano-ide issue #157, slices S3 + S4).
|
|
2
2
|
//
|
|
3
3
|
// Boots this whole Urban app in-process against the WASM engine and a virtual clock via
|
|
4
4
|
// `bootTestApp`, then drives its real ADR-0059 OpenAPI operations by `operationId` — the same
|
|
5
5
|
// spec-driven `/app/api/*` surface a browser, a CI relay, or Swagger hit in production. No socket
|
|
6
6
|
// is opened, no wall-clock is waited on, and no GitHub network is touched.
|
|
7
7
|
//
|
|
8
|
+
// It also adopts the S4 coverage-exhaustive gate: booted with `{ coverage: true }`, it gates a
|
|
9
|
+
// curated `pilot:operations` surface (the operations this pilot claims to own) and prints an
|
|
10
|
+
// informational whole-app coverage snapshot for the operations/workers it does not yet drive.
|
|
11
|
+
//
|
|
8
12
|
// Network isolation: the app's GitHub transport (app/github.ts) is forced to `token` mode with no
|
|
9
13
|
// token, so every best-effort GitHub read short-circuits to `null`/idle instead of reaching out.
|
|
10
14
|
// That keeps the pilot hermetic and deterministic in CI.
|
|
@@ -60,6 +64,14 @@ const HARNESS_ENV = {
|
|
|
60
64
|
NANO_APP_DB_URL: `file:${join(DB_DIR, "app.db")}`,
|
|
61
65
|
} as const;
|
|
62
66
|
|
|
67
|
+
// The operations this pilot claims to own — the slice of the app's OpenAPI surface it drives end
|
|
68
|
+
// to end. The coverage gate below is scoped to exactly these: if you add one here without a test
|
|
69
|
+
// that drives it, `assertFullCoverage` goes red. (The app declares 10 operations + 16 workers in
|
|
70
|
+
// total; this pilot is deliberately a representative slice, not a whole-app sweep, so it does NOT
|
|
71
|
+
// gate the auto-declared "operations"/"workers" surfaces — it reports them instead, see below.)
|
|
72
|
+
const PILOT_OPERATIONS = ["appendBlackboard", "readBlackboard", "startConvergenceLoop"] as const;
|
|
73
|
+
const PILOT_SURFACE = "pilot:operations";
|
|
74
|
+
|
|
63
75
|
describe("nano-workforce e2e (urban-testkit pilot)", () => {
|
|
64
76
|
let app: TestApp;
|
|
65
77
|
|
|
@@ -68,9 +80,13 @@ describe("nano-workforce e2e (urban-testkit pilot)", () => {
|
|
|
68
80
|
savedEnv.set(k, process.env[k]);
|
|
69
81
|
process.env[k] = v;
|
|
70
82
|
}
|
|
71
|
-
|
|
83
|
+
// Enable the S4 coverage gate: `app.coverage` is pre-declared with this app's "operations"
|
|
84
|
+
// (from openapi.yaml) and "workers" (from nano.app.json) surfaces, and records each hit
|
|
85
|
+
// automatically as the pilot drives operations / the engine runs jobs.
|
|
86
|
+
app = await bootTestApp(APP_ROOT, { env: HARNESS_ENV, coverage: true });
|
|
72
87
|
// This app declares an `api` binding, so the spec-driven driver must be present.
|
|
73
88
|
assert.ok(app.api, "app.api driver should be defined (nano.app.json declares an `api` binding)");
|
|
89
|
+
assert.ok(app.coverage, "app.coverage should be defined (booted with { coverage: true })");
|
|
74
90
|
});
|
|
75
91
|
|
|
76
92
|
after(async () => {
|
|
@@ -172,4 +188,35 @@ describe("nano-workforce e2e (urban-testkit pilot)", () => {
|
|
|
172
188
|
const reconciled = await prs.findOne({ pr_key: prKey });
|
|
173
189
|
assert.equal(reconciled?.status, "abandoned", "reconciler abandoned the terminated PR's row");
|
|
174
190
|
});
|
|
191
|
+
|
|
192
|
+
test("coverage gate: every operation the pilot claims to own was exercised", () => {
|
|
193
|
+
const coverage = app.coverage;
|
|
194
|
+
assert.ok(coverage);
|
|
195
|
+
|
|
196
|
+
// The auto-declared "operations" surface has recorded (across the tests above) exactly the
|
|
197
|
+
// operations this pilot drove. Mirror the pilot-owned ones into a curated surface and gate
|
|
198
|
+
// THAT — so the pilot fails the moment a `PILOT_OPERATIONS` entry lacks a driving test, without
|
|
199
|
+
// demanding whole-app coverage (this is a representative slice, not a full sweep).
|
|
200
|
+
const report = coverage.report();
|
|
201
|
+
const opsExercised = new Set(
|
|
202
|
+
report.surfaces.find((s) => s.surface === "operations")?.exercised ?? [],
|
|
203
|
+
);
|
|
204
|
+
coverage.declareSurface(PILOT_SURFACE, PILOT_OPERATIONS);
|
|
205
|
+
for (const id of PILOT_OPERATIONS) {
|
|
206
|
+
if (opsExercised.has(id)) coverage.record(PILOT_SURFACE, id);
|
|
207
|
+
}
|
|
208
|
+
// Fails, naming any un-driven pilot operation, if PILOT_OPERATIONS grows without a test.
|
|
209
|
+
coverage.assertFullCoverage({ surfaces: [PILOT_SURFACE] });
|
|
210
|
+
|
|
211
|
+
// Informational (non-failing) whole-app snapshot: how much of the app's total surface this
|
|
212
|
+
// pilot exercises. Surfaces the remaining operations/workers as a roadmap for widening the
|
|
213
|
+
// pilot, without turning the deliberate slice into a red build.
|
|
214
|
+
for (const surface of report.surfaces) {
|
|
215
|
+
const { exercised, declared, missing } = surface;
|
|
216
|
+
console.log(
|
|
217
|
+
`[coverage] ${surface.surface}: ${exercised.length}/${declared.length} exercised` +
|
|
218
|
+
(missing.length ? ` — not yet driven: ${missing.join(", ")}` : ""),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
175
222
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.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",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@biomejs/biome": "^2.4.11",
|
|
53
|
-
"@nanobpm/urban-testkit": "^0.
|
|
53
|
+
"@nanobpm/urban-testkit": "^0.4.0",
|
|
54
54
|
"@semantic-release/changelog": "^6.0.3",
|
|
55
55
|
"@semantic-release/git": "^10.0.1",
|
|
56
56
|
"@semantic-release/npm": "^13.1.5",
|
package/pages/home.page.json
CHANGED
|
@@ -27,6 +27,20 @@
|
|
|
27
27
|
"variant": "sub"
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
"type": "button",
|
|
32
|
+
"id": "agent-instructions",
|
|
33
|
+
"props": {
|
|
34
|
+
"label": "🤖 Agent Instructions",
|
|
35
|
+
"variant": "ghost",
|
|
36
|
+
"modal": {
|
|
37
|
+
"title": "Point your agent at Nano Workforce",
|
|
38
|
+
"description": "Copy this prompt and paste it into your coding agent (Copilot, Claude, etc.). It tells the agent to read this workforce's live operator guide, then help you drive and debug it.",
|
|
39
|
+
"copyLabel": "Copy prompt",
|
|
40
|
+
"copyText": "You are helping me operate a running Nano Workforce instance — a durable orchestration app that drives pull requests to review convergence against an automated reviewer, merges them, and can take a whole issue and plan \u2192 implement \u2192 converge it across a fleet of coding agents.\n\nFirst, fetch and read its live operator guide. It tells you how to submit PRs and epics for convergence (including whether to go all the way to merge or stop at review consensus), how to find engine instances and relate them to PRs via the Nano/Camunda-8 REST API, how to inspect the models and prompts, and how to help me untangle escalations:\n\n curl -sS {{appBase}}app/api/agent\n\nIf this instance is secured with a shared secret (NANO_PR_WEBHOOK_SECRET), add its value as an x-hook-secret header, otherwise the request returns 401:\n\n curl -sS -H \"x-hook-secret: <secret>\" {{appBase}}app/api/agent\n\nThen follow that guide to help me drive and debug this workforce. If you find a bug or a stuck process, the guide explains how to raise an issue or a PR against nanobpm/nano-workforce."
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
30
44
|
{
|
|
31
45
|
"type": "actionForm",
|
|
32
46
|
"id": "submit",
|
package/prompts/fix-ci.md
CHANGED
|
@@ -84,3 +84,48 @@ Return a structured result:
|
|
|
84
84
|
Never report `fixed` unless you actually pushed a change. If nothing was wrong on
|
|
85
85
|
the branch (the failure was transient infrastructure), say so in `summary` and
|
|
86
86
|
return `blocked` so a human can decide whether to just retry the merge.
|
|
87
|
+
|
|
88
|
+
### How to return it (the wire mechanism)
|
|
89
|
+
|
|
90
|
+
Your result variables only reach the process if you emit them through the harness's
|
|
91
|
+
result channel. Prose in your normal output is **not** parsed — if you only "say"
|
|
92
|
+
your status in the transcript, the process can't read it, falls back to its safe
|
|
93
|
+
default (a merge escalation a human must clear), and the merge stalls. So emit a
|
|
94
|
+
machine-readable result one of two ways:
|
|
95
|
+
|
|
96
|
+
1. **Write a JSON object to the file at `$AGENT_RESULT_FILE`** (an env var the
|
|
97
|
+
harness sets for you). The object's keys become process variables. Examples:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
# pushed a fix you believe turns the failing checks green:
|
|
101
|
+
printf '%s' '{"status":"fixed","summary":"Fixed the flaky timeout in auth.test.ts and pushed"}' > "$AGENT_RESULT_FILE"
|
|
102
|
+
# ordering constraint — must wait for another PR to land first:
|
|
103
|
+
printf '%s' '{"status":"waiting-on-pr","summary":"Blocked by the linked-issue gate","dependsOn":"owner/repo#123"}' > "$AGENT_RESULT_FILE"
|
|
104
|
+
# genuinely stuck — a human must decide:
|
|
105
|
+
printf '%s' '{"status":"blocked","summary":"CI needs an NPM_TOKEN secret I cannot set","question":"Add the NPM_TOKEN repo secret, then answer to rerun."}' > "$AGENT_RESULT_FILE"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Write this file **once**, at the very end, with your final result. Keep it a flat
|
|
109
|
+
JSON object of exactly the variables named in the return contract above.
|
|
110
|
+
|
|
111
|
+
2. **Fallback** (only if you truly cannot write the file): print a single line to
|
|
112
|
+
stdout of the form `::nano:result:: {json}` — e.g.
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
::nano:result:: {"status":"fixed","summary":"Corrected the type error in handler.ts and pushed"}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The harness reads the **last** such line. A trailing fenced JSON code block is also
|
|
119
|
+
accepted as a last resort.
|
|
120
|
+
|
|
121
|
+
Do not put the result file inside the repo checkout or `git add` it — it lives
|
|
122
|
+
outside your workspace. Exit `0` for every status (including `blocked`/`waiting-on-pr`);
|
|
123
|
+
a non-zero exit means a genuine crash and the job is retried.
|
|
124
|
+
|
|
125
|
+
**Emitting a machine-readable result is your mandatory final step — never exit
|
|
126
|
+
silently.** It is the last thing you do on every path out of this job (including after
|
|
127
|
+
a push, or when you conclude nothing can be fixed). If you are ever unsure which status
|
|
128
|
+
applies, return **`blocked`** with a `summary` and a concrete `question` rather than
|
|
129
|
+
leaving without a result — a missing result is treated as an unclassified merge
|
|
130
|
+
escalation that pulls in a human and stalls the merge, so relying on that default
|
|
131
|
+
wastes the attempt.
|
package/prompts/rebase.md
CHANGED
|
@@ -100,3 +100,48 @@ was needed, so the process simply re-attempts the merge; the rebase budget
|
|
|
100
100
|
bounds how many times a still-stuck PR can loop here before it escalates. Reserve
|
|
101
101
|
`blocked` for a genuine semantic conflict you cannot resolve mechanically (or a
|
|
102
102
|
branch that is un-rebaseable), so a human can decide.
|
|
103
|
+
|
|
104
|
+
### How to return it (the wire mechanism)
|
|
105
|
+
|
|
106
|
+
Your result variables only reach the process if you emit them through the harness's
|
|
107
|
+
result channel. Prose in your normal output is **not** parsed — if you only "say"
|
|
108
|
+
your status in the transcript, the process can't read it, falls back to its safe
|
|
109
|
+
default (a merge escalation a human must clear), and the merge stalls. So emit a
|
|
110
|
+
machine-readable result one of two ways:
|
|
111
|
+
|
|
112
|
+
1. **Write a JSON object to the file at `$AGENT_RESULT_FILE`** (an env var the
|
|
113
|
+
harness sets for you). The object's keys become process variables. Examples:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
# branch tip now contains the latest base (you pushed a resolved rebase, or it was already up to date):
|
|
117
|
+
printf '%s' '{"status":"rebased","summary":"Rebased onto main, resolved 2 conflicts in router.ts, pushed"}' > "$AGENT_RESULT_FILE"
|
|
118
|
+
# ordering constraint — must wait for another PR to land first:
|
|
119
|
+
printf '%s' '{"status":"waiting-on-pr","summary":"Stacked on the base PR that has not merged","dependsOn":"owner/repo#123"}' > "$AGENT_RESULT_FILE"
|
|
120
|
+
# genuine semantic conflict — a human must decide which behaviour wins:
|
|
121
|
+
printf '%s' '{"status":"blocked","summary":"main and this branch both rewrote retry() incompatibly","question":"Should retries stay capped at 3 (main) or become unbounded (this PR)?"}' > "$AGENT_RESULT_FILE"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Write this file **once**, at the very end, with your final result. Keep it a flat
|
|
125
|
+
JSON object of exactly the variables named in the return contract above.
|
|
126
|
+
|
|
127
|
+
2. **Fallback** (only if you truly cannot write the file): print a single line to
|
|
128
|
+
stdout of the form `::nano:result:: {json}` — e.g.
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
::nano:result:: {"status":"rebased","summary":"Already up to date; no push needed"}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The harness reads the **last** such line. A trailing fenced JSON code block is also
|
|
135
|
+
accepted as a last resort.
|
|
136
|
+
|
|
137
|
+
Do not put the result file inside the repo checkout or `git add` it — it lives
|
|
138
|
+
outside your workspace. Exit `0` for every status (including `blocked`/`waiting-on-pr`);
|
|
139
|
+
a non-zero exit means a genuine crash and the job is retried.
|
|
140
|
+
|
|
141
|
+
**Emitting a machine-readable result is your mandatory final step — never exit
|
|
142
|
+
silently.** It is the last thing you do on every path out of this job (including after
|
|
143
|
+
a force-push, or when the branch was already up to date). If you are ever unsure which
|
|
144
|
+
status applies and the branch tip contains the latest base, return **`rebased`** with a
|
|
145
|
+
`summary`; otherwise return **`blocked`** with a concrete `question` — never leave
|
|
146
|
+
without a result. A missing result is treated as an unclassified merge escalation that
|
|
147
|
+
pulls in a human and stalls the merge, so relying on that default wastes the attempt.
|
package/prompts/review-round.md
CHANGED
|
@@ -181,7 +181,7 @@ default, and you waste a round. So emit a machine-readable result one of two way
|
|
|
181
181
|
::nano:result:: {"status":"converged","summary":"No actionable comments left"}
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
-
The harness reads the **last** such line. A trailing
|
|
184
|
+
The harness reads the **last** such line. A trailing fenced JSON code block is also
|
|
185
185
|
accepted as a last resort.
|
|
186
186
|
|
|
187
187
|
Do not put the result file inside the repo checkout or `git add` it — it lives
|
|
@@ -30,11 +30,11 @@ function fixture(files: Record<string, string>): string {
|
|
|
30
30
|
return root;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
test("passes when every {{token}} resolves to a non-blank template", async () => {
|
|
33
|
+
test("passes when every {{token}} resolves to a non-blank template that emits a result", async () => {
|
|
34
34
|
const root = await fixture({
|
|
35
35
|
"nano.app.json": MANIFEST,
|
|
36
36
|
"resources/processes/loop.bpmn": header("{{review-round}}"),
|
|
37
|
-
"prompts/review-round.md": "# Round\nDo the thing
|
|
37
|
+
"prompts/review-round.md": "# Round\nDo the thing, then write your result to `$AGENT_RESULT_FILE`.",
|
|
38
38
|
});
|
|
39
39
|
const res = checkAgentPrompts(root);
|
|
40
40
|
assertEquals(res.errors, []);
|
|
@@ -75,6 +75,31 @@ test("fails when a reserved agent-prompt header is blank", async () => {
|
|
|
75
75
|
assert(res.errors.some((e) => e.includes("is empty")));
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
test("fails when an agent-prompt template omits the machine-readable result mechanism", async () => {
|
|
79
|
+
// A prompt wired as an agent's base prompt must tell it to write $AGENT_RESULT_FILE (or use the
|
|
80
|
+
// ::nano:result:: fallback). Without it the agent finishes with prose only, `status` comes back
|
|
81
|
+
// blank, and the status gateway escalates/stalls — the fix-ci/rebase gap behind #746's stuck merge.
|
|
82
|
+
const root = await fixture({
|
|
83
|
+
"nano.app.json": MANIFEST,
|
|
84
|
+
"resources/processes/loop.bpmn": header("{{review-round}}"),
|
|
85
|
+
"prompts/review-round.md": "# Round\nReturn status: converged. (but never says how to emit it)",
|
|
86
|
+
});
|
|
87
|
+
const res = checkAgentPrompts(root);
|
|
88
|
+
assert(!res.ok);
|
|
89
|
+
assert(res.errors.some((e) => e.includes("{{review-round}}") && e.includes("AGENT_RESULT_FILE")));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("passes when an agent-prompt template emits via the ::nano:result:: fallback", async () => {
|
|
93
|
+
const root = await fixture({
|
|
94
|
+
"nano.app.json": MANIFEST,
|
|
95
|
+
"resources/processes/loop.bpmn": header("{{review-round}}"),
|
|
96
|
+
"prompts/review-round.md": "# Round\nEmit `::nano:result:: {\"status\":\"converged\"}` at the end.",
|
|
97
|
+
});
|
|
98
|
+
const res = checkAgentPrompts(root);
|
|
99
|
+
assertEquals(res.errors, []);
|
|
100
|
+
assert(res.ok);
|
|
101
|
+
});
|
|
102
|
+
|
|
78
103
|
test("checks the real repo: all committed agent prompts resolve", () => {
|
|
79
104
|
// The guard must be green against the actual app it protects — this is the case CI relies on.
|
|
80
105
|
const repoRoot = decodeURIComponent(new URL("../", import.meta.url).pathname);
|
|
@@ -82,7 +107,7 @@ test("checks the real repo: all committed agent prompts resolve", () => {
|
|
|
82
107
|
assertEquals(res.errors, []);
|
|
83
108
|
assert(res.ok);
|
|
84
109
|
// Every senior:* agent prompt header in the three processes must have resolved.
|
|
85
|
-
for (const t of ["review-round", "fix-ci", "plan", "plan-review", "feature", "trial-merge"]) {
|
|
110
|
+
for (const t of ["review-round", "fix-ci", "plan", "plan-review", "feature", "trial-merge", "rebase", "retro"]) {
|
|
86
111
|
assert(res.resolved.includes(t), `expected template ${t} to resolve`);
|
|
87
112
|
}
|
|
88
113
|
});
|
|
@@ -72,6 +72,33 @@ function hasBlankAgentPromptHeader(bpmn: string): boolean {
|
|
|
72
72
|
return false;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// The template tokens a model wires as an agent's base prompt, e.g. the `fix-ci` in
|
|
76
|
+
// `value="{{fix-ci}}"` on an `io.nanobpm.agentTask.task.prompt` header. These templates *drive an
|
|
77
|
+
// agent*, so each must teach it to emit a machine-readable result (see agentPromptEmitsResult).
|
|
78
|
+
function agentPromptTokens(bpmn: string): string[] {
|
|
79
|
+
const tokens: string[] = [];
|
|
80
|
+
const re = /<zeebe:header\s+key="([^"]*)"\s+value="([^"]*)"\s*\/?>/g;
|
|
81
|
+
let m = re.exec(bpmn);
|
|
82
|
+
while (m !== null) {
|
|
83
|
+
if (m[1] === AGENT_PROMPT_HEADER) {
|
|
84
|
+
const tok = /^\{\{\s*([^}]+?)\s*\}\}$/.exec(m[2].trim());
|
|
85
|
+
if (tok) tokens.push(tok[1]);
|
|
86
|
+
}
|
|
87
|
+
m = re.exec(bpmn);
|
|
88
|
+
}
|
|
89
|
+
return tokens;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// A prompt that drives an agent must tell it how to return a machine-readable result — the
|
|
93
|
+
// `$AGENT_RESULT_FILE` write (or the `::nano:result::` stdout fallback). Without it the agent can
|
|
94
|
+
// finish with prose only, its `status` variable comes back empty, the status gateway falls through
|
|
95
|
+
// to its default escalation arm, and the run parks a human escalation / stalls the merge (the
|
|
96
|
+
// fix-ci/rebase gap behind Magikcraft/nano-bpm#746's stuck merge). Prose is never parsed, so this
|
|
97
|
+
// instruction is load-bearing, not documentation.
|
|
98
|
+
function agentPromptEmitsResult(body: string): boolean {
|
|
99
|
+
return body.includes("AGENT_RESULT_FILE") || body.includes("::nano:result::");
|
|
100
|
+
}
|
|
101
|
+
|
|
75
102
|
export interface CheckResult {
|
|
76
103
|
ok: boolean;
|
|
77
104
|
errors: string[];
|
|
@@ -82,6 +109,7 @@ export interface CheckResult {
|
|
|
82
109
|
export function checkAgentPrompts(root: string): CheckResult {
|
|
83
110
|
const errors: string[] = [];
|
|
84
111
|
const resolved = new Set<string>();
|
|
112
|
+
const agentTokens = new Set<string>();
|
|
85
113
|
|
|
86
114
|
const manifestPath = join(root, "nano.app.json");
|
|
87
115
|
if (!existsSync(manifestPath)) {
|
|
@@ -127,6 +155,20 @@ export function checkAgentPrompts(root: string): CheckResult {
|
|
|
127
155
|
if (hasBlankAgentPromptHeader(content)) {
|
|
128
156
|
errors.push(`${rel}: a reserved "${AGENT_PROMPT_HEADER}" header is empty (agent would run prompt-less)`);
|
|
129
157
|
}
|
|
158
|
+
for (const tok of agentPromptTokens(content)) agentTokens.add(tok);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Every template wired as an agent's base prompt must teach the agent to emit a machine-readable
|
|
162
|
+
// result; a prose-only agent leaves `status` blank and the process escalates/stalls.
|
|
163
|
+
for (const tok of [...agentTokens].sort()) {
|
|
164
|
+
const body = templates[tok];
|
|
165
|
+
if (body != null && body.trim() !== "" && !agentPromptEmitsResult(body)) {
|
|
166
|
+
errors.push(
|
|
167
|
+
`template {{${tok}}} drives an agent but never tells it to write $AGENT_RESULT_FILE ` +
|
|
168
|
+
`(or the ::nano:result:: fallback) — the agent can finish with prose only, leaving its ` +
|
|
169
|
+
`status blank so the process escalates/stalls`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
130
172
|
}
|
|
131
173
|
|
|
132
174
|
return { ok: errors.length === 0, errors, resolved: [...resolved].sort() };
|