@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/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): the run either names EXACTLY the
2089
- `repository` + `baseBranch` its `agent` nodes implement against, OR opts out with `repoless: true`
2090
- for a genuinely checkout-less graph — never both, never neither. Modeled as `oneOf` named variants
2091
- (Camunda REST v2 pattern, as the convergence/plan/feature start bodies are) so the runtime rejects
2092
- the "neither/nor" and "both" shapes AT THE EDGE with a 400 that names the allowed shapes, rather
2093
- than the delegate being the only thing that enforces the mutual exclusivity. Dispatching without
2094
- either would silently share the worker's launch dir across concurrent fan-out agents (the issue
2095
- #684 field failure).
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. Invalid input (empty `issues`, an
2847
- unparseable ref) is a 400 carrying `issues: [{ path, message }]`; nothing is staged.
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-workforce#567
2854
- - nanobpm/nano-workforce#568
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
- type: string
2872
- minLength: 1
2873
- maxLength: 255
2874
- description: An `owner/repo#N` issue reference to implement + converge + merge, in sequence.
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 64 (keeps the generated graph within the
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-workforce#567
4171
- - nanobpm/nano-workforce#568
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
- type: string
4186
- minLength: 1
4187
- maxLength: 255
4188
- description: An `owner/repo#N` issue reference to implement + converge + merge, in sequence.
4189
- 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. At least one; at most 64 (keeps the generated graph within the compiler's node ceiling).
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 NEITHER repository/baseBranch NOR repoless is rejected at submit → 400, nothing launched (#729)", async () => {
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
- const staged = await api.call<{ digest: string }>("compileDeliveryGraph", { body: HUMAN_ONLY });
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
- // The `oneOf` edge validator names the two allowed shapes — the neither/nor body matches neither.
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 is REQUIRED on this fan-out door (issue #729): a delivery-graph run whose
112
- // `agent` nodes implement against a repo MUST be dispatched with a resolvable `repository` + base
113
- // branch so every `senior:*` job provisions an ISOLATED clone. Silently dispatching envelope-less
114
- // (issue #684's field failure, re-opened as a silent fallback) let concurrent fan-out workers on one
115
- // host share and clobber a single launch-dir checkout. So the operator must EITHER supply BOTH
116
- // `repository` and `baseBranch`, OR explicitly opt out with `repoless: true` for a genuinely
117
- // checkout-less graph the default can never silently share a checkout.
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 repository variant (`additionalProperties: false`).
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. Otherwise a caller that bypasses schema validation
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`), or set `repoless: true` to dispatch a checkout-less graph",
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 });