@nanobpm/nano-workforce 0.35.2 → 0.36.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 +3 -0
- package/CHANGELOG.md +7 -0
- 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/blackboard.ts +7 -6
- package/app/github.ts +24 -2
- package/app/mergeExclusion.ts +8 -3
- package/app/mergeProtocol.ts +1 -0
- package/app/plan.ts +1 -1
- package/app/retro.ts +4 -2
- package/app/service.ts +8 -4
- package/app/taskDelta.ts +1 -0
- package/app/version.ts +5 -2
- package/biome.json +143 -0
- package/deno.lock +1 -0
- package/operations/postMessage.ts +3 -3
- package/package.json +5 -2
- package/plugins/no-unsafe-type-assertion.grit +12 -0
- package/scripts/check-agent-prompts.ts +4 -2
- package/scripts/layout-bpmn.ts +3 -1
- package/scripts/purge-db.ts +2 -1
- package/scripts/upgrade-from-pack.ts +8 -4
- 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.ts +2 -1
- package/workers/record-results/worker.ts +2 -1
- package/workers/record-trial-merge/worker.ts +6 -6
- package/workers/record-wave/worker.ts +7 -8
- package/workers/retro-record/worker.ts +3 -3
package/.github/workflows/ci.yml
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [0.36.0](https://github.com/nanobpm/nano-workforce/compare/v0.35.2...v0.36.0) (2026-08-10)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* adopt Biome + GritQL ban-`as` lint gate ([#107](https://github.com/nanobpm/nano-workforce/issues/107)) ([38c16fb](https://github.com/nanobpm/nano-workforce/commit/38c16fb319e8c5d6b8c20faf95318de39b948a96)), closes [#105](https://github.com/nanobpm/nano-workforce/issues/105)
|
|
7
|
+
|
|
1
8
|
## [0.35.2](https://github.com/nanobpm/nano-workforce/compare/v0.35.1...v0.35.2) (2026-08-10)
|
|
2
9
|
|
|
3
10
|
|
package/actions/blackboard.ts
CHANGED
|
@@ -35,6 +35,7 @@ const handler: ActionHandler = async ({ req, body }, app) => {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
if (req.method === "POST") {
|
|
38
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
38
39
|
const b = (body ?? {}) as Record<string, unknown>;
|
|
39
40
|
const text = typeof b.body === "string" ? b.body.trim() : "";
|
|
40
41
|
if (!text) return { status: 400, body: { error: "'body' (the note text) is required" } };
|
|
@@ -20,6 +20,7 @@ const handler: ActionHandler = async ({ req, body }, app) => {
|
|
|
20
20
|
if (WEBHOOK_SECRET && req.headers.get("x-hook-secret") !== WEBHOOK_SECRET) {
|
|
21
21
|
return { status: 401, body: { error: "unauthorized" } };
|
|
22
22
|
}
|
|
23
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
23
24
|
const b = (body ?? {}) as {
|
|
24
25
|
corrKey?: unknown;
|
|
25
26
|
plan?: unknown;
|
package/actions/plan-hook.ts
CHANGED
|
@@ -10,8 +10,9 @@ const handler: ActionHandler = async ({ req, body }, app) => {
|
|
|
10
10
|
if (WEBHOOK_SECRET && req.headers.get("x-hook-secret") !== WEBHOOK_SECRET) {
|
|
11
11
|
return { status: 401, body: { error: "unauthorized" } };
|
|
12
12
|
}
|
|
13
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
13
14
|
const b = (body ?? {}) as { url?: unknown; issue?: unknown };
|
|
14
|
-
const parsed = parseIssue(String(
|
|
15
|
+
const parsed = parseIssue(String(b.issue ?? b.url ?? ""));
|
|
15
16
|
if (!parsed) return { status: 400, body: { error: "could not parse issue (use owner/repo#123 or an issue URL)" } };
|
|
16
17
|
return { status: 202, body: await startPlan(app.data, app.engine, parsed) };
|
|
17
18
|
};
|
|
@@ -10,8 +10,9 @@ const handler: ActionHandler = async ({ req, body }, app) => {
|
|
|
10
10
|
if (WEBHOOK_SECRET && req.headers.get("x-hook-secret") !== WEBHOOK_SECRET) {
|
|
11
11
|
return { status: 401, body: { error: "unauthorized" } };
|
|
12
12
|
}
|
|
13
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
13
14
|
const b = (body ?? {}) as { url?: unknown; pr?: unknown; dependsOn?: unknown; maxRounds?: unknown };
|
|
14
|
-
const parsed = parsePr(String(
|
|
15
|
+
const parsed = parsePr(String(b.url ?? b.pr ?? ""));
|
|
15
16
|
if (!parsed) return { status: 400, body: { error: "could not parse PR url" } };
|
|
16
17
|
const dependsOn = Array.isArray(b.dependsOn) ? b.dependsOn.map((d) => String(d)) : [];
|
|
17
18
|
const maxRounds = clampRounds(b.maxRounds, MAX_ROUNDS);
|
package/app/blackboard.ts
CHANGED
|
@@ -58,11 +58,9 @@ export interface BlackboardInput {
|
|
|
58
58
|
dedupe_key?: string;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
const KIND_SET = new Set<string>(BLACKBOARD_KINDS);
|
|
62
|
-
|
|
63
61
|
/** Coerce an arbitrary `kind` to a known value, defaulting to "note" for anything unrecognised. */
|
|
64
62
|
export function normalizeKind(kind: unknown): BlackboardKind {
|
|
65
|
-
return
|
|
63
|
+
return BLACKBOARD_KINDS.find((k) => k === kind) ?? "note";
|
|
66
64
|
}
|
|
67
65
|
|
|
68
66
|
/** A URL-safe, unguessable capability token (192 bits of randomness, base64url, no padding). */
|
|
@@ -79,9 +77,10 @@ export function mintBlackboardToken(): string {
|
|
|
79
77
|
export function publicBaseUrl(env: string | undefined = process.env.NANO_PR_PUBLIC_BASE_URL): string {
|
|
80
78
|
// Cascade through the fallback chain, skipping any value that is unset OR blank/whitespace, so an
|
|
81
79
|
// explicitly-set-but-empty NANO_PR_PUBLIC_BASE_URL can't yield a malformed capability URL.
|
|
82
|
-
const base =
|
|
83
|
-
.
|
|
84
|
-
|
|
80
|
+
const base =
|
|
81
|
+
[env, process.env.NANO_PR_BASE_URL, "http://localhost:3000"]
|
|
82
|
+
.map((v) => v?.trim())
|
|
83
|
+
.find((v): v is string => Boolean(v)) ?? "http://localhost:3000";
|
|
85
84
|
return base.replace(/\/+$/, "");
|
|
86
85
|
}
|
|
87
86
|
|
|
@@ -313,8 +312,10 @@ export async function appendEntry(
|
|
|
313
312
|
* corruption, not a benign duplicate) is always rethrown rather than silently swallowed. */
|
|
314
313
|
export function isUniqueViolation(err: unknown): boolean {
|
|
315
314
|
if (!err || typeof err !== "object") return false;
|
|
315
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
316
316
|
const code = (err as { code?: unknown }).code;
|
|
317
317
|
if (code === "SQLITE_CONSTRAINT_UNIQUE" || code === "SQLITE_CONSTRAINT_PRIMARYKEY") return true;
|
|
318
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
318
319
|
const message = (err as { message?: unknown }).message;
|
|
319
320
|
return typeof message === "string" &&
|
|
320
321
|
/(unique|primary key) constraint failed|duplicate/i.test(message);
|
package/app/github.ts
CHANGED
|
@@ -37,6 +37,7 @@ interface DenoCommandCtor {
|
|
|
37
37
|
* `repo`/`number` from the datastore cannot inject a command). Resolves stdout, rejects on a
|
|
38
38
|
* non-zero exit with stderr as the message. */
|
|
39
39
|
async function runGh(args: string[]): Promise<string> {
|
|
40
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
40
41
|
const g = globalThis as { Deno?: { Command?: DenoCommandCtor } };
|
|
41
42
|
if (g.Deno?.Command) {
|
|
42
43
|
const { code, stdout, stderr } = await new g.Deno.Command("gh", {
|
|
@@ -66,7 +67,10 @@ async function runGh(args: string[]): Promise<string> {
|
|
|
66
67
|
let ghAvailable: Promise<boolean> | undefined;
|
|
67
68
|
/** Whether the host `gh` CLI is present (memoized — probed at most once per process). */
|
|
68
69
|
function isGhAvailable(): Promise<boolean> {
|
|
69
|
-
|
|
70
|
+
if (!ghAvailable) {
|
|
71
|
+
ghAvailable = runGh(["--version"]).then(() => true, () => false);
|
|
72
|
+
}
|
|
73
|
+
return ghAvailable;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
/** Fetch the reviews for one PR via the configured transport. Throws on transport failure so
|
|
@@ -81,6 +85,7 @@ export async function fetchPrReviews(
|
|
|
81
85
|
const path = `repos/${repo}/pulls/${number}/reviews?per_page=100`;
|
|
82
86
|
if (useGh) {
|
|
83
87
|
const out = await runGh(["api", path, "-H", "Accept: application/vnd.github+json"]);
|
|
88
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
84
89
|
return JSON.parse(out) as GhReview[];
|
|
85
90
|
}
|
|
86
91
|
if (!token) return null; // token mode with no token → poller idles
|
|
@@ -88,6 +93,7 @@ export async function fetchPrReviews(
|
|
|
88
93
|
headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json" },
|
|
89
94
|
});
|
|
90
95
|
if (!r.ok) throw new Error(`github ${r.status} ${r.statusText}`.trim());
|
|
96
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
91
97
|
return (await r.json()) as GhReview[];
|
|
92
98
|
}
|
|
93
99
|
|
|
@@ -122,6 +128,7 @@ export async function hasPendingCopilotReviewer(
|
|
|
122
128
|
let users: { login?: string }[];
|
|
123
129
|
if (await useGh()) {
|
|
124
130
|
const out = await runGh(["api", path, "-H", "Accept: application/vnd.github+json"]);
|
|
131
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
125
132
|
users = (JSON.parse(out) as { users?: { login?: string }[] }).users ?? [];
|
|
126
133
|
} else {
|
|
127
134
|
if (!token) return null;
|
|
@@ -129,6 +136,7 @@ export async function hasPendingCopilotReviewer(
|
|
|
129
136
|
headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json" },
|
|
130
137
|
});
|
|
131
138
|
if (!r.ok) throw new Error(`github ${r.status} ${r.statusText}`.trim());
|
|
139
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
132
140
|
users = ((await r.json()) as { users?: { login?: string }[] }).users ?? [];
|
|
133
141
|
}
|
|
134
142
|
return users.some((u) => isCopilot(u.login));
|
|
@@ -198,6 +206,7 @@ export async function fetchPrMeta(
|
|
|
198
206
|
): Promise<PrMeta | null> {
|
|
199
207
|
if (await useGh()) {
|
|
200
208
|
const out = await runGh(["pr", "view", String(number), "--repo", repo, "--json", "title,body"]);
|
|
209
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
201
210
|
const j = JSON.parse(out) as { title?: string; body?: string };
|
|
202
211
|
return { title: j.title ?? null, body: j.body ?? "" };
|
|
203
212
|
}
|
|
@@ -206,6 +215,7 @@ export async function fetchPrMeta(
|
|
|
206
215
|
headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json" },
|
|
207
216
|
});
|
|
208
217
|
if (!r.ok) throw new Error(`github ${r.status} ${r.statusText}`.trim());
|
|
218
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
209
219
|
const j = (await r.json()) as { title?: string; body?: string };
|
|
210
220
|
return { title: j.title ?? null, body: j.body ?? "" };
|
|
211
221
|
}
|
|
@@ -273,6 +283,7 @@ export async function fetchPrState(
|
|
|
273
283
|
"--json",
|
|
274
284
|
"state,mergedAt,mergeStateStatus,statusCheckRollup,isDraft,headRefOid",
|
|
275
285
|
]);
|
|
286
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
276
287
|
const j = JSON.parse(out) as {
|
|
277
288
|
state?: string;
|
|
278
289
|
mergedAt?: string | null;
|
|
@@ -298,6 +309,7 @@ export async function fetchPrState(
|
|
|
298
309
|
headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json" },
|
|
299
310
|
});
|
|
300
311
|
if (!r.ok) throw new Error(`github ${r.status} ${r.statusText}`.trim());
|
|
312
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
301
313
|
const j = (await r.json()) as {
|
|
302
314
|
merged?: boolean;
|
|
303
315
|
merged_at?: string | null;
|
|
@@ -328,6 +340,7 @@ export async function fetchPrFiles(
|
|
|
328
340
|
): Promise<string[] | null> {
|
|
329
341
|
if (await useGh()) {
|
|
330
342
|
const out = await runGh(["pr", "view", String(number), "--repo", repo, "--json", "files"]);
|
|
343
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
331
344
|
const j = JSON.parse(out) as { files?: { path?: string }[] };
|
|
332
345
|
return (j.files ?? []).map((f) => f.path ?? "").filter((p) => p !== "");
|
|
333
346
|
}
|
|
@@ -341,6 +354,7 @@ export async function fetchPrFiles(
|
|
|
341
354
|
{ headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json" } },
|
|
342
355
|
);
|
|
343
356
|
if (!r.ok) throw new Error(`github ${r.status} ${r.statusText}`.trim());
|
|
357
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
344
358
|
const batch = (await r.json()) as { filename?: string }[];
|
|
345
359
|
for (const f of batch) if (f.filename) paths.push(f.filename);
|
|
346
360
|
// A short final page means we've read every file — the list is complete.
|
|
@@ -367,6 +381,7 @@ export async function fetchPrHead(
|
|
|
367
381
|
): Promise<{ headRef: string | null; headSha: string | null } | null> {
|
|
368
382
|
if (await useGh()) {
|
|
369
383
|
const out = await runGh(["pr", "view", String(number), "--repo", repo, "--json", "headRefName,headRefOid"]);
|
|
384
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
370
385
|
const j = JSON.parse(out) as { headRefName?: string | null; headRefOid?: string | null };
|
|
371
386
|
return { headRef: j.headRefName ?? null, headSha: j.headRefOid ?? null };
|
|
372
387
|
}
|
|
@@ -375,6 +390,7 @@ export async function fetchPrHead(
|
|
|
375
390
|
headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json" },
|
|
376
391
|
});
|
|
377
392
|
if (!r.ok) throw new Error(`github ${r.status} ${r.statusText}`.trim());
|
|
393
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
378
394
|
const j = (await r.json()) as { head?: { ref?: string | null; sha?: string | null } };
|
|
379
395
|
return { headRef: j.head?.ref ?? null, headSha: j.head?.sha ?? null };
|
|
380
396
|
}
|
|
@@ -389,6 +405,7 @@ export async function fetchPrBase(
|
|
|
389
405
|
): Promise<string | null> {
|
|
390
406
|
if (await useGh()) {
|
|
391
407
|
const out = await runGh(["pr", "view", String(number), "--repo", repo, "--json", "baseRefName"]);
|
|
408
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
392
409
|
const j = JSON.parse(out) as { baseRefName?: string };
|
|
393
410
|
return j.baseRefName ?? null;
|
|
394
411
|
}
|
|
@@ -397,6 +414,7 @@ export async function fetchPrBase(
|
|
|
397
414
|
headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json" },
|
|
398
415
|
});
|
|
399
416
|
if (!r.ok) throw new Error(`github ${r.status} ${r.statusText}`.trim());
|
|
417
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
400
418
|
const j = (await r.json()) as { base?: { ref?: string } };
|
|
401
419
|
return j.base?.ref ?? null;
|
|
402
420
|
}
|
|
@@ -413,6 +431,7 @@ export async function fetchDefaultBranch(repo: string, token: string): Promise<s
|
|
|
413
431
|
let name: string | null = null;
|
|
414
432
|
if (await useGh()) {
|
|
415
433
|
const out = await runGh(["repo", "view", repo, "--json", "defaultBranchRef"]);
|
|
434
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
416
435
|
const j = JSON.parse(out) as { defaultBranchRef?: { name?: string } };
|
|
417
436
|
name = j.defaultBranchRef?.name ?? null;
|
|
418
437
|
} else if (token) {
|
|
@@ -420,6 +439,7 @@ export async function fetchDefaultBranch(repo: string, token: string): Promise<s
|
|
|
420
439
|
headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json" },
|
|
421
440
|
});
|
|
422
441
|
if (!r.ok) throw new Error(`github ${r.status} ${r.statusText}`.trim());
|
|
442
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
423
443
|
const j = (await r.json()) as { default_branch?: string };
|
|
424
444
|
name = j.default_branch ?? null;
|
|
425
445
|
} else {
|
|
@@ -455,6 +475,7 @@ export async function baseBranchLanded(
|
|
|
455
475
|
"--limit",
|
|
456
476
|
"20",
|
|
457
477
|
]);
|
|
478
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
458
479
|
const arr = JSON.parse(out) as { state?: string }[];
|
|
459
480
|
if (arr.some((p) => (p.state ?? "").toUpperCase() === "MERGED")) return "landed";
|
|
460
481
|
if (arr.some((p) => (p.state ?? "").toUpperCase() === "OPEN")) return "open";
|
|
@@ -467,6 +488,7 @@ export async function baseBranchLanded(
|
|
|
467
488
|
{ headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json" } },
|
|
468
489
|
);
|
|
469
490
|
if (!r.ok) throw new Error(`github ${r.status} ${r.statusText}`.trim());
|
|
491
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
470
492
|
const arr = (await r.json()) as { state?: string; merged_at?: string | null }[];
|
|
471
493
|
if (arr.some((p) => p.merged_at || (p.state ?? "").toUpperCase() === "MERGED")) return "landed";
|
|
472
494
|
if (arr.some((p) => (p.state ?? "").toLowerCase() === "open")) return "open";
|
|
@@ -491,7 +513,6 @@ export function classifyMergeability(s: PrState): Mergeability {
|
|
|
491
513
|
// A required check failed -> a human must act. Pending checks / awaiting review -> wait.
|
|
492
514
|
// When we can't enumerate checks (failingChecks < 0, token mode) stay conservative: wait.
|
|
493
515
|
return s.failingChecks > 0 ? "blocked" : "waiting";
|
|
494
|
-
case "DRAFT":
|
|
495
516
|
default: // UNKNOWN / "" — GitHub is still computing mergeability
|
|
496
517
|
return "waiting";
|
|
497
518
|
}
|
|
@@ -550,6 +571,7 @@ export async function mergePr(
|
|
|
550
571
|
// in this pass. Trust `merged` when true; otherwise verify the PR's actual state and report
|
|
551
572
|
// `queued` when it hasn't landed yet, so the merge-loop waits for `merge-landed` rather than
|
|
552
573
|
// marking it merged prematurely.
|
|
574
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
553
575
|
const body = (await r.json().catch(() => ({}))) as { merged?: boolean };
|
|
554
576
|
if (body.merged) return { outcome: "merged", detail: "merged" };
|
|
555
577
|
const st = await fetchPrState(repo, number, token).catch(() => null);
|
package/app/mergeExclusion.ts
CHANGED
|
@@ -169,12 +169,17 @@ export async function clearExclusions(data: DataLayer, planKey: string): Promise
|
|
|
169
169
|
export function mergeLanes(edges: ExclusionEdge[], allTasks: Iterable<string> = []): string[][] {
|
|
170
170
|
const parent = new Map<string, string>();
|
|
171
171
|
const find = (x: string): string => {
|
|
172
|
+
const parentOf = (node: string): string => {
|
|
173
|
+
const p = parent.get(node);
|
|
174
|
+
if (p === undefined) throw new Error(`missing union-find parent for ${node}`);
|
|
175
|
+
return p;
|
|
176
|
+
};
|
|
172
177
|
let root = x;
|
|
173
|
-
while (
|
|
178
|
+
while (parentOf(root) !== root) root = parentOf(root);
|
|
174
179
|
// Path-compress so repeated finds stay near-flat.
|
|
175
180
|
let cur = x;
|
|
176
|
-
while (
|
|
177
|
-
const next =
|
|
181
|
+
while (parentOf(cur) !== root) {
|
|
182
|
+
const next = parentOf(cur);
|
|
178
183
|
parent.set(cur, root);
|
|
179
184
|
cur = next;
|
|
180
185
|
}
|
package/app/mergeProtocol.ts
CHANGED
|
@@ -72,6 +72,7 @@ function strArray(v: unknown): string[] | undefined {
|
|
|
72
72
|
}
|
|
73
73
|
function oneOf<T extends string>(v: unknown, allowed: ReadonlySet<string>): T | undefined {
|
|
74
74
|
const s = str(v);
|
|
75
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
75
76
|
return s !== undefined && allowed.has(s) ? (s as T) : undefined;
|
|
76
77
|
}
|
|
77
78
|
|
package/app/plan.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
// hand-written SQL — matching app/service.ts.
|
|
12
12
|
import type { DataLayer, EngineClient } from "@nanobpm/urban";
|
|
13
13
|
import { blackboardUrl, mintBlackboardToken, renderCoordinationBrief } from "./blackboard.ts";
|
|
14
|
-
import { clearTaskDeltas } from "./taskDelta.ts";
|
|
15
14
|
import { clearExclusions } from "./mergeExclusion.ts";
|
|
15
|
+
import { clearTaskDeltas } from "./taskDelta.ts";
|
|
16
16
|
|
|
17
17
|
/** The BPMN process this module drives (resources/processes/plan-fanout.bpmn). */
|
|
18
18
|
export const PLAN_PROCESS_ID = "plan-fanout";
|
package/app/retro.ts
CHANGED
|
@@ -14,16 +14,17 @@
|
|
|
14
14
|
// Data access goes through the record gateway (`data.table`), never hand-written SQL — matching
|
|
15
15
|
// app/plan.ts, app/blackboard.ts, and app/taskDelta.ts.
|
|
16
16
|
import type { DataLayer, EngineClient } from "@nanobpm/urban";
|
|
17
|
-
import { planReviews, planTasks } from "./plan.ts";
|
|
18
17
|
import { isUniqueViolation, readBlackboard } from "./blackboard.ts";
|
|
19
|
-
import {
|
|
18
|
+
import { planReviews, planTasks } from "./plan.ts";
|
|
20
19
|
import { TERMINAL_STATUSES } from "./service.ts";
|
|
20
|
+
import { aggregateEpicDeltas } from "./taskDelta.ts";
|
|
21
21
|
|
|
22
22
|
export const RETRO_PROCESS_ID = "retro";
|
|
23
23
|
|
|
24
24
|
/** Opt-out env toggle. Retro runs by default; set NANO_AUTO_RETRO=0/false to disable (e.g. in a
|
|
25
25
|
* review-only deployment that doesn't want the fleet opening promotion PRs). */
|
|
26
26
|
export function autoRetroEnabled(): boolean {
|
|
27
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
27
28
|
const v = (globalThis as { process?: { env?: Record<string, string | undefined> } })
|
|
28
29
|
.process?.env?.NANO_AUTO_RETRO;
|
|
29
30
|
if (v == null) return true;
|
|
@@ -128,6 +129,7 @@ export async function gatherRetro(data: DataLayer, planKey: string): Promise<Ret
|
|
|
128
129
|
.sort((a, b) => a.round - b.round);
|
|
129
130
|
const reviewRejections = reviews
|
|
130
131
|
.filter((r) => r.approved === 0 && (r.findings ?? "").trim() !== "")
|
|
132
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
131
133
|
.map((r) => ({ round: r.round, findings: r.findings as string }));
|
|
132
134
|
const planApproved = reviews.length > 0 && reviews[reviews.length - 1].approved === 1;
|
|
133
135
|
|
package/app/service.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// Data access goes through the record-oriented gateway (`data.table<T>(name, pk)` — the RAD
|
|
9
9
|
// `Table<T>` surface), not hand-written SQL. Row shapes are declared inline here.
|
|
10
10
|
import type { DataLayer, EngineClient } from "@nanobpm/urban";
|
|
11
|
+
import { abandonUrl, mintAbandonToken, renderAbandonBrief } from "./abandon.ts";
|
|
11
12
|
import {
|
|
12
13
|
classifyMergeability,
|
|
13
14
|
ensureFreshHeadRun,
|
|
@@ -18,13 +19,12 @@ import {
|
|
|
18
19
|
type MergeMethod,
|
|
19
20
|
requestCopilotReview,
|
|
20
21
|
} from "./github.ts";
|
|
21
|
-
import {
|
|
22
|
-
import { abandonUrl, mintAbandonToken, renderAbandonBrief } from "./abandon.ts";
|
|
22
|
+
import { mergeLanes, readExclusions } from "./mergeExclusion.ts";
|
|
23
23
|
import { freshHeadRunAction, loadMergeProtocol } from "./mergeProtocol.ts";
|
|
24
|
+
import { type PrLaneDecision, planPrLane, taskDependencyDepths } from "./mergeTrain.ts";
|
|
25
|
+
import { plans, planTaskDeps, planTasks } from "./plan.ts";
|
|
24
26
|
import { clampNudgeMinutes, reviewWaitTimeout } from "./reviewWait.ts";
|
|
25
27
|
import { waveMergeTargets } from "./waves.ts";
|
|
26
|
-
import { mergeLanes, readExclusions } from "./mergeExclusion.ts";
|
|
27
|
-
import { planPrLane, type PrLaneDecision, taskDependencyDepths } from "./mergeTrain.ts";
|
|
28
28
|
|
|
29
29
|
/** The BPMN process that drives review convergence (`resources/processes/convergence-loop.bpmn`). */
|
|
30
30
|
export const PROCESS_ID = "convergence-loop";
|
|
@@ -34,10 +34,12 @@ export const MERGE_PROCESS_ID = "merge-loop";
|
|
|
34
34
|
* in convergence-loop.bpmn). Deliberately NOT hosted here — an external harness services it; the
|
|
35
35
|
* activation poll keys off it to tell "agent working" from "queued". */
|
|
36
36
|
const REVIEW_JOB_TYPE = "senior:pr-review";
|
|
37
|
+
|
|
37
38
|
/** Default round cap before the loop escalates to a human. A per-submit override (submit form /
|
|
38
39
|
* webhook / start action) takes precedence; this env var sets the fleet-wide default. The cap
|
|
39
40
|
* coercion + ceiling live in the pure `./rounds.ts` module (re-exported for callers). */
|
|
40
41
|
export { clampCiFixBudget, clampRounds, MAX_CI_FIX_CEILING, MAX_ROUNDS_CEILING } from "./rounds.ts";
|
|
42
|
+
|
|
41
43
|
import { clampCiFixBudget, clampRounds } from "./rounds.ts";
|
|
42
44
|
export const MAX_ROUNDS = clampRounds(process.env.NANO_PR_MAX_ROUNDS, 20);
|
|
43
45
|
|
|
@@ -830,6 +832,7 @@ async function pollJobActivation(
|
|
|
830
832
|
}),
|
|
831
833
|
});
|
|
832
834
|
if (!res.ok) continue; // engine unhappy → keep last-known, retry next pass
|
|
835
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
833
836
|
const body = (await res.json()) as { items?: JobSearchItem[] };
|
|
834
837
|
// An open job with a leasing worker means an agent has activated it. Prefer the one with
|
|
835
838
|
// the latest deadline if several are open (there is normally at most one).
|
|
@@ -929,6 +932,7 @@ export async function pollIncidentsImpl(
|
|
|
929
932
|
}),
|
|
930
933
|
});
|
|
931
934
|
if (!res.ok) continue; // engine unhappy → keep last-known, retry next pass
|
|
935
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
932
936
|
const body = (await res.json()) as { items?: IncidentSearchItem[] };
|
|
933
937
|
// Surface the oldest ACTIVE incident (the first thing that broke — a stable choice if the
|
|
934
938
|
// instance somehow parks more than one). Re-filter on state defensively in case the wire
|
package/app/taskDelta.ts
CHANGED
|
@@ -71,6 +71,7 @@ function decodeArray(raw: string | null): string[] {
|
|
|
71
71
|
* nothing actionable (so callers persist/broadcast only real deltas, never empty noise). */
|
|
72
72
|
export function parseTaskDelta(raw: unknown): TaskDelta | null {
|
|
73
73
|
if (!raw || typeof raw !== "object") return null;
|
|
74
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
74
75
|
const o = raw as Record<string, unknown>;
|
|
75
76
|
const delta: TaskDelta = {
|
|
76
77
|
contractChange: trimStr(o.contractChange),
|
package/app/version.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
// Every probe is best-effort: a missing file or unavailable `.git` yields `null` for that field
|
|
12
12
|
// rather than throwing, so `/app/api/version` never fails just because one source is absent.
|
|
13
13
|
import { readFileSync } from "node:fs";
|
|
14
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
14
15
|
import { fileURLToPath } from "node:url";
|
|
15
|
-
import { dirname, join, resolve, isAbsolute } from "node:path";
|
|
16
16
|
|
|
17
17
|
// Captured once, at module load — i.e. when the running process booted this code.
|
|
18
18
|
const STARTED_AT = new Date();
|
|
@@ -31,6 +31,7 @@ function readJson(path: string): Record<string, unknown> | null {
|
|
|
31
31
|
const text = readText(path);
|
|
32
32
|
if (text == null) return null;
|
|
33
33
|
try {
|
|
34
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
34
35
|
return JSON.parse(text) as Record<string, unknown>;
|
|
35
36
|
} catch {
|
|
36
37
|
return null;
|
|
@@ -44,6 +45,7 @@ function readJson(path: string): Record<string, unknown> | null {
|
|
|
44
45
|
export function envVar(name: string): string | null {
|
|
45
46
|
const fromProcess = globalThis.process?.env?.[name];
|
|
46
47
|
if (typeof fromProcess === "string" && fromProcess.trim()) return fromProcess.trim();
|
|
48
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
47
49
|
const deno = (globalThis as { Deno?: { env?: { get?(k: string): string | undefined } } }).Deno;
|
|
48
50
|
try {
|
|
49
51
|
const fromDeno = deno?.env?.get?.(name);
|
|
@@ -129,7 +131,7 @@ function gitSha(): string | null {
|
|
|
129
131
|
const refPath = ref.slice(4).trim(); // e.g. "refs/heads/main"
|
|
130
132
|
// A loose ref may live in the per-worktree dir or the common dir; check both.
|
|
131
133
|
const loose = readText(join(dirs.gitDir, refPath)) ?? readText(join(dirs.commonDir, refPath));
|
|
132
|
-
if (loose
|
|
134
|
+
if (loose?.trim()) return loose.trim();
|
|
133
135
|
|
|
134
136
|
// Packed refs fallback (always in the common dir): lines of "<sha> <refname>".
|
|
135
137
|
const packed = readText(join(dirs.commonDir, "packed-refs"));
|
|
@@ -159,6 +161,7 @@ function gitBranch(): string | null {
|
|
|
159
161
|
function runtime(): string {
|
|
160
162
|
const proc = globalThis.process;
|
|
161
163
|
// Deno exposes `Deno.version.deno`; Node exposes `process.version` (e.g. "v24.15.0").
|
|
164
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
162
165
|
const deno = (globalThis as { Deno?: { version?: { deno?: string } } }).Deno;
|
|
163
166
|
if (deno?.version?.deno) return `deno ${deno.version.deno}`;
|
|
164
167
|
if (proc?.version) return `node ${proc.version}`;
|
package/biome.json
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
3
|
+
"files": {
|
|
4
|
+
"includes": [
|
|
5
|
+
"app/**/*.ts",
|
|
6
|
+
"operations/**/*.ts",
|
|
7
|
+
"actions/**/*.ts",
|
|
8
|
+
"workers/**/*.ts",
|
|
9
|
+
"pages/**/*.ts",
|
|
10
|
+
"components/**/*.ts",
|
|
11
|
+
"scripts/**/*.ts",
|
|
12
|
+
"main.ts",
|
|
13
|
+
"!**/*.test.ts",
|
|
14
|
+
"!nano-generated/**",
|
|
15
|
+
"!node_modules/**"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"linter": {
|
|
19
|
+
"enabled": true,
|
|
20
|
+
"includes": [
|
|
21
|
+
"app/**/*.ts",
|
|
22
|
+
"operations/**/*.ts",
|
|
23
|
+
"actions/**/*.ts",
|
|
24
|
+
"workers/**/*.ts",
|
|
25
|
+
"pages/**/*.ts",
|
|
26
|
+
"components/**/*.ts",
|
|
27
|
+
"scripts/**/*.ts",
|
|
28
|
+
"main.ts",
|
|
29
|
+
"!**/*.test.ts",
|
|
30
|
+
"!nano-generated/**",
|
|
31
|
+
"!node_modules/**"
|
|
32
|
+
],
|
|
33
|
+
"rules": {
|
|
34
|
+
"recommended": true,
|
|
35
|
+
"complexity": {
|
|
36
|
+
"noAdjacentSpacesInRegex": "error",
|
|
37
|
+
"noArguments": "error",
|
|
38
|
+
"noBannedTypes": "error",
|
|
39
|
+
"noCommaOperator": "error",
|
|
40
|
+
"noEmptyTypeParameters": "error",
|
|
41
|
+
"noExtraBooleanCast": "error",
|
|
42
|
+
"noFlatMapIdentity": "error",
|
|
43
|
+
"noImportantStyles": "error",
|
|
44
|
+
"noStaticOnlyClass": "error",
|
|
45
|
+
"noThisInStatic": "error",
|
|
46
|
+
"noUselessCatch": "error",
|
|
47
|
+
"noUselessConstructor": "error",
|
|
48
|
+
"noUselessContinue": "error",
|
|
49
|
+
"noUselessEmptyExport": "error",
|
|
50
|
+
"noUselessEscapeInRegex": "error",
|
|
51
|
+
"noUselessFragments": "error",
|
|
52
|
+
"noUselessLabel": "error",
|
|
53
|
+
"noUselessLoneBlockStatements": "error",
|
|
54
|
+
"noUselessRename": "error",
|
|
55
|
+
"noUselessStringRaw": "error",
|
|
56
|
+
"noUselessSwitchCase": "error",
|
|
57
|
+
"noUselessTernary": "error",
|
|
58
|
+
"noUselessThisAlias": "error",
|
|
59
|
+
"noUselessTypeConstraint": "error",
|
|
60
|
+
"noUselessUndefinedInitialization": "error",
|
|
61
|
+
"useArrowFunction": "error",
|
|
62
|
+
"useDateNow": "error",
|
|
63
|
+
"useFlatMap": "error",
|
|
64
|
+
"useIndexOf": "error",
|
|
65
|
+
"useLiteralKeys": "error",
|
|
66
|
+
"useNumericLiterals": "error",
|
|
67
|
+
"useOptionalChain": "error",
|
|
68
|
+
"useRegexLiterals": "error",
|
|
69
|
+
"useSimpleNumberKeys": "error"
|
|
70
|
+
},
|
|
71
|
+
"correctness": {
|
|
72
|
+
"noUnusedFunctionParameters": "error",
|
|
73
|
+
"noUnusedImports": "error",
|
|
74
|
+
"noUnusedLabels": "error",
|
|
75
|
+
"noUnusedPrivateClassMembers": "error",
|
|
76
|
+
"noUnusedVariables": "error",
|
|
77
|
+
"useParseIntRadix": "error"
|
|
78
|
+
},
|
|
79
|
+
"performance": {
|
|
80
|
+
"noAccumulatingSpread": "error",
|
|
81
|
+
"noDynamicNamespaceImportAccess": "error"
|
|
82
|
+
},
|
|
83
|
+
"style": {
|
|
84
|
+
"noDescendingSpecificity": "error",
|
|
85
|
+
"noNonNullAssertion": "error",
|
|
86
|
+
"useArrayLiterals": "error",
|
|
87
|
+
"useConst": "error",
|
|
88
|
+
"useDeprecatedReason": "error",
|
|
89
|
+
"useExponentiationOperator": "error",
|
|
90
|
+
"useExportType": "error",
|
|
91
|
+
"useImportType": "error",
|
|
92
|
+
"useLiteralEnumMembers": "error",
|
|
93
|
+
"useNodejsImportProtocol": "error",
|
|
94
|
+
"useShorthandFunctionType": "error",
|
|
95
|
+
"useTemplate": "error"
|
|
96
|
+
},
|
|
97
|
+
"suspicious": {
|
|
98
|
+
"noApproximativeNumericConstant": "error",
|
|
99
|
+
"noCatchAssign": "error",
|
|
100
|
+
"noConfusingLabels": "error",
|
|
101
|
+
"noConfusingVoidType": "error",
|
|
102
|
+
"noConstEnum": "error",
|
|
103
|
+
"noDocumentCookie": "error",
|
|
104
|
+
"noDuplicateFields": "error",
|
|
105
|
+
"noEmptyBlock": "error",
|
|
106
|
+
"noEvolvingTypes": "error",
|
|
107
|
+
"noExplicitAny": "error",
|
|
108
|
+
"noExtraNonNullAssertion": "error",
|
|
109
|
+
"noGlobalIsFinite": "error",
|
|
110
|
+
"noGlobalIsNan": "error",
|
|
111
|
+
"noImplicitAnyLet": "error",
|
|
112
|
+
"noIrregularWhitespace": "error",
|
|
113
|
+
"noOctalEscape": "error",
|
|
114
|
+
"noPrototypeBuiltins": "error",
|
|
115
|
+
"noQuickfixBiome": "error",
|
|
116
|
+
"noRedundantUseStrict": "error",
|
|
117
|
+
"noSuspiciousSemicolonInJsx": "error",
|
|
118
|
+
"noTemplateCurlyInString": "error",
|
|
119
|
+
"noTsIgnore": "error",
|
|
120
|
+
"noUselessEscapeInString": "error",
|
|
121
|
+
"noUselessRegexBackrefs": "error",
|
|
122
|
+
"useAdjacentOverloadSignatures": "error",
|
|
123
|
+
"useBiomeIgnoreFolder": "error",
|
|
124
|
+
"useDefaultSwitchClauseLast": "error",
|
|
125
|
+
"useGoogleFontDisplay": "error",
|
|
126
|
+
"useIsArray": "error"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"domains": {
|
|
130
|
+
"react": "none",
|
|
131
|
+
"solid": "none",
|
|
132
|
+
"next": "none",
|
|
133
|
+
"qwik": "none",
|
|
134
|
+
"vue": "none"
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"plugins": [
|
|
138
|
+
"./plugins/no-unsafe-type-assertion.grit"
|
|
139
|
+
],
|
|
140
|
+
"formatter": {
|
|
141
|
+
"enabled": false
|
|
142
|
+
}
|
|
143
|
+
}
|
package/deno.lock
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// for free); this delegate keeps the message-name dispatch — the discriminator + downstream behavior
|
|
10
10
|
// is app logic, not something the JSON schema can express.
|
|
11
11
|
import { defineOperation } from "@nanobpm/urban";
|
|
12
|
-
import { answerEscalation } from "../app/service.ts";
|
|
13
12
|
import { answerTaskEscalation, FEATURE_ESCALATION_MESSAGE } from "../app/plan.ts";
|
|
13
|
+
import { answerEscalation } from "../app/service.ts";
|
|
14
14
|
|
|
15
15
|
interface Body {
|
|
16
16
|
name?: unknown;
|
|
@@ -28,7 +28,7 @@ export default defineOperation<
|
|
|
28
28
|
|
|
29
29
|
if (name === "escalation-answered") {
|
|
30
30
|
const prKey = String(b.correlationKey ?? "");
|
|
31
|
-
const answer = String(
|
|
31
|
+
const answer = String(b.variables?.answer ?? "").trim();
|
|
32
32
|
if (!prKey) return { status: 400, body: { error: "correlationKey is required" } };
|
|
33
33
|
if (!answer) return { status: 400, body: { error: "answer is required" } };
|
|
34
34
|
const r = await answerEscalation(app.data, app.engine, prKey, answer);
|
|
@@ -40,7 +40,7 @@ export default defineOperation<
|
|
|
40
40
|
// `<plan_key>:<task_id>`; record the answer, resume the parked child, and re-surface the next
|
|
41
41
|
// open escalation.
|
|
42
42
|
const corrKey = String(b.correlationKey ?? "");
|
|
43
|
-
const answer = String(
|
|
43
|
+
const answer = String(b.variables?.answer ?? "").trim();
|
|
44
44
|
if (!corrKey) return { status: 400, body: { error: "correlationKey is required" } };
|
|
45
45
|
if (!answer) return { status: 400, body: { error: "answer is required" } };
|
|
46
46
|
const r = await answerTaskEscalation(app.data, app.engine, corrKey, answer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.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",
|
|
@@ -37,12 +37,15 @@
|
|
|
37
37
|
"layout:check": "node --experimental-strip-types scripts/layout-bpmn.ts --check",
|
|
38
38
|
"dev": "urban dev",
|
|
39
39
|
"compile": "deno task compile",
|
|
40
|
-
"test": "deno test -A"
|
|
40
|
+
"test": "deno test -A",
|
|
41
|
+
"lint": "biome check app operations actions workers pages components scripts main.ts",
|
|
42
|
+
"lint:fix": "biome check --write app operations actions workers pages components scripts main.ts"
|
|
41
43
|
},
|
|
42
44
|
"dependencies": {
|
|
43
45
|
"@nanobpm/urban": "^0.33.0"
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|
|
48
|
+
"@biomejs/biome": "^2.4.11",
|
|
46
49
|
"@semantic-release/changelog": "^6.0.3",
|
|
47
50
|
"@semantic-release/git": "^10.0.1",
|
|
48
51
|
"@semantic-release/npm": "^13.1.5",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
engine biome(1.0)
|
|
2
|
+
language js(typescript)
|
|
3
|
+
|
|
4
|
+
`$expr as $type` as $assertion where {
|
|
5
|
+
$type <: not r"^const$",
|
|
6
|
+
$assertion <: not within JsImport(),
|
|
7
|
+
register_diagnostic(
|
|
8
|
+
span = $assertion,
|
|
9
|
+
message = "Type assertions (`as T`) bypass the type system. Use a type guard or `satisfies` instead. If unavoidable, add `// biome-ignore lint/plugin: <reason>` above.",
|
|
10
|
+
severity = "error"
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -64,9 +64,10 @@ function templateMap(root: string, patterns: string[]): Record<string, string> {
|
|
|
64
64
|
// signal can't see (an empty value carries no `{{token}}` to be unresolved).
|
|
65
65
|
function hasBlankAgentPromptHeader(bpmn: string): boolean {
|
|
66
66
|
const re = /<zeebe:header\s+key="([^"]*)"\s+value="([^"]*)"\s*\/?>/g;
|
|
67
|
-
let m
|
|
68
|
-
while (
|
|
67
|
+
let m = re.exec(bpmn);
|
|
68
|
+
while (m !== null) {
|
|
69
69
|
if (m[1] === AGENT_PROMPT_HEADER && m[2].trim() === "") return true;
|
|
70
|
+
m = re.exec(bpmn);
|
|
70
71
|
}
|
|
71
72
|
return false;
|
|
72
73
|
}
|
|
@@ -86,6 +87,7 @@ export function checkAgentPrompts(root: string): CheckResult {
|
|
|
86
87
|
if (!existsSync(manifestPath)) {
|
|
87
88
|
return { ok: false, errors: [`nano.app.json not found under ${root}`], resolved: [] };
|
|
88
89
|
}
|
|
90
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
89
91
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as AppManifest;
|
|
90
92
|
const models = manifest.models ?? {};
|
|
91
93
|
const templates = templateMap(root, models.templates ?? []);
|
package/scripts/layout-bpmn.ts
CHANGED
|
@@ -12,9 +12,11 @@ import { layoutBpmn } from "@nanobpm/urban";
|
|
|
12
12
|
|
|
13
13
|
// Host-agnostic file I/O: Deno inside a compiled binary, else node:fs under Node — mirrors
|
|
14
14
|
// app/plan.ts's readAsset seam so this runs the same under `npm run` and `deno task`.
|
|
15
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
15
16
|
const g = globalThis as {
|
|
16
17
|
Deno?: {
|
|
17
18
|
args: string[];
|
|
19
|
+
exit(c: number): never;
|
|
18
20
|
readDir(p: string): AsyncIterable<{ name: string; isFile: boolean }>;
|
|
19
21
|
readTextFile(p: string): Promise<string>;
|
|
20
22
|
writeTextFile(p: string, s: string): Promise<void>;
|
|
@@ -52,7 +54,7 @@ const countDi = (xml: string) => ({
|
|
|
52
54
|
});
|
|
53
55
|
|
|
54
56
|
function exit(code: number): never {
|
|
55
|
-
if (g.Deno) return
|
|
57
|
+
if (g.Deno) return g.Deno.exit(code);
|
|
56
58
|
process.exit(code);
|
|
57
59
|
}
|
|
58
60
|
|
package/scripts/purge-db.ts
CHANGED
|
@@ -51,7 +51,8 @@ for (const suffix of ["", "-wal", "-shm"]) {
|
|
|
51
51
|
rmSync(path + suffix);
|
|
52
52
|
console.log(`removed ${path}${suffix}`);
|
|
53
53
|
} catch (err) {
|
|
54
|
-
|
|
54
|
+
const code = typeof err === "object" && err !== null ? Reflect.get(err, "code") : undefined;
|
|
55
|
+
if (code !== "ENOENT") throw err;
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
console.log("app db purged");
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
// (skips `npm pack`; --version/--package are ignored)
|
|
30
30
|
// --force overlay even if the cwd doesn't look like this app
|
|
31
31
|
// -h, --help show this help
|
|
32
|
+
|
|
33
|
+
import { execFileSync } from "node:child_process";
|
|
32
34
|
import {
|
|
33
35
|
cpSync,
|
|
34
36
|
existsSync,
|
|
@@ -39,7 +41,6 @@ import {
|
|
|
39
41
|
rmSync,
|
|
40
42
|
statSync,
|
|
41
43
|
} from "node:fs";
|
|
42
|
-
import { execFileSync } from "node:child_process";
|
|
43
44
|
import { tmpdir } from "node:os";
|
|
44
45
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
45
46
|
|
|
@@ -199,10 +200,12 @@ function runTar(tarArgs: string[], tgz: string): string {
|
|
|
199
200
|
try {
|
|
200
201
|
return execFileSync("tar", tarArgs, { encoding: "utf8" });
|
|
201
202
|
} catch (e) {
|
|
202
|
-
|
|
203
|
+
const code = typeof e === "object" && e !== null ? Reflect.get(e, "code") : undefined;
|
|
204
|
+
if (code === "ENOENT") {
|
|
203
205
|
throw new Error(`'tar' not found on PATH — install it to extract ${tgz}`, { cause: e });
|
|
204
206
|
}
|
|
205
|
-
|
|
207
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
208
|
+
throw new Error(`tar failed on ${tgz}: ${message}`, { cause: e });
|
|
206
209
|
}
|
|
207
210
|
}
|
|
208
211
|
|
|
@@ -329,6 +332,7 @@ function report(label: string, files: string[]): void {
|
|
|
329
332
|
try {
|
|
330
333
|
main();
|
|
331
334
|
} catch (err) {
|
|
332
|
-
|
|
335
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
336
|
+
console.error(`upgrade failed: ${message}`);
|
|
333
337
|
process.exit(1);
|
|
334
338
|
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// merge stage (start the `merge-loop` process and park the PR in `waiting_deps`) when auto-merge
|
|
3
3
|
// is on, or (b) close the PR out as `converged` (review-only mode).
|
|
4
4
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
5
|
-
import { AUTO_MERGE, ensurePr, startMerge } from "../../app/service.ts";
|
|
6
5
|
import { abandonTokenFromUrl } from "../../app/abandon.ts";
|
|
7
6
|
import { maybeStartRetro } from "../../app/retro.ts";
|
|
7
|
+
import { AUTO_MERGE, ensurePr, startMerge } from "../../app/service.ts";
|
|
8
8
|
|
|
9
9
|
// Extends Record so the declared fields are typed while the job may still carry
|
|
10
10
|
// other process variables (e.g. io.nanobpm.agentResult, read by transcriptOf).
|
|
@@ -22,6 +22,7 @@ interface In extends Record<string, unknown> {
|
|
|
22
22
|
|
|
23
23
|
const AGENT_RESULT_KEY = "io.nanobpm.agentResult";
|
|
24
24
|
function transcriptOf(vars: Record<string, unknown>): string | null {
|
|
25
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
25
26
|
const env = vars[AGENT_RESULT_KEY] as { output?: unknown } | undefined;
|
|
26
27
|
return typeof env?.output === "string" ? env.output : null;
|
|
27
28
|
}
|
package/workers/merge/worker.ts
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
// calls live in app/github.ts; this worker records the attempt in the `merges` audit table and
|
|
10
10
|
// shapes the escalation payload on a block.
|
|
11
11
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
12
|
-
import { enqueueViaComment, mergePr } from "../../app/github.ts";
|
|
13
|
-
import { MERGE_ADMIN, MERGE_METHOD, ensurePr } from "../../app/service.ts";
|
|
14
12
|
import { abandonTokenFromUrl } from "../../app/abandon.ts";
|
|
15
|
-
import { loadMergeProtocol } from "../../app/mergeProtocol.ts";
|
|
16
13
|
import { checkBaseTarget } from "../../app/baseGuard.ts";
|
|
14
|
+
import { enqueueViaComment, mergePr } from "../../app/github.ts";
|
|
15
|
+
import { loadMergeProtocol } from "../../app/mergeProtocol.ts";
|
|
16
|
+
import { ensurePr, MERGE_ADMIN, MERGE_METHOD } from "../../app/service.ts";
|
|
17
17
|
|
|
18
18
|
interface In extends Record<string, unknown> {
|
|
19
19
|
prKey: string;
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// and the MAX_ROUNDS guard (status = blocked, question set by the process). Returns
|
|
4
4
|
// `escalationId` for the UI.
|
|
5
5
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
6
|
-
import { ensurePr, parsePr } from "../../app/service.ts";
|
|
7
6
|
import { abandonTokenFromUrl } from "../../app/abandon.ts";
|
|
7
|
+
import { ensurePr, parsePr } from "../../app/service.ts";
|
|
8
8
|
|
|
9
9
|
// Extends Record so the declared fields are typed while the job may still carry
|
|
10
10
|
// other process variables (e.g. io.nanobpm.agentResult, read by transcriptOf).
|
|
@@ -39,6 +39,7 @@ function nonBlank(v: unknown): string | undefined {
|
|
|
39
39
|
|
|
40
40
|
const AGENT_RESULT_KEY = "io.nanobpm.agentResult";
|
|
41
41
|
function transcriptOf(vars: Record<string, unknown>): string | null {
|
|
42
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
42
43
|
const env = vars[AGENT_RESULT_KEY] as { output?: unknown } | undefined;
|
|
43
44
|
return typeof env?.output === "string" ? env.output : null;
|
|
44
45
|
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// Data access goes through the injected app datasource gateway (`app.data.table<T>`), the RAD
|
|
6
6
|
// `Table<T>` surface — `rounds.insert(...)` / `pull_requests.update(...)`, not hand-written SQL.
|
|
7
7
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
8
|
-
import { ensurePr, parsePr } from "../../app/service.ts";
|
|
9
8
|
import { abandonTokenFromUrl } from "../../app/abandon.ts";
|
|
9
|
+
import { ensurePr, parsePr } from "../../app/service.ts";
|
|
10
10
|
|
|
11
11
|
// Extends Record so the declared fields are typed while the job may still carry
|
|
12
12
|
// other process variables (e.g. io.nanobpm.agentResult, read by transcriptOf).
|
|
@@ -29,6 +29,7 @@ interface In extends Record<string, unknown> {
|
|
|
29
29
|
// for audit so a human can see what the agent did this round.
|
|
30
30
|
const AGENT_RESULT_KEY = "io.nanobpm.agentResult";
|
|
31
31
|
function transcriptOf(vars: Record<string, unknown>): string | null {
|
|
32
|
+
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
32
33
|
const env = vars[AGENT_RESULT_KEY] as { output?: unknown } | undefined;
|
|
33
34
|
return typeof env?.output === "string" ? env.output : null;
|
|
34
35
|
}
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
// the whole epic complete GREEN having done nothing. The cap still bounds the loop; it now bounds
|
|
17
17
|
// it into an incident, not a silent proceed. A missing/ambiguous `approved` is treated as NOT
|
|
18
18
|
// approved (revise until the cap).
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
21
|
+
import { BpmnError } from "@nanobpm/urban";
|
|
21
22
|
import { MAX_PLAN_REVIEW_ROUNDS, type PlanReview, planReviews } from "../../app/plan.ts";
|
|
22
23
|
|
|
23
24
|
interface In extends Record<string, unknown> {
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
// • zero PRs opened → record a `failed` outcome for observability, then throw the non-retryable
|
|
11
11
|
// `NO_WORK_DISPATCHED` BpmnError so the engine parks the instance on an incident instead of
|
|
12
12
|
// completing green (issue #86). The `failed` status is terminal, so `startPlan` can re-plan it.
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
15
|
+
import { BpmnError } from "@nanobpm/urban";
|
|
15
16
|
import { planTasks } from "../../app/plan.ts";
|
|
16
17
|
|
|
17
18
|
interface In extends Record<string, unknown> {
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
5
5
|
import {
|
|
6
6
|
recordTrialMergeAudit,
|
|
7
|
+
type TrialMergeResult,
|
|
7
8
|
trialMergeDecision,
|
|
8
9
|
trialMergeTaskId,
|
|
9
|
-
type TrialMergeResult,
|
|
10
10
|
} from "../../app/trialMerge.ts";
|
|
11
11
|
|
|
12
12
|
interface In extends Record<string, unknown> {
|
|
@@ -26,7 +26,6 @@ interface Out extends Record<string, unknown> {
|
|
|
26
26
|
summary?: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const RESULTS = new Set<TrialMergeResult>(["clean", "merge-conflict", "suite-failed"]);
|
|
30
29
|
const str = (v: unknown): string | undefined =>
|
|
31
30
|
typeof v === "string" && v.trim().length > 0 ? v.trim() : undefined;
|
|
32
31
|
const waveNo = (v: unknown): number => {
|
|
@@ -43,7 +42,8 @@ const safeJson = (v: unknown): string => {
|
|
|
43
42
|
|
|
44
43
|
function parseResult(v: unknown): TrialMergeResult {
|
|
45
44
|
const s = typeof v === "string" ? v.trim() : "";
|
|
46
|
-
|
|
45
|
+
if (s === "clean" || s === "merge-conflict" || s === "suite-failed") return s;
|
|
46
|
+
return "suite-failed";
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const handler: AppJobHandler<In, Out> = async (job, app) => {
|
|
@@ -52,8 +52,8 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
|
|
|
52
52
|
const result = parseResult(job.variables.result);
|
|
53
53
|
const summary = str(job.variables.summary) ??
|
|
54
54
|
(result === "suite-failed" ? "Trial merge suite failed or returned no machine-readable result" : result);
|
|
55
|
-
const
|
|
56
|
-
const jobKey =
|
|
55
|
+
const legacyJobKey = Reflect.get(job, "key");
|
|
56
|
+
const jobKey = job.jobKey ?? (legacyJobKey == null ? null : String(legacyJobKey));
|
|
57
57
|
|
|
58
58
|
try {
|
|
59
59
|
await recordTrialMergeAudit(app.data, {
|
|
@@ -80,7 +80,7 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
|
|
|
80
80
|
trialMergeRed,
|
|
81
81
|
summary,
|
|
82
82
|
task: { id: trialMergeTaskId(wave), title: `Trial merge gate for wave ${wave}` },
|
|
83
|
-
question: `${summary}${failing}\n\nD3 needs a human decision: either the PR heads merged cleanly and the combined suite failed, or the trial-merge agent did not return a valid machine-readable result. Decide the design/infrastructure fix, update the PR heads if needed, then answer to rerun the trial merge; answer exactly
|
|
83
|
+
question: `${summary}${failing}\n\nD3 needs a human decision: either the PR heads merged cleanly and the combined suite failed, or the trial-merge agent did not return a valid machine-readable result. Decide the design/infrastructure fix, update the PR heads if needed, then answer to rerun the trial merge; answer exactly "proceed" only to override and continue without rerunning.`,
|
|
84
84
|
};
|
|
85
85
|
};
|
|
86
86
|
|
|
@@ -14,19 +14,19 @@
|
|
|
14
14
|
// Enrollment lives here (not in the finalizer) so a PR is enrolled the moment its wave lands —
|
|
15
15
|
// and, crucially, so a later wave's `dependsOn` can reference the PR keys earlier waves produced.
|
|
16
16
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
17
|
+
import { appendEntry } from "../../app/blackboard.ts";
|
|
18
|
+
import { fetchPrFiles, fetchPrHead } from "../../app/github.ts";
|
|
19
|
+
import { deriveExclusions, recordExclusions } from "../../app/mergeExclusion.ts";
|
|
20
|
+
import { loadMergeProtocol } from "../../app/mergeProtocol.ts";
|
|
17
21
|
import {
|
|
18
22
|
type PlanTask,
|
|
19
23
|
type PlanTaskStatus,
|
|
24
|
+
plans,
|
|
20
25
|
planTaskDeps,
|
|
21
26
|
planTasks,
|
|
22
|
-
plans,
|
|
23
27
|
} from "../../app/plan.ts";
|
|
24
28
|
import { parsePr, submitPr } from "../../app/service.ts";
|
|
25
29
|
import { parseTaskDelta, readTaskDeltas, recordTaskDelta } from "../../app/taskDelta.ts";
|
|
26
|
-
import { appendEntry } from "../../app/blackboard.ts";
|
|
27
|
-
import { deriveExclusions, recordExclusions } from "../../app/mergeExclusion.ts";
|
|
28
|
-
import { fetchPrFiles, fetchPrHead } from "../../app/github.ts";
|
|
29
|
-
import { loadMergeProtocol } from "../../app/mergeProtocol.ts";
|
|
30
30
|
import { shouldRunTrialMerge, type TrialMergeHead } from "../../app/trialMerge.ts";
|
|
31
31
|
|
|
32
32
|
interface Result {
|
|
@@ -61,9 +61,8 @@ const str = (v: unknown): string | undefined =>
|
|
|
61
61
|
// including a missing status — is treated as `blocked`: we must not assume a PR was opened,
|
|
62
62
|
// and we only hand off / persist a PR when the status is `opened`.
|
|
63
63
|
type WaveResultStatus = Extract<PlanTaskStatus, "opened" | "blocked" | "skipped">;
|
|
64
|
-
const ALLOWED_STATUSES = new Set<WaveResultStatus>(["opened", "blocked", "skipped"]);
|
|
65
64
|
const isWaveResultStatus = (s: string): s is WaveResultStatus =>
|
|
66
|
-
|
|
65
|
+
s === "opened" || s === "blocked" || s === "skipped";
|
|
67
66
|
|
|
68
67
|
// Coerce a wave index/count to a non-negative integer, falling back to 0. A NaN here would make
|
|
69
68
|
// `nextWave < waveCount` mis-evaluate and end the loop early, leaving tasks `pending`.
|
|
@@ -103,7 +102,7 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
|
|
|
103
102
|
const readyHeadsThisWave: { repo: string; number: number | string }[] = [];
|
|
104
103
|
|
|
105
104
|
for (let i = 0; i < waveTasks.length; i++) {
|
|
106
|
-
const taskId = str(
|
|
105
|
+
const taskId = str(waveTasks[i]?.id);
|
|
107
106
|
if (!taskId) continue;
|
|
108
107
|
const res = results[i] ?? {};
|
|
109
108
|
const rawStatus = str(res.status);
|
|
@@ -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
|
|