@nanobpm/nano-workforce 0.63.0 → 0.65.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/app/agentCompletion.test.ts +51 -0
- package/app/agentCompletion.ts +50 -12
- package/app/feature.ts +80 -2
- package/app/featureEscalation.test.ts +180 -0
- package/app/service.ts +45 -1
- package/db/migrations/031_feature_escalation_surface.sql +28 -0
- package/e2e/feature-run.e2e.ts +58 -0
- package/nano.app.json +6 -1
- package/openapi.yaml +52 -0
- package/operations/answerFeatureEscalation.ts +68 -0
- package/package.json +2 -2
- package/pages/feature.page.json +39 -3
- package/pages/overview.page.json +37 -2
- package/resources/processes/convergence-loop.bpmn +10 -0
- package/resources/processes/feature.bpmn +104 -58
- package/resources/processes/merge-loop.bpmn +2 -0
- package/resources/processes/plan-fanout.bpmn +42 -1
- package/resources/processes/retro.bpmn +20 -0
- package/scripts/pages-contract.test.ts +1 -1
- package/workers/answer-escalation/worker.ts +3 -5
- package/workers/arm-merge/worker.ts +3 -3
- package/workers/converge-feature/worker.ts +3 -5
- package/workers/ensure-base-branch/worker.ts +3 -4
- package/workers/finalize/worker.ts +4 -16
- package/workers/mark-merged/worker.ts +3 -3
- package/workers/merge/worker.ts +3 -11
- package/workers/persist-escalation/worker.ts +5 -22
- package/workers/persist-round/worker.ts +6 -16
- package/workers/record-dependency/worker.ts +5 -4
- package/workers/record-feature/worker.ts +9 -6
- package/workers/record-feature-escalation/worker.test.ts +74 -0
- package/workers/record-feature-escalation/worker.ts +42 -0
- package/workers/record-plan/worker.ts +3 -0
- package/workers/record-plan-review/worker.ts +3 -6
- package/workers/record-results/worker.ts +3 -3
- package/workers/record-trial-merge/worker.ts +4 -0
- package/workers/record-wave/worker.ts +3 -0
- package/workers/resolve-trial-attention/worker.ts +3 -5
- package/workers/retro-gather/worker.ts +3 -3
- package/workers/retro-record/worker.ts +4 -8
- package/workers/select-wave/worker.ts +3 -4
|
@@ -1,17 +1,46 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" id="Definitions_nano_workforce_plan" targetNamespace="http://nanobpm.io/nano-workforce">
|
|
2
|
+
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:nano="https://nanobpm.io/schema/shapes/1.0" id="Definitions_nano_workforce_plan" targetNamespace="http://nanobpm.io/nano-workforce">
|
|
3
3
|
<bpmn:message id="Message_waveMerged" name="wave-merged">
|
|
4
4
|
<bpmn:extensionElements>
|
|
5
5
|
<zeebe:subscription correlationKey="=planKey" />
|
|
6
6
|
</bpmn:extensionElements>
|
|
7
7
|
</bpmn:message>
|
|
8
8
|
<bpmn:process id="plan-fanout" name="Agent Fleet — plan & implement" isExecutable="true">
|
|
9
|
+
<bpmn:extensionElements>
|
|
10
|
+
<nano:shapes>
|
|
11
|
+
<nano:shape id="EnsureBaseBranchIn" name="Ensure base branch — input">
|
|
12
|
+
<nano:extend name="repo" type="string" />
|
|
13
|
+
<nano:extend name="baseBranch" type="string" />
|
|
14
|
+
</nano:shape>
|
|
15
|
+
<nano:shape id="RecordPlanReviewIn" name="Record plan review — input">
|
|
16
|
+
<nano:extend name="planKey" type="string" />
|
|
17
|
+
<nano:extend name="approved" type="boolean" optional="true" />
|
|
18
|
+
<nano:extend name="findings" type="string" optional="true" />
|
|
19
|
+
<nano:extend name="planReviewEpoch" type="integer" optional="true" />
|
|
20
|
+
</nano:shape>
|
|
21
|
+
<nano:shape id="SelectWaveIn" name="Select wave — input">
|
|
22
|
+
<nano:extend name="planKey" type="string" />
|
|
23
|
+
<nano:extend name="currentWave" type="integer" />
|
|
24
|
+
</nano:shape>
|
|
25
|
+
<nano:shape id="ResolveTrialAttentionIn" name="Clear trial attention — input">
|
|
26
|
+
<nano:extend name="planKey" type="string" />
|
|
27
|
+
<nano:extend name="currentWave" type="integer" optional="true" />
|
|
28
|
+
<nano:extend name="trialMergeWave" type="integer" optional="true" />
|
|
29
|
+
</nano:shape>
|
|
30
|
+
<nano:shape id="RecordResultsIn" name="Finalize plan — input">
|
|
31
|
+
<nano:extend name="planKey" type="string" />
|
|
32
|
+
</nano:shape>
|
|
33
|
+
</nano:shapes>
|
|
34
|
+
</bpmn:extensionElements>
|
|
9
35
|
<bpmn:startEvent id="Start" name="Issue submitted">
|
|
10
36
|
<bpmn:outgoing>f_toEnsureBase</bpmn:outgoing>
|
|
11
37
|
</bpmn:startEvent>
|
|
12
38
|
<bpmn:serviceTask id="ensure-base-branch" name="Ensure base branch">
|
|
13
39
|
<bpmn:extensionElements>
|
|
14
40
|
<zeebe:taskDefinition type="pr.ensure-base-branch" />
|
|
41
|
+
<zeebe:properties>
|
|
42
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="EnsureBaseBranchIn" />
|
|
43
|
+
</zeebe:properties>
|
|
15
44
|
<zeebe:ioMapping>
|
|
16
45
|
<zeebe:input source="=repo" target="repo" />
|
|
17
46
|
<zeebe:input source="=baseBranch" target="baseBranch" />
|
|
@@ -56,6 +85,9 @@
|
|
|
56
85
|
<bpmn:serviceTask id="record-plan-review" name="Record review & decide">
|
|
57
86
|
<bpmn:extensionElements>
|
|
58
87
|
<zeebe:taskDefinition type="pr.record-plan-review" />
|
|
88
|
+
<zeebe:properties>
|
|
89
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="RecordPlanReviewIn" />
|
|
90
|
+
</zeebe:properties>
|
|
59
91
|
</bpmn:extensionElements>
|
|
60
92
|
<bpmn:incoming>f_toRecordPlanReview</bpmn:incoming>
|
|
61
93
|
<bpmn:outgoing>f_toGwPlanReview</bpmn:outgoing>
|
|
@@ -93,6 +125,9 @@
|
|
|
93
125
|
<bpmn:serviceTask id="select-wave" name="Select wave">
|
|
94
126
|
<bpmn:extensionElements>
|
|
95
127
|
<zeebe:taskDefinition type="pr.select-wave" />
|
|
128
|
+
<zeebe:properties>
|
|
129
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="SelectWaveIn" />
|
|
130
|
+
</zeebe:properties>
|
|
96
131
|
</bpmn:extensionElements>
|
|
97
132
|
<bpmn:incoming>f_plan_proceed</bpmn:incoming>
|
|
98
133
|
<bpmn:incoming>f_plan_answer_proceed</bpmn:incoming>
|
|
@@ -238,6 +273,9 @@
|
|
|
238
273
|
<bpmn:serviceTask id="resolve-trial-attention" name="Clear trial attention">
|
|
239
274
|
<bpmn:extensionElements>
|
|
240
275
|
<zeebe:taskDefinition type="pr.resolve-trial-attention" />
|
|
276
|
+
<zeebe:properties>
|
|
277
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="ResolveTrialAttentionIn" />
|
|
278
|
+
</zeebe:properties>
|
|
241
279
|
</bpmn:extensionElements>
|
|
242
280
|
<bpmn:incoming>f_toGwTrialAnswer</bpmn:incoming>
|
|
243
281
|
<bpmn:outgoing>f_resolveToTrialAnswer</bpmn:outgoing>
|
|
@@ -263,6 +301,9 @@
|
|
|
263
301
|
<bpmn:serviceTask id="record-results" name="Finalize plan">
|
|
264
302
|
<bpmn:extensionElements>
|
|
265
303
|
<zeebe:taskDefinition type="pr.record-results" />
|
|
304
|
+
<zeebe:properties>
|
|
305
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="RecordResultsIn" />
|
|
306
|
+
</zeebe:properties>
|
|
266
307
|
</bpmn:extensionElements>
|
|
267
308
|
<bpmn:incoming>f_done</bpmn:incoming>
|
|
268
309
|
<bpmn:incoming>f_trialAnswerAbandon</bpmn:incoming>
|
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
2
|
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:nano="https://nanobpm.io/schema/shapes/1.0" id="Definitions_nano_workforce_retro" targetNamespace="http://nanobpm.io/nano-workforce">
|
|
3
3
|
<bpmn:process id="retro" name="Epic Retrospective" isExecutable="true">
|
|
4
|
+
<bpmn:extensionElements>
|
|
5
|
+
<nano:shapes>
|
|
6
|
+
<nano:shape id="RetroGatherIn" name="Gather learnings — input">
|
|
7
|
+
<nano:extend name="planKey" type="string" />
|
|
8
|
+
</nano:shape>
|
|
9
|
+
<nano:shape id="RetroRecordIn" name="Record retro — input">
|
|
10
|
+
<nano:extend name="planKey" type="string" />
|
|
11
|
+
<nano:extend name="retroLearnings" type="integer" optional="true" />
|
|
12
|
+
<nano:extend name="status" type="string" optional="true" />
|
|
13
|
+
<nano:extend name="pr" type="string" optional="true" />
|
|
14
|
+
<nano:extend name="summary" type="string" optional="true" />
|
|
15
|
+
</nano:shape>
|
|
16
|
+
</nano:shapes>
|
|
17
|
+
</bpmn:extensionElements>
|
|
4
18
|
<bpmn:startEvent id="Start" name="Epic complete">
|
|
5
19
|
<bpmn:outgoing>f_start</bpmn:outgoing>
|
|
6
20
|
</bpmn:startEvent>
|
|
7
21
|
<bpmn:serviceTask id="gather" name="Gather learnings">
|
|
8
22
|
<bpmn:extensionElements>
|
|
9
23
|
<zeebe:taskDefinition type="pr.retro-gather" />
|
|
24
|
+
<zeebe:properties>
|
|
25
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="RetroGatherIn" />
|
|
26
|
+
</zeebe:properties>
|
|
10
27
|
</bpmn:extensionElements>
|
|
11
28
|
<bpmn:incoming>f_start</bpmn:incoming>
|
|
12
29
|
<bpmn:outgoing>f_toSynthesize</bpmn:outgoing>
|
|
@@ -27,6 +44,9 @@
|
|
|
27
44
|
<bpmn:serviceTask id="record" name="Record retro">
|
|
28
45
|
<bpmn:extensionElements>
|
|
29
46
|
<zeebe:taskDefinition type="pr.retro-record" />
|
|
47
|
+
<zeebe:properties>
|
|
48
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="RetroRecordIn" />
|
|
49
|
+
</zeebe:properties>
|
|
30
50
|
</bpmn:extensionElements>
|
|
31
51
|
<bpmn:incoming>f_toRecord</bpmn:incoming>
|
|
32
52
|
<bpmn:outgoing>f_toEnd</bpmn:outgoing>
|
|
@@ -267,7 +267,7 @@ test("issue #205: overview is the landing page and first nav item", async () =>
|
|
|
267
267
|
"merging",
|
|
268
268
|
],
|
|
269
269
|
plans: ["planning", "dispatched"],
|
|
270
|
-
feature_runs: ["running", "awaiting_operator"],
|
|
270
|
+
feature_runs: ["running", "escalated", "awaiting_operator"],
|
|
271
271
|
};
|
|
272
272
|
const grids = (overview.nodes ?? []).filter((n: Json) => n.type === "dataGrid");
|
|
273
273
|
for (const [table, statuses] of Object.entries(expected)) {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// durable rows; it returns no variables, leaving the submitted `answer` untouched so it flows on to
|
|
18
18
|
// the next review round.
|
|
19
19
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
20
|
+
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
20
21
|
|
|
21
22
|
interface Escalation extends Record<string, unknown> {
|
|
22
23
|
id: number;
|
|
@@ -34,11 +35,8 @@ interface PullRequest extends Record<string, unknown> {
|
|
|
34
35
|
updated_at: string;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
answer?: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
38
|
+
// Input typed off the model data envelope (`PrAnswerEscalationIn` in convergence-loop.bpmn) — ADR 0040.
|
|
39
|
+
type In = WorkerInputs["pr.answer-escalation"];
|
|
42
40
|
// A string variable, or undefined when it is absent/blank. The form marks `answer` required, so a
|
|
43
41
|
// blank here would be an out-of-band completion; record it as NULL rather than an empty string.
|
|
44
42
|
function nonBlank(v: unknown): string | undefined {
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// evaluates its mergeability and correlates `merge-ready`. Reached both on entry to the merge
|
|
3
3
|
// stage (after dependencies clear) and after a human answers a merge escalation (re-check).
|
|
4
4
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
5
|
+
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
7
|
+
// Input typed off the model data envelope (`MergeArmIn` in merge-loop.bpmn) — ADR 0040.
|
|
8
|
+
type In = WorkerInputs["pr.arm-merge"];
|
|
9
9
|
|
|
10
10
|
const handler: AppJobHandler<In> = async (job, app) => {
|
|
11
11
|
await app.data.table("pull_requests", "pr_key").update(job.variables.prKey, {
|
|
@@ -13,12 +13,10 @@
|
|
|
13
13
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
14
14
|
import { featureRuns } from "../../app/feature.ts";
|
|
15
15
|
import { MAX_ROUNDS, parsePr, submitPr } from "../../app/service.ts";
|
|
16
|
+
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
prKey?: unknown;
|
|
20
|
-
autoMerge?: unknown;
|
|
21
|
-
}
|
|
18
|
+
// Input typed off the model data envelope (`ConvergeFeatureIn` in feature.bpmn) — ADR 0040.
|
|
19
|
+
type In = WorkerInputs["pr.converge-feature"];
|
|
22
20
|
|
|
23
21
|
const handler: AppJobHandler<In, Record<string, never>> = async (job, app) => {
|
|
24
22
|
const featureKey = job.variables.featureKey;
|
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
// wrong-rooted branch).
|
|
11
11
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
12
12
|
import { type EnsureBaseBranchResult, ensureBaseBranch } from "../../app/github.ts";
|
|
13
|
+
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
baseBranch: string;
|
|
17
|
-
}
|
|
15
|
+
// Input typed off the model data envelope (`EnsureBaseBranchIn` in plan-fanout.bpmn) — ADR 0040.
|
|
16
|
+
type In = WorkerInputs["pr.ensure-base-branch"];
|
|
18
17
|
interface Out extends Record<string, unknown> {
|
|
19
18
|
baseBranchResult: EnsureBaseBranchResult;
|
|
20
19
|
}
|
|
@@ -5,23 +5,11 @@ import type { AppJobHandler } from "@nanobpm/urban";
|
|
|
5
5
|
import { abandonTokenFromUrl } from "../../app/abandon.ts";
|
|
6
6
|
import { maybeStartRetro } from "../../app/retro.ts";
|
|
7
7
|
import { AUTO_MERGE, ensurePr, startMerge } from "../../app/service.ts";
|
|
8
|
+
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
8
9
|
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
prKey: string;
|
|
13
|
-
repo: string;
|
|
14
|
-
prNumber: number;
|
|
15
|
-
prUrl: string;
|
|
16
|
-
round: number;
|
|
17
|
-
summary?: string;
|
|
18
|
-
// Per-request review-only override: when true, stop at `converged` and never hand off to the
|
|
19
|
-
// merge-loop even if auto-merge is on globally. Set at submit time, carried on the instance.
|
|
20
|
-
convergeOnly?: boolean;
|
|
21
|
-
// The per-PR abandon capability URL the agent was handed; its token is preserved on a heal so
|
|
22
|
-
// the agent's cooperative-abort check keeps resolving (see ensurePr).
|
|
23
|
-
abandonUrl?: string;
|
|
24
|
-
}
|
|
10
|
+
// Input typed off the model data envelope (`PrFinalizeIn` in convergence-loop.bpmn) — ADR 0040.
|
|
11
|
+
// Framework-injected `io.nanobpm.agentResult` is read through the `Record`-typed `transcriptOf`.
|
|
12
|
+
type In = WorkerInputs["pr.finalize"];
|
|
25
13
|
|
|
26
14
|
const AGENT_RESULT_KEY = "io.nanobpm.agentResult";
|
|
27
15
|
function transcriptOf(vars: Record<string, unknown>): string | null {
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// `merged` state; the merge audit trail is written by pr.merge, so this only closes the row out.
|
|
3
3
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
4
4
|
import { maybeStartRetro } from "../../app/retro.ts";
|
|
5
|
+
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
7
|
+
// Input typed off the model data envelope (`MarkMergedIn` in merge-loop.bpmn) — ADR 0040.
|
|
8
|
+
type In = WorkerInputs["pr.mark-merged"];
|
|
9
9
|
|
|
10
10
|
const handler: AppJobHandler<In> = async (job, app) => {
|
|
11
11
|
const now = new Date().toISOString();
|
package/workers/merge/worker.ts
CHANGED
|
@@ -15,18 +15,10 @@ import { checkBaseTarget, classifyBaseGuard } from "../../app/baseGuard.ts";
|
|
|
15
15
|
import { enqueueViaComment, fetchPrState, mergePr } from "../../app/github.ts";
|
|
16
16
|
import { classifyMergeLanding, DEFAULT_MERGE_PROTOCOL, loadMergeProtocol } from "../../app/mergeProtocol.ts";
|
|
17
17
|
import { ensurePr, MERGE_ADMIN, MERGE_METHOD } from "../../app/service.ts";
|
|
18
|
+
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
repo: string;
|
|
22
|
-
prNumber: number;
|
|
23
|
-
// Carried by the merge-loop instance (startMerge sets it to the converged round); passed to
|
|
24
|
-
// the heal so a reconstructed row reflects the real round, not a 1-based default.
|
|
25
|
-
round?: number;
|
|
26
|
-
// The per-PR abandon capability URL the agent was handed; its token is preserved on a heal so
|
|
27
|
-
// the agent's cooperative-abort check keeps resolving (see ensurePr).
|
|
28
|
-
abandonUrl?: string;
|
|
29
|
-
}
|
|
20
|
+
// Input typed off the model data envelope (`MergeAttemptIn` in merge-loop.bpmn) — ADR 0040.
|
|
21
|
+
type In = WorkerInputs["pr.merge"];
|
|
30
22
|
|
|
31
23
|
interface Out extends Record<string, unknown> {
|
|
32
24
|
mergeStatus: "merged" | "queued" | "blocked";
|
|
@@ -17,29 +17,12 @@ import type { AppJobHandler } from "@nanobpm/urban";
|
|
|
17
17
|
import { abandonTokenFromUrl } from "../../app/abandon.ts";
|
|
18
18
|
import { classifyEscalation } from "../../app/escalationTaxonomy.ts";
|
|
19
19
|
import { ensurePr, parsePr } from "../../app/service.ts";
|
|
20
|
+
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
20
21
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
round: number;
|
|
26
|
-
status?: string;
|
|
27
|
-
summary?: string;
|
|
28
|
-
question?: string;
|
|
29
|
-
// Carried by the convergence-loop instance so a missing `pull_requests` parent can be
|
|
30
|
-
// reconstructed before the FK-child `rounds`/`escalations` inserts.
|
|
31
|
-
repo?: string;
|
|
32
|
-
prNumber?: number;
|
|
33
|
-
prUrl?: string;
|
|
34
|
-
// The per-PR abandon capability URL the agent was handed; its token is preserved on a heal so
|
|
35
|
-
// the agent's cooperative-abort check keeps resolving (see ensurePr).
|
|
36
|
-
abandonUrl?: string;
|
|
37
|
-
// False on the "review stalled" arm: `persist-round` already recorded this `round` as
|
|
38
|
-
// `addressed`, so this escalation must not insert a second `rounds` row for the same
|
|
39
|
-
// `pr_key`/`round_no` (which would record one round as both addressed and blocked). Absent
|
|
40
|
-
// on the agent-raised / max-rounds arms, where no prior round row exists — so it defaults on.
|
|
41
|
-
recordRound?: boolean;
|
|
42
|
-
}
|
|
22
|
+
// Input typed off the model data envelope (`EscalationIn` in convergence-loop.bpmn) — ADR 0040.
|
|
23
|
+
// Framework-injected variables (`io.nanobpm.agentResult`, `agent`) are read through the
|
|
24
|
+
// `Record`-typed helpers below rather than the envelope.
|
|
25
|
+
type In = WorkerInputs["pr.persist-escalation"];
|
|
43
26
|
|
|
44
27
|
// A string variable, or undefined when it is absent, empty, or whitespace-only.
|
|
45
28
|
// The write boundary owns *type* defaults (undefined -> column DEFAULT/NULL); this
|
|
@@ -7,23 +7,13 @@
|
|
|
7
7
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
8
8
|
import { abandonTokenFromUrl } from "../../app/abandon.ts";
|
|
9
9
|
import { ensurePr, parsePr } from "../../app/service.ts";
|
|
10
|
+
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
10
11
|
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
status?: string;
|
|
17
|
-
summary?: string;
|
|
18
|
-
// Carried by the convergence-loop instance (set at createInstance) so a missing
|
|
19
|
-
// `pull_requests` parent can be reconstructed before the FK-child `rounds` insert.
|
|
20
|
-
repo?: string;
|
|
21
|
-
prNumber?: number;
|
|
22
|
-
prUrl?: string;
|
|
23
|
-
// The per-PR abandon capability URL the review agent was handed; its token is preserved on a
|
|
24
|
-
// heal so the agent's cooperative-abort check keeps resolving (see ensurePr).
|
|
25
|
-
abandonUrl?: string;
|
|
26
|
-
}
|
|
12
|
+
// Input is typed off the model data envelope (`PrPersistRoundIn` in convergence-loop.bpmn),
|
|
13
|
+
// the single source of truth for this worker's wire contract (ADR 0040). Framework-injected
|
|
14
|
+
// variables the handler still reads (`io.nanobpm.agentResult`, `agent`) are accessed through the
|
|
15
|
+
// `Record<string, unknown>`-typed helpers below rather than the envelope.
|
|
16
|
+
type In = WorkerInputs["pr.persist-round"];
|
|
27
17
|
|
|
28
18
|
// The harness records the agent's full (byte-capped) stdout on the result envelope; keep it
|
|
29
19
|
// for audit so a human can see what the agent did this round.
|
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
// already-recorded edge is a no-op.
|
|
14
14
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
15
15
|
import { ensurePr, parsePr } from "../../app/service.ts";
|
|
16
|
+
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
// Input typed off the model data envelope (`RecordDepIn` in merge-loop.bpmn) — ADR 0040. The
|
|
19
|
+
// scalar `dependsOn` carries the agent's `owner/repo#N` refs as one string; `parseDependsOn` below
|
|
20
|
+
// still tolerates the legacy array shape defensively.
|
|
21
|
+
type In = WorkerInputs["pr.record-dependency"];
|
|
21
22
|
|
|
22
23
|
interface DependencyRow {
|
|
23
24
|
pr_key: string;
|
|
@@ -14,13 +14,10 @@
|
|
|
14
14
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
15
15
|
import { type FeatureRunStatus, featureRuns } from "../../app/feature.ts";
|
|
16
16
|
import { parsePr } from "../../app/service.ts";
|
|
17
|
+
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
status?: unknown;
|
|
21
|
-
pr?: unknown;
|
|
22
|
-
summary?: unknown;
|
|
23
|
-
}
|
|
19
|
+
// Input typed off the model data envelope (`RecordFeatureIn` in feature.bpmn) — ADR 0040.
|
|
20
|
+
type In = WorkerInputs["pr.record-feature"];
|
|
24
21
|
interface Out extends Record<string, unknown> {
|
|
25
22
|
featureStatus: FeatureRunStatus;
|
|
26
23
|
prKey: string | null;
|
|
@@ -57,6 +54,12 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
|
|
|
57
54
|
status: rowStatus,
|
|
58
55
|
pr_key: prKey,
|
|
59
56
|
outcome: summary ?? null,
|
|
57
|
+
// The run has left the `feature-escalation` wait (answered → abandon, SLA auto-abandon, or the
|
|
58
|
+
// agent finished without escalating), so clear the denormalised escalation pointer the pages gate
|
|
59
|
+
// on. pollFeatureEscalations only sweeps `running`/`escalated` runs, so a terminal-ward transition
|
|
60
|
+
// through here must clear it itself or the stale question would linger on the row.
|
|
61
|
+
escalation_question: null,
|
|
62
|
+
escalation_user_task_key: null,
|
|
60
63
|
updated_at: ts,
|
|
61
64
|
});
|
|
62
65
|
app.log.info("record-feature", { featureKey, featureStatus, rowStatus, prKey });
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Unit coverage for pr.record-feature-escalation — a feature run parked on the native
|
|
2
|
+
// `feature-escalation` user task (issue #210). Runs on the `escalated` arm, before the user task
|
|
3
|
+
// exists, and must flip the row to the non-terminal `escalated` status and persist the agent's
|
|
4
|
+
// `question` so the nwf UI can surface it (the poller can't read task-local vars, so this is the
|
|
5
|
+
// question's source of truth). It clears the completable-task pointer to NULL (the task does not
|
|
6
|
+
// exist yet, so any non-null value is stale); the poller fills the real key once it is observable.
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import { assertEquals } from "#test-assert";
|
|
9
|
+
import { noopLog } from "../../test/log.ts";
|
|
10
|
+
import handler from "./worker.ts";
|
|
11
|
+
|
|
12
|
+
// biome-ignore lint/suspicious/noExplicitAny: tiny in-memory app double, mirrors record-blocked-ack.worker.test
|
|
13
|
+
function fakeApp(rows: Record<string, unknown>[]): any {
|
|
14
|
+
const stores: Record<string, Record<string, unknown>[]> = { feature_runs: rows };
|
|
15
|
+
return {
|
|
16
|
+
data: {
|
|
17
|
+
table(name: string, key: string) {
|
|
18
|
+
const store = (stores[name] ??= []);
|
|
19
|
+
return {
|
|
20
|
+
// biome-ignore lint/suspicious/noExplicitAny: test double
|
|
21
|
+
get: (k: any) => Promise.resolve(store.find((r) => r[key] === k)),
|
|
22
|
+
// biome-ignore lint/suspicious/noExplicitAny: test double
|
|
23
|
+
find: (q: any) => Promise.resolve(store.filter((r) => Object.entries(q).every(([f, v]) => r[f] === v))),
|
|
24
|
+
// biome-ignore lint/suspicious/noExplicitAny: test double
|
|
25
|
+
insert: (row: any) => {
|
|
26
|
+
store.push(row);
|
|
27
|
+
return Promise.resolve(store.length);
|
|
28
|
+
},
|
|
29
|
+
// biome-ignore lint/suspicious/noExplicitAny: test double
|
|
30
|
+
update: (k: any, patch: any) => {
|
|
31
|
+
const row = store.find((r) => r[key] === k);
|
|
32
|
+
if (row) Object.assign(row, patch);
|
|
33
|
+
return Promise.resolve(row);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
log: noopLog(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test("record-feature-escalation: flips the run to escalated and persists the question", async () => {
|
|
43
|
+
const rows = [{ feature_key: "owner/repo#7", status: "running", escalation_question: null, escalation_user_task_key: null }];
|
|
44
|
+
const app = fakeApp(rows);
|
|
45
|
+
const out = await handler(
|
|
46
|
+
{ variables: { featureKey: "owner/repo#7", question: "Which API should I use?" } } as never,
|
|
47
|
+
app,
|
|
48
|
+
);
|
|
49
|
+
assertEquals(out, {});
|
|
50
|
+
assertEquals(rows[0].status, "escalated");
|
|
51
|
+
assertEquals(rows[0].escalation_question, "Which API should I use?");
|
|
52
|
+
// The user task does not exist yet — the pointer is cleared to NULL here (poller fills the real key).
|
|
53
|
+
assertEquals(rows[0].escalation_user_task_key, null);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("record-feature-escalation: clears a stale completable-task pointer at escalation entry", async () => {
|
|
57
|
+
// The task does not exist yet here, so a non-null key can only be stale (a prior escalation's key
|
|
58
|
+
// left behind, a manual DB repair, etc). Leaving it would bind the pages' answer/abandon affordance
|
|
59
|
+
// to the wrong task until the poller overwrites it; clear it so the row reads "key unknown here".
|
|
60
|
+
const rows = [{ feature_key: "owner/repo#9", status: "running", escalation_question: null, escalation_user_task_key: "stale-ut" }];
|
|
61
|
+
const app = fakeApp(rows);
|
|
62
|
+
await handler({ variables: { featureKey: "owner/repo#9", question: "Q?" } } as never, app);
|
|
63
|
+
assertEquals(rows[0].status, "escalated");
|
|
64
|
+
assertEquals(rows[0].escalation_question, "Q?");
|
|
65
|
+
assertEquals(rows[0].escalation_user_task_key, null);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("record-feature-escalation: a blank/absent question is persisted as NULL (badge/affordance stay off)", async () => {
|
|
69
|
+
const rows = [{ feature_key: "owner/repo#8", status: "running", escalation_question: null }];
|
|
70
|
+
const app = fakeApp(rows);
|
|
71
|
+
await handler({ variables: { featureKey: "owner/repo#8", question: " " } } as never, app);
|
|
72
|
+
assertEquals(rows[0].status, "escalated");
|
|
73
|
+
assertEquals(rows[0].escalation_question, null);
|
|
74
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// pr.record-feature-escalation — a feature run has parked on the native `feature-escalation`
|
|
2
|
+
// user task (the agent reported `status:"escalated"` with a non-blank `question`). This service
|
|
3
|
+
// task runs on the `escalated` arm, immediately BEFORE the user task is created, and persists the
|
|
4
|
+
// escalation onto the `feature_runs` row so the nwf UI can surface it (issue #210):
|
|
5
|
+
// • flips `status` to the non-terminal `escalated` so status-based views and counts flag it, and
|
|
6
|
+
// • denormalises the agent's `question` for the Escalation column + the answer affordance.
|
|
7
|
+
//
|
|
8
|
+
// It deliberately does NOT record a REAL completable `userTaskKey` — the task does not exist yet at
|
|
9
|
+
// this point — and clears any stale pointer so the row reads "key unknown until observed".
|
|
10
|
+
// `pollFeatureEscalations` (app/service.ts) fills that pointer in once the user task is
|
|
11
|
+
// observable via `searchUserTasks`, which is also the reason the question is captured HERE rather
|
|
12
|
+
// than by the poller: the WASM testkit engine does not surface a user task's ioMapping-mapped local
|
|
13
|
+
// variables through `searchUserTasks`, so the process variable must be persisted while it is still
|
|
14
|
+
// in scope on the job.
|
|
15
|
+
import type { AppJobHandler } from "@nanobpm/urban";
|
|
16
|
+
import { featureRuns } from "../../app/feature.ts";
|
|
17
|
+
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
18
|
+
|
|
19
|
+
// Input typed off the model data envelope (`RecordFeatureEscalationIn` in feature.bpmn) — ADR 0040.
|
|
20
|
+
type In = WorkerInputs["pr.record-feature-escalation"];
|
|
21
|
+
|
|
22
|
+
const nonBlank = (v: unknown): string | null =>
|
|
23
|
+
typeof v === "string" && v.trim().length > 0 ? v.trim() : null;
|
|
24
|
+
|
|
25
|
+
const handler: AppJobHandler<In> = async (job, app) => {
|
|
26
|
+
const featureKey = job.variables.featureKey;
|
|
27
|
+
const question = nonBlank(job.variables.question);
|
|
28
|
+
await featureRuns(app.data).update(featureKey, {
|
|
29
|
+
status: "escalated",
|
|
30
|
+
escalation_question: question,
|
|
31
|
+
// The user task does not exist yet, so any non-null pointer here can only be stale (a prior
|
|
32
|
+
// escalation's key, a manual DB repair). Clear it so the row reads "key unknown until observed"
|
|
33
|
+
// and the pages don't bind the answer/abandon affordance to a dead task; the poller re-fills the
|
|
34
|
+
// real key (it can always re-derive it from `searchUserTasks`), so this clear is never lossy.
|
|
35
|
+
escalation_user_task_key: null,
|
|
36
|
+
updated_at: new Date().toISOString(),
|
|
37
|
+
});
|
|
38
|
+
app.log.info("record-feature-escalation", { featureKey, hasQuestion: question !== null });
|
|
39
|
+
return {};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default handler;
|
|
@@ -25,6 +25,9 @@ interface RawTask {
|
|
|
25
25
|
prompt?: unknown;
|
|
26
26
|
dependsOn?: unknown;
|
|
27
27
|
}
|
|
28
|
+
// NOTE (issue #211): this worker reads an ARRAY input (`tasks: RawTask[]`), which the scalar-only
|
|
29
|
+
// `nano:dataEnvelope` cannot express, so it keeps a hand-written `interface In` pending a decision
|
|
30
|
+
// on extending the envelope schema to arrays. See the PR body's "array inputs" escalation.
|
|
28
31
|
interface In extends Record<string, unknown> {
|
|
29
32
|
planKey: string;
|
|
30
33
|
tasks?: RawTask[];
|
|
@@ -24,13 +24,10 @@ import {
|
|
|
24
24
|
type PlanReview,
|
|
25
25
|
planReviews,
|
|
26
26
|
} from "../../app/plan.ts";
|
|
27
|
+
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
approved?: unknown;
|
|
31
|
-
findings?: unknown;
|
|
32
|
-
planReviewEpoch?: unknown;
|
|
33
|
-
}
|
|
29
|
+
// Input typed off the model data envelope (`RecordPlanReviewIn` in plan-fanout.bpmn) — ADR 0040.
|
|
30
|
+
type In = WorkerInputs["pr.record-plan-review"];
|
|
34
31
|
interface Out extends Record<string, unknown> {
|
|
35
32
|
planApproved: boolean;
|
|
36
33
|
planEscalated: boolean;
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
15
15
|
import { BpmnError } from "@nanobpm/urban";
|
|
16
16
|
import { planTasks } from "../../app/plan.ts";
|
|
17
|
+
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
19
|
+
// Input typed off the model data envelope (`RecordResultsIn` in plan-fanout.bpmn) — ADR 0040.
|
|
20
|
+
type In = WorkerInputs["pr.record-results"];
|
|
21
21
|
|
|
22
22
|
const handler: AppJobHandler<In> = async (job, app) => {
|
|
23
23
|
const { planKey } = job.variables;
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
trialMergeTaskId,
|
|
10
10
|
} from "../../app/trialMerge.ts";
|
|
11
11
|
|
|
12
|
+
// NOTE (issue #211): this worker reads ARRAY inputs (`waveOpenHeads`, `conflicts`, `failing`), which
|
|
13
|
+
// the scalar-only `nano:dataEnvelope` cannot express, so it keeps a hand-written `interface In`
|
|
14
|
+
// pending a decision on extending the envelope schema to arrays. See the PR body's "array inputs"
|
|
15
|
+
// escalation.
|
|
12
16
|
interface In extends Record<string, unknown> {
|
|
13
17
|
planKey: string;
|
|
14
18
|
currentWave?: unknown;
|
|
@@ -38,6 +38,9 @@ interface Result {
|
|
|
38
38
|
interface WaveTaskIn {
|
|
39
39
|
id?: unknown;
|
|
40
40
|
}
|
|
41
|
+
// NOTE (issue #211): this worker reads ARRAY inputs (`waveTasks[]`, `waveResults[]`), which the
|
|
42
|
+
// scalar-only `nano:dataEnvelope` cannot express, so it keeps a hand-written `interface In` pending
|
|
43
|
+
// a decision on extending the envelope schema to arrays. See the PR body's "array inputs" escalation.
|
|
41
44
|
interface In extends Record<string, unknown> {
|
|
42
45
|
planKey: string;
|
|
43
46
|
currentWave: number;
|
|
@@ -14,12 +14,10 @@
|
|
|
14
14
|
// and logged rather than thrown.
|
|
15
15
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
16
16
|
import { resolveTrialMergeAttention } from "../../app/trialMerge.ts";
|
|
17
|
+
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
currentWave?: unknown;
|
|
21
|
-
trialMergeWave?: unknown;
|
|
22
|
-
}
|
|
19
|
+
// Input typed off the model data envelope (`ResolveTrialAttentionIn` in plan-fanout.bpmn) — ADR 0040.
|
|
20
|
+
type In = WorkerInputs["pr.resolve-trial-attention"];
|
|
23
21
|
|
|
24
22
|
const waveNo = (v: unknown): number => {
|
|
25
23
|
const n = Math.trunc(Number(v));
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
// agent reflects on real material rather than re-deriving it.
|
|
6
6
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
7
7
|
import { gatherRetro, renderRetroBrief } from "../../app/retro.ts";
|
|
8
|
+
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
10
|
+
// Input typed off the model data envelope (`RetroGatherIn` in retro.bpmn) — ADR 0040.
|
|
11
|
+
type In = WorkerInputs["pr.retro-gather"];
|
|
12
12
|
|
|
13
13
|
interface Out extends Record<string, unknown> {
|
|
14
14
|
retroDigest: string;
|