@nanobpm/nano-workforce 0.209.1 → 0.210.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.
@@ -108,3 +108,53 @@ test("a head read that resolves no SHA degrades to the checkpoint restore (no ov
108
108
  await makeHandler(submit, head)({ processInstanceKey: "4242", variables: { prKey: "acme/w#7" } } as any, app);
109
109
  assertEquals(args[11], null, "no live head SHA → no override (checkpoint restore, the pre-fix behaviour)");
110
110
  });
111
+
112
+ // Issue #956 review (Copilot thread 4547353262): a merge-stage reconverge re-enters `submitPr`
113
+ // WITHOUT the run's operator custom instructions, so the replacement convergence loop runs
114
+ // unsteered (and `submitPr`'s run-scoped reopen resets `custom_instructions` to 0). The worker must
115
+ // read the durable text off the PR row (`custom_instructions_text`, stamped by `submitPr`) and pass
116
+ // it through, so convergence → merge → reconvergence keeps the steering.
117
+ test("preserves the run's custom instructions across the merge→reconvergence handoff (#956)", async () => {
118
+ const app = {
119
+ data: {
120
+ table: (name: string) => ({
121
+ get: async () =>
122
+ name === "pull_requests"
123
+ ? { auto_resolve_escalations: 1, custom_instructions_text: " commit as josh.wulf@camunda.com " }
124
+ : undefined,
125
+ insert: async () => 1,
126
+ }),
127
+ },
128
+ engine: {},
129
+ log: noopLog(),
130
+ } as any;
131
+ let args: unknown[] = [];
132
+ const submit = (async (...a: unknown[]) => {
133
+ args = a;
134
+ return { prKey: "acme/w#7", processKey: "999" };
135
+ }) as unknown as Submit;
136
+ const head = (async () => null) as unknown as FetchPrHead;
137
+ await makeHandler(submit, head)({ processInstanceKey: "4242", variables: { prKey: "acme/w#7" } } as any, app);
138
+ assertEquals(args[12], " commit as josh.wulf@camunda.com ", "the durable instruction text rides the handoff into the fresh convergence run");
139
+ });
140
+
141
+ test("a PR row with no stored instructions hands off with null (no steering)", async () => {
142
+ const app = {
143
+ data: {
144
+ table: (name: string) => ({
145
+ get: async () => (name === "pull_requests" ? { auto_resolve_escalations: 0, custom_instructions_text: null } : undefined),
146
+ insert: async () => 1,
147
+ }),
148
+ },
149
+ engine: {},
150
+ log: noopLog(),
151
+ } as any;
152
+ let args: unknown[] = [];
153
+ const submit = (async (...a: unknown[]) => {
154
+ args = a;
155
+ return { prKey: "acme/w#7", processKey: "999" };
156
+ }) as unknown as Submit;
157
+ const head = (async () => null) as unknown as FetchPrHead;
158
+ await makeHandler(submit, head)({ processInstanceKey: "4242", variables: { prKey: "acme/w#7" } } as any, app);
159
+ assertEquals(args[12], null, "no stored text → null (the fresh run's prompts stay byte-identical to no-instructions)");
160
+ });
@@ -27,6 +27,12 @@ export function makeHandler(submit: Submit, fetchHead: FetchPrHead = fetchPrHead
27
27
  const row = await app.data.table("pull_requests", "pr_key").get(prKey);
28
28
  // biome-ignore lint/plugin: runtime/framework contract boundary for the untyped row shape
29
29
  const optIn = (row as { auto_resolve_escalations?: number } | undefined)?.auto_resolve_escalations === 1;
30
+ // Operator steering (issue #956 review): re-supply the run's durable instruction text so the
31
+ // merge→reconvergence handoff never starts an unsteered replacement loop (and never resets the
32
+ // `custom_instructions` projection). Read the SAME `pull_requests` row the opt-in came from.
33
+ // biome-ignore lint/plugin: runtime/framework contract boundary for the untyped row shape
34
+ const rowInstructions = (row as { custom_instructions_text?: string | null } | undefined)?.custom_instructions_text;
35
+ const customInstructions = typeof rowInstructions === "string" && rowInstructions.trim() !== "" ? rowInstructions : null;
30
36
  await app.data.table("merges", "id").insert({
31
37
  pr_key: prKey,
32
38
  outcome: "reconverge",
@@ -63,6 +69,7 @@ export function makeHandler(submit: Submit, fetchHead: FetchPrHead = fetchPrHead
63
69
  true,
64
70
  job.processInstanceKey != null ? String(job.processInstanceKey) : null,
65
71
  headSha,
72
+ customInstructions,
66
73
  );
67
74
  // `submitPr` can return `processKey: null` after rewriting the row to `converging` with no owner
68
75
  // (the create-instance failure window). Completing HERE would end the merge loop at