@nanobpm/nano-workforce 0.180.0 → 0.182.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 +12 -0
- package/app/agentGuide.test.ts +87 -0
- package/app/agentGuide.ts +61 -0
- package/app/deliveryGraph.ts +30 -1
- package/app/deliveryGraphCompiler.ts +39 -20
- package/app/deliveryGraphProposals.test.ts +60 -0
- package/app/deliveryGraphProposals.ts +61 -3
- package/app/deliveryGraphStage.ts +11 -1
- package/app/deliveryRunner.test.ts +120 -58
- package/app/deliveryRunner.ts +154 -55
- package/app/repoEnvelope.ts +73 -1
- package/app/sequenceIssues.test.ts +143 -1
- package/app/sequenceIssues.ts +246 -31
- package/e2e/addressable-guide.e2e.ts +43 -0
- package/openapi.yaml +310 -29
- package/operations/compileDeliveryGraph.test.ts +4 -0
- package/operations/dispatchDeliveryGraph.test.ts +50 -4
- package/operations/dispatchDeliveryGraph.ts +44 -27
- package/operations/getAgentGuide.test.ts +13 -0
- package/operations/getAgentGuide.ts +40 -5
- package/package.json +1 -1
package/openapi.yaml
CHANGED
|
@@ -1248,7 +1248,20 @@ components:
|
|
|
1248
1248
|
- markdown
|
|
1249
1249
|
instructions:
|
|
1250
1250
|
type: string
|
|
1251
|
-
description: The section's markdown, with example commands keyed to this instance.
|
|
1251
|
+
description: The section's markdown (or a bounded page of it when paginating), with example commands keyed to this instance.
|
|
1252
|
+
start:
|
|
1253
|
+
type: integer
|
|
1254
|
+
description: The CHARACTER offset this page starts at (present only when paginating, issue #740).
|
|
1255
|
+
length:
|
|
1256
|
+
type: integer
|
|
1257
|
+
description: The number of characters returned in this page (present only when paginating).
|
|
1258
|
+
totalLength:
|
|
1259
|
+
type: integer
|
|
1260
|
+
description: The total number of characters in the fully-rendered section (present only when paginating).
|
|
1261
|
+
nextStart:
|
|
1262
|
+
type: integer
|
|
1263
|
+
nullable: true
|
|
1264
|
+
description: The `start` to pass for the next page, or null when this is the last page (present only when paginating).
|
|
1252
1265
|
SubmitResult:
|
|
1253
1266
|
type: object
|
|
1254
1267
|
required:
|
|
@@ -1836,6 +1849,30 @@ components:
|
|
|
1836
1849
|
type: string
|
|
1837
1850
|
maxLength: 20000
|
|
1838
1851
|
description: OPTIONAL steering prompt appended to the node's job brief.
|
|
1852
|
+
repository:
|
|
1853
|
+
type: string
|
|
1854
|
+
maxLength: 255
|
|
1855
|
+
pattern: '^[A-Za-z0-9-]+/(?!.*\.[Gg][Ii][Tt]$)[A-Za-z0-9._-]+$'
|
|
1856
|
+
description: >-
|
|
1857
|
+
OPTIONAL per-node `owner/repo` this agent node implements against (#739). A delivery
|
|
1858
|
+
graph provisions the `io.nanobpm.agentTask.repository` isolation envelope PER agent
|
|
1859
|
+
cell from THIS field, so a genuinely cross-repo graph (each node a different repo)
|
|
1860
|
+
isolates correctly without the operator ticking `repoless`. Absent → the node falls
|
|
1861
|
+
back to the run-level dispatch `repository`. When set it must be exactly `owner/repo`
|
|
1862
|
+
(a trailing `.git` and any non-`owner/repo` shape are rejected at submit), the same
|
|
1863
|
+
allowlist `repoEnvelopeVars`/the dispatch door apply. The `sequenceIssues` generator
|
|
1864
|
+
populates this automatically from the `owner/repo#N` each node implements.
|
|
1865
|
+
baseBranch:
|
|
1866
|
+
type: string
|
|
1867
|
+
maxLength: 255
|
|
1868
|
+
pattern: '^(?![/.-])(?!.*[/.]$)(?!.*\.\.)(?!.*//)(?!.*/\.)(?!.*\.lock(?:/|$))[A-Za-z0-9._/-]+$'
|
|
1869
|
+
description: >-
|
|
1870
|
+
OPTIONAL per-node base branch this agent node branches off (#739) — the `ref` the
|
|
1871
|
+
harness checks out in the isolated clone (the pre-PR shape: the agent cuts its own
|
|
1872
|
+
`feat/<node.id>` branch off this base). Absent → the run-level dispatch `baseBranch`
|
|
1873
|
+
(else the node's repository default branch). A value that is not a plausible git
|
|
1874
|
+
branch name is rejected at submit; the pattern mirrors the authoritative server-side
|
|
1875
|
+
gate (`isPlausibleBranchName`, app/baseBranch.ts).
|
|
1839
1876
|
converge:
|
|
1840
1877
|
type: boolean
|
|
1841
1878
|
description: >-
|
|
@@ -2085,17 +2122,20 @@ components:
|
|
|
2085
2122
|
action — this request carries NO graph and NO token (the graph is already staged; the operator's
|
|
2086
2123
|
click is the approval).
|
|
2087
2124
|
|
|
2088
|
-
Repository provisioning is REQUIRED by default (#729)
|
|
2089
|
-
`repository` + `baseBranch` its `agent` nodes
|
|
2090
|
-
for a genuinely checkout-less graph
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2125
|
+
Repository provisioning is REQUIRED by default (#729) unless the graph is FULLY node-provisioned
|
|
2126
|
+
(#739). The run EITHER names the run-level `repository` + `baseBranch` its `agent` nodes fall back
|
|
2127
|
+
to, OR opts out with `repoless: true` for a genuinely checkout-less graph, OR — when EVERY `agent`
|
|
2128
|
+
node declares its OWN `repository` (a cross-repo graph, #739) — supplies NEITHER (the node-
|
|
2129
|
+
provisioned shape). Modeled as `oneOf` named variants (Camunda REST v2 pattern, as the
|
|
2130
|
+
convergence/plan/feature start bodies are) so the runtime rejects the "both" shape AT THE EDGE
|
|
2131
|
+
with a 400 that names the allowed shapes. The remaining invariant — that no `agent` node resolves
|
|
2132
|
+
to NO repository on a non-`repoless` run — is enforced after the staged graph is loaded (the door
|
|
2133
|
+
has the graph; the request alone does not), so a graph with an unprovisioned node still fails
|
|
2134
|
+
loudly rather than silently sharing the worker's launch dir (the issue #684 field failure).
|
|
2096
2135
|
oneOf:
|
|
2097
2136
|
- $ref: "#/components/schemas/DeliveryGraphDispatchWithRepository"
|
|
2098
2137
|
- $ref: "#/components/schemas/DeliveryGraphDispatchRepoless"
|
|
2138
|
+
- $ref: "#/components/schemas/DeliveryGraphDispatchNodeProvisioned"
|
|
2099
2139
|
DeliveryGraphDispatchWithRepository:
|
|
2100
2140
|
description: >-
|
|
2101
2141
|
The repository-provisioned dispatch shape (#729): names the `repository` + `baseBranch` the run's
|
|
@@ -2215,6 +2255,52 @@ components:
|
|
|
2215
2255
|
fields are not members of this variant, so supplying either alongside `repoless: true` fails
|
|
2216
2256
|
`oneOf` matching and is a 400. To provision a repository, use the repository variant (supply
|
|
2217
2257
|
`repository` + `baseBranch`) and omit `repoless` entirely.
|
|
2258
|
+
DeliveryGraphDispatchNodeProvisioned:
|
|
2259
|
+
description: >-
|
|
2260
|
+
The FULLY node-provisioned dispatch shape (#739): supplies NEITHER a run-level `repository`/
|
|
2261
|
+
`baseBranch` NOR `repoless`, for a cross-repo graph in which EVERY `agent` node declares its own
|
|
2262
|
+
`repository` (so the run needs no uniform fallback and no checkout-less opt-out). The runner
|
|
2263
|
+
seeds each agent cell's `io.nanobpm.agentTask.repository` isolation envelope from that node's own
|
|
2264
|
+
declared repository. The post-load invariant still holds: if ANY `agent` node resolves to no
|
|
2265
|
+
repository under this shape (it declared none and there is no run-level fallback), the dispatch
|
|
2266
|
+
fails loudly with a 400 rather than silently sharing the launch dir — so this shape is only valid
|
|
2267
|
+
for a graph whose every agent node is self-provisioned. `repository`/`baseBranch`/`repoless` are
|
|
2268
|
+
not members of this variant (`additionalProperties: false`), so supplying any of them selects a
|
|
2269
|
+
different variant instead.
|
|
2270
|
+
type: object
|
|
2271
|
+
additionalProperties: false
|
|
2272
|
+
required:
|
|
2273
|
+
- digest
|
|
2274
|
+
properties:
|
|
2275
|
+
digest:
|
|
2276
|
+
type: string
|
|
2277
|
+
description: The staged proposal's content digest (its primary key) — the proposal to dispatch.
|
|
2278
|
+
idempotencyKey:
|
|
2279
|
+
type: string
|
|
2280
|
+
maxLength: 255
|
|
2281
|
+
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.
|
|
2282
|
+
nodeTimeout:
|
|
2283
|
+
type: string
|
|
2284
|
+
pattern: '^[Pp](?!$)(\d+[Yy])?(\d+[Mm])?(\d+[Ww])?(\d+[Dd])?([Tt](?=\d)(\d+[Hh])?(\d+[Mm])?(\d+[Ss])?)?$'
|
|
2285
|
+
maxLength: 64
|
|
2286
|
+
description: >-
|
|
2287
|
+
OPTIONAL run-level ISO-8601 SLA timeout for `agent`/`connector` nodes (#505) — the
|
|
2288
|
+
bounded-timeout → escalate boundary bound every such node inherits unless it declares its own
|
|
2289
|
+
per-node `timeout`. Absent → the `PT1H` default. An invalid duration is rejected at submit.
|
|
2290
|
+
probeTimeout:
|
|
2291
|
+
type: string
|
|
2292
|
+
pattern: '^[Pp](?!$)(\d+[Yy])?(\d+[Mm])?(\d+[Ww])?(\d+[Dd])?([Tt](?=\d)(\d+[Hh])?(\d+[Mm])?(\d+[Ss])?)?$'
|
|
2293
|
+
maxLength: 64
|
|
2294
|
+
description: >-
|
|
2295
|
+
OPTIONAL run-level ISO-8601 poll budget for `wait` gates (#505) before they escalate. Absent →
|
|
2296
|
+
the `PT30M` default. An invalid duration is rejected at submit.
|
|
2297
|
+
escalationSlaTimeout:
|
|
2298
|
+
type: string
|
|
2299
|
+
pattern: '^[Pp](?!$)(\d+[Yy])?(\d+[Mm])?(\d+[Ww])?(\d+[Dd])?([Tt](?=\d)(\d+[Hh])?(\d+[Mm])?(\d+[Ss])?)?$'
|
|
2300
|
+
maxLength: 64
|
|
2301
|
+
description: >-
|
|
2302
|
+
OPTIONAL run-level ISO-8601 SLA for `human` nodes (#505) before they record an `escalated`
|
|
2303
|
+
outcome. Absent → the `P1D` default. An invalid duration is rejected at submit.
|
|
2218
2304
|
DeliveryGraphDismissRequest:
|
|
2219
2305
|
description: >-
|
|
2220
2306
|
The OPERATOR dismiss request (#520). The cockpit's staged-proposals grid posts the content
|
|
@@ -2832,6 +2918,95 @@ components:
|
|
|
2832
2918
|
description: >-
|
|
2833
2919
|
A NAVIGATIONAL cockpit deep-link to the staged proposal (helps the agent hand the human a
|
|
2834
2920
|
link). It is a pointer only — NOT a dispatch handle; nothing in this response can start a run.
|
|
2921
|
+
superseded:
|
|
2922
|
+
type: array
|
|
2923
|
+
items:
|
|
2924
|
+
type: string
|
|
2925
|
+
description: >-
|
|
2926
|
+
Digests of prior staged proposals sharing this graph's LOGICAL key (derived from its `name`)
|
|
2927
|
+
that this stage replaced — flipped to `superseded` (issue #740). Empty on a first stage. Lets
|
|
2928
|
+
the agent tell the operator precisely which proposals it retired.
|
|
2929
|
+
siblingsStaged:
|
|
2930
|
+
type: integer
|
|
2931
|
+
description: >-
|
|
2932
|
+
How many OTHER live staged proposals remain after this stage (a DIFFERENT logical key from
|
|
2933
|
+
this one) — i.e. proposals this stage did NOT supersede (issue #740). A non-zero count flags
|
|
2934
|
+
potential ORPHANED SIBLINGS (e.g. an earlier stage of the "same" runbook under a different
|
|
2935
|
+
`name`, which supersede does not catch) cluttering the operator's Delivery Graphs list, so
|
|
2936
|
+
the agent can name them for cleanup.
|
|
2937
|
+
SequenceGate:
|
|
2938
|
+
description: >-
|
|
2939
|
+
An interleaved `wait` GATE (issue #740) that must go green before a given issue's agent starts
|
|
2940
|
+
— reusing the exact probe schema `wait` nodes accept (`npm`/`github-check`/`http`/`command`/
|
|
2941
|
+
`capability`/`pr`/`epic`). It lets an author splice e.g. "wait for `@nanobpm/agentic@0.13.0` to
|
|
2942
|
+
publish" between two sequence steps without hand-authoring raw node/edge JSON.
|
|
2943
|
+
type: object
|
|
2944
|
+
additionalProperties: false
|
|
2945
|
+
required:
|
|
2946
|
+
- kind
|
|
2947
|
+
- target
|
|
2948
|
+
properties:
|
|
2949
|
+
kind:
|
|
2950
|
+
type: string
|
|
2951
|
+
minLength: 1
|
|
2952
|
+
description: >-
|
|
2953
|
+
The wait-probe kind — one of the S3 vocabulary kinds (`npm`, `github-check`, `http`,
|
|
2954
|
+
`command`, `capability`, `pr`, `epic`). An unknown kind is a 400.
|
|
2955
|
+
target:
|
|
2956
|
+
type: string
|
|
2957
|
+
minLength: 1
|
|
2958
|
+
description: >-
|
|
2959
|
+
The probe target — kind-specific: `pkg@version` for `npm`, `owner/repo@ref` for
|
|
2960
|
+
`github-check`, a URL for `http`, an `owner/repo#N` PR for `pr`, etc.
|
|
2961
|
+
match:
|
|
2962
|
+
type: object
|
|
2963
|
+
description: >-
|
|
2964
|
+
OPTIONAL kind-specific readiness match fields (e.g. `{ version }` for `npm`,
|
|
2965
|
+
`{ conclusion, checkName }` for `github-check`).
|
|
2966
|
+
poll:
|
|
2967
|
+
type: object
|
|
2968
|
+
additionalProperties: false
|
|
2969
|
+
description: >-
|
|
2970
|
+
OPTIONAL poll budget. Defaults to the bounded merge-gate budget (re-probe every 5 minutes,
|
|
2971
|
+
budget 3 days) so a gate never falls into the 30-minute default trap.
|
|
2972
|
+
properties:
|
|
2973
|
+
everyMs:
|
|
2974
|
+
type: integer
|
|
2975
|
+
minimum: 1
|
|
2976
|
+
description: How often to re-probe (milliseconds).
|
|
2977
|
+
timeoutMs:
|
|
2978
|
+
type: integer
|
|
2979
|
+
minimum: 1
|
|
2980
|
+
description: The total budget before the gate escalates/continues (milliseconds).
|
|
2981
|
+
onTimeout:
|
|
2982
|
+
type: string
|
|
2983
|
+
enum:
|
|
2984
|
+
- escalate
|
|
2985
|
+
- continue
|
|
2986
|
+
description: >-
|
|
2987
|
+
What to do when the gate never goes green within its budget — `escalate` (default) parks on
|
|
2988
|
+
a human; `continue` proceeds anyway. `fail` is not supported on a `wait` node.
|
|
2989
|
+
credentialEnv:
|
|
2990
|
+
type: string
|
|
2991
|
+
minLength: 1
|
|
2992
|
+
description: OPTIONAL env-key name supplying a credential for the probe (`http`/`capability`).
|
|
2993
|
+
SequenceIssueEntry:
|
|
2994
|
+
description: >-
|
|
2995
|
+
One `issues[]` entry with an OPTIONAL leading gate (issue #740) — an object form of a sequence
|
|
2996
|
+
step. The `issue` is implemented + converged + merged like a bare-string entry; the optional
|
|
2997
|
+
`gate` must go green (in addition to the prior issue merging) before this issue's agent starts.
|
|
2998
|
+
type: object
|
|
2999
|
+
additionalProperties: false
|
|
3000
|
+
required:
|
|
3001
|
+
- issue
|
|
3002
|
+
properties:
|
|
3003
|
+
gate:
|
|
3004
|
+
$ref: "#/components/schemas/SequenceGate"
|
|
3005
|
+
issue:
|
|
3006
|
+
type: string
|
|
3007
|
+
minLength: 1
|
|
3008
|
+
maxLength: 255
|
|
3009
|
+
description: An `owner/repo#N` issue reference to implement + converge + merge, in sequence.
|
|
2835
3010
|
SequenceIssuesIntent:
|
|
2836
3011
|
description: >-
|
|
2837
3012
|
The `sequenceIssues` INTENT (epic nano-workforce#605, S4) — a high-level shape that GENERATES
|
|
@@ -2843,15 +3018,21 @@ components:
|
|
|
2843
3018
|
`compileDeliveryGraph` — it never dispatches (dispatch is an operator-only cockpit action, ADR
|
|
2844
3019
|
0005 Decision 7). For each issue it emits `agent` (`senior:feature`, emits a `pr` fact) →
|
|
2845
3020
|
`connector` (`converge-merge`, late-binding that `pr`) → `wait[pr, merged]` (a realistic
|
|
2846
|
-
`poll.timeoutMs`), threading the `pr` fact per §9.4.
|
|
2847
|
-
|
|
3021
|
+
`poll.timeoutMs`), threading the `pr` fact per §9.4. An `issues[]` entry may be a bare
|
|
3022
|
+
`owner/repo#N` string OR a `{ gate?, issue }` object that interleaves a `wait` gate (the
|
|
3023
|
+
`npm`/`github-check`/… vocabulary) before that issue's agent (issue #740). Invalid input (empty
|
|
3024
|
+
`issues`, an unparseable ref, an unknown gate kind) is a 400 carrying `issues: [{ path, message
|
|
3025
|
+
}]`; nothing is staged.
|
|
2848
3026
|
type: object
|
|
2849
3027
|
additionalProperties: false
|
|
2850
3028
|
example:
|
|
2851
|
-
behind: nanobpm/nano-ide#488
|
|
2852
3029
|
issues:
|
|
2853
|
-
- nanobpm/nano-
|
|
2854
|
-
-
|
|
3030
|
+
- nanobpm/nano-ide#557
|
|
3031
|
+
- gate:
|
|
3032
|
+
kind: npm
|
|
3033
|
+
target: "@nanobpm/agentic@0.13.0"
|
|
3034
|
+
issue: jwulf/c8ctl-plugin-nano#186
|
|
3035
|
+
- nanobpm/nano-workforce#738
|
|
2855
3036
|
required:
|
|
2856
3037
|
- issues
|
|
2857
3038
|
properties:
|
|
@@ -2868,15 +3049,22 @@ components:
|
|
|
2868
3049
|
minItems: 1
|
|
2869
3050
|
maxItems: 64
|
|
2870
3051
|
items:
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
3052
|
+
oneOf:
|
|
3053
|
+
- type: string
|
|
3054
|
+
minLength: 1
|
|
3055
|
+
maxLength: 255
|
|
3056
|
+
description: A bare `owner/repo#N` issue reference (no gate) — today's behaviour, byte-for-byte.
|
|
3057
|
+
- $ref: "#/components/schemas/SequenceIssueEntry"
|
|
3058
|
+
description: >-
|
|
3059
|
+
A sequence step — a bare `owner/repo#N` string, or a `{ gate?, issue }` object that
|
|
3060
|
+
interleaves a `wait` gate before that issue's agent (issue #740).
|
|
2875
3061
|
description: >-
|
|
2876
3062
|
The ordered issues to sequence — each is implemented by a `senior:feature` agent that opens
|
|
2877
3063
|
a PR, driven to convergence + merge, and the NEXT issue's implementation starts only once
|
|
2878
|
-
the prior issue has merged. At least one; at most
|
|
2879
|
-
compiler's node ceiling
|
|
3064
|
+
the prior issue has merged (and any interleaved gate has gone green). At least one; at most
|
|
3065
|
+
64. The compiler's node ceiling is enforced SEPARATELY by a node-budget guard (interleaved
|
|
3066
|
+
gates each add nodes, so a fully-gated 64-issue sequence can exceed the ceiling and is
|
|
3067
|
+
rejected there) — `maxItems: 64` bounds the entry count, not the generated node count.
|
|
2880
3068
|
SequenceIssuesRejected:
|
|
2881
3069
|
description: >-
|
|
2882
3070
|
A rejected `sequenceIssues` intent — the input failed validation (empty/oversized `issues`, an
|
|
@@ -3847,6 +4035,31 @@ paths:
|
|
|
3847
4035
|
with no argument), e.g. `orient`, `submit-pr`, `submit-epic`, `escalations`, `lifecycle`,
|
|
3848
4036
|
`debug`, `debug-models`, `unstick`, `raise-issue`, `delivery-graphs`, `tool-crosswalk`. Omit it to get the
|
|
3849
4037
|
table of contents. An unknown id yields a 400 listing the valid ids.
|
|
4038
|
+
- name: start
|
|
4039
|
+
in: query
|
|
4040
|
+
required: false
|
|
4041
|
+
schema:
|
|
4042
|
+
type: integer
|
|
4043
|
+
minimum: 0
|
|
4044
|
+
example: 0
|
|
4045
|
+
description: >-
|
|
4046
|
+
OPTIONAL pagination cursor (issue #740) — the CHARACTER offset into a `section`'s markdown to
|
|
4047
|
+
start at. Passing `start` and/or `length` engages bounded-chunk retrieval so a large section
|
|
4048
|
+
(e.g. `delivery-graphs`) can be paged through without overflowing a tool-result limit; the
|
|
4049
|
+
response echoes `start`, `length`, `totalLength`, and a `nextStart` cursor (null on the last
|
|
4050
|
+
page). Ignored when no `section` is given. Omit both `start` and `length` to get the whole
|
|
4051
|
+
section unchanged.
|
|
4052
|
+
- name: length
|
|
4053
|
+
in: query
|
|
4054
|
+
required: false
|
|
4055
|
+
schema:
|
|
4056
|
+
type: integer
|
|
4057
|
+
minimum: 1
|
|
4058
|
+
example: 12000
|
|
4059
|
+
description: >-
|
|
4060
|
+
OPTIONAL pagination window (issue #740) — the maximum number of CHARACTERS to return for a
|
|
4061
|
+
`section`. Defaults to a bounded page size when `start` is given without `length`. Pair with
|
|
4062
|
+
`start`/`nextStart` to page through an oversized section.
|
|
3850
4063
|
responses:
|
|
3851
4064
|
"200":
|
|
3852
4065
|
description: Either the table of contents (no `section`) or a single section's markdown.
|
|
@@ -4161,14 +4374,17 @@ paths:
|
|
|
4161
4374
|
application/json:
|
|
4162
4375
|
schema:
|
|
4163
4376
|
# BEGIN generated:mcp-body source=#/components/schemas/SequenceIssuesIntent (scripts/inline-mcp-bodies.ts — do not hand-edit)
|
|
4164
|
-
description: 'The `sequenceIssues` INTENT (epic nano-workforce#605, S4) — a high-level shape that GENERATES the canonical "implement issue → converge → merge" delivery graph (operator-guide §9.4) instead of making an agent hand-author its node/edge JSON. It names an ordered list of `issues` to sequence (each issue''s implementation starts once the PRIOR issue has merged) and an OPTIONAL leading `behind` gate (wait for that issue/epic/feature to be fully merged first, §9.5). The door GENERATES, then STAGES the graph through the same compile+stage flow as `compileDeliveryGraph` — it never dispatches (dispatch is an operator-only cockpit action, ADR 0005 Decision 7). For each issue it emits `agent` (`senior:feature`, emits a `pr` fact) → `connector` (`converge-merge`, late-binding that `pr`) → `wait[pr, merged]` (a realistic `poll.timeoutMs`), threading the `pr` fact per §9.4. Invalid input (empty `issues`, an unparseable ref) is a 400 carrying `issues: [{ path, message }]`; nothing is staged.'
|
|
4377
|
+
description: 'The `sequenceIssues` INTENT (epic nano-workforce#605, S4) — a high-level shape that GENERATES the canonical "implement issue → converge → merge" delivery graph (operator-guide §9.4) instead of making an agent hand-author its node/edge JSON. It names an ordered list of `issues` to sequence (each issue''s implementation starts once the PRIOR issue has merged) and an OPTIONAL leading `behind` gate (wait for that issue/epic/feature to be fully merged first, §9.5). The door GENERATES, then STAGES the graph through the same compile+stage flow as `compileDeliveryGraph` — it never dispatches (dispatch is an operator-only cockpit action, ADR 0005 Decision 7). For each issue it emits `agent` (`senior:feature`, emits a `pr` fact) → `connector` (`converge-merge`, late-binding that `pr`) → `wait[pr, merged]` (a realistic `poll.timeoutMs`), threading the `pr` fact per §9.4. An `issues[]` entry may be a bare `owner/repo#N` string OR a `{ gate?, issue }` object that interleaves a `wait` gate (the `npm`/`github-check`/… vocabulary) before that issue''s agent (issue #740). Invalid input (empty `issues`, an unparseable ref, an unknown gate kind) is a 400 carrying `issues: [{ path, message }]`; nothing is staged.'
|
|
4165
4378
|
type: object
|
|
4166
4379
|
additionalProperties: false
|
|
4167
4380
|
example:
|
|
4168
|
-
behind: nanobpm/nano-ide#488
|
|
4169
4381
|
issues:
|
|
4170
|
-
- nanobpm/nano-
|
|
4171
|
-
-
|
|
4382
|
+
- nanobpm/nano-ide#557
|
|
4383
|
+
- gate:
|
|
4384
|
+
kind: npm
|
|
4385
|
+
target: '@nanobpm/agentic@0.13.0'
|
|
4386
|
+
issue: jwulf/c8ctl-plugin-nano#186
|
|
4387
|
+
- nanobpm/nano-workforce#738
|
|
4172
4388
|
required:
|
|
4173
4389
|
- issues
|
|
4174
4390
|
properties:
|
|
@@ -4182,11 +4398,66 @@ paths:
|
|
|
4182
4398
|
minItems: 1
|
|
4183
4399
|
maxItems: 64
|
|
4184
4400
|
items:
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4401
|
+
oneOf:
|
|
4402
|
+
- type: string
|
|
4403
|
+
minLength: 1
|
|
4404
|
+
maxLength: 255
|
|
4405
|
+
description: A bare `owner/repo#N` issue reference (no gate) — today's behaviour, byte-for-byte.
|
|
4406
|
+
- description: "One `issues[]` entry with an OPTIONAL leading gate (issue #740) — an object form of a sequence step. The `issue` is implemented + converged + merged like a bare-string entry; the optional `gate` must go green (in addition to the prior issue merging) before this issue's agent starts."
|
|
4407
|
+
type: object
|
|
4408
|
+
additionalProperties: false
|
|
4409
|
+
required:
|
|
4410
|
+
- issue
|
|
4411
|
+
properties:
|
|
4412
|
+
gate:
|
|
4413
|
+
description: 'An interleaved `wait` GATE (issue #740) that must go green before a given issue''s agent starts — reusing the exact probe schema `wait` nodes accept (`npm`/`github-check`/`http`/`command`/ `capability`/`pr`/`epic`). It lets an author splice e.g. "wait for `@nanobpm/agentic@0.13.0` to publish" between two sequence steps without hand-authoring raw node/edge JSON.'
|
|
4414
|
+
type: object
|
|
4415
|
+
additionalProperties: false
|
|
4416
|
+
required:
|
|
4417
|
+
- kind
|
|
4418
|
+
- target
|
|
4419
|
+
properties:
|
|
4420
|
+
kind:
|
|
4421
|
+
type: string
|
|
4422
|
+
minLength: 1
|
|
4423
|
+
description: The wait-probe kind — one of the S3 vocabulary kinds (`npm`, `github-check`, `http`, `command`, `capability`, `pr`, `epic`). An unknown kind is a 400.
|
|
4424
|
+
target:
|
|
4425
|
+
type: string
|
|
4426
|
+
minLength: 1
|
|
4427
|
+
description: 'The probe target — kind-specific: `pkg@version` for `npm`, `owner/repo@ref` for `github-check`, a URL for `http`, an `owner/repo#N` PR for `pr`, etc.'
|
|
4428
|
+
match:
|
|
4429
|
+
type: object
|
|
4430
|
+
description: OPTIONAL kind-specific readiness match fields (e.g. `{ version }` for `npm`, `{ conclusion, checkName }` for `github-check`).
|
|
4431
|
+
poll:
|
|
4432
|
+
type: object
|
|
4433
|
+
additionalProperties: false
|
|
4434
|
+
description: OPTIONAL poll budget. Defaults to the bounded merge-gate budget (re-probe every 5 minutes, budget 3 days) so a gate never falls into the 30-minute default trap.
|
|
4435
|
+
properties:
|
|
4436
|
+
everyMs:
|
|
4437
|
+
type: integer
|
|
4438
|
+
minimum: 1
|
|
4439
|
+
description: How often to re-probe (milliseconds).
|
|
4440
|
+
timeoutMs:
|
|
4441
|
+
type: integer
|
|
4442
|
+
minimum: 1
|
|
4443
|
+
description: The total budget before the gate escalates/continues (milliseconds).
|
|
4444
|
+
onTimeout:
|
|
4445
|
+
type: string
|
|
4446
|
+
enum:
|
|
4447
|
+
- escalate
|
|
4448
|
+
- continue
|
|
4449
|
+
description: What to do when the gate never goes green within its budget — `escalate` (default) parks on a human; `continue` proceeds anyway. `fail` is not supported on a `wait` node.
|
|
4450
|
+
credentialEnv:
|
|
4451
|
+
type: string
|
|
4452
|
+
minLength: 1
|
|
4453
|
+
description: OPTIONAL env-key name supplying a credential for the probe (`http`/`capability`).
|
|
4454
|
+
issue:
|
|
4455
|
+
type: string
|
|
4456
|
+
minLength: 1
|
|
4457
|
+
maxLength: 255
|
|
4458
|
+
description: An `owner/repo#N` issue reference to implement + converge + merge, in sequence.
|
|
4459
|
+
description: "A sequence step — a bare `owner/repo#N` string, or a `{ gate?, issue }` object that interleaves a `wait` gate before that issue's agent (issue #740)."
|
|
4460
|
+
description: "The ordered issues to sequence — each is implemented by a `senior:feature` agent that opens a PR, driven to convergence + merge, and the NEXT issue's implementation starts only once the prior issue has merged (and any interleaved gate has gone green). At least one; at most 64. The compiler's node ceiling is enforced SEPARATELY by a node-budget guard (interleaved gates each add nodes, so a fully-gated 64-issue sequence can exceed the ceiling and is rejected there) — `maxItems: 64` bounds the entry count, not the generated node count."
|
|
4190
4461
|
# END generated:mcp-body
|
|
4191
4462
|
responses:
|
|
4192
4463
|
"200":
|
|
@@ -4330,6 +4601,16 @@ paths:
|
|
|
4330
4601
|
type: string
|
|
4331
4602
|
maxLength: 20000
|
|
4332
4603
|
description: OPTIONAL steering prompt appended to the node's job brief.
|
|
4604
|
+
repository:
|
|
4605
|
+
type: string
|
|
4606
|
+
maxLength: 255
|
|
4607
|
+
pattern: ^[A-Za-z0-9-]+/(?!.*\.[Gg][Ii][Tt]$)[A-Za-z0-9._-]+$
|
|
4608
|
+
description: OPTIONAL per-node `owner/repo` this agent node implements against (#739). A delivery graph provisions the `io.nanobpm.agentTask.repository` isolation envelope PER agent cell from THIS field, so a genuinely cross-repo graph (each node a different repo) isolates correctly without the operator ticking `repoless`. Absent → the node falls back to the run-level dispatch `repository`. When set it must be exactly `owner/repo` (a trailing `.git` and any non-`owner/repo` shape are rejected at submit), the same allowlist `repoEnvelopeVars`/the dispatch door apply. The `sequenceIssues` generator populates this automatically from the `owner/repo#N` each node implements.
|
|
4609
|
+
baseBranch:
|
|
4610
|
+
type: string
|
|
4611
|
+
maxLength: 255
|
|
4612
|
+
pattern: ^(?![/.-])(?!.*[/.]$)(?!.*\.\.)(?!.*//)(?!.*/\.)(?!.*\.lock(?:/|$))[A-Za-z0-9._/-]+$
|
|
4613
|
+
description: "OPTIONAL per-node base branch this agent node branches off (#739) — the `ref` the harness checks out in the isolated clone (the pre-PR shape: the agent cuts its own `feat/<node.id>` branch off this base). Absent → the run-level dispatch `baseBranch` (else the node's repository default branch). A value that is not a plausible git branch name is rejected at submit; the pattern mirrors the authoritative server-side gate (`isPlausibleBranchName`, app/baseBranch.ts)."
|
|
4333
4614
|
converge:
|
|
4334
4615
|
type: boolean
|
|
4335
4616
|
description: 'OPTIONAL first-class CONVERGE policy (ADR 0006 §3 / S5) — a DECLARED, compiler- validated completion-policy flag on this cell node. It declares that the node''s opened PR is to be driven through the review-convergence loop to green as an edge-gated completion policy; this slice adds and validates the flag, with the delivery-graph execution wiring that consumes it landing in a follow-up slice. It supersedes (in intent) the emergent `feature.bpmn` `gw-converge` gateway and the "un-draft + merge #B" prompt prose a delivery-graph `agent` node used to smuggle. Converge and merge are SEPARABLE phases; a node may converge without merging (stop at green and gate the landing behind a downstream node).'
|
|
@@ -127,6 +127,10 @@ test("compile-delivery-graph: a changed graph with the same name SUPERSEDES the
|
|
|
127
127
|
assert(a.body.digest !== b.body.digest, "the changed graph has a new digest");
|
|
128
128
|
assertEquals((await deliveryGraphProposals(data).get(a.body.digest))?.status, "superseded");
|
|
129
129
|
assertEquals((await deliveryGraphProposals(data).get(b.body.digest))?.status, "staged");
|
|
130
|
+
// The supersede is surfaced in the compile result body so the operator sees the retirement (#740).
|
|
131
|
+
assertEquals(b.body.superseded, [a.body.digest]);
|
|
132
|
+
assertEquals(b.body.siblingsStaged, 0);
|
|
133
|
+
assertEquals(a.body.superseded, []); // the first stage retired nothing
|
|
130
134
|
});
|
|
131
135
|
});
|
|
132
136
|
|
|
@@ -318,23 +318,69 @@ describe("dispatchDeliveryGraph — operator dispatch by staged-proposal digest"
|
|
|
318
318
|
// dispatch envelope-less and let concurrent fan-out agents share (and clobber) the worker's launch
|
|
319
319
|
// dir (issue #684's field failure re-opened as a silent fallback). This "neither/nor" shape is now
|
|
320
320
|
// rejected at the EDGE by the request `oneOf` (the two allowed variants), not only inside the door.
|
|
321
|
-
test("a dispatch with
|
|
321
|
+
test("a bare dispatch of a graph with an UNPROVISIONED agent node is rejected → 400, nothing launched (#729/#739)", async () => {
|
|
322
322
|
const app = await boot();
|
|
323
323
|
assert.ok(app.api);
|
|
324
324
|
const api = app.api;
|
|
325
|
-
|
|
325
|
+
// SIDE_EFFECTING's `open-b` agent node declares NO repository, and this bare dispatch supplies no
|
|
326
|
+
// run-level fallback and no `repoless` — so the cell would silently share the worker's launch dir.
|
|
327
|
+
// The door rejects it LOUDLY (#729) rather than launching or degrading (#684).
|
|
328
|
+
const staged = await api.call<{ digest: string }>("compileDeliveryGraph", { body: SIDE_EFFECTING });
|
|
326
329
|
const res = await api.call<{ ok?: boolean; error?: string }>("dispatchDeliveryGraph", {
|
|
327
330
|
body: { digest: staged.body.digest },
|
|
328
331
|
});
|
|
329
332
|
assert.equal(res.status, 400);
|
|
330
|
-
|
|
331
|
-
assert.ok(typeof res.body.error === "string" && res.body.error.length > 0);
|
|
333
|
+
assert.ok(typeof res.body.error === "string" && res.body.error.includes("open-b"), `error should name the unprovisioned node, got ${res.body.error}`);
|
|
332
334
|
// Loud, not silent: nothing launched and the proposal stays staged (re-dispatchable once a repo /
|
|
333
335
|
// repoless choice is supplied).
|
|
334
336
|
assert.equal((await deliveryGraphRuns(app.db).all()).length, 0);
|
|
335
337
|
assert.equal((await deliveryGraphProposals(app.db).get(staged.body.digest))?.status, "staged");
|
|
336
338
|
});
|
|
337
339
|
|
|
340
|
+
test("a bare dispatch of a HUMAN-ONLY graph (nothing to provision) SUCCEEDS → 202 running (#739)", async () => {
|
|
341
|
+
const app = await boot();
|
|
342
|
+
assert.ok(app.api);
|
|
343
|
+
const api = app.api;
|
|
344
|
+
// A graph with no `agent` nodes needs neither a run-level repository nor `repoless` — there is nothing
|
|
345
|
+
// to isolate. Under #739 a bare `{ digest }` dispatch (the node-provisioned variant) launches cleanly.
|
|
346
|
+
const staged = await api.call<{ digest: string }>("compileDeliveryGraph", { body: HUMAN_ONLY });
|
|
347
|
+
const res = await api.call<{ ok: boolean; status: string }>("dispatchDeliveryGraph", {
|
|
348
|
+
body: { digest: staged.body.digest },
|
|
349
|
+
});
|
|
350
|
+
assert.equal(res.status, 202);
|
|
351
|
+
assert.equal(res.body.ok, true);
|
|
352
|
+
assert.equal(res.body.status, "running");
|
|
353
|
+
await app.settle();
|
|
354
|
+
assert.equal((await deliveryGraphRuns(app.db).all()).length, 1);
|
|
355
|
+
assert.equal((await deliveryGraphProposals(app.db).get(staged.body.digest))?.status, "dispatched");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("a bare dispatch of a fully NODE-PROVISIONED agent graph SUCCEEDS → 202 running (#739)", async () => {
|
|
359
|
+
const app = await boot();
|
|
360
|
+
assert.ok(app.api);
|
|
361
|
+
const api = app.api;
|
|
362
|
+
// Every agent node declares its OWN repository, so the graph self-provisions each cell's isolation
|
|
363
|
+
// envelope — no run-level repository, no `repoless`. This is the cross-repo delivery graph #739 enables.
|
|
364
|
+
const NODE_PROVISIONED = {
|
|
365
|
+
name: "cross-repo",
|
|
366
|
+
nodes: [
|
|
367
|
+
{ id: "open-b", kind: "agent", agent: { jobType: "senior:demo", prompt: "implement", repository: "acme/one" } },
|
|
368
|
+
{ id: "publish", kind: "human", human: { prompt: "publish" } },
|
|
369
|
+
],
|
|
370
|
+
edges: [{ from: "open-b", to: "publish" }],
|
|
371
|
+
};
|
|
372
|
+
const staged = await api.call<{ digest: string }>("compileDeliveryGraph", { body: NODE_PROVISIONED });
|
|
373
|
+
const res = await api.call<{ ok: boolean; status: string }>("dispatchDeliveryGraph", {
|
|
374
|
+
body: { digest: staged.body.digest },
|
|
375
|
+
});
|
|
376
|
+
assert.equal(res.status, 202);
|
|
377
|
+
assert.equal(res.body.ok, true);
|
|
378
|
+
assert.equal(res.body.status, "running");
|
|
379
|
+
await app.settle();
|
|
380
|
+
assert.equal((await deliveryGraphRuns(app.db).all()).length, 1);
|
|
381
|
+
assert.equal((await deliveryGraphProposals(app.db).get(staged.body.digest))?.status, "dispatched");
|
|
382
|
+
});
|
|
383
|
+
|
|
338
384
|
test("a dispatch with ONLY repository (no baseBranch) is rejected → 400, nothing launched (#729)", async () => {
|
|
339
385
|
const app = await boot();
|
|
340
386
|
assert.ok(app.api);
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
// superseded / already-dispatched digest is a clean 400.
|
|
12
12
|
|
|
13
13
|
import { isPlausibleBranchName } from "../app/baseBranch.ts";
|
|
14
|
+
import { validateDeliveryGraph } from "../app/deliveryGraph.ts";
|
|
14
15
|
import { dispatchDeliveryGraphRun } from "../app/deliveryGraphDispatch.ts";
|
|
15
16
|
import { getStagedProposal, markProposalDispatched, markProposalExpired } from "../app/deliveryGraphProposals.ts";
|
|
17
|
+
import { unresolvedAgentRepoNodes } from "../app/deliveryRunner.ts";
|
|
16
18
|
import { isValidIsoDuration } from "../app/reviewWait.ts";
|
|
17
|
-
import type { DeliveryGraphTextResult } from "../nano-generated/api-io.d.ts";
|
|
19
|
+
import type { DeliveryGraph, DeliveryGraphTextResult } from "../nano-generated/api-io.d.ts";
|
|
18
20
|
import { defineOperation } from "../nano-generated/operations.ts";
|
|
19
21
|
|
|
20
22
|
/** Cap an untrusted, rejected duration string before it is echoed into logs/response bodies. `openapi.yaml`
|
|
@@ -108,28 +110,27 @@ export default defineOperation("dispatchDeliveryGraph", async ({ body }, app) =>
|
|
|
108
110
|
baseBranch = baseRaw;
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
// Repository provisioning
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
113
|
+
// Repository provisioning (issue #729 / #739): a delivery-graph run whose `agent` nodes implement
|
|
114
|
+
// against a repo MUST provision an ISOLATED clone per cell — silently dispatching envelope-less (issue
|
|
115
|
+
// #684's field failure) let concurrent fan-out workers on one host share and clobber a single launch
|
|
116
|
+
// dir. The operator supplies EITHER a run-level `repository` + `baseBranch` (the fallback default for
|
|
117
|
+
// nodes that don't declare their own), OR `repoless: true` for a genuinely checkout-less graph, OR —
|
|
118
|
+
// when every agent node declares its OWN `repository` (#739) — NEITHER. The remaining invariant (no
|
|
119
|
+
// agent node resolves to NO repository on a non-`repoless` run) is checked AFTER the staged graph is
|
|
120
|
+
// loaded below, since the request alone cannot see whether every node is self-provisioned.
|
|
118
121
|
const hasRepoless = body !== null && typeof body === "object" && "repoless" in body;
|
|
119
122
|
const repolessRaw = hasRepoless ? body.repoless : undefined;
|
|
120
123
|
// `repoless` is a `true`-ONLY opt-out, exactly as the OpenAPI `oneOf` models it: it is `enum: [true]`
|
|
121
|
-
// on the repoless variant and NOT a member of the
|
|
124
|
+
// on the repoless variant and NOT a member of the other variants (`additionalProperties: false`).
|
|
122
125
|
// So reject any present-but-not-`true` value — `false`, `"yes"`, `0`, … — rather than silently folding
|
|
123
|
-
// `repoless: false` into the "not repoless" path.
|
|
124
|
-
// could send `{ digest, repository, baseBranch, repoless: false }` (or `{ digest, repoless: false }`)
|
|
125
|
-
// and slip through with a confusing tri-state the `oneOf` contract never permits.
|
|
126
|
+
// `repoless: false` into the "not repoless" path.
|
|
126
127
|
if (hasRepoless && repolessRaw !== true) {
|
|
127
128
|
app.log.warn("dispatch-delivery-graph rejected: repoless is a true-only opt-out", { type: typeof repolessRaw });
|
|
128
129
|
return {
|
|
129
130
|
status: 400,
|
|
130
131
|
body: {
|
|
131
132
|
ok: false,
|
|
132
|
-
error: "`repoless` is a `true`-only opt-out — omit it to provision a repository (with `repository` + `baseBranch
|
|
133
|
+
error: "`repoless` is a `true`-only opt-out — omit it to provision a repository (with `repository` + `baseBranch`, or per-node declared repositories), or set `repoless: true` to dispatch a checkout-less graph",
|
|
133
134
|
},
|
|
134
135
|
};
|
|
135
136
|
}
|
|
@@ -141,20 +142,6 @@ export default defineOperation("dispatchDeliveryGraph", async ({ body }, app) =>
|
|
|
141
142
|
body: { ok: false, error: "`repoless: true` is mutually exclusive with `repository`/`baseBranch` — pass one or the other, not both" },
|
|
142
143
|
};
|
|
143
144
|
}
|
|
144
|
-
if (!repoless && (repository === undefined || baseBranch === undefined)) {
|
|
145
|
-
app.log.warn("dispatch-delivery-graph rejected: missing repository/baseBranch (no repoless opt-in)", {
|
|
146
|
-
hasRepository: repository !== undefined,
|
|
147
|
-
hasBaseBranch: baseBranch !== undefined,
|
|
148
|
-
});
|
|
149
|
-
return {
|
|
150
|
-
status: 400,
|
|
151
|
-
body: {
|
|
152
|
-
ok: false,
|
|
153
|
-
error:
|
|
154
|
-
"a delivery-graph dispatch must provision an isolated checkout: supply BOTH `repository` (`owner/repo`) and `baseBranch`, or set `repoless: true` to dispatch a checkout-less graph — dispatching without either would silently share the worker's launch dir across agents (issue #729)",
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
145
|
|
|
159
146
|
// Load the live staged proposal for this digest — refuses an unknown/expired/superseded/already-
|
|
160
147
|
// dispatched digest cleanly (no run is launched).
|
|
@@ -179,6 +166,36 @@ export default defineOperation("dispatchDeliveryGraph", async ({ body }, app) =>
|
|
|
179
166
|
return { status: 400, body: { ok: false, error: `staged proposal ${digest} is corrupt: ${err instanceof Error ? err.message : String(err)}` } };
|
|
180
167
|
}
|
|
181
168
|
|
|
169
|
+
// Per-node provisioning invariant (#739): now that the graph is loaded, reject a non-`repoless` run
|
|
170
|
+
// in which some `agent` node resolves to NO repository — it declared none AND no run-level fallback
|
|
171
|
+
// was supplied. Such a cell would silently share the worker's launch dir (issue #684), so fail loudly
|
|
172
|
+
// with a clean 400 here rather than surfacing later as a launch-time throw (a 500). A graph in which
|
|
173
|
+
// EVERY agent node resolves a repository (declared, or defaulted from the run level) passes — a fully
|
|
174
|
+
// node-provisioned cross-repo graph needs neither a run-level repository nor `repoless`. The runner
|
|
175
|
+
// re-enforces this at seed time (defense in depth). Guarded on a well-formed graph; a malformed one
|
|
176
|
+
// falls through to `dispatchDeliveryGraphRun`'s own validation below.
|
|
177
|
+
if (!repoless) {
|
|
178
|
+
const graphErrors = validateDeliveryGraph(graph);
|
|
179
|
+
if (graphErrors.length === 0) {
|
|
180
|
+
// biome-ignore lint/plugin: validated staged graph narrowed to its contract after validateDeliveryGraph
|
|
181
|
+
const typedGraph = graph as DeliveryGraph;
|
|
182
|
+
const unresolved = unresolvedAgentRepoNodes(typedGraph, { repository, baseBranch });
|
|
183
|
+
if (unresolved.length > 0) {
|
|
184
|
+
app.log.warn("dispatch-delivery-graph rejected: unprovisioned agent node(s)", { digest, unresolved });
|
|
185
|
+
return {
|
|
186
|
+
status: 400,
|
|
187
|
+
body: {
|
|
188
|
+
ok: false,
|
|
189
|
+
error:
|
|
190
|
+
`${unresolved.length} agent node(s) resolve to no repository (${unresolved.join(", ")}): each must declare its own ` +
|
|
191
|
+
"`repository`, or the dispatch must supply a run-level `repository` + `baseBranch` fallback, or set `repoless: true` " +
|
|
192
|
+
"for a genuinely checkout-less graph — dispatching an unprovisioned node would silently share the worker's launch dir (issue #684/#739)",
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
182
199
|
const dispatched = await dispatchDeliveryGraphRun(app, graph, { runKey: idempotencyKey, title: proposal.title, repository, baseBranch, repoless, ...timeouts });
|
|
183
200
|
if (!dispatched.ok) {
|
|
184
201
|
app.log.warn("dispatch-delivery-graph refused: compile", { digest, errors: dispatched.errors.length });
|