@nanobpm/nano-workforce 0.73.1 → 0.74.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 +7 -0
- package/app/feature.test.ts +36 -0
- package/app/feature.ts +11 -0
- package/openapi.yaml +13 -0
- package/operations/startFeature.ts +7 -1
- package/package.json +1 -1
- package/pages/feature.page.json +1 -0
- package/resources/processes/feature.bpmn +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [0.74.0](https://github.com/nanobpm/nano-workforce/compare/v0.73.1...v0.74.0) (2026-08-16)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **feature:** thread optional custom instructions to the implementation agent ([#247](https://github.com/nanobpm/nano-workforce/issues/247)) ([8b65495](https://github.com/nanobpm/nano-workforce/commit/8b65495a739b01560937fa108ed4def16c3ffd85))
|
|
7
|
+
|
|
1
8
|
## [0.73.1](https://github.com/nanobpm/nano-workforce/compare/v0.73.0...v0.73.1) (2026-08-16)
|
|
2
9
|
|
|
3
10
|
|
package/app/feature.test.ts
CHANGED
|
@@ -114,6 +114,42 @@ test("startFeature: seeds the single task slice + base-branch brief onto the ins
|
|
|
114
114
|
assertEquals(v.status, null);
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
test("startFeature: custom instructions ride the instance as a variable (trimmed)", async () => {
|
|
118
|
+
let captured: any = null;
|
|
119
|
+
const engine = {
|
|
120
|
+
createInstance: (req: any) => {
|
|
121
|
+
captured = req;
|
|
122
|
+
return Promise.resolve({ processInstanceKey: "PI-3" });
|
|
123
|
+
},
|
|
124
|
+
} as any;
|
|
125
|
+
await startFeature(
|
|
126
|
+
memData({ feature_runs: { rows: [], key: "feature_key" } }),
|
|
127
|
+
engine,
|
|
128
|
+
PARSED,
|
|
129
|
+
"main",
|
|
130
|
+
false,
|
|
131
|
+
false,
|
|
132
|
+
" prefer Deno; keep the diff small ",
|
|
133
|
+
);
|
|
134
|
+
assertEquals(captured.variables.customInstructions, "prefer Deno; keep the diff small");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("startFeature: blank/absent custom instructions are seeded as null", async () => {
|
|
138
|
+
let captured: any = null;
|
|
139
|
+
const engine = {
|
|
140
|
+
createInstance: (req: any) => {
|
|
141
|
+
captured = req;
|
|
142
|
+
return Promise.resolve({ processInstanceKey: "PI-4" });
|
|
143
|
+
},
|
|
144
|
+
} as any;
|
|
145
|
+
// Absent (default arg) → null.
|
|
146
|
+
await startFeature(memData({ feature_runs: { rows: [], key: "feature_key" } }), engine, PARSED, "main", false, false);
|
|
147
|
+
assertEquals(captured.variables.customInstructions, null);
|
|
148
|
+
// Whitespace-only → null (so the appendPrompt FEEL skips the block instead of appending an empty heading).
|
|
149
|
+
await startFeature(memData({ feature_runs: { rows: [], key: "feature_key" } }), engine, PARSED, "main", false, false, " ");
|
|
150
|
+
assertEquals(captured.variables.customInstructions, null);
|
|
151
|
+
});
|
|
152
|
+
|
|
117
153
|
test("startFeature: an already-running run short-circuits (no new instance)", async () => {
|
|
118
154
|
const stores = {
|
|
119
155
|
feature_runs: {
|
package/app/feature.ts
CHANGED
|
@@ -267,7 +267,14 @@ export async function startFeature(
|
|
|
267
267
|
baseBranch: string,
|
|
268
268
|
converge: boolean,
|
|
269
269
|
autoMerge: boolean,
|
|
270
|
+
customInstructions: string | null = null,
|
|
270
271
|
) {
|
|
272
|
+
// Operator free-text steering for the implementation agent (issue #172 follow-on): blank/absent →
|
|
273
|
+
// null so the implement task's `appendPrompt` FEEL (`customInstructions = null`) skips the block
|
|
274
|
+
// rather than appending an empty "Operator custom instructions" heading.
|
|
275
|
+
const instructions = typeof customInstructions === "string" && customInstructions.trim() !== ""
|
|
276
|
+
? customInstructions.trim()
|
|
277
|
+
: null;
|
|
271
278
|
const table = featureRuns(data);
|
|
272
279
|
const existing = await table.get(parsed.planKey);
|
|
273
280
|
if (existing && !FEATURE_TERMINAL_STATUSES.includes(existing.status)) {
|
|
@@ -356,6 +363,10 @@ export async function startFeature(
|
|
|
356
363
|
// brief rides `appendPrompt` in the implement task, exactly like the epic implementer.
|
|
357
364
|
baseBranch: base,
|
|
358
365
|
baseBranchBrief: renderBaseBranchBrief(base),
|
|
366
|
+
// Optional operator steering, appended to the implement agent's prompt via the implement
|
|
367
|
+
// task's `appendPrompt` FEEL (feature.bpmn). Null when none was supplied; persists on the
|
|
368
|
+
// instance so it also rides the answer-loop redispatch back into the same implement task.
|
|
369
|
+
customInstructions: instructions,
|
|
359
370
|
},
|
|
360
371
|
});
|
|
361
372
|
const processKey = processInstanceKey == null ? null : String(processInstanceKey);
|
package/openapi.yaml
CHANGED
|
@@ -668,6 +668,13 @@ components:
|
|
|
668
668
|
description: >-
|
|
669
669
|
Opt in to sharing a custom integration base branch with another already-active epic. See
|
|
670
670
|
`PlanStartByIssue.allowSharedBase`.
|
|
671
|
+
customInstructions:
|
|
672
|
+
type: string
|
|
673
|
+
maxLength: 8000
|
|
674
|
+
description: >-
|
|
675
|
+
OPTIONAL free-text steering appended to the implementation agent's prompt for this run
|
|
676
|
+
(via the implement task's `appendPrompt`). Blank/whitespace is treated as absent. Persists
|
|
677
|
+
on the instance, so it also applies to the agent's answer-loop redispatch.
|
|
671
678
|
FeatureStartByUrl:
|
|
672
679
|
type: object
|
|
673
680
|
additionalProperties: false
|
|
@@ -698,6 +705,12 @@ components:
|
|
|
698
705
|
allowSharedBase:
|
|
699
706
|
type: boolean
|
|
700
707
|
description: Share a custom integration base with another active epic. See `PlanStartByIssue.allowSharedBase`.
|
|
708
|
+
customInstructions:
|
|
709
|
+
type: string
|
|
710
|
+
maxLength: 8000
|
|
711
|
+
description: >-
|
|
712
|
+
OPTIONAL free-text steering appended to the implementation agent's prompt for this run.
|
|
713
|
+
See `FeatureStartByIssue.customInstructions`.
|
|
701
714
|
MessageResult:
|
|
702
715
|
type: object
|
|
703
716
|
description: The result of publishing a message / answering an escalation. Shape varies by message
|
|
@@ -115,12 +115,18 @@ export default defineOperation("startFeature", async ({ body }, app) => {
|
|
|
115
115
|
// Auto-merge is only meaningful as a follow-on to convergence; pin it off when converge is off so
|
|
116
116
|
// the persisted row and the process variable can't disagree.
|
|
117
117
|
const autoMerge = converge && "autoMerge" in body && body.autoMerge === true;
|
|
118
|
-
|
|
118
|
+
// Optional operator steering threaded to the implementation agent's prompt. Empty/whitespace is
|
|
119
|
+
// normalized to null downstream (startFeature) so it never appends an empty instruction block.
|
|
120
|
+
const customInstructions = "customInstructions" in body && typeof body.customInstructions === "string"
|
|
121
|
+
? body.customInstructions
|
|
122
|
+
: null;
|
|
123
|
+
const result = await startFeature(app.data, app.engine, parsed, normalizedBase, converge, autoMerge, customInstructions);
|
|
119
124
|
app.log.info("feature run started", {
|
|
120
125
|
featureKey: parsed.planKey,
|
|
121
126
|
requestedBaseBranch: normalizedBase,
|
|
122
127
|
converge,
|
|
123
128
|
autoMerge,
|
|
129
|
+
hasCustomInstructions: typeof customInstructions === "string" && customInstructions.trim() !== "",
|
|
124
130
|
alreadyRunning: "alreadyRunning" in result && result.alreadyRunning === true,
|
|
125
131
|
});
|
|
126
132
|
return { status: 202, body: result };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.74.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",
|
package/pages/feature.page.json
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"fields": [
|
|
43
43
|
{ "key": "issue", "label": "owner/repo#123 or a GitHub issue URL", "type": "text", "required": true, "requiredMessage": "An issue reference is required (owner/repo#123 or an issue URL)" },
|
|
44
44
|
{ "key": "baseBranch", "label": "Base branch: the branch the PR targets, e.g. main. A missing epic/* branch is auto-created off default HEAD; a non-epic/* branch must already exist.", "type": "text", "required": true, "requiredMessage": "Name the branch the PR targets (e.g. main or epic/agent-protocol)" },
|
|
45
|
+
{ "key": "customInstructions", "label": "Custom instructions (optional) \u2014 extra steering appended to the implementation agent's prompt, e.g. \u201cprefer Deno; keep the diff small; add integration tests\u201d", "type": "text" },
|
|
45
46
|
{ "key": "converge", "label": "Converge \u2014 hand the raised PR to the review-convergence loop", "type": "checkbox" },
|
|
46
47
|
{ "key": "autoMerge", "label": "Auto-merge \u2014 after convergence, drive the merge-loop (only applies when Converge is on)", "type": "checkbox" },
|
|
47
48
|
{ "key": "confirmDefaultBase", "label": "Confirm landing on the default branch \u2014 required only when the base above IS the repository default", "type": "checkbox" },
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
<zeebe:linkedResource resourceId="feature.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
|
|
45
45
|
</zeebe:linkedResources>
|
|
46
46
|
<zeebe:ioMapping>
|
|
47
|
-
<zeebe:input source="=" --- " + task.prompt + (if (baseBranchBrief = null) then "" else baseBranchBrief)" target="appendPrompt" />
|
|
47
|
+
<zeebe:input source="=" --- " + task.prompt + (if (baseBranchBrief = null) then "" else baseBranchBrief) + (if (customInstructions = null) then "" else " --- ## Operator custom instructions The operator supplied these instructions for this run — follow them: " + customInstructions)" target="appendPrompt" />
|
|
48
48
|
<zeebe:output source="=status" target="status" />
|
|
49
49
|
<zeebe:output source="=question" target="question" />
|
|
50
50
|
</zeebe:ioMapping>
|