@nanobpm/nano-workforce 0.117.0 → 0.118.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 +7 -0
- package/README.md +21 -0
- package/app/deliveryGraphText.ts +41 -0
- package/docs/agent-guide.md +173 -0
- package/openapi.yaml +166 -0
- package/operations/dispatchDeliveryGraph.test.ts +166 -0
- package/operations/dispatchDeliveryGraph.ts +113 -0
- package/operations/getAgentInstructions.test.ts +22 -0
- package/operations/previewDeliveryGraph.test.ts +74 -0
- package/operations/previewDeliveryGraph.ts +59 -0
- package/package.json +1 -1
- package/pages/_nav.json +1 -0
- package/pages/board.page.json +4 -0
- package/pages/cockpit.page.json +4 -0
- package/pages/delivery-graph-detail.page.json +114 -0
- package/pages/delivery-graphs.page.json +153 -0
- package/pages/epic-detail.page.json +4 -0
- package/pages/epic.page.json +4 -0
- package/pages/feature.page.json +4 -0
- package/pages/home.page.json +4 -0
- package/pages/lineage.page.json +4 -0
- package/pages/overview.page.json +6 -2
- package/pages/tasks.page.json +4 -0
- package/pages/velocity.page.json +4 -0
- package/scripts/pages-contract.test.ts +67 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// POST /app/api/actions/delivery-graph/dispatch → operationId `dispatchDeliveryGraph` (issue #386,
|
|
2
|
+
// ADR 0005 slice S5). The human-facing UI JSON-paste DISPATCH ingress: the Delivery Graphs page's
|
|
3
|
+
// "Dispatch" action posts the operator's pasted delivery-graph as a raw JSON STRING plus an explicit
|
|
4
|
+
// `approve` flag; this door parses it (`parseDeliveryGraphText`) and DELEGATES to the SAME gated,
|
|
5
|
+
// idempotent `startDeliveryGraph` handler the agent-facing / REST paths use. There is deliberately NO
|
|
6
|
+
// parallel dispatch path — this is a thin UI text adapter onto the ONE contract (S5's stated "UI
|
|
7
|
+
// JSON-paste" ingress that S5 named but did not build).
|
|
8
|
+
//
|
|
9
|
+
// • APPROVAL. When the operator ticks `approve` (having reviewed the preview), the door derives the
|
|
10
|
+
// graph's content digest via the SAME pure compiler `startDeliveryGraph` uses and presents it as
|
|
11
|
+
// the `approvalToken`, so a side-effecting graph dispatches. Without `approve`, a side-effecting
|
|
12
|
+
// graph is PARKED at approval by the start door (a 400 whose `awaiting-approval` run shows in the
|
|
13
|
+
// in-flight grid) and a non-side-effecting graph dispatches straight away.
|
|
14
|
+
// • The start door owns idempotency, the durable run row, and the at-most-once launch fence — this
|
|
15
|
+
// adapter adds nothing to that; it only parses the paste and surfaces a human `error` banner.
|
|
16
|
+
|
|
17
|
+
import { compileDeliveryGraph } from "../app/deliveryGraphCompiler.ts";
|
|
18
|
+
import { parseDeliveryGraphText } from "../app/deliveryGraphText.ts";
|
|
19
|
+
import { deliveryGraphDigest } from "../app/deliveryRunner.ts";
|
|
20
|
+
import type { DeliveryCompileError, DeliveryGraphTextResult } from "../nano-generated/api-io.d.ts";
|
|
21
|
+
import { defineOperation } from "../nano-generated/operations.ts";
|
|
22
|
+
import startDeliveryGraph from "./startDeliveryGraph.ts";
|
|
23
|
+
|
|
24
|
+
export default defineOperation("dispatchDeliveryGraph", async (input, app) => {
|
|
25
|
+
const body = input.body;
|
|
26
|
+
const parsed = parseDeliveryGraphText(body);
|
|
27
|
+
if (!parsed.ok) {
|
|
28
|
+
app.log.warn("dispatch-delivery-graph rejected: parse", { message: parsed.error });
|
|
29
|
+
return { status: 400, body: { ok: false, error: parsed.error } };
|
|
30
|
+
}
|
|
31
|
+
const approve = body?.approve === true;
|
|
32
|
+
const idemRaw = typeof body?.idempotencyKey === "string" ? body.idempotencyKey.trim() : "";
|
|
33
|
+
const idempotencyKey = idemRaw !== "" ? idemRaw : undefined;
|
|
34
|
+
|
|
35
|
+
// When the operator approves, derive the graph's content digest (the approval token) via the SAME
|
|
36
|
+
// pure compiler the start door uses, so a side-effecting graph they reviewed in the preview
|
|
37
|
+
// dispatches. A compile failure here surfaces as a clean 400 rather than reaching the start door.
|
|
38
|
+
let approvalToken: string | undefined;
|
|
39
|
+
if (approve) {
|
|
40
|
+
const compiled = compileDeliveryGraph(parsed.graph);
|
|
41
|
+
if (!compiled.ok) {
|
|
42
|
+
app.log.warn("dispatch-delivery-graph rejected: compile", { errors: compiled.errors.length });
|
|
43
|
+
return {
|
|
44
|
+
status: 400,
|
|
45
|
+
body: {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: `graph failed validation: ${compiled.errors.length} error(s)`,
|
|
48
|
+
errors: compiled.errors,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
approvalToken = deliveryGraphDigest(compiled.bpmn);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const startBody: Record<string, unknown> = { graph: parsed.graph };
|
|
56
|
+
if (approvalToken !== undefined) startBody.approvalToken = approvalToken;
|
|
57
|
+
if (idempotencyKey !== undefined) startBody.idempotencyKey = idempotencyKey;
|
|
58
|
+
|
|
59
|
+
// Delegate to the ONE dispatch contract — the exact `startDeliveryGraph` handler, no re-implementation.
|
|
60
|
+
// The parsed graph is `unknown` (the paste is runtime-validated inside the door), so the handler's
|
|
61
|
+
// typed input signature is bridged here; nothing re-implements dispatch.
|
|
62
|
+
// biome-ignore lint/plugin: bridging the runtime-validated paste onto the start door's typed input; the door owns validation.
|
|
63
|
+
const delegate = startDeliveryGraph as (
|
|
64
|
+
i: { req: unknown; params: unknown; query: unknown; body: unknown },
|
|
65
|
+
a: typeof app,
|
|
66
|
+
) => Promise<{ status?: number; body?: Record<string, unknown> } | undefined>;
|
|
67
|
+
const res = await delegate(
|
|
68
|
+
{ req: input.req, params: input.params, query: input.query, body: startBody },
|
|
69
|
+
app,
|
|
70
|
+
);
|
|
71
|
+
const resBody: Record<string, unknown> = res?.body ?? {};
|
|
72
|
+
const status = res?.status ?? 202;
|
|
73
|
+
|
|
74
|
+
// Re-shape the start door's result onto this ingress contract by narrowing each field — no assertions.
|
|
75
|
+
const outBody: DeliveryGraphTextResult = { ok: resBody.ok === true };
|
|
76
|
+
if (typeof resBody.status === "string") outBody.status = resBody.status;
|
|
77
|
+
if (typeof resBody.runKey === "string") outBody.runKey = resBody.runKey;
|
|
78
|
+
if (typeof resBody.digest === "string") outBody.digest = resBody.digest;
|
|
79
|
+
if (typeof resBody.sideEffecting === "boolean") outBody.sideEffecting = resBody.sideEffecting;
|
|
80
|
+
if (typeof resBody.alreadyRunning === "boolean") outBody.alreadyRunning = resBody.alreadyRunning;
|
|
81
|
+
if (typeof resBody.processInstanceKey === "string") outBody.processInstanceKey = resBody.processInstanceKey;
|
|
82
|
+
if (typeof resBody.processDefinitionId === "string") outBody.processDefinitionId = resBody.processDefinitionId;
|
|
83
|
+
if (typeof resBody.approvalToken === "string") outBody.approvalToken = resBody.approvalToken;
|
|
84
|
+
if (typeof resBody.message === "string") outBody.message = resBody.message;
|
|
85
|
+
|
|
86
|
+
// Carry through the start door's path-qualified validation/compile `errors` so callers/UI keep the
|
|
87
|
+
// structured detail (not just the summary banner). Narrow each entry to the wire pair — no assertions.
|
|
88
|
+
if (Array.isArray(resBody.errors)) {
|
|
89
|
+
const errors: DeliveryCompileError[] = [];
|
|
90
|
+
for (const e of resBody.errors) {
|
|
91
|
+
if (
|
|
92
|
+
typeof e === "object" &&
|
|
93
|
+
e !== null &&
|
|
94
|
+
"path" in e &&
|
|
95
|
+
"message" in e &&
|
|
96
|
+
typeof e.path === "string" &&
|
|
97
|
+
typeof e.message === "string"
|
|
98
|
+
) {
|
|
99
|
+
errors.push({ path: e.path, message: e.message });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (errors.length > 0) outBody.errors = errors;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Surface a human error banner for the page on a refusal (parked-at-approval / validation).
|
|
106
|
+
if (status >= 400 && typeof outBody.error !== "string") {
|
|
107
|
+
if (typeof resBody.error === "string") outBody.error = resBody.error;
|
|
108
|
+
else if (typeof resBody.message === "string") outBody.error = resBody.message;
|
|
109
|
+
else if (outBody.errors !== undefined) outBody.error = `graph failed validation: ${outBody.errors.length} error(s)`;
|
|
110
|
+
else outBody.error = "dispatch was refused";
|
|
111
|
+
}
|
|
112
|
+
return { status, body: outBody };
|
|
113
|
+
});
|
|
@@ -50,6 +50,28 @@ test("the guide covers every capability the endpoint promises", async () => {
|
|
|
50
50
|
assert(md.includes("nanobpm/nano-workforce"), "covers raising issues/PRs against the repo");
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
test("the guide documents the delivery-graph surface (ADR 0005)", async () => {
|
|
54
|
+
const md = ((await handler(input(), app)) as any).body.instructions as string;
|
|
55
|
+
// The two doors, with their exact action paths.
|
|
56
|
+
assert(md.includes("compile-delivery-graph"), "documents the pure compile door");
|
|
57
|
+
assert(md.includes("start/delivery-graph"), "documents the gated dispatch door");
|
|
58
|
+
// The closed node vocabulary: assert the exact config snippet for each of the four kinds,
|
|
59
|
+
// so the test fails if §9's node-kind table is removed or reworded — not merely if the bare
|
|
60
|
+
// words "agent"/"wait"/"human"/"connector" appear anywhere else in the guide.
|
|
61
|
+
assert(md.includes("agent: { jobType, prompt? }"), "documents the agent node config");
|
|
62
|
+
assert(md.includes("wait: <ReadinessProbe>"), "documents the wait node config");
|
|
63
|
+
assert(md.includes("human?: { formKey?, prompt? }"), "documents the human node config");
|
|
64
|
+
assert(md.includes("connector: { target, dedupeKey?, payload? }"), "documents the connector node config");
|
|
65
|
+
// The fact-edge syntax: an edge is `{ from, to }` and `from` may be a qualified `<nodeId>.<fact>`.
|
|
66
|
+
assert(md.includes("each edge is `{ from, to }`"), "documents the edge shape");
|
|
67
|
+
assert(md.includes("qualified `<nodeId>.<fact>`"), "documents the qualified fact-edge syntax");
|
|
68
|
+
// The approval gate + idempotency of the start door.
|
|
69
|
+
assert(md.includes("approvalToken"), "documents the approval gate");
|
|
70
|
+
assert(md.includes("idempotencyKey"), "documents idempotency");
|
|
71
|
+
// The worked example: a human emit node handing a version to a downstream edge.
|
|
72
|
+
assert(md.includes("manual-publish.publishedVersion"), "includes the worked example's human-emit fact edge");
|
|
73
|
+
});
|
|
74
|
+
|
|
53
75
|
test("examples are keyed to the request's control-API base and leave no placeholders", async () => {
|
|
54
76
|
const forwarded = input({ host: "wf.example.com", "x-forwarded-proto": "https" });
|
|
55
77
|
const md = ((await handler(forwarded, app)) as any).body.instructions as string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Tests for the POST /app/api/actions/delivery-graph/preview operation `previewDeliveryGraph`
|
|
2
|
+
// (issue #386, ADR 0005 slice S1) — the human-facing UI JSON-paste PREVIEW ingress. It parses the
|
|
3
|
+
// operator's pasted JSON STRING and runs the SAME pure `compileDeliveryGraph` compiler the agent door
|
|
4
|
+
// uses, mapping the result onto a compact summary (200) or a human `error` + path-qualified `errors`
|
|
5
|
+
// (400). It is PURE — no data layer, no dispatch — so it mirrors the compile door's test harness.
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import { assert, assertEquals } from "#test-assert";
|
|
8
|
+
import type { AppApi } from "@nanobpm/urban";
|
|
9
|
+
import { noopLog } from "../test/log.ts";
|
|
10
|
+
import handler from "./previewDeliveryGraph.ts";
|
|
11
|
+
|
|
12
|
+
const app = { log: noopLog() } as unknown as AppApi;
|
|
13
|
+
|
|
14
|
+
async function call(body: unknown) {
|
|
15
|
+
return (await handler({ req: {} as any, params: {}, query: {}, body } as any, app)) as any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const GOOD = JSON.stringify({
|
|
19
|
+
name: "runbook",
|
|
20
|
+
nodes: [
|
|
21
|
+
{ id: "a", kind: "agent", agent: { jobType: "senior:feature" } },
|
|
22
|
+
{ id: "b", kind: "human", human: { prompt: "do X" } },
|
|
23
|
+
],
|
|
24
|
+
edges: [{ from: "a", to: "b" }],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("preview-delivery-graph: a pasted well-formed graph → 200 summary with digest + counts", async () => {
|
|
28
|
+
const res = await call({ graphJson: GOOD });
|
|
29
|
+
assertEquals(res.status, 200);
|
|
30
|
+
assertEquals(res.body.ok, true);
|
|
31
|
+
assert(typeof res.body.digest === "string" && res.body.digest.length > 0);
|
|
32
|
+
assertEquals(res.body.nodeCount, 2);
|
|
33
|
+
assertEquals(res.body.humanNodeCount, 1);
|
|
34
|
+
assertEquals(res.body.sideEffectCount, 1);
|
|
35
|
+
assertEquals(res.body.sideEffecting, true);
|
|
36
|
+
assert(typeof res.body.diagram === "string" && res.body.diagram.length > 0);
|
|
37
|
+
assertEquals(res.body.title, "runbook");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("preview-delivery-graph: is PURE — repeated previews return the identical digest", async () => {
|
|
41
|
+
const a = await call({ graphJson: GOOD });
|
|
42
|
+
const b = await call({ graphJson: GOOD });
|
|
43
|
+
assertEquals(a.body.digest, b.body.digest);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("preview-delivery-graph: text that is not valid JSON → 400 with a human error", async () => {
|
|
47
|
+
const res = await call({ graphJson: "{ not json" });
|
|
48
|
+
assertEquals(res.status, 400);
|
|
49
|
+
assertEquals(res.body.ok, false);
|
|
50
|
+
assert(typeof res.body.error === "string" && res.body.error.includes("not valid JSON"));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("preview-delivery-graph: a blank paste → 400, never a 500", async () => {
|
|
54
|
+
const res = await call({ graphJson: " " });
|
|
55
|
+
assertEquals(res.status, 400);
|
|
56
|
+
assertEquals(res.body.ok, false);
|
|
57
|
+
assert(typeof res.body.error === "string" && res.body.error.length > 0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("preview-delivery-graph: a valid-JSON but malformed graph → 400 with path-qualified errors", async () => {
|
|
61
|
+
const res = await call({
|
|
62
|
+
graphJson: JSON.stringify({ nodes: [{ id: "a", kind: "agent", agent: { jobType: "j" } }], edges: [{ from: "a", to: "ghost" }] }),
|
|
63
|
+
});
|
|
64
|
+
assertEquals(res.status, 400);
|
|
65
|
+
assertEquals(res.body.ok, false);
|
|
66
|
+
assert(Array.isArray(res.body.errors) && res.body.errors.length > 0);
|
|
67
|
+
assert(res.body.errors.every((e: { path: string; message: string }) => typeof e.path === "string"));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("preview-delivery-graph: a pasted JSON array (not an object) → 400", async () => {
|
|
71
|
+
const res = await call({ graphJson: "[]" });
|
|
72
|
+
assertEquals(res.status, 400);
|
|
73
|
+
assertEquals(res.body.ok, false);
|
|
74
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// POST /app/api/actions/delivery-graph/preview → operationId `previewDeliveryGraph` (issue #386,
|
|
2
|
+
// ADR 0005 slice S1). The human-facing UI JSON-paste PREVIEW ingress: the Delivery Graphs page's
|
|
3
|
+
// "Preview" action posts the operator's pasted delivery-graph as a raw JSON STRING; this door parses
|
|
4
|
+
// it (`parseDeliveryGraphText`) and runs the SAME pure `compileDeliveryGraph` compiler the agent-facing
|
|
5
|
+
// door uses, returning a compact summary — the content `digest` (the approval token to dispatch with),
|
|
6
|
+
// the node / human-stop / side-effect counts, and the mermaid `diagram`.
|
|
7
|
+
//
|
|
8
|
+
// It is PURE and side-effect-free (compile and start are separate doors — Decision 5/7): nothing is
|
|
9
|
+
// deployed or dispatched, so an operator can Preview repeatedly while fixing the JSON. A blank/invalid
|
|
10
|
+
// paste, or a graph that fails validation, is a 400 carrying a human `error` (and, for a compile
|
|
11
|
+
// failure, the path-qualified `errors`). This is a thin UI adapter over the ONE compile contract, not
|
|
12
|
+
// a parallel compile path.
|
|
13
|
+
|
|
14
|
+
import { compileDeliveryGraph } from "../app/deliveryGraphCompiler.ts";
|
|
15
|
+
import { parseDeliveryGraphText } from "../app/deliveryGraphText.ts";
|
|
16
|
+
import { deliveryGraphDigest } from "../app/deliveryRunner.ts";
|
|
17
|
+
import { defineOperation } from "../nano-generated/operations.ts";
|
|
18
|
+
|
|
19
|
+
export default defineOperation("previewDeliveryGraph", async ({ body }, app) => {
|
|
20
|
+
const parsed = parseDeliveryGraphText(body);
|
|
21
|
+
if (!parsed.ok) {
|
|
22
|
+
app.log.warn("preview-delivery-graph rejected: parse", { message: parsed.error });
|
|
23
|
+
return { status: 400, body: { ok: false, error: parsed.error } };
|
|
24
|
+
}
|
|
25
|
+
const compiled = compileDeliveryGraph(parsed.graph);
|
|
26
|
+
if (!compiled.ok) {
|
|
27
|
+
app.log.warn("preview-delivery-graph rejected: compile", { errors: compiled.errors.length });
|
|
28
|
+
return {
|
|
29
|
+
status: 400,
|
|
30
|
+
body: {
|
|
31
|
+
ok: false,
|
|
32
|
+
error: `graph failed validation: ${compiled.errors.length} error(s)`,
|
|
33
|
+
errors: compiled.errors,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const digest = deliveryGraphDigest(compiled.bpmn);
|
|
38
|
+
app.log.info("preview-delivery-graph compiled", {
|
|
39
|
+
nodes: compiled.resolved.nodes.length,
|
|
40
|
+
humanNodes: compiled.humanNodes.length,
|
|
41
|
+
sideEffects: compiled.sideEffects.length,
|
|
42
|
+
digest,
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
status: 200,
|
|
46
|
+
body: {
|
|
47
|
+
ok: true,
|
|
48
|
+
digest,
|
|
49
|
+
...(typeof compiled.resolved.name === "string" && compiled.resolved.name !== ""
|
|
50
|
+
? { title: compiled.resolved.name }
|
|
51
|
+
: {}),
|
|
52
|
+
sideEffecting: compiled.sideEffects.length > 0,
|
|
53
|
+
nodeCount: compiled.resolved.nodes.length,
|
|
54
|
+
humanNodeCount: compiled.humanNodes.length,
|
|
55
|
+
sideEffectCount: compiled.sideEffects.length,
|
|
56
|
+
diagram: compiled.diagram,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.118.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",
|
package/pages/_nav.json
CHANGED
package/pages/board.page.json
CHANGED
package/pages/cockpit.page.json
CHANGED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0",
|
|
3
|
+
"title": "Delivery graph",
|
|
4
|
+
"nodes": [
|
|
5
|
+
{
|
|
6
|
+
"type": "nav",
|
|
7
|
+
"id": "nav",
|
|
8
|
+
"props": {
|
|
9
|
+
"variant": "bar",
|
|
10
|
+
"title": "Nano Workforce",
|
|
11
|
+
"items": [
|
|
12
|
+
{
|
|
13
|
+
"label": "Overview",
|
|
14
|
+
"page": "overview"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"label": "Lineage",
|
|
18
|
+
"page": "lineage"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"label": "Convergence",
|
|
22
|
+
"page": "home"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"label": "Epics",
|
|
26
|
+
"page": "epic"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"label": "Feature",
|
|
30
|
+
"page": "feature"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"label": "Delivery Graphs",
|
|
34
|
+
"page": "delivery-graphs"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"label": "Tasks",
|
|
38
|
+
"page": "tasks",
|
|
39
|
+
"badge": {
|
|
40
|
+
"source": "app",
|
|
41
|
+
"table": "user_tasks",
|
|
42
|
+
"filter": [],
|
|
43
|
+
"tone": "danger",
|
|
44
|
+
"refreshMs": 5000,
|
|
45
|
+
"hideWhenZero": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"label": "Cockpit",
|
|
50
|
+
"page": "cockpit"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"label": "Board",
|
|
54
|
+
"page": "board"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"label": "Velocity",
|
|
58
|
+
"page": "velocity"
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"sticky": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"type": "text",
|
|
66
|
+
"id": "title",
|
|
67
|
+
"props": { "text": "Delivery graph {{param}}", "variant": "heading" }
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"type": "text",
|
|
71
|
+
"id": "subtitle",
|
|
72
|
+
"props": {
|
|
73
|
+
"text": "This delivery graph's run aggregate \u2014 its lifecycle status, the derived phase (where it is parked, e.g. on a human node), its compiled shape (node / human-stop / side-effect counts), and the content digest that is its approval token. Follow the Status cell into the process explorer to watch the compiled process advance node-by-node and edge-by-edge (the engine owns per-node token state).",
|
|
74
|
+
"variant": "sub"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"type": "dataGrid",
|
|
79
|
+
"id": "delivery-graph-run",
|
|
80
|
+
"props": {
|
|
81
|
+
"title": "Run",
|
|
82
|
+
"rowKey": "run_key",
|
|
83
|
+
"refreshMs": 5000,
|
|
84
|
+
"empty": "No delivery graph found for this key.",
|
|
85
|
+
"data": {
|
|
86
|
+
"kind": "datasource",
|
|
87
|
+
"source": "app",
|
|
88
|
+
"table": "delivery_graph_runs",
|
|
89
|
+
"orderBy": { "field": "updated_at", "dir": "desc" },
|
|
90
|
+
"filter": [{ "field": "run_key", "eqParam": true }]
|
|
91
|
+
},
|
|
92
|
+
"columns": [
|
|
93
|
+
{ "field": "title", "template": "{{title}}", "header": "Graph", "subtitleField": "run_key", "truncate": true, "width": "30%" },
|
|
94
|
+
{ "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
|
|
95
|
+
{ "field": "phase", "header": "Phase", "truncate": true, "width": "30%" },
|
|
96
|
+
{ "field": "node_count", "header": "Nodes" },
|
|
97
|
+
{ "field": "human_node_count", "header": "Human" },
|
|
98
|
+
{ "field": "side_effect_count", "header": "Side effects" },
|
|
99
|
+
{ "field": "updated_at", "header": "Updated", "width": "9rem", "format": "datetime" }
|
|
100
|
+
],
|
|
101
|
+
"detail": {
|
|
102
|
+
"fields": [
|
|
103
|
+
{ "field": "process_definition_id", "label": "Process definition" },
|
|
104
|
+
{ "field": "process_key", "label": "Process instance key" },
|
|
105
|
+
{ "field": "digest", "label": "Content digest (approval token)" },
|
|
106
|
+
{ "field": "side_effecting", "label": "Side-effecting (1 = requires approval)" },
|
|
107
|
+
{ "field": "phase_node_id", "label": "Parked human node element" },
|
|
108
|
+
{ "field": "created_at", "label": "Created" }
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0",
|
|
3
|
+
"title": "Delivery Graphs",
|
|
4
|
+
"nodes": [
|
|
5
|
+
{
|
|
6
|
+
"type": "nav",
|
|
7
|
+
"id": "nav",
|
|
8
|
+
"props": {
|
|
9
|
+
"variant": "bar",
|
|
10
|
+
"title": "Nano Workforce",
|
|
11
|
+
"items": [
|
|
12
|
+
{
|
|
13
|
+
"label": "Overview",
|
|
14
|
+
"page": "overview"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"label": "Lineage",
|
|
18
|
+
"page": "lineage"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"label": "Convergence",
|
|
22
|
+
"page": "home"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"label": "Epics",
|
|
26
|
+
"page": "epic"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"label": "Feature",
|
|
30
|
+
"page": "feature"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"label": "Delivery Graphs",
|
|
34
|
+
"page": "delivery-graphs"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"label": "Tasks",
|
|
38
|
+
"page": "tasks",
|
|
39
|
+
"badge": {
|
|
40
|
+
"source": "app",
|
|
41
|
+
"table": "user_tasks",
|
|
42
|
+
"filter": [],
|
|
43
|
+
"tone": "danger",
|
|
44
|
+
"refreshMs": 5000,
|
|
45
|
+
"hideWhenZero": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"label": "Cockpit",
|
|
50
|
+
"page": "cockpit"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"label": "Board",
|
|
54
|
+
"page": "board"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"label": "Velocity",
|
|
58
|
+
"page": "velocity"
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"sticky": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"type": "text",
|
|
66
|
+
"id": "title",
|
|
67
|
+
"props": { "text": "Delivery Graphs", "variant": "heading" }
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"type": "text",
|
|
71
|
+
"id": "subtitle",
|
|
72
|
+
"props": {
|
|
73
|
+
"text": "The human front door for delivery graphs (ADR 0005). Paste an agent-authored delivery-graph JSON, Preview it (a pure compile \u2014 nothing is dispatched) to see it validates and read its content digest, then Dispatch it. A graph with any side-effecting node (it merges PRs / publishes packages) dispatches only when you tick Approve; without approval it is parked in the in-flight grid below for review. Preview and Dispatch are thin UIs over the same compileDeliveryGraph / startDeliveryGraph doors \u2014 there is no parallel submit path.",
|
|
74
|
+
"variant": "sub"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"type": "actionForm",
|
|
79
|
+
"id": "delivery-graph-preview",
|
|
80
|
+
"props": {
|
|
81
|
+
"title": "1 \u00b7 Preview (compile \u2014 pure, nothing is dispatched)",
|
|
82
|
+
"submitLabel": "Preview",
|
|
83
|
+
"action": {
|
|
84
|
+
"path": "/app/api/actions/delivery-graph/preview",
|
|
85
|
+
"body": "{{form}}",
|
|
86
|
+
"successLabel": "\u2713 Valid \u2014 the graph compiled. Nothing was dispatched. Review its shape in the in-flight grid, then Dispatch it below."
|
|
87
|
+
},
|
|
88
|
+
"fields": [
|
|
89
|
+
{ "key": "graphJson", "label": "Delivery-graph JSON \u2014 paste the agent-authored DeliveryGraph (nodes/edges). Preview validates + compiles it without dispatching.", "type": "text", "required": true, "requiredMessage": "Paste a delivery-graph JSON to preview" }
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"type": "actionForm",
|
|
95
|
+
"id": "delivery-graph-dispatch",
|
|
96
|
+
"props": {
|
|
97
|
+
"title": "2 \u00b7 Dispatch (start \u2014 gated, idempotent)",
|
|
98
|
+
"submitLabel": "Dispatch",
|
|
99
|
+
"action": {
|
|
100
|
+
"path": "/app/api/actions/delivery-graph/dispatch",
|
|
101
|
+
"body": "{{form}}",
|
|
102
|
+
"successLabel": "\u2713 Dispatched \u2014 watch it advance in the in-flight grid below."
|
|
103
|
+
},
|
|
104
|
+
"fields": [
|
|
105
|
+
{ "key": "graphJson", "label": "Delivery-graph JSON \u2014 paste the same DeliveryGraph you previewed.", "type": "text", "required": true, "requiredMessage": "Paste the delivery-graph JSON to dispatch" },
|
|
106
|
+
{ "key": "approve", "label": "Approve \u2014 I reviewed the preview and approve dispatching this graph's side effects (required for any agent/connector node)", "type": "checkbox" },
|
|
107
|
+
{ "key": "idempotencyKey", "label": "Idempotency key (optional) \u2014 a re-dispatch with the same key (or, blank, the same graph) will not double-launch an in-flight run", "type": "text" }
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"type": "dataGrid",
|
|
113
|
+
"id": "delivery-graphs-inflight",
|
|
114
|
+
"props": {
|
|
115
|
+
"title": "In-flight delivery graphs",
|
|
116
|
+
"collapsible": true,
|
|
117
|
+
"defaultCollapsed": false,
|
|
118
|
+
"showCount": true,
|
|
119
|
+
"rowKey": "run_key",
|
|
120
|
+
"refreshMs": 5000,
|
|
121
|
+
"empty": "No delivery graphs in flight. Preview and Dispatch one above.",
|
|
122
|
+
"data": {
|
|
123
|
+
"kind": "datasource",
|
|
124
|
+
"source": "app",
|
|
125
|
+
"table": "delivery_graph_runs",
|
|
126
|
+
"orderBy": { "field": "updated_at", "dir": "desc" },
|
|
127
|
+
"filter": [{ "field": "status", "in": ["awaiting-approval", "running"] }]
|
|
128
|
+
},
|
|
129
|
+
"tabs": [
|
|
130
|
+
{ "label": "In-flight", "filter": [{ "field": "status", "in": ["awaiting-approval", "running"] }] },
|
|
131
|
+
{ "label": "History", "filter": [{ "field": "status", "in": ["done", "failed", "abandoned"] }] },
|
|
132
|
+
{ "label": "All", "filter": [] }
|
|
133
|
+
],
|
|
134
|
+
"columns": [
|
|
135
|
+
{ "field": "title", "template": "{{title}}", "header": "Graph", "subtitleField": "run_key", "truncate": true, "width": "28%", "link": { "kind": "page", "page": "delivery-graph-detail", "keyField": "run_key" } },
|
|
136
|
+
{ "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
|
|
137
|
+
{ "field": "phase", "header": "Phase", "truncate": true, "width": "26%" },
|
|
138
|
+
{ "field": "node_count", "header": "Nodes" },
|
|
139
|
+
{ "field": "human_node_count", "header": "Human" },
|
|
140
|
+
{ "field": "side_effect_count", "header": "Side effects" },
|
|
141
|
+
{ "field": "updated_at", "header": "Updated", "width": "9rem", "format": "datetime" }
|
|
142
|
+
],
|
|
143
|
+
"detail": {
|
|
144
|
+
"fields": [
|
|
145
|
+
{ "field": "process_definition_id", "label": "Definition" },
|
|
146
|
+
{ "field": "digest", "label": "Digest (approval token)" },
|
|
147
|
+
{ "field": "phase_node_id", "label": "Parked node" }
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
package/pages/epic.page.json
CHANGED
package/pages/feature.page.json
CHANGED
package/pages/home.page.json
CHANGED