@nanobpm/nano-workforce 0.35.2 → 0.37.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/.github/workflows/ci.yml +10 -17
- package/AGENTS.md +8 -7
- package/CHANGELOG.md +14 -0
- package/README.md +1 -21
- package/actions/abandon.test.ts +8 -16
- package/actions/blackboard.test.ts +13 -21
- package/actions/blackboard.ts +1 -0
- package/actions/feature-answer-hook.ts +1 -0
- package/actions/plan-hook.ts +2 -1
- package/actions/webhook-submit.ts +2 -1
- package/app/abandon.test.ts +10 -15
- package/app/baseGuard.test.ts +7 -6
- package/app/blackboard.test.ts +22 -32
- package/app/blackboard.ts +7 -6
- package/app/ensure-pr.test.ts +10 -12
- package/app/github.test.ts +9 -8
- package/app/github.ts +24 -22
- package/app/instance-tracking.test.ts +8 -6
- package/app/mergeExclusion.test.ts +11 -20
- package/app/mergeExclusion.ts +8 -3
- package/app/mergeProtocol.test.ts +14 -13
- package/app/mergeProtocol.ts +1 -0
- package/app/mergeRebaseArm.test.ts +8 -6
- package/app/mergeTrain.test.ts +10 -9
- package/app/persist-escalation.test.ts +9 -30
- package/app/persist-round.test.ts +6 -19
- package/app/plan.test.ts +19 -42
- package/app/plan.ts +1 -1
- package/app/record-plan-review.test.ts +8 -7
- package/app/retro.test.ts +24 -48
- package/app/retro.ts +4 -2
- package/app/reviewWait.test.ts +12 -11
- package/app/rounds.test.ts +13 -12
- package/app/service.test.ts +14 -27
- package/app/service.ts +8 -4
- package/app/taskDelta.test.ts +8 -17
- package/app/taskDelta.ts +1 -0
- package/app/trialMerge.test.ts +4 -3
- package/app/version.ts +9 -21
- package/app/waves.test.ts +17 -16
- package/biome.json +143 -0
- package/operations/getVersion.test.ts +8 -12
- package/operations/getVersion.ts +2 -3
- package/operations/listActivePrs.test.ts +8 -14
- package/operations/listActivePrs.ts +2 -3
- package/operations/postMessage.ts +3 -3
- package/operations/startAndMessage.test.ts +6 -12
- package/package.json +9 -4
- package/plugins/no-unsafe-type-assertion.grit +12 -0
- package/scripts/check-agent-prompts.test.ts +15 -11
- package/scripts/check-agent-prompts.ts +4 -2
- package/scripts/layout-bpmn.ts +6 -26
- package/scripts/pages-contract.test.ts +14 -13
- package/scripts/purge-db.ts +3 -2
- package/scripts/upgrade-from-pack.ts +9 -5
- package/test/assert.ts +74 -0
- package/tsconfig.json +4 -1
- package/workers/finalize/worker.ts +2 -1
- package/workers/merge/worker.ts +3 -3
- package/workers/persist-escalation/worker.ts +2 -1
- package/workers/persist-round/worker.ts +2 -1
- package/workers/record-plan-review/worker.test.ts +6 -9
- package/workers/record-plan-review/worker.ts +2 -1
- package/workers/record-results/worker.test.ts +5 -8
- package/workers/record-results/worker.ts +2 -1
- package/workers/record-trial-merge/worker.test.ts +7 -18
- package/workers/record-trial-merge/worker.ts +6 -6
- package/workers/record-wave/worker.test.ts +13 -20
- package/workers/record-wave/worker.ts +7 -8
- package/workers/retro-gather/worker.test.ts +4 -17
- package/workers/retro-record/worker.test.ts +8 -27
- package/workers/retro-record/worker.ts +3 -3
- package/workers/select-wave/worker.test.ts +5 -8
- package/deno.json +0 -24
- package/deno.lock +0 -1776
|
@@ -1,35 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import { assertEquals } from "#test-assert";
|
|
2
3
|
import handler from "./worker.ts";
|
|
3
4
|
|
|
4
5
|
function fakeApp() {
|
|
5
|
-
// deno-lint-ignore no-explicit-any
|
|
6
6
|
const stores: Record<string, any[]> = { plan_retros: [] };
|
|
7
7
|
const seq: Record<string, number> = {};
|
|
8
8
|
function tbl(name: string, pk = "id") {
|
|
9
|
-
// deno-lint-ignore no-explicit-any
|
|
10
9
|
const rows = (stores[name] ??= [] as any[]);
|
|
11
|
-
// deno-lint-ignore no-explicit-any
|
|
12
10
|
const match = (r: any, where: any) => Object.entries(where).every(([k, v]) => r[k] === v);
|
|
13
11
|
return {
|
|
14
|
-
// deno-lint-ignore no-explicit-any require-await
|
|
15
12
|
async insert(row: any) {
|
|
16
13
|
const id = (seq[name] = (seq[name] ?? 0) + 1);
|
|
17
14
|
rows.push(pk === "id" ? { id, ...row } : { ...row });
|
|
18
15
|
return pk === "id" ? id : row[pk];
|
|
19
16
|
},
|
|
20
|
-
// deno-lint-ignore no-explicit-any require-await
|
|
21
17
|
async find(where: any = {}) {
|
|
22
18
|
return rows.filter((r) => match(r, where));
|
|
23
19
|
},
|
|
24
|
-
// deno-lint-ignore no-explicit-any require-await
|
|
25
20
|
async findOne(where: any = {}) {
|
|
26
21
|
return rows.find((r) => match(r, where));
|
|
27
22
|
},
|
|
28
|
-
// deno-lint-ignore no-explicit-any require-await
|
|
29
23
|
async get(id: any) {
|
|
30
24
|
return rows.find((row) => row[pk] === id);
|
|
31
25
|
},
|
|
32
|
-
// deno-lint-ignore no-explicit-any require-await
|
|
33
26
|
async update(id: any, patch: any) {
|
|
34
27
|
const r = rows.find((row) => row[pk] === id);
|
|
35
28
|
if (r) Object.assign(r, patch);
|
|
@@ -40,10 +33,9 @@ function fakeApp() {
|
|
|
40
33
|
return { app, stores };
|
|
41
34
|
}
|
|
42
35
|
|
|
43
|
-
|
|
36
|
+
test("retro-record: persists a filed retro from hoisted result vars", async () => {
|
|
44
37
|
const { app, stores } = fakeApp();
|
|
45
38
|
await handler(
|
|
46
|
-
// deno-lint-ignore no-explicit-any
|
|
47
39
|
{
|
|
48
40
|
variables: {
|
|
49
41
|
planKey: "o/r#5",
|
|
@@ -54,7 +46,6 @@ Deno.test("retro-record: persists a filed retro from hoisted result vars", async
|
|
|
54
46
|
"io.nanobpm.agentResult": { output: "the full report" },
|
|
55
47
|
},
|
|
56
48
|
} as any,
|
|
57
|
-
// deno-lint-ignore no-explicit-any
|
|
58
49
|
app as any,
|
|
59
50
|
);
|
|
60
51
|
|
|
@@ -67,59 +58,49 @@ Deno.test("retro-record: persists a filed retro from hoisted result vars", async
|
|
|
67
58
|
assertEquals(row.report, "the full report");
|
|
68
59
|
});
|
|
69
60
|
|
|
70
|
-
|
|
61
|
+
test("retro-record: defaults to skipped when the agent filed no PR", async () => {
|
|
71
62
|
const { app, stores } = fakeApp();
|
|
72
63
|
await handler(
|
|
73
|
-
// deno-lint-ignore no-explicit-any
|
|
74
64
|
{ variables: { planKey: "o/r#6", summary: "nothing durable" } } as any,
|
|
75
|
-
// deno-lint-ignore no-explicit-any
|
|
76
65
|
app as any,
|
|
77
66
|
);
|
|
78
67
|
assertEquals(stores.plan_retros[0].status, "skipped");
|
|
79
68
|
assertEquals(stores.plan_retros[0].pr_key, null);
|
|
80
69
|
});
|
|
81
70
|
|
|
82
|
-
|
|
71
|
+
test("retro-record: honours an explicit blocked status", async () => {
|
|
83
72
|
const { app, stores } = fakeApp();
|
|
84
73
|
await handler(
|
|
85
|
-
// deno-lint-ignore no-explicit-any
|
|
86
74
|
{ variables: { planKey: "o/r#7", status: "blocked", summary: "no write access" } } as any,
|
|
87
|
-
// deno-lint-ignore no-explicit-any
|
|
88
75
|
app as any,
|
|
89
76
|
);
|
|
90
77
|
assertEquals(stores.plan_retros[0].status, "blocked");
|
|
91
78
|
});
|
|
92
79
|
|
|
93
|
-
|
|
80
|
+
test("retro-record: ignores a PR unless the status is filed", async () => {
|
|
94
81
|
const { app, stores } = fakeApp();
|
|
95
82
|
await handler(
|
|
96
|
-
// deno-lint-ignore no-explicit-any
|
|
97
83
|
{ variables: { planKey: "o/r#8", status: "skipped", pr: "o/r#43", summary: "not durable" } } as any,
|
|
98
|
-
// deno-lint-ignore no-explicit-any
|
|
99
84
|
app as any,
|
|
100
85
|
);
|
|
101
86
|
assertEquals(stores.plan_retros[0].status, "skipped");
|
|
102
87
|
assertEquals(stores.plan_retros[0].pr_key, null);
|
|
103
88
|
});
|
|
104
89
|
|
|
105
|
-
|
|
90
|
+
test("retro-record: defaults invalid status from PR presence", async () => {
|
|
106
91
|
const { app, stores } = fakeApp();
|
|
107
92
|
await handler(
|
|
108
|
-
// deno-lint-ignore no-explicit-any
|
|
109
93
|
{ variables: { planKey: "o/r#9", status: "done", pr: "o/r#44" } } as any,
|
|
110
|
-
// deno-lint-ignore no-explicit-any
|
|
111
94
|
app as any,
|
|
112
95
|
);
|
|
113
96
|
assertEquals(stores.plan_retros[0].status, "filed");
|
|
114
97
|
assertEquals(stores.plan_retros[0].pr_key, "o/r#44");
|
|
115
98
|
});
|
|
116
99
|
|
|
117
|
-
|
|
100
|
+
test("retro-record: coerces filed without a PR to skipped", async () => {
|
|
118
101
|
const { app, stores } = fakeApp();
|
|
119
102
|
await handler(
|
|
120
|
-
// deno-lint-ignore no-explicit-any
|
|
121
103
|
{ variables: { planKey: "o/r#10", status: "filed", summary: "forgot the PR" } } as any,
|
|
122
|
-
// deno-lint-ignore no-explicit-any
|
|
123
104
|
app as any,
|
|
124
105
|
);
|
|
125
106
|
assertEquals(stores.plan_retros[0].status, "skipped");
|
|
@@ -10,7 +10,6 @@ import type { AppJobHandler } from "@nanobpm/urban";
|
|
|
10
10
|
import { recordRetro } from "../../app/retro.ts";
|
|
11
11
|
|
|
12
12
|
const AGENT_RESULT_KEY = "io.nanobpm.agentResult";
|
|
13
|
-
const VALID_STATUSES = new Set(["filed", "skipped", "blocked"]);
|
|
14
13
|
|
|
15
14
|
interface In extends Record<string, unknown> {
|
|
16
15
|
planKey: string;
|
|
@@ -26,9 +25,9 @@ function asStr(v: unknown): string | null {
|
|
|
26
25
|
|
|
27
26
|
function asStatus(v: unknown, hasPr: boolean): "filed" | "skipped" | "blocked" {
|
|
28
27
|
const s = asStr(v);
|
|
29
|
-
if (s
|
|
28
|
+
if (s === "filed" || s === "skipped" || s === "blocked") {
|
|
30
29
|
if (s === "filed" && !hasPr) return "skipped";
|
|
31
|
-
return s
|
|
30
|
+
return s;
|
|
32
31
|
}
|
|
33
32
|
return hasPr ? "filed" : "skipped";
|
|
34
33
|
}
|
|
@@ -42,6 +41,7 @@ const handler: AppJobHandler<In> = async (job, app) => {
|
|
|
42
41
|
const prKey = status === "filed" ? rawPrKey : null;
|
|
43
42
|
const summary = asStr(job.variables.summary);
|
|
44
43
|
|
|
44
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
45
45
|
const env = job.variables[AGENT_RESULT_KEY] as { output?: unknown } | undefined;
|
|
46
46
|
const report = typeof env?.output === "string" ? env.output : null;
|
|
47
47
|
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// A predecessor parked behind a merge lane is not a failed slice: dependents must remain pending
|
|
4
4
|
// so a later retry can dispatch them once the lane clears. Real non-open failures still cascade
|
|
5
5
|
// to `skipped` as before.
|
|
6
|
-
import {
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import { assertEquals } from "#test-assert";
|
|
7
8
|
import handler from "./worker.ts";
|
|
8
9
|
import type { PlanTaskStatus } from "../../app/plan.ts";
|
|
9
10
|
|
|
@@ -30,7 +31,6 @@ function fakeApp(rows: Row[], deps: DepRow[]) {
|
|
|
30
31
|
table(name: string, key: string) {
|
|
31
32
|
const store = name === "plan_tasks" ? rows : deps;
|
|
32
33
|
return {
|
|
33
|
-
// deno-lint-ignore no-explicit-any
|
|
34
34
|
find: (q: any) =>
|
|
35
35
|
Promise.resolve(
|
|
36
36
|
store.filter((r) =>
|
|
@@ -39,7 +39,6 @@ function fakeApp(rows: Row[], deps: DepRow[]) {
|
|
|
39
39
|
)
|
|
40
40
|
),
|
|
41
41
|
),
|
|
42
|
-
// deno-lint-ignore no-explicit-any
|
|
43
42
|
update: (k: any, patch: any) => {
|
|
44
43
|
const row = store.find((r) =>
|
|
45
44
|
((r as unknown) as Record<string, unknown>)[key] === k
|
|
@@ -50,20 +49,18 @@ function fakeApp(rows: Row[], deps: DepRow[]) {
|
|
|
50
49
|
};
|
|
51
50
|
},
|
|
52
51
|
},
|
|
53
|
-
// deno-lint-ignore no-explicit-any
|
|
54
52
|
} as any;
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
async function selectWave(rows: Row[], deps: DepRow[]) {
|
|
58
56
|
const out = await handler(
|
|
59
|
-
// deno-lint-ignore no-explicit-any
|
|
60
57
|
{ variables: { planKey: "owner/repo#63", currentWave: 1 } } as any,
|
|
61
58
|
fakeApp(rows, deps),
|
|
62
59
|
);
|
|
63
60
|
return out as { waveTasks: unknown[] };
|
|
64
61
|
}
|
|
65
62
|
|
|
66
|
-
|
|
63
|
+
test("select-wave leaves dependents pending behind a waiting-for-lane dependency", async () => {
|
|
67
64
|
const rows: Row[] = [
|
|
68
65
|
{
|
|
69
66
|
id: 1,
|
|
@@ -95,9 +92,9 @@ Deno.test("select-wave leaves dependents pending behind a waiting-for-lane depen
|
|
|
95
92
|
assertEquals(rows[1].summary, undefined);
|
|
96
93
|
});
|
|
97
94
|
|
|
98
|
-
|
|
95
|
+
test("select-wave still skips dependents behind failed or otherwise non-open dependencies", async (t) => {
|
|
99
96
|
for (const depStatus of ["blocked", "skipped", "pending"] as const) {
|
|
100
|
-
await t.
|
|
97
|
+
await t.test(depStatus, async () => {
|
|
101
98
|
const rows: Row[] = [
|
|
102
99
|
{
|
|
103
100
|
id: 1,
|
package/deno.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"lib": [
|
|
4
|
-
"deno.window",
|
|
5
|
-
"deno.ns"
|
|
6
|
-
]
|
|
7
|
-
},
|
|
8
|
-
"imports": {
|
|
9
|
-
"@nanobpm/urban": "npm:@nanobpm/urban@^0.33.0"
|
|
10
|
-
},
|
|
11
|
-
"tasks": {
|
|
12
|
-
"start": "deno run --allow-net --allow-read --allow-write --allow-run=gh --allow-env main.ts",
|
|
13
|
-
"purge": "deno run --allow-read --allow-write --allow-env scripts/purge-db.ts",
|
|
14
|
-
"check": "deno run -A @nanobpm/urban check",
|
|
15
|
-
"check:prompts": "deno run -A scripts/check-agent-prompts.ts",
|
|
16
|
-
"gen": "deno run -A @nanobpm/urban gen",
|
|
17
|
-
"gen:check": "deno run -A @nanobpm/urban gen --check",
|
|
18
|
-
"layout": "deno run -A scripts/layout-bpmn.ts",
|
|
19
|
-
"layout:check": "deno run -A scripts/layout-bpmn.ts --check",
|
|
20
|
-
"dev": "deno run -A @nanobpm/urban dev",
|
|
21
|
-
"compile": "deno compile --allow-net --allow-read --allow-write --allow-run=gh --allow-env -o dist/nano-workforce main.ts",
|
|
22
|
-
"test": "deno test -A"
|
|
23
|
-
}
|
|
24
|
-
}
|