@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 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
 
@@ -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
- const result = await startFeature(app.data, app.engine, parsed, normalizedBase, converge, autoMerge);
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.73.1",
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",
@@ -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="=&#34;&#10;&#10;---&#10;&#10;&#34; + task.prompt + (if (baseBranchBrief = null) then &#34;&#34; else baseBranchBrief)" target="appendPrompt" />
47
+ <zeebe:input source="=&#34;&#10;&#10;---&#10;&#10;&#34; + task.prompt + (if (baseBranchBrief = null) then &#34;&#34; else baseBranchBrief) + (if (customInstructions = null) then &#34;&#34; else &#34;&#10;&#10;---&#10;&#10;## Operator custom instructions&#10;&#10;The operator supplied these instructions for this run — follow them:&#10;&#10;&#34; + customInstructions)" target="appendPrompt" />
48
48
  <zeebe:output source="=status" target="status" />
49
49
  <zeebe:output source="=question" target="question" />
50
50
  </zeebe:ioMapping>