@nanobpm/nano-workforce 0.36.0 → 0.38.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 +7 -17
- package/AGENTS.md +8 -7
- package/CHANGELOG.md +14 -0
- package/README.md +6 -27
- package/SPEC.md +15 -13
- package/app/abandon.test.ts +14 -19
- package/app/abandon.ts +2 -2
- package/app/baseGuard.test.ts +7 -6
- package/app/blackboard.test.ts +25 -35
- package/app/blackboard.ts +2 -2
- package/app/ensure-pr.test.ts +10 -12
- package/app/github.test.ts +9 -8
- package/app/github.ts +1 -21
- package/app/instance-tracking.test.ts +8 -6
- package/app/mergeExclusion.test.ts +11 -20
- package/app/mergeProtocol.test.ts +14 -13
- package/app/mergeRebaseArm.test.ts +8 -6
- package/app/mergeTrain.test.ts +10 -9
- package/app/persist-escalation.test.ts +10 -31
- package/app/persist-round.test.ts +7 -20
- package/app/plan.test.ts +19 -42
- package/app/record-plan-review.test.ts +8 -7
- package/app/retro.test.ts +24 -48
- 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 +1 -1
- package/app/taskDelta.test.ts +8 -17
- package/app/trialMerge.test.ts +4 -3
- package/app/version.ts +6 -21
- package/app/waves.test.ts +17 -16
- package/biome.json +0 -2
- package/main.ts +3 -3
- package/nano.app.json +2 -25
- package/openapi.yaml +649 -0
- package/operations/answerFeatureEscalation.test.ts +100 -0
- package/operations/answerFeatureEscalation.ts +49 -0
- package/operations/appendBlackboard.ts +61 -0
- package/{actions → operations}/blackboard.test.ts +28 -30
- package/{actions/abandon.test.ts → operations/checkAbandon.test.ts} +12 -26
- package/{actions/abandon.ts → operations/checkAbandon.ts} +8 -7
- package/operations/getVersion.test.ts +8 -12
- package/operations/getVersion.ts +2 -3
- package/operations/listActivePrs.test.ts +8 -14
- package/operations/listActivePrs.ts +3 -4
- package/operations/postMessage.ts +1 -1
- package/operations/readBlackboard.ts +29 -0
- package/operations/startAndMessage.test.ts +11 -15
- package/operations/startConvergenceLoop.ts +17 -11
- package/operations/startPlanFanout.ts +13 -7
- package/package.json +9 -7
- package/pages/epic.page.json +1 -1
- package/pages/home.page.json +1 -1
- package/scripts/check-agent-prompts.test.ts +15 -11
- package/scripts/layout-bpmn.ts +6 -28
- package/scripts/pages-contract.test.ts +14 -13
- package/scripts/purge-db.ts +1 -1
- package/scripts/upgrade-from-pack.ts +1 -1
- package/test/assert.ts +74 -0
- package/tsconfig.json +4 -1
- package/workers/persist-task-escalation/worker.ts +1 -1
- package/workers/record-plan-review/worker.test.ts +6 -9
- package/workers/record-results/worker.test.ts +5 -8
- package/workers/record-trial-merge/worker.test.ts +7 -18
- package/workers/record-wave/worker.test.ts +13 -20
- package/workers/retro-gather/worker.test.ts +4 -17
- package/workers/retro-record/worker.test.ts +8 -27
- package/workers/select-wave/worker.test.ts +5 -8
- package/actions/blackboard.ts +0 -77
- package/actions/feature-answer-hook.ts +0 -45
- package/actions/plan-hook.ts +0 -20
- package/actions/webhook-submit.ts +0 -22
- package/deno.json +0 -24
- package/deno.lock +0 -1777
- package/openapi.json +0 -248
package/actions/blackboard.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
// GET/POST /hooks/blackboard?token=<capabilityToken> — the epic coordination blackboard endpoint
|
|
2
|
-
// (Tier 1, issues #51 / #49 D4).
|
|
3
|
-
//
|
|
4
|
-
// This is a DIRECT side-channel for agents, distinct from the c8ctl-nano activation/completion
|
|
5
|
-
// channel. The per-plan capability token (query string) IS the credential: it scopes every read
|
|
6
|
-
// and write to exactly one plan, so no shared secret is needed — the agent curls the exact URL it
|
|
7
|
-
// was handed in its prompt. An unknown token is a 404 (never leaks which plans exist).
|
|
8
|
-
//
|
|
9
|
-
// GET → { planKey, entries: [ { id, author_task, kind, files, body, wave, created_at } ], cursor }
|
|
10
|
-
// optional ?since=<id> returns only entries with id > since (incremental poll). `cursor` is
|
|
11
|
-
// the plan's current head id; pass it back as `since` on the next poll (Tier 2).
|
|
12
|
-
// POST → append one entry: { author_task?, kind?, files?, body, wave?, dedupe_key? }. Idempotent
|
|
13
|
-
// on (plan, dedupe_key). Returns { id, inserted, conflicts } — `conflicts` lists prior
|
|
14
|
-
// sibling `file-claim`s on the same file(s) (advisory first-writer-wins; never a lock).
|
|
15
|
-
import type { ActionHandler } from "@nanobpm/urban";
|
|
16
|
-
import {
|
|
17
|
-
appendEntry,
|
|
18
|
-
detectFileClaimConflicts,
|
|
19
|
-
normalizeKind,
|
|
20
|
-
planKeyForToken,
|
|
21
|
-
readBlackboardPage,
|
|
22
|
-
} from "../app/blackboard.ts";
|
|
23
|
-
|
|
24
|
-
const handler: ActionHandler = async ({ req, body }, app) => {
|
|
25
|
-
const token = (req.query.get("token") ?? req.headers.get("x-blackboard-token") ?? "").trim();
|
|
26
|
-
if (!token) return { status: 400, body: { error: "missing blackboard token" } };
|
|
27
|
-
const planKey = await planKeyForToken(app.data, token);
|
|
28
|
-
if (!planKey) return { status: 404, body: { error: "unknown blackboard token" } };
|
|
29
|
-
|
|
30
|
-
if (req.method === "GET") {
|
|
31
|
-
const rawSince = req.query.get("since");
|
|
32
|
-
const since = rawSince != null && /^\d+$/.test(rawSince) ? Number(rawSince) : undefined;
|
|
33
|
-
const { entries, cursor } = await readBlackboardPage(app.data, planKey, { since });
|
|
34
|
-
return { status: 200, body: { planKey, entries, cursor } };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (req.method === "POST") {
|
|
38
|
-
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
39
|
-
const b = (body ?? {}) as Record<string, unknown>;
|
|
40
|
-
const text = typeof b.body === "string" ? b.body.trim() : "";
|
|
41
|
-
if (!text) return { status: 400, body: { error: "'body' (the note text) is required" } };
|
|
42
|
-
const kind = normalizeKind(b.kind);
|
|
43
|
-
const files = Array.isArray(b.files) ? b.files.map(String) : [];
|
|
44
|
-
// Normalize once (trim + default to "system") so the value we send to appendEntry matches the
|
|
45
|
-
// value we send to detectFileClaimConflicts. Otherwise an omitted/blank author_task is stored as
|
|
46
|
-
// "system" but conflict detection sees "", and the caller's own prior "system" claims are wrongly
|
|
47
|
-
// reported as sibling conflicts.
|
|
48
|
-
const author_task = (typeof b.author_task === "string" ? b.author_task.trim() : "") || "system";
|
|
49
|
-
const res = await appendEntry(app.data, planKey, {
|
|
50
|
-
author_task,
|
|
51
|
-
kind,
|
|
52
|
-
files,
|
|
53
|
-
body: text,
|
|
54
|
-
wave: typeof b.wave === "number" ? b.wave : null,
|
|
55
|
-
dedupe_key: typeof b.dedupe_key === "string" ? b.dedupe_key : undefined,
|
|
56
|
-
});
|
|
57
|
-
// Advisory conflict-of-intent: surface prior sibling claims on the same file(s). Computed AFTER
|
|
58
|
-
// the append and filtered to claims strictly before ours (id < res.id), so first-writer-wins is
|
|
59
|
-
// decided by insertion order — a sibling that raced a claim in between is still caught, and our
|
|
60
|
-
// own just-written row is never reported. Never blocks the append — the agent decides how to react.
|
|
61
|
-
const conflicts = kind === "file-claim"
|
|
62
|
-
? await detectFileClaimConflicts(app.data, planKey, {
|
|
63
|
-
author_task,
|
|
64
|
-
files,
|
|
65
|
-
beforeId: Number(res.id),
|
|
66
|
-
})
|
|
67
|
-
: [];
|
|
68
|
-
return {
|
|
69
|
-
status: res.inserted ? 201 : 200,
|
|
70
|
-
body: { id: Number(res.id), inserted: res.inserted, conflicts },
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return { status: 405, body: { error: "method not allowed (use GET or POST)" } };
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export default handler;
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// POST /hooks/feature-answer — answer an implementation-phase task escalation out
|
|
2
|
-
// of band (optional shared-secret guard via X-Hook-Secret, enforced only when
|
|
3
|
-
// NANO_PR_WEBHOOK_SECRET is set — mirrors /hooks/submit and /hooks/plan), issue #25.
|
|
4
|
-
// Lets an external
|
|
5
|
-
// system (a chat relay, a CI job, a human via curl) resume a parked implementation
|
|
6
|
-
// agent without the page. Same idempotent `answerTaskEscalation` path the page's
|
|
7
|
-
// answer form uses.
|
|
8
|
-
//
|
|
9
|
-
// Body accepts either the raw correlation key or a plan+task pair:
|
|
10
|
-
// { "corrKey": "owner/repo#12:task-3", "answer": "…" }
|
|
11
|
-
// { "plan": "owner/repo#12", "task": "task-3", "answer": "…" }
|
|
12
|
-
import type { ActionHandler } from "@nanobpm/urban";
|
|
13
|
-
import { answerTaskEscalation, featureCorrKey } from "../app/plan.ts";
|
|
14
|
-
|
|
15
|
-
const WEBHOOK_SECRET = process.env.NANO_PR_WEBHOOK_SECRET ?? "";
|
|
16
|
-
|
|
17
|
-
const str = (v: unknown): string => (typeof v === "string" ? v.trim() : "");
|
|
18
|
-
|
|
19
|
-
const handler: ActionHandler = async ({ req, body }, app) => {
|
|
20
|
-
if (WEBHOOK_SECRET && req.headers.get("x-hook-secret") !== WEBHOOK_SECRET) {
|
|
21
|
-
return { status: 401, body: { error: "unauthorized" } };
|
|
22
|
-
}
|
|
23
|
-
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
24
|
-
const b = (body ?? {}) as {
|
|
25
|
-
corrKey?: unknown;
|
|
26
|
-
plan?: unknown;
|
|
27
|
-
task?: unknown;
|
|
28
|
-
answer?: unknown;
|
|
29
|
-
};
|
|
30
|
-
const answer = str(b.answer);
|
|
31
|
-
if (!answer) return { status: 400, body: { error: "answer is required" } };
|
|
32
|
-
|
|
33
|
-
const corrKey = str(b.corrKey) || (str(b.plan) && str(b.task) ? featureCorrKey(str(b.plan), str(b.task)) : "");
|
|
34
|
-
if (!corrKey) {
|
|
35
|
-
return {
|
|
36
|
-
status: 400,
|
|
37
|
-
body: { error: "provide corrKey, or both plan (owner/repo#N) and task" },
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const r = await answerTaskEscalation(app.data, app.engine, corrKey, answer);
|
|
42
|
-
return { status: r.ok ? 200 : 404, body: r };
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export default handler;
|
package/actions/plan-hook.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// POST /hooks/plan — kick off a planning fan-out out of band (shared-secret auth via
|
|
2
|
-
// X-Hook-Secret). Lets an external system (a GitHub webhook relay on issue open/label, a CI job)
|
|
3
|
-
// hand an issue to the fleet. Same idempotent startPlan path as the page's "Plan issue" action.
|
|
4
|
-
import type { ActionHandler } from "@nanobpm/urban";
|
|
5
|
-
import { parseIssue, startPlan } from "../app/plan.ts";
|
|
6
|
-
|
|
7
|
-
const WEBHOOK_SECRET = process.env.NANO_PR_WEBHOOK_SECRET ?? "";
|
|
8
|
-
|
|
9
|
-
const handler: ActionHandler = async ({ req, body }, app) => {
|
|
10
|
-
if (WEBHOOK_SECRET && req.headers.get("x-hook-secret") !== WEBHOOK_SECRET) {
|
|
11
|
-
return { status: 401, body: { error: "unauthorized" } };
|
|
12
|
-
}
|
|
13
|
-
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
14
|
-
const b = (body ?? {}) as { url?: unknown; issue?: unknown };
|
|
15
|
-
const parsed = parseIssue(String(b.issue ?? b.url ?? ""));
|
|
16
|
-
if (!parsed) return { status: 400, body: { error: "could not parse issue (use owner/repo#123 or an issue URL)" } };
|
|
17
|
-
return { status: 202, body: await startPlan(app.data, app.engine, parsed) };
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export default handler;
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// POST /hooks/submit — submit a PR out-of-band (shared-secret auth via X-Hook-Secret). Not
|
|
2
|
-
// part of the page UI; lets an external system (a GitHub webhook relay, a CI job) kick off a
|
|
3
|
-
// convergence run. Same idempotent submit path as the page's "Start review" action.
|
|
4
|
-
import type { ActionHandler } from "@nanobpm/urban";
|
|
5
|
-
import { clampRounds, MAX_ROUNDS, parsePr, submitPr } from "../app/service.ts";
|
|
6
|
-
|
|
7
|
-
const WEBHOOK_SECRET = process.env.NANO_PR_WEBHOOK_SECRET ?? "";
|
|
8
|
-
|
|
9
|
-
const handler: ActionHandler = async ({ req, body }, app) => {
|
|
10
|
-
if (WEBHOOK_SECRET && req.headers.get("x-hook-secret") !== WEBHOOK_SECRET) {
|
|
11
|
-
return { status: 401, body: { error: "unauthorized" } };
|
|
12
|
-
}
|
|
13
|
-
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
14
|
-
const b = (body ?? {}) as { url?: unknown; pr?: unknown; dependsOn?: unknown; maxRounds?: unknown };
|
|
15
|
-
const parsed = parsePr(String(b.url ?? b.pr ?? ""));
|
|
16
|
-
if (!parsed) return { status: 400, body: { error: "could not parse PR url" } };
|
|
17
|
-
const dependsOn = Array.isArray(b.dependsOn) ? b.dependsOn.map((d) => String(d)) : [];
|
|
18
|
-
const maxRounds = clampRounds(b.maxRounds, MAX_ROUNDS);
|
|
19
|
-
return { status: 202, body: await submitPr(app.data, app.engine, parsed, dependsOn, maxRounds) };
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export default handler;
|
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
|
-
}
|