@nanobpm/nano-workforce 0.123.2 → 0.124.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 +13 -0
- package/README.md +9 -5
- package/app/deliveryGraphDispatch.test.ts +143 -0
- package/app/deliveryGraphDispatch.ts +168 -0
- package/app/deliveryGraphProposals.test.ts +267 -0
- package/app/deliveryGraphProposals.ts +269 -0
- package/app/deliveryGraphRun.test.ts +6 -52
- package/app/deliveryGraphRun.ts +21 -76
- package/app/deliveryGraphText.ts +3 -3
- package/app/deliveryRunner.ts +4 -3
- package/app/service.ts +15 -0
- package/db/migrations/075_delivery_graph_proposals.sql +48 -0
- package/docs/adr/0005-agent-authored-delivery-graphs.md +18 -0
- package/docs/adr/0006-delivery-units-one-representation.md +221 -0
- package/docs/agent-guide.md +50 -58
- package/e2e/delivery-graph-dispatch.e2e.ts +155 -0
- package/openapi.yaml +118 -161
- package/operations/compileDeliveryGraph.test.ts +100 -37
- package/operations/compileDeliveryGraph.ts +64 -18
- package/operations/dispatchDeliveryGraph.test.ts +171 -152
- package/operations/dispatchDeliveryGraph.ts +79 -99
- package/operations/getAgentInstructions.test.ts +10 -6
- package/operations/previewDeliveryGraph.test.ts +90 -51
- package/operations/previewDeliveryGraph.ts +45 -18
- package/package.json +1 -1
- package/pages/cockpit/mount.js +19 -12
- package/pages/delivery-graphs/mount.js +37 -137
- package/pages/delivery-graphs.page.json +50 -3
- package/scripts/check-migrations.test.ts +9 -0
- package/scripts/check-migrations.ts +11 -1
- package/test/cockpit-embed-endpoints.test.ts +59 -36
- package/test/delivery-graphs-embed.test.ts +36 -34
- package/e2e/delivery-graph-start.e2e.ts +0 -145
- package/operations/startDeliveryGraph.integration.test.ts +0 -316
- package/operations/startDeliveryGraph.ts +0 -222
package/openapi.yaml
CHANGED
|
@@ -1529,35 +1529,31 @@ components:
|
|
|
1529
1529
|
graphJson:
|
|
1530
1530
|
type: string
|
|
1531
1531
|
description: The pasted delivery-graph JSON (a serialised `DeliveryGraph`), parsed server-side.
|
|
1532
|
-
|
|
1532
|
+
DeliveryGraphDispatchRequest:
|
|
1533
1533
|
description: >-
|
|
1534
|
-
The
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1534
|
+
The OPERATOR dispatch request (ADR 0005 Decision 7, issue #460). The cockpit's staged-proposals
|
|
1535
|
+
grid posts the content `digest` of the proposal the operator picked; the door loads that staged
|
|
1536
|
+
proposal and launches the retained runner for its previewed graph. Dispatch is an operator
|
|
1537
|
+
action — this request carries NO graph and NO token (the graph is already staged; the operator's
|
|
1538
|
+
click is the approval).
|
|
1538
1539
|
type: object
|
|
1539
1540
|
additionalProperties: false
|
|
1540
1541
|
required:
|
|
1541
|
-
-
|
|
1542
|
+
- digest
|
|
1542
1543
|
properties:
|
|
1543
|
-
|
|
1544
|
+
digest:
|
|
1544
1545
|
type: string
|
|
1545
|
-
description: The
|
|
1546
|
-
approve:
|
|
1547
|
-
type: boolean
|
|
1548
|
-
description: >-
|
|
1549
|
-
The operator's approval OF the previewed graph. When true, the door derives the graph's
|
|
1550
|
-
content digest and presents it as the `approvalToken`, so a side-effecting graph the
|
|
1551
|
-
operator reviewed dispatches; when false/absent a side-effecting graph is parked at approval.
|
|
1546
|
+
description: The staged proposal's content digest (its primary key) — the proposal to dispatch.
|
|
1552
1547
|
idempotencyKey:
|
|
1553
1548
|
type: string
|
|
1554
1549
|
maxLength: 255
|
|
1555
|
-
description: OPTIONAL idempotency key
|
|
1550
|
+
description: OPTIONAL idempotency key. A re-dispatch with the same key (or, when omitted, the same digest) does not double-launch. Blank/whitespace is treated as absent.
|
|
1556
1551
|
DeliveryGraphTextResult:
|
|
1557
1552
|
description: >-
|
|
1558
|
-
The
|
|
1559
|
-
|
|
1560
|
-
|
|
1553
|
+
The delivery-graph text-ingress outcome (issue #460) — a single shape covering the JSON-paste
|
|
1554
|
+
PREVIEW+STAGE summary, the by-`digest` OPERATOR DISPATCH outcome (dispatch takes only a staged
|
|
1555
|
+
proposal's `digest`, not pasted JSON), and any parse/validation error. `ok` discriminates
|
|
1556
|
+
success; a failure carries a human `error` (and, for a compile failure, path-qualified `errors`).
|
|
1561
1557
|
type: object
|
|
1562
1558
|
additionalProperties: false
|
|
1563
1559
|
required:
|
|
@@ -1565,7 +1561,7 @@ components:
|
|
|
1565
1561
|
properties:
|
|
1566
1562
|
ok:
|
|
1567
1563
|
type: boolean
|
|
1568
|
-
description: True on a successful preview/dispatch; false on a parse/validation failure
|
|
1564
|
+
description: True on a successful preview+stage / dispatch; false on a parse/validation/dispatch failure.
|
|
1569
1565
|
error:
|
|
1570
1566
|
type: string
|
|
1571
1567
|
description: A human-readable failure message (surfaced by the page's action banner).
|
|
@@ -1573,16 +1569,16 @@ components:
|
|
|
1573
1569
|
type: array
|
|
1574
1570
|
items:
|
|
1575
1571
|
$ref: "#/components/schemas/DeliveryCompileError"
|
|
1576
|
-
description: Path-qualified validation/compile failures, when the
|
|
1572
|
+
description: Path-qualified validation/compile failures, when the submitted or staged graph was malformed.
|
|
1577
1573
|
status:
|
|
1578
1574
|
type: string
|
|
1579
|
-
description: The dispatch run's lifecycle position (`running`
|
|
1575
|
+
description: The dispatch run's lifecycle position (`running`), when dispatched.
|
|
1580
1576
|
runKey:
|
|
1581
1577
|
type: string
|
|
1582
1578
|
description: The dispatch run's idempotency key, when dispatched.
|
|
1583
1579
|
digest:
|
|
1584
1580
|
type: string
|
|
1585
|
-
description: The graph's content digest — the
|
|
1581
|
+
description: The graph's content digest — the content-address of the staged proposal / dispatched run.
|
|
1586
1582
|
sideEffecting:
|
|
1587
1583
|
type: boolean
|
|
1588
1584
|
description: Whether the graph has any side-effecting (`agent`/`connector`) node.
|
|
@@ -1595,12 +1591,15 @@ components:
|
|
|
1595
1591
|
processDefinitionId:
|
|
1596
1592
|
type: string
|
|
1597
1593
|
description: The started process definition id, when dispatched.
|
|
1598
|
-
approvalToken:
|
|
1599
|
-
type: string
|
|
1600
|
-
description: The approval token to re-submit with, when a side-effecting graph was parked pending approval.
|
|
1601
1594
|
message:
|
|
1602
1595
|
type: string
|
|
1603
|
-
description: Additional detail from the
|
|
1596
|
+
description: Additional human-readable detail from the door (e.g. the staged/dispatched summary).
|
|
1597
|
+
reviewUrl:
|
|
1598
|
+
type: string
|
|
1599
|
+
description: A navigational cockpit deep-link to the staged proposal (a pointer only — NOT a dispatch handle).
|
|
1600
|
+
staged:
|
|
1601
|
+
type: boolean
|
|
1602
|
+
description: True when a valid preview persisted the compiled graph as a staged proposal (issue #460).
|
|
1604
1603
|
title:
|
|
1605
1604
|
type: string
|
|
1606
1605
|
description: The graph's human-readable name, echoed on a successful preview.
|
|
@@ -1828,85 +1827,68 @@ components:
|
|
|
1828
1827
|
items:
|
|
1829
1828
|
$ref: "#/components/schemas/DeliveryCompileError"
|
|
1830
1829
|
description: The path-qualified validation/compile failures (at least one).
|
|
1831
|
-
|
|
1830
|
+
DeliveryProposalPreview:
|
|
1832
1831
|
description: >-
|
|
1833
|
-
The
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
(S1), and launches the S4 runner. Three ingress paths — an agent-ergonomic POST, a raw REST call,
|
|
1838
|
-
and a UI JSON-paste — all hit this ONE contract.
|
|
1832
|
+
The operator-facing preview a staged proposal carries (ADR 0005 Decision 7, issue #460) — WHAT
|
|
1833
|
+
the graph does. Carries the mermaid `diagram`, the `sideEffects[]` a dispatch authorises, and
|
|
1834
|
+
the `humanNodes[]` where it parks on a person. It carries NO dispatch handle by construction —
|
|
1835
|
+
a preview, not an affordance to start a run.
|
|
1839
1836
|
type: object
|
|
1840
1837
|
additionalProperties: false
|
|
1841
1838
|
required:
|
|
1842
|
-
-
|
|
1839
|
+
- diagram
|
|
1840
|
+
- sideEffects
|
|
1841
|
+
- humanNodes
|
|
1843
1842
|
properties:
|
|
1844
|
-
|
|
1845
|
-
$ref: "#/components/schemas/DeliveryGraph"
|
|
1846
|
-
approvalToken:
|
|
1847
|
-
type: string
|
|
1848
|
-
description: >-
|
|
1849
|
-
The approval OF the rendered preview (Decision 7). Because a delivery graph merges PRs and
|
|
1850
|
-
publishes packages, a graph with any SIDE-EFFECTING node (`agent`/`connector`) dispatches
|
|
1851
|
-
ONLY when the caller presents the graph's content-addressed approval token (returned as
|
|
1852
|
-
`approvalToken` on a prior unapproved submit's `awaiting-approval` response). A graph with
|
|
1853
|
-
no side effects (only `wait`/`human` nodes) needs none and dispatches straight away.
|
|
1854
|
-
idempotencyKey:
|
|
1843
|
+
diagram:
|
|
1855
1844
|
type: string
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1845
|
+
description: A human-readable mermaid `flowchart` of the resolved graph.
|
|
1846
|
+
sideEffects:
|
|
1847
|
+
type: array
|
|
1848
|
+
items:
|
|
1849
|
+
$ref: "#/components/schemas/DeliverySideEffect"
|
|
1850
|
+
description: The side-effecting actions the graph WILL perform once an operator dispatches it.
|
|
1851
|
+
humanNodes:
|
|
1852
|
+
type: array
|
|
1853
|
+
items:
|
|
1854
|
+
$ref: "#/components/schemas/DeliveryHumanStop"
|
|
1855
|
+
description: The human stop-points where the graph parks on a person.
|
|
1856
|
+
CompileDeliveryGraphStaged:
|
|
1863
1857
|
description: >-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1858
|
+
A successful agent-facing compile (ADR 0005 Decision 7, issue #460) — the graph validated and
|
|
1859
|
+
compiled, and was persisted as a `staged` proposal for operator review. The response is
|
|
1860
|
+
deliberately a PREVIEW plus a navigational pointer and NOTHING that can trigger a run: no run
|
|
1861
|
+
key, no token, no process-instance key. This is capability-by-absence — the agent's surface ends
|
|
1862
|
+
at propose → compile → stage; a HUMAN dispatches the staged proposal in the cockpit.
|
|
1868
1863
|
type: object
|
|
1869
1864
|
additionalProperties: false
|
|
1870
1865
|
required:
|
|
1871
|
-
- ok
|
|
1872
1866
|
- status
|
|
1873
|
-
-
|
|
1867
|
+
- message
|
|
1874
1868
|
- digest
|
|
1875
|
-
-
|
|
1869
|
+
- preview
|
|
1870
|
+
- reviewUrl
|
|
1876
1871
|
properties:
|
|
1877
|
-
ok:
|
|
1878
|
-
type: boolean
|
|
1879
|
-
description: True when the graph dispatched (or was already running); false when parked at approval.
|
|
1880
1872
|
status:
|
|
1881
1873
|
type: string
|
|
1882
|
-
enum: [
|
|
1883
|
-
description:
|
|
1884
|
-
|
|
1874
|
+
enum: [ready]
|
|
1875
|
+
description: Discriminant — `ready` means the graph compiled and is staged for operator review.
|
|
1876
|
+
message:
|
|
1885
1877
|
type: string
|
|
1886
|
-
description:
|
|
1878
|
+
description: >-
|
|
1879
|
+
A human-readable instruction telling the agent its role ends here — ask the operator to
|
|
1880
|
+
preview and approve (or request modifications) in the cockpit; dispatch is an operator
|
|
1881
|
+
action and there is no start endpoint.
|
|
1887
1882
|
digest:
|
|
1888
1883
|
type: string
|
|
1889
|
-
description: The graph's content digest — the
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
alreadyRunning:
|
|
1894
|
-
type: boolean
|
|
1895
|
-
description: True when a re-submit short-circuited onto an already-running run (no second launch).
|
|
1896
|
-
processInstanceKey:
|
|
1897
|
-
type: string
|
|
1898
|
-
description: The started engine instance key. Present when `status` is `running`; absent while parked at approval.
|
|
1899
|
-
processDefinitionId:
|
|
1900
|
-
type: string
|
|
1901
|
-
description: The content-addressed deployed process id (`delivery-graph-<digest>`). Present when dispatched.
|
|
1902
|
-
approvalToken:
|
|
1884
|
+
description: The compiled graph's content digest — so the agent can NAME the proposal to the operator.
|
|
1885
|
+
preview:
|
|
1886
|
+
$ref: "#/components/schemas/DeliveryProposalPreview"
|
|
1887
|
+
reviewUrl:
|
|
1903
1888
|
type: string
|
|
1904
1889
|
description: >-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
message:
|
|
1908
|
-
type: string
|
|
1909
|
-
description: A human-readable summary of the outcome (e.g. the approval-required reason).
|
|
1890
|
+
A NAVIGATIONAL cockpit deep-link to the staged proposal (helps the agent hand the human a
|
|
1891
|
+
link). It is a pointer only — NOT a dispatch handle; nothing in this response can start a run.
|
|
1910
1892
|
FeatureStart:
|
|
1911
1893
|
description: The start-feature request body — a SINGLE-issue feature run. Names the target issue
|
|
1912
1894
|
by EXACTLY ONE of `issue` (an `owner/repo#123` reference) or `url` (a bare issue URL), plus a
|
|
@@ -2659,17 +2641,19 @@ paths:
|
|
|
2659
2641
|
/actions/compile-delivery-graph:
|
|
2660
2642
|
post:
|
|
2661
2643
|
operationId: compileDeliveryGraph
|
|
2662
|
-
summary: Validate + compile a delivery graph
|
|
2644
|
+
summary: Validate + compile a delivery graph and STAGE it for operator review (never dispatches). (ADR 0005 Decision 7 / #460)
|
|
2663
2645
|
description: >-
|
|
2664
|
-
The
|
|
2665
|
-
agent-authored `DeliveryGraph` (the closed
|
|
2666
|
-
the trust boundary), it runs the pure
|
|
2667
|
-
deterministic, human-written compiler,
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2646
|
+
The agent-facing delivery-graph door — the END of the agent's surface (ADR 0005 Decision 7,
|
|
2647
|
+
issue #460). Given an agent-authored `DeliveryGraph` (the closed
|
|
2648
|
+
`agent`/`wait`/`human`/`connector` node vocabulary — the trust boundary), it runs the pure
|
|
2649
|
+
`validateDeliveryGraph` semantic check and the deterministic, human-written compiler, and — when
|
|
2650
|
+
the graph is valid — persists the compiled graph as a `staged` proposal (content-addressed by
|
|
2651
|
+
its `digest`). It returns a PREVIEW and a navigational `reviewUrl` and NOTHING that can trigger a
|
|
2652
|
+
run: no run key, no token, no process-instance key. Dispatch is an OPERATOR action performed in
|
|
2653
|
+
the cockpit — there is no `start` endpoint. This is capability-by-absence: if the agent never
|
|
2654
|
+
receives a dispatch handle, there is nothing to replay (closing the self-approval hole the old
|
|
2655
|
+
replayable `approvalToken` left open). A malformed graph is a 400 carrying path-qualified errors;
|
|
2656
|
+
nothing is staged.
|
|
2673
2657
|
requestBody:
|
|
2674
2658
|
required: true
|
|
2675
2659
|
content:
|
|
@@ -2678,70 +2662,32 @@ paths:
|
|
|
2678
2662
|
$ref: "#/components/schemas/DeliveryGraph"
|
|
2679
2663
|
responses:
|
|
2680
2664
|
"200":
|
|
2681
|
-
description: The graph validated and compiled — the
|
|
2665
|
+
description: The graph validated and compiled — it is STAGED for operator review; the response carries a preview and a navigational reviewUrl (no dispatch handle).
|
|
2682
2666
|
content:
|
|
2683
2667
|
application/json:
|
|
2684
2668
|
schema:
|
|
2685
|
-
$ref: "#/components/schemas/
|
|
2669
|
+
$ref: "#/components/schemas/CompileDeliveryGraphStaged"
|
|
2686
2670
|
"400":
|
|
2687
|
-
description: The graph failed shape or semantic validation — path-qualified errors, nothing compiled.
|
|
2671
|
+
description: The graph failed shape or semantic validation — path-qualified errors, nothing compiled or staged.
|
|
2688
2672
|
content:
|
|
2689
2673
|
application/json:
|
|
2690
2674
|
schema:
|
|
2691
2675
|
$ref: "#/components/schemas/CompileDeliveryGraphErrors"
|
|
2692
|
-
/actions/start/delivery-graph:
|
|
2693
|
-
post:
|
|
2694
|
-
operationId: startDeliveryGraph
|
|
2695
|
-
summary: Validate + compile + DISPATCH a delivery graph as a running engine-native process (gated, idempotent). (ADR 0005 slice S5)
|
|
2696
|
-
description: >-
|
|
2697
|
-
The delivery-graph DISPATCH door (ADR 0005 Decision 7) — the single gated OUTER action that
|
|
2698
|
-
turns an agent-authored `DeliveryGraph` into a RUNNING engine-native process. Distinct from S1's
|
|
2699
|
-
pure `compileDeliveryGraph`: compile and start are SEPARATE operations (Decision 5/7), so there
|
|
2700
|
-
is deliberately no `dryRun` flag. The door re-validates the graph (`validateDeliveryGraph`, S0),
|
|
2701
|
-
compiles it (`compileDeliveryGraph`, S1), and LAUNCHES the S4 runner (`runDeliveryGraph`).
|
|
2702
|
-
Because these graphs merge PRs and publish packages, dispatch is GATED: a graph with any
|
|
2703
|
-
side-effecting (`agent`/`connector`) node must present the content-addressed `approvalToken` of
|
|
2704
|
-
its rendered preview, else it is PARKED at approval (a 400 carrying the token, plus a visible
|
|
2705
|
-
`awaiting-approval` run in the cockpit) rather than dispatched. Idempotent: a re-POST of the same
|
|
2706
|
-
graph (or the same `idempotencyKey`) short-circuits an already-running run instead of
|
|
2707
|
-
double-launching. Three ingress paths — an agent-ergonomic POST, a raw REST call, and a UI
|
|
2708
|
-
JSON-paste — all hit this ONE contract.
|
|
2709
|
-
requestBody:
|
|
2710
|
-
required: true
|
|
2711
|
-
content:
|
|
2712
|
-
application/json:
|
|
2713
|
-
schema:
|
|
2714
|
-
$ref: "#/components/schemas/DeliveryGraphStart"
|
|
2715
|
-
responses:
|
|
2716
|
-
"202":
|
|
2717
|
-
description: The graph dispatched (or a re-submit short-circuited an already-running run).
|
|
2718
|
-
content:
|
|
2719
|
-
application/json:
|
|
2720
|
-
schema:
|
|
2721
|
-
$ref: "#/components/schemas/StartDeliveryGraphResult"
|
|
2722
|
-
"400":
|
|
2723
|
-
description: >-
|
|
2724
|
-
The graph failed validation/compilation (path-qualified errors), OR a side-effecting graph
|
|
2725
|
-
was submitted without a valid approval token — it is refused and PARKED at approval (the
|
|
2726
|
-
body carries the `approvalToken` to re-submit with).
|
|
2727
|
-
content:
|
|
2728
|
-
application/json:
|
|
2729
|
-
schema:
|
|
2730
|
-
oneOf:
|
|
2731
|
-
- $ref: "#/components/schemas/CompileDeliveryGraphErrors"
|
|
2732
|
-
- $ref: "#/components/schemas/StartDeliveryGraphResult"
|
|
2733
2676
|
/actions/delivery-graph/preview:
|
|
2734
2677
|
post:
|
|
2735
2678
|
operationId: previewDeliveryGraph
|
|
2736
|
-
summary: UI JSON-paste PREVIEW — parse a pasted delivery-graph JSON string and
|
|
2679
|
+
summary: UI JSON-paste PREVIEW+STAGE — parse a pasted delivery-graph JSON string, compile it and stage it for operator dispatch. (ADR 0005 Decision 7 / #460)
|
|
2737
2680
|
description: >-
|
|
2738
|
-
The human-facing UI JSON-paste PREVIEW ingress
|
|
2739
|
-
"
|
|
2740
|
-
SAME
|
|
2741
|
-
|
|
2742
|
-
`
|
|
2743
|
-
|
|
2744
|
-
|
|
2681
|
+
The human-facing UI JSON-paste PREVIEW+STAGE ingress. The Delivery Graphs page's "Preview &
|
|
2682
|
+
stage" action posts the operator's pasted JSON as a STRING; this door parses it and runs the
|
|
2683
|
+
SAME `compileDeliveryGraph` compiler the agent-facing door uses, and — like the agent compile
|
|
2684
|
+
door (issue #460) — persists the compiled graph as a `staged` proposal (content-addressed by its
|
|
2685
|
+
`digest`). It returns a compact preview summary (the `digest`, node/human/side-effect counts,
|
|
2686
|
+
the mermaid `diagram`, the human stops and side effects) plus a navigational `reviewUrl`. It
|
|
2687
|
+
never deploys or dispatches — dispatch is a separate operator action on the staged proposal
|
|
2688
|
+
(the Dispatch button on the staged-proposals grid). A blank/invalid JSON string, or a graph that
|
|
2689
|
+
fails validation, is a 400 carrying a human `error` (and path-qualified `errors` for a compile
|
|
2690
|
+
failure); nothing is staged.
|
|
2745
2691
|
requestBody:
|
|
2746
2692
|
required: true
|
|
2747
2693
|
content:
|
|
@@ -2750,7 +2696,7 @@ paths:
|
|
|
2750
2696
|
$ref: "#/components/schemas/DeliveryGraphTextSubmit"
|
|
2751
2697
|
responses:
|
|
2752
2698
|
"200":
|
|
2753
|
-
description: The pasted graph parsed, validated and compiled — the
|
|
2699
|
+
description: The pasted graph parsed, validated and compiled — staged for operator dispatch; the preview summary is returned.
|
|
2754
2700
|
content:
|
|
2755
2701
|
application/json:
|
|
2756
2702
|
schema:
|
|
@@ -2764,33 +2710,44 @@ paths:
|
|
|
2764
2710
|
/actions/delivery-graph/dispatch:
|
|
2765
2711
|
post:
|
|
2766
2712
|
operationId: dispatchDeliveryGraph
|
|
2767
|
-
summary:
|
|
2713
|
+
summary: OPERATOR DISPATCH — launch a staged delivery-graph proposal by its digest (idempotent). (ADR 0005 Decision 7 / #460)
|
|
2768
2714
|
description: >-
|
|
2769
|
-
The
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2715
|
+
The OPERATOR-ONLY dispatch door (ADR 0005 Decision 7, issue #460). The cockpit's staged-proposals
|
|
2716
|
+
grid posts the `digest` of the proposal the operator picked; this door loads that `staged`
|
|
2717
|
+
proposal, runs the retained S4 runner for its previewed graph, and marks the proposal
|
|
2718
|
+
`dispatched`. The operator clicking Dispatch IS the approval — there is no replayable token. It
|
|
2719
|
+
is NOT part of the agent surface: the agent compile door returns no digest-as-dispatch-handle
|
|
2720
|
+
(only a navigational preview), so an agent cannot reach a run through the documented surface.
|
|
2721
|
+
Idempotent: a re-dispatch of an already-running run short-circuits with `alreadyRunning` instead
|
|
2722
|
+
of double-launching. An unknown/expired/superseded/already-dispatched digest is a 400. If the
|
|
2723
|
+
supplied `idempotencyKey` is already bound to a DIFFERENT running graph, the door refuses with a
|
|
2724
|
+
409 and leaves the proposal staged (it was never launched).
|
|
2777
2725
|
requestBody:
|
|
2778
2726
|
required: true
|
|
2779
2727
|
content:
|
|
2780
2728
|
application/json:
|
|
2781
2729
|
schema:
|
|
2782
|
-
$ref: "#/components/schemas/
|
|
2730
|
+
$ref: "#/components/schemas/DeliveryGraphDispatchRequest"
|
|
2783
2731
|
responses:
|
|
2784
2732
|
"202":
|
|
2785
|
-
description: The
|
|
2733
|
+
description: The staged proposal dispatched (or a re-dispatch short-circuited an already-running run).
|
|
2786
2734
|
content:
|
|
2787
2735
|
application/json:
|
|
2788
2736
|
schema:
|
|
2789
2737
|
$ref: "#/components/schemas/DeliveryGraphTextResult"
|
|
2790
2738
|
"400":
|
|
2791
2739
|
description: >-
|
|
2792
|
-
The
|
|
2793
|
-
|
|
2740
|
+
The digest was missing, or named no live staged proposal (unknown / expired / superseded /
|
|
2741
|
+
already dispatched), or the stored graph failed to compile (the body carries a human `error`).
|
|
2742
|
+
content:
|
|
2743
|
+
application/json:
|
|
2744
|
+
schema:
|
|
2745
|
+
$ref: "#/components/schemas/DeliveryGraphTextResult"
|
|
2746
|
+
"409":
|
|
2747
|
+
description: >-
|
|
2748
|
+
The supplied `idempotencyKey` is already bound to a DIFFERENT running delivery graph, so this
|
|
2749
|
+
proposal's graph was NOT launched and the staged proposal was left untouched (not marked
|
|
2750
|
+
dispatched). Retry with a fresh `idempotencyKey` (or none). The body carries a human `error`.
|
|
2794
2751
|
content:
|
|
2795
2752
|
application/json:
|
|
2796
2753
|
schema:
|
|
@@ -1,17 +1,36 @@
|
|
|
1
|
-
// Tests for the POST /app/api/actions/compile-delivery-graph operation `compileDeliveryGraph`
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Tests for the POST /app/api/actions/compile-delivery-graph operation `compileDeliveryGraph` (ADR
|
|
2
|
+
// 0005 Decision 7, issue #460). This is the END of the agent's surface: a well-formed graph compiles
|
|
3
|
+
// and is PERSISTED as a `staged` proposal, and the response carries only a preview + a navigational
|
|
4
|
+
// `reviewUrl` — never a run key, token, or process-instance key. A malformed graph is a 400 with
|
|
5
|
+
// path-qualified errors and nothing is staged. The Red/Green guard here is that the response exposes
|
|
6
|
+
// NO dispatch handle, closing the self-approval hole the old replayable `approvalToken` left open.
|
|
7
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
6
10
|
import { test } from "node:test";
|
|
7
11
|
import { assert, assertEquals } from "#test-assert";
|
|
8
|
-
import type { AppApi } from "@nanobpm/urban";
|
|
12
|
+
import type { AppApi, DataLayer } from "@nanobpm/urban";
|
|
13
|
+
import { bootTestApp } from "@nanobpm/urban-testkit";
|
|
14
|
+
import { deliveryGraphProposals } from "../app/deliveryGraphProposals.ts";
|
|
9
15
|
import { noopLog } from "../test/log.ts";
|
|
10
16
|
import handler from "./compileDeliveryGraph.ts";
|
|
11
17
|
|
|
12
|
-
const
|
|
18
|
+
const APP_ROOT = resolve(import.meta.dirname, "..");
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
/** Boot an app purely for its provisioned data layer (migration 075 applied), run `fn`, tear down. */
|
|
21
|
+
async function withApp(fn: (app: AppApi, data: DataLayer) => Promise<void>): Promise<void> {
|
|
22
|
+
const dir = mkdtempSync(join(tmpdir(), "nwf-dgcompile-"));
|
|
23
|
+
const app = await bootTestApp(APP_ROOT, { env: { NANO_APP_DB_URL: `file:${join(dir, "app.db")}` } });
|
|
24
|
+
try {
|
|
25
|
+
const edge = { data: app.db, log: noopLog() } as unknown as AppApi;
|
|
26
|
+
await fn(edge, app.db);
|
|
27
|
+
} finally {
|
|
28
|
+
await app.stop?.();
|
|
29
|
+
rmSync(dir, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function call(app: AppApi, body: unknown) {
|
|
15
34
|
return (await handler({ req: {} as any, params: {}, query: {}, body } as any, app)) as any;
|
|
16
35
|
}
|
|
17
36
|
|
|
@@ -24,40 +43,84 @@ const GOOD = {
|
|
|
24
43
|
edges: [{ from: "a", to: "b" }],
|
|
25
44
|
};
|
|
26
45
|
|
|
27
|
-
test("compile-delivery-graph: a well-formed graph → 200 with the
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
test("compile-delivery-graph: a well-formed graph → 200 ready, staged as a proposal, with the preview", async () => {
|
|
47
|
+
await withApp(async (app, data) => {
|
|
48
|
+
const res = await call(app, GOOD);
|
|
49
|
+
assertEquals(res.status, 200);
|
|
50
|
+
assertEquals(res.body.status, "ready");
|
|
51
|
+
assert(typeof res.body.message === "string" && res.body.message.length > 0);
|
|
52
|
+
assert(typeof res.body.digest === "string" && res.body.digest.length > 0);
|
|
53
|
+
assert(typeof res.body.preview === "object" && res.body.preview !== null);
|
|
54
|
+
assert(typeof res.body.preview.diagram === "string" && res.body.preview.diagram.length > 0);
|
|
55
|
+
assertEquals(res.body.preview.humanNodes.length, 1);
|
|
56
|
+
// The compiled graph was persisted as a staged proposal keyed by its content digest.
|
|
57
|
+
const row = await deliveryGraphProposals(data).get(res.body.digest);
|
|
58
|
+
assert(row, "the compiled graph is staged for operator dispatch");
|
|
59
|
+
assertEquals(row?.status, "staged");
|
|
60
|
+
assertEquals(row?.logical_key, "runbook");
|
|
61
|
+
});
|
|
38
62
|
});
|
|
39
63
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
64
|
+
// ── Red/Green: the agent surface hands back NO dispatch handle (issue #460) ────
|
|
65
|
+
test("compile-delivery-graph: the response exposes NO dispatch handle — no runKey, token, or process key to replay", async () => {
|
|
66
|
+
await withApp(async (app) => {
|
|
67
|
+
const res = await call(app, GOOD);
|
|
68
|
+
assertEquals(res.status, 200);
|
|
69
|
+
// The self-approval hole is closed by ABSENCE: there is nothing in the response a caller can
|
|
70
|
+
// replay to start a run. A regression that re-adds any of these fields fails here (Red/Green).
|
|
71
|
+
assertEquals(res.body.runKey, undefined);
|
|
72
|
+
assertEquals(res.body.approvalToken, undefined);
|
|
73
|
+
assertEquals(res.body.processInstanceKey, undefined);
|
|
74
|
+
assertEquals(res.body.processDefinitionId, undefined);
|
|
75
|
+
assertEquals(res.body.alreadyRunning, undefined);
|
|
76
|
+
// `reviewUrl` is navigational only — it points at the cockpit page, not an API dispatch endpoint.
|
|
77
|
+
assert(typeof res.body.reviewUrl === "string");
|
|
78
|
+
assert(!/\/actions\/start\/delivery-graph/.test(res.body.reviewUrl), "reviewUrl is not a dispatch endpoint");
|
|
44
79
|
});
|
|
45
|
-
assertEquals(res.status, 400);
|
|
46
|
-
assertEquals(res.body.ok, false);
|
|
47
|
-
assert(Array.isArray(res.body.errors) && res.body.errors.length > 0);
|
|
48
|
-
assert(res.body.errors.every((e: { path: string; message: string }) => typeof e.path === "string"));
|
|
49
80
|
});
|
|
50
81
|
|
|
51
|
-
test("compile-delivery-graph: is
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
test("compile-delivery-graph: re-compiling the same graph is idempotent — one staged proposal, TTL anchored to the first stage", async () => {
|
|
83
|
+
await withApp(async (app, data) => {
|
|
84
|
+
const first = await call(app, GOOD);
|
|
85
|
+
const firstRow = await deliveryGraphProposals(data).get(first.body.digest);
|
|
86
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
87
|
+
const second = await call(app, GOOD);
|
|
88
|
+
assertEquals(second.body.digest, first.body.digest);
|
|
89
|
+
const rows = await deliveryGraphProposals(data).find({ digest: first.body.digest });
|
|
90
|
+
assertEquals(rows.length, 1);
|
|
91
|
+
assertEquals(rows[0].created_at, firstRow?.created_at); // TTL anchor preserved across re-stage
|
|
92
|
+
});
|
|
57
93
|
});
|
|
58
94
|
|
|
59
|
-
test("compile-delivery-graph: a
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
95
|
+
test("compile-delivery-graph: a changed graph with the same name SUPERSEDES the prior staged proposal", async () => {
|
|
96
|
+
await withApp(async (app, data) => {
|
|
97
|
+
const a = await call(app, GOOD);
|
|
98
|
+
const b = await call(app, { ...GOOD, nodes: [...GOOD.nodes, { id: "c", kind: "human", human: { prompt: "do Y" } }], edges: [...GOOD.edges, { from: "b", to: "c" }] });
|
|
99
|
+
assert(a.body.digest !== b.body.digest, "the changed graph has a new digest");
|
|
100
|
+
assertEquals((await deliveryGraphProposals(data).get(a.body.digest))?.status, "superseded");
|
|
101
|
+
assertEquals((await deliveryGraphProposals(data).get(b.body.digest))?.status, "staged");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("compile-delivery-graph: a malformed graph → 400 with path-qualified errors, nothing staged", async () => {
|
|
106
|
+
await withApp(async (app, data) => {
|
|
107
|
+
const res = await call(app, {
|
|
108
|
+
nodes: [{ id: "a", kind: "agent", agent: { jobType: "j" } }],
|
|
109
|
+
edges: [{ from: "a", to: "ghost" }],
|
|
110
|
+
});
|
|
111
|
+
assertEquals(res.status, 400);
|
|
112
|
+
assertEquals(res.body.ok, false);
|
|
113
|
+
assert(Array.isArray(res.body.errors) && res.body.errors.length > 0);
|
|
114
|
+
assert(res.body.errors.every((e: { path: string; message: string }) => typeof e.path === "string"));
|
|
115
|
+
assertEquals((await deliveryGraphProposals(data).all()).length, 0);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("compile-delivery-graph: a missing/empty body → 400, never a 500, nothing staged", async () => {
|
|
120
|
+
await withApp(async (app, data) => {
|
|
121
|
+
const res = await call(app, undefined);
|
|
122
|
+
assertEquals(res.status, 400);
|
|
123
|
+
assertEquals(res.body.ok, false);
|
|
124
|
+
assertEquals((await deliveryGraphProposals(data).all()).length, 0);
|
|
125
|
+
});
|
|
63
126
|
});
|