@nanobpm/nano-workforce 0.189.3 → 0.190.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 +6 -0
- package/README.md +5 -4
- package/SPEC.md +5 -5
- package/app/agentGuide.ts +1 -1
- package/app/deliveryGraphVocabulary.ts +6 -6
- package/docs/adr/0005-agent-authored-delivery-graphs.md +1 -1
- package/docs/agent-guide.md +9 -9
- package/openapi.yaml +29 -17
- package/operations/getAgentInstructions.test.ts +2 -1
- package/operations/startAndMessage.test.ts +44 -2
- package/operations/startConvergenceLoop.ts +13 -10
- package/package.json +1 -1
- package/pages/home.page.json +2 -2
- package/workers/delivery-connector/worker.test.ts +8 -0
- package/workers/delivery-connector/worker.ts +13 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.190.0](https://github.com/nanobpm/nano-workforce/compare/v0.189.3...v0.190.0) (2026-09-21)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* **ui:** make Convergence auto-merge opt-in ([#752](https://github.com/nanobpm/nano-workforce/issues/752)) ([db18b17](https://github.com/nanobpm/nano-workforce/commit/db18b17ceadc1cccd48e50a12c98d867f657f3d5)), closes [#751](https://github.com/nanobpm/nano-workforce/issues/751)
|
|
6
|
+
|
|
1
7
|
## [0.189.3](https://github.com/nanobpm/nano-workforce/compare/v0.189.2...v0.189.3) (2026-09-20)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -285,9 +285,10 @@ branch before escalating (`NANO_PR_MAX_CI_FIX_ROUNDS`, default 3). Conflicts, an
|
|
|
285
285
|
exhausted budget, or an agent that can't fix the build escalate to a human; answer in
|
|
286
286
|
the UI and the process re-arms and retries.
|
|
287
287
|
|
|
288
|
-
A single submission can
|
|
289
|
-
|
|
290
|
-
`converged`
|
|
288
|
+
A single submission can choose its merge behavior explicitly with the positive
|
|
289
|
+
`autoMerge` field on the `start/convergence-loop` request: `false` stops at
|
|
290
|
+
`converged` regardless of the global default, while `true` enables the merge-loop
|
|
291
|
+
after convergence only when `NANO_PR_AUTO_MERGE` is enabled.
|
|
291
292
|
|
|
292
293
|
### Fleet mode: hand it an issue (plan → implement → converge)
|
|
293
294
|
|
|
@@ -356,7 +357,7 @@ agent at that URL to author, compile, and submit a graph unaided. See
|
|
|
356
357
|
| `NANO_PR_POLL_MS` | `60000` | review-ready poll interval |
|
|
357
358
|
| `NANO_PR_MAX_ROUNDS` | `20` | default cap: escalate after N rounds (per-submit override via the form / the `maxRounds` field on `start/convergence-loop`; clamped 1–100) |
|
|
358
359
|
| `NANO_PR_WEBHOOK_SECRET` | — | optional shared secret (`x-hook-secret`) for guarded operations (e.g. `GET /app/api/agent`, `/app/api/version`, `/app/api/status`); unset = open |
|
|
359
|
-
| `NANO_PR_AUTO_MERGE` | `1` | after convergence, run the merge stage; `0` = stop at `converged` (review-only). Per-submit override via the `
|
|
360
|
+
| `NANO_PR_AUTO_MERGE` | `1` | after convergence, run the merge stage; `0` = stop at `converged` (review-only). Per-submit override via the positive `autoMerge` field on `start/convergence-loop` |
|
|
360
361
|
| `NANO_PR_MERGE_METHOD` | `squash` | merge method: `squash`, `merge`, or `rebase` |
|
|
361
362
|
| `NANO_PR_MERGE_ADMIN` | `0` | pass `--admin` to override failing non-required checks (use with care) |
|
|
362
363
|
| `NANO_PR_MAX_CI_FIX_ROUNDS` | `3` | max `senior:fix-ci` attempts to green a `blocked` PR before escalating; `0` disables (escalate immediately), clamped 0–20 |
|
package/SPEC.md
CHANGED
|
@@ -582,10 +582,10 @@ same `prKey`, sharing the datasource and poller. It merges the PR, honouring
|
|
|
582
582
|
merge-queue branches and cross-PR dependencies, and reuses the review stage's
|
|
583
583
|
escalation machinery for anything it can't resolve autonomously.
|
|
584
584
|
|
|
585
|
-
A per-submit `
|
|
586
|
-
to review-only regardless of the global default
|
|
587
|
-
|
|
588
|
-
|
|
585
|
+
A per-submit `autoMerge` setting on the `start/convergence-loop` request is the preferred
|
|
586
|
+
positive control: `false` pins that PR to review-only regardless of the global default, while
|
|
587
|
+
`true` enables the merge-loop after convergence only when `NANO_PR_AUTO_MERGE` is on. No per-submit
|
|
588
|
+
setting forces the merge stage on when the global toggle is off.
|
|
589
589
|
|
|
590
590
|
Flow:
|
|
591
591
|
|
|
@@ -691,7 +691,7 @@ queries skip (`merging`), so a slow pass can't double-signal.
|
|
|
691
691
|
| `NANO_PR_POLL_MS` | 60000 | poll interval |
|
|
692
692
|
| `NANO_PR_MAX_ROUNDS` | 20 | default round cap (per-submit `maxRounds` override, clamped 1–100) |
|
|
693
693
|
| `NANO_PR_WEBHOOK_SECRET` | — | optional shared secret (`x-hook-secret`) for guarded operations (e.g. `/app/api/agent`, `/app/api/version`, `/app/api/status`) |
|
|
694
|
-
| `NANO_PR_AUTO_MERGE` | 1 | run the merge stage after convergence (`0` = review-only; per-submit `
|
|
694
|
+
| `NANO_PR_AUTO_MERGE` | 1 | run the merge stage after convergence (`0` = review-only; per-submit `autoMerge: false` override) |
|
|
695
695
|
| `NANO_PR_MERGE_METHOD` | squash | `squash` \| `merge` \| `rebase` |
|
|
696
696
|
| `NANO_PR_MERGE_ADMIN` | 0 | pass `--admin` on merge |
|
|
697
697
|
| `NANO_PR_REVIEW_WAIT_TIMEOUT` | PT30M | ISO-8601 wait before a stalled review escalates (timer arm of the `wait-review` event-based gateway); malformed → default |
|
package/app/agentGuide.ts
CHANGED
|
@@ -31,7 +31,7 @@ const RAW_GUIDE: string = (() => {
|
|
|
31
31
|
"Key endpoints (under the app control-API base `__BASE__`):",
|
|
32
32
|
"- `GET /status` — every PR in flight, with its engine `processKey` and any open escalation.",
|
|
33
33
|
"- `GET /version` — which code is live.",
|
|
34
|
-
"- `POST /actions/start/convergence-loop` — submit a PR (`{ pr,
|
|
34
|
+
"- `POST /actions/start/convergence-loop` — submit a PR (`{ pr, autoMerge?, maxRounds?, dependsOn? }`; `autoMerge` is the positive per-request merge control).",
|
|
35
35
|
"- `POST /actions/start/plan-fanout` — submit an epic (`{ issue, baseBranch }` or `{ url, baseBranch }`; base is required — a missing `epic/*` base is auto-created, and `confirmDefaultBase`/`allowSharedBase` gate the default-branch and shared-base cases — see ADR 0003).",
|
|
36
36
|
"- `POST /actions/complete-user-task` — answer an escalation (`{ userTaskKey, variables }`); the parked user task's key comes from `GET /status`/the Tasks inbox, and the typed variables match its `.form` (e.g. a PR escalation's `{ answer }`).",
|
|
37
37
|
"- `POST /actions/message` — publish a BPMN message (optionally correlated) into the engine.",
|
|
@@ -67,7 +67,7 @@ export interface WaitProbeEntry {
|
|
|
67
67
|
export interface ConnectorTargetEntry {
|
|
68
68
|
target: string;
|
|
69
69
|
status: "real" | "forward-declared";
|
|
70
|
-
|
|
70
|
+
autoMergeDefault?: boolean;
|
|
71
71
|
summary: string;
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -143,7 +143,7 @@ const NODE_KIND_DETAIL: Record<string, Omit<NodeKindEntry, "kind">> = {
|
|
|
143
143
|
mayEmit: true,
|
|
144
144
|
summary:
|
|
145
145
|
"An automated, side-effecting outbound action. `payload` for a converge target is " +
|
|
146
|
-
"`{ pr,
|
|
146
|
+
"`{ pr, autoMerge?, dependsOn? }` (`pr` may be a literal `owner/repo#N`, a `<node>.pr` fact " +
|
|
147
147
|
"reference, or omitted to auto-bind the single incoming `pr` fact). Carries a `dedupeKey` " +
|
|
148
148
|
"(at-least-once safe). See connectorTargets for which targets are real vs. forward-declared.",
|
|
149
149
|
},
|
|
@@ -218,13 +218,13 @@ const REAL_CONNECTOR_TARGETS: ConnectorTargetEntry[] = [
|
|
|
218
218
|
{
|
|
219
219
|
target: CONVERGE_TARGET,
|
|
220
220
|
status: "real",
|
|
221
|
-
|
|
222
|
-
summary: "
|
|
221
|
+
autoMergeDefault: !convergeOnlyForTarget(CONVERGE_TARGET),
|
|
222
|
+
summary: "Review-only: drive convergence and STOP at `converged`, never handing off to the merge loop.",
|
|
223
223
|
},
|
|
224
224
|
{
|
|
225
225
|
target: CONVERGE_MERGE_TARGET,
|
|
226
226
|
status: "real",
|
|
227
|
-
|
|
227
|
+
autoMergeDefault: !convergeOnlyForTarget(CONVERGE_MERGE_TARGET),
|
|
228
228
|
summary:
|
|
229
229
|
"Unit-level land: drive review convergence AND the merge loop, landing the PR onto its OWN base branch " +
|
|
230
230
|
"(for a unit inside an epic that base is the epic integration branch, never `main` directly).",
|
|
@@ -232,7 +232,7 @@ const REAL_CONNECTOR_TARGETS: ConnectorTargetEntry[] = [
|
|
|
232
232
|
{
|
|
233
233
|
target: MERGE_MAIN_TARGET,
|
|
234
234
|
status: "real",
|
|
235
|
-
|
|
235
|
+
autoMergeDefault: !convergeOnlyForTarget(MERGE_MAIN_TARGET),
|
|
236
236
|
summary:
|
|
237
237
|
"Graph-level top-level land (two-level merge, ADR 0006 §3): land the graph/epic INTEGRATION PR onto `main`. " +
|
|
238
238
|
"Dispatch-identical to `converge-merge`; the distinction is the LEVEL, kept a first-class literal.",
|
|
@@ -113,7 +113,7 @@ The closed set (extensible only by a deliberate ADR/PR, never by graph authors):
|
|
|
113
113
|
> PR back to `converging`. `submitPr`'s own `prKey` idempotency additionally makes a resumed re-perform
|
|
114
114
|
> double-safe on a still-live row. This retires the manual `land-*` human gate whose only job was "go run convergence
|
|
115
115
|
> yourself" — the canonical shape is now `agent (opens PR) → connector[converge-merge] →
|
|
116
|
-
> wait[pr, merged]` with no human node. The payload is `{ pr,
|
|
116
|
+
> wait[pr, merged]` with no human node. The payload is `{ pr, autoMerge?, dependsOn? }`; the `pr`
|
|
117
117
|
> may be a literal `owner/repo#N`, a `<node>.pr` fact reference late-bound from an upstream `agent`
|
|
118
118
|
> node's emitted `pr` fact, or omitted to auto-bind the single incoming `pr` fact (issue #548 —
|
|
119
119
|
> shipped: the connector resolves it via `resolveConvergePr`, and the `wait[pr]` target is late-bound
|
package/docs/agent-guide.md
CHANGED
|
@@ -73,7 +73,7 @@ The body is flat. Fields:
|
|
|
73
73
|
| field | type | meaning |
|
|
74
74
|
|---|---|---|
|
|
75
75
|
| `pr` (or `url`) | string | the PR — `owner/repo#123` or a full PR URL. Required. |
|
|
76
|
-
| `
|
|
76
|
+
| `autoMerge` | boolean | **Positive per-request setting.** `false` = review only, stopping at `converged`, regardless of the global default; `true` = converge **then merge** when `NANO_PR_AUTO_MERGE` is enabled. |
|
|
77
77
|
| `maxRounds` | integer | per-submit cap before escalating (clamped 1–100; default from `NANO_PR_MAX_ROUNDS`, 20). |
|
|
78
78
|
| `dependsOn` | string[] | other `prKey`s that must land before this one merges (merge-loop barrier). |
|
|
79
79
|
|
|
@@ -84,12 +84,12 @@ The body is flat. Fields:
|
|
|
84
84
|
# or is only after a review pass):
|
|
85
85
|
curl -sS -X POST __BASE__/actions/start/convergence-loop \
|
|
86
86
|
-H 'content-type: application/json' \
|
|
87
|
-
-d '{ "pr": "owner/repo#123", "
|
|
87
|
+
-d '{ "pr": "owner/repo#123", "autoMerge": false }'
|
|
88
88
|
|
|
89
89
|
# Converge then merge, with a dependency barrier and a tighter round cap:
|
|
90
90
|
curl -sS -X POST __BASE__/actions/start/convergence-loop \
|
|
91
91
|
-H 'content-type: application/json' \
|
|
92
|
-
-d '{ "pr": "owner/repo#42", "maxRounds": 8, "dependsOn": ["owner/repo#40"] }'
|
|
92
|
+
-d '{ "pr": "owner/repo#42", "autoMerge": true, "maxRounds": 8, "dependsOn": ["owner/repo#40"] }'
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
Submitting is **idempotent on the PR key** — re-POSTing the same PR refreshes the
|
|
@@ -311,7 +311,7 @@ the answer becomes the agent's next-round context.
|
|
|
311
311
|
```
|
|
312
312
|
submit ──► convergence-loop
|
|
313
313
|
round (senior:pr-review) ──► addressed ──► wait review-ready ─┐
|
|
314
|
-
├─ converged ──► finalize ──► merge-loop (
|
|
314
|
+
├─ converged ──► finalize ──► merge-loop (when autoMerge and NANO_PR_AUTO_MERGE are enabled)
|
|
315
315
|
└─ needs_input/blocked ──► escalate ──► wait-answer userTask (task inbox)
|
|
316
316
|
merge-loop: wait deps ─► arm merge ─► (queue-aware) merge / land
|
|
317
317
|
blocked (CI red) ─► senior:fix-ci ─► retry conflict ─► senior:rebase ─► retry
|
|
@@ -774,13 +774,13 @@ only job was "go run convergence yourself".
|
|
|
774
774
|
- **`converge-merge`** — the **unit-level** land: drive review convergence **and then the merge
|
|
775
775
|
loop**, landing the PR onto **its own base branch** (for a unit inside an epic that base is the
|
|
776
776
|
epic integration branch, never `main` directly — ADR 0003 base-branch admission). Equivalent to a
|
|
777
|
-
submit with `
|
|
777
|
+
submit with `autoMerge: true`.
|
|
778
778
|
- **`merge-main`** — the **graph-level** top-level land: the second level of the two-level merge
|
|
779
779
|
(ADR 0006 §3), landing the graph/epic **integration** PR onto **`main`**. Dispatch-identical to
|
|
780
780
|
`converge-merge` (both enroll + merge); the distinction is the *level*, kept a first-class literal
|
|
781
781
|
so the two levels are authored explicitly rather than left emergent.
|
|
782
782
|
- **`converge`** — **converge-only**: drive review convergence and stop at `converged`, never
|
|
783
|
-
handing off to the merge loop (equivalent to `
|
|
783
|
+
handing off to the merge loop (equivalent to `autoMerge: false`).
|
|
784
784
|
|
|
785
785
|
> **converge/merge are cell POLICY, not raw nodes.** A raw `senior:converge` / `senior:merge`
|
|
786
786
|
> **agent** job is **not expressible** — the compiler rejects it (`raw-converge-node`). Express
|
|
@@ -788,10 +788,10 @@ only job was "go run convergence yourself".
|
|
|
788
788
|
> policy flags (`merge` requires `converge`), or, for enrolling an already-open PR, the
|
|
789
789
|
> `connector` targets above (ADR 0006 §3 / S5).
|
|
790
790
|
|
|
791
|
-
**Payload:** `{ pr: "owner/repo#123",
|
|
791
|
+
**Payload:** `{ pr: "owner/repo#123", autoMerge?: boolean, dependsOn?: string[] }`. `pr` is
|
|
792
792
|
required (a literal `owner/repo#N`, identical to how a `wait: pr` node targets a known PR).
|
|
793
|
-
`
|
|
794
|
-
|
|
793
|
+
`autoMerge` defaults from the target and may be overridden per-node; `dependsOn` is unioned into
|
|
794
|
+
the PR's merge-stage dependency set. The enrollment is idempotent (the connector's
|
|
795
795
|
at-least-once dedupe fence **plus** `submitPr`'s own `prKey` idempotency), so a graph resume /
|
|
796
796
|
redelivery never double-enrolls.
|
|
797
797
|
|
package/openapi.yaml
CHANGED
|
@@ -1823,11 +1823,13 @@ components:
|
|
|
1823
1823
|
type: integer
|
|
1824
1824
|
minimum: 1
|
|
1825
1825
|
description: Values above 100 are accepted and clamped to 100 by the delegate.
|
|
1826
|
+
autoMerge:
|
|
1827
|
+
type: boolean
|
|
1828
|
+
description: Preferred per-request setting. When false, stop at `converged`; when true, run the merge-loop after convergence only if `NANO_PR_AUTO_MERGE` is enabled. When omitted, the global default applies.
|
|
1826
1829
|
convergeOnly:
|
|
1827
1830
|
type: boolean
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
per-request review-only override; defaults to false (the global auto-merge default applies).
|
|
1831
|
+
deprecated: true
|
|
1832
|
+
description: "Deprecated negative alias. If `autoMerge` is omitted, `true` means `autoMerge: false`; `false` means `autoMerge: true` (still gated by `NANO_PR_AUTO_MERGE`). Prefer `autoMerge`."
|
|
1831
1833
|
ConvergenceStartByUrl:
|
|
1832
1834
|
type: object
|
|
1833
1835
|
additionalProperties: false
|
|
@@ -1845,11 +1847,13 @@ components:
|
|
|
1845
1847
|
type: integer
|
|
1846
1848
|
minimum: 1
|
|
1847
1849
|
description: Values above 100 are accepted and clamped to 100 by the delegate.
|
|
1850
|
+
autoMerge:
|
|
1851
|
+
type: boolean
|
|
1852
|
+
description: Preferred per-request setting. When false, stop at `converged`; when true, run the merge-loop after convergence only if `NANO_PR_AUTO_MERGE` is enabled. When omitted, the global default applies.
|
|
1848
1853
|
convergeOnly:
|
|
1849
1854
|
type: boolean
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
per-request review-only override; defaults to false (the global auto-merge default applies).
|
|
1855
|
+
deprecated: true
|
|
1856
|
+
description: "Deprecated negative alias. If `autoMerge` is omitted, `true` means `autoMerge: false`; `false` means `autoMerge: true` (still gated by `NANO_PR_AUTO_MERGE`). Prefer `autoMerge`."
|
|
1853
1857
|
PlanStart:
|
|
1854
1858
|
description: The start-plan-fanout request body. Names the target issue by EXACTLY ONE of
|
|
1855
1859
|
`issue` (an `owner/repo#123` reference) or `url` (a bare issue URL), plus a REQUIRED
|
|
@@ -3435,8 +3439,8 @@ components:
|
|
|
3435
3439
|
autoMerge:
|
|
3436
3440
|
type: boolean
|
|
3437
3441
|
description: >-
|
|
3438
|
-
When true (and `converge` is true), drive the merge-loop after convergence
|
|
3439
|
-
`
|
|
3442
|
+
When true (and `converge` is true), drive the merge-loop after convergence only when
|
|
3443
|
+
`NANO_PR_AUTO_MERGE` is enabled. When false the run stops at `converged`.
|
|
3440
3444
|
Moot when `converge` is false.
|
|
3441
3445
|
confirmDefaultBase:
|
|
3442
3446
|
type: boolean
|
|
@@ -4573,9 +4577,13 @@ paths:
|
|
|
4573
4577
|
type: integer
|
|
4574
4578
|
minimum: 1
|
|
4575
4579
|
description: Values above 100 are accepted and clamped to 100 by the delegate.
|
|
4580
|
+
autoMerge:
|
|
4581
|
+
type: boolean
|
|
4582
|
+
description: Preferred per-request setting. When false, stop at `converged`; when true, run the merge-loop after convergence only if `NANO_PR_AUTO_MERGE` is enabled. When omitted, the global default applies.
|
|
4576
4583
|
convergeOnly:
|
|
4577
4584
|
type: boolean
|
|
4578
|
-
|
|
4585
|
+
deprecated: true
|
|
4586
|
+
description: 'Deprecated negative alias. If `autoMerge` is omitted, `true` means `autoMerge: false`; `false` means `autoMerge: true` (still gated by `NANO_PR_AUTO_MERGE`). Prefer `autoMerge`.'
|
|
4579
4587
|
- type: object
|
|
4580
4588
|
additionalProperties: false
|
|
4581
4589
|
required:
|
|
@@ -4592,9 +4600,13 @@ paths:
|
|
|
4592
4600
|
type: integer
|
|
4593
4601
|
minimum: 1
|
|
4594
4602
|
description: Values above 100 are accepted and clamped to 100 by the delegate.
|
|
4603
|
+
autoMerge:
|
|
4604
|
+
type: boolean
|
|
4605
|
+
description: Preferred per-request setting. When false, stop at `converged`; when true, run the merge-loop after convergence only if `NANO_PR_AUTO_MERGE` is enabled. When omitted, the global default applies.
|
|
4595
4606
|
convergeOnly:
|
|
4596
4607
|
type: boolean
|
|
4597
|
-
|
|
4608
|
+
deprecated: true
|
|
4609
|
+
description: 'Deprecated negative alias. If `autoMerge` is omitted, `true` means `autoMerge: false`; `false` means `autoMerge: true` (still gated by `NANO_PR_AUTO_MERGE`). Prefer `autoMerge`.'
|
|
4598
4610
|
type: object
|
|
4599
4611
|
# END generated:mcp-body
|
|
4600
4612
|
responses:
|
|
@@ -5809,9 +5821,9 @@ paths:
|
|
|
5809
5821
|
type: string
|
|
5810
5822
|
enum: [real, forward-declared]
|
|
5811
5823
|
description: real ⇒ dispatches a real side effect; forward-declared ⇒ a no-op stub.
|
|
5812
|
-
|
|
5824
|
+
autoMergeDefault:
|
|
5813
5825
|
type: boolean
|
|
5814
|
-
description: The default `
|
|
5826
|
+
description: The default `autoMerge` for a real converge target.
|
|
5815
5827
|
summary:
|
|
5816
5828
|
type: string
|
|
5817
5829
|
description: What the target does.
|
|
@@ -5872,7 +5884,7 @@ paths:
|
|
|
5872
5884
|
optionalFields: [dedupeKey, payload]
|
|
5873
5885
|
sideEffecting: true
|
|
5874
5886
|
mayEmit: true
|
|
5875
|
-
summary: "An automated outbound action; payload for a converge target is { pr,
|
|
5887
|
+
summary: "An automated outbound action; payload for a converge target is { pr, autoMerge?, dependsOn? }."
|
|
5876
5888
|
factTypes: [string, number, boolean, artifact, version, url, pr]
|
|
5877
5889
|
guardScalarTypes: [string, number, boolean]
|
|
5878
5890
|
waitProbeKinds:
|
|
@@ -5893,15 +5905,15 @@ paths:
|
|
|
5893
5905
|
connectorTargets:
|
|
5894
5906
|
- target: converge
|
|
5895
5907
|
status: real
|
|
5896
|
-
|
|
5908
|
+
autoMergeDefault: false
|
|
5897
5909
|
summary: "Converge-only: drive review convergence and stop at converged."
|
|
5898
5910
|
- target: converge-merge
|
|
5899
5911
|
status: real
|
|
5900
|
-
|
|
5912
|
+
autoMergeDefault: true
|
|
5901
5913
|
summary: "Unit-level land: converge AND merge onto the PR's own base branch."
|
|
5902
5914
|
- target: merge-main
|
|
5903
5915
|
status: real
|
|
5904
|
-
|
|
5916
|
+
autoMergeDefault: true
|
|
5905
5917
|
summary: "Graph-level top-level land onto main (two-level merge)."
|
|
5906
5918
|
- target: "<any other target>"
|
|
5907
5919
|
status: forward-declared
|
|
@@ -6168,7 +6180,7 @@ paths:
|
|
|
6168
6180
|
description: When true, hand the opened PR to the convergence loop (review rounds) as a follow-on step. When false (default) the run ends at "PR raised, not reviewed".
|
|
6169
6181
|
autoMerge:
|
|
6170
6182
|
type: boolean
|
|
6171
|
-
description: When true (and `converge` is true), drive the merge-loop after convergence
|
|
6183
|
+
description: When true (and `converge` is true), drive the merge-loop after convergence only when `NANO_PR_AUTO_MERGE` is enabled. When false the run stops at `converged`. Moot when `converge` is false.
|
|
6172
6184
|
confirmDefaultBase:
|
|
6173
6185
|
type: boolean
|
|
6174
6186
|
description: Acknowledge that `baseBranch` is the repository default branch (the PR would target it directly). See `PlanStartByIssue.confirmDefaultBase`.
|
|
@@ -40,7 +40,8 @@ test("the guide covers every capability the endpoint promises", async () => {
|
|
|
40
40
|
const md = ((await handler(input(), app)) as any).body.instructions as string;
|
|
41
41
|
// Submit a PR (converge vs. merge), submit an epic, answer escalations…
|
|
42
42
|
assert(md.includes("start/convergence-loop"), "covers submitting a PR for convergence");
|
|
43
|
-
assert(md.includes("
|
|
43
|
+
assert(md.includes("autoMerge"), "documents the per-request merge control");
|
|
44
|
+
assert(!md.includes("convergeOnly"), "does not expose the legacy merge control in agent guidance");
|
|
44
45
|
assert(md.includes("start/plan-fanout"), "covers submitting an epic");
|
|
45
46
|
assert(md.includes("complete-user-task"), "covers answering escalations via the native user-task door");
|
|
46
47
|
// …debug the system.
|
|
@@ -143,15 +143,57 @@ test("startConvergenceLoop forwards convergeOnly:true to the loop", async () =>
|
|
|
143
143
|
});
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
test("startConvergenceLoop maps autoMerge:true to merge-enabled convergence", async () => {
|
|
147
|
+
await withGithubOff(async () => {
|
|
148
|
+
const { app: capApp, get } = captureApp();
|
|
149
|
+
const res = await startConvergenceLoop(input({ pr: "owner/repo#9", autoMerge: true }), capApp);
|
|
150
|
+
assertEquals((res as any).status, 202);
|
|
151
|
+
assertEquals(get(), false);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("startConvergenceLoop maps autoMerge:false to review-only convergence", async () => {
|
|
156
|
+
await withGithubOff(async () => {
|
|
157
|
+
const { app: capApp, get } = captureApp();
|
|
158
|
+
const res = await startConvergenceLoop(input({ pr: "owner/repo#10", autoMerge: false }), capApp);
|
|
159
|
+
assertEquals((res as any).status, 202);
|
|
160
|
+
assertEquals(get(), true);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("startConvergenceLoop lets autoMerge override the legacy convergeOnly alias", async () => {
|
|
165
|
+
await withGithubOff(async () => {
|
|
166
|
+
const merge = captureApp();
|
|
167
|
+
const mergeRes = await startConvergenceLoop(
|
|
168
|
+
input({ pr: "owner/repo#13", autoMerge: true, convergeOnly: true }),
|
|
169
|
+
merge.app,
|
|
170
|
+
);
|
|
171
|
+
assertEquals((mergeRes as any).status, 202);
|
|
172
|
+
assertEquals(merge.get(), false);
|
|
173
|
+
|
|
174
|
+
const review = captureApp();
|
|
175
|
+
const reviewRes = await startConvergenceLoop(
|
|
176
|
+
input({ pr: "owner/repo#14", autoMerge: false, convergeOnly: false }),
|
|
177
|
+
review.app,
|
|
178
|
+
);
|
|
179
|
+
assertEquals((reviewRes as any).status, 202);
|
|
180
|
+
assertEquals(review.get(), true);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
146
184
|
test("startConvergenceLoop defaults convergeOnly to false and does not truthy-coerce a non-boolean", async () => {
|
|
147
185
|
await withGithubOff(async () => {
|
|
148
186
|
const omitted = captureApp();
|
|
149
|
-
await startConvergenceLoop(input({ pr: "owner/repo#
|
|
187
|
+
await startConvergenceLoop(input({ pr: "owner/repo#11" }), omitted.app);
|
|
150
188
|
assertEquals(omitted.get(), false);
|
|
151
189
|
|
|
152
190
|
const stringy = captureApp();
|
|
153
|
-
await startConvergenceLoop(input({ pr: "owner/repo#
|
|
191
|
+
await startConvergenceLoop(input({ pr: "owner/repo#12", convergeOnly: "true" }), stringy.app);
|
|
154
192
|
assertEquals(stringy.get(), false);
|
|
193
|
+
|
|
194
|
+
const stringyAutoMerge = captureApp();
|
|
195
|
+
await startConvergenceLoop(input({ pr: "owner/repo#15", autoMerge: "true" }), stringyAutoMerge.app);
|
|
196
|
+
assertEquals(stringyAutoMerge.get(), false);
|
|
155
197
|
});
|
|
156
198
|
});
|
|
157
199
|
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
// external webhook relay, a CI job, and Swagger all POST here. Parse the PR reference and
|
|
4
4
|
// register/refresh the PR aggregate (idempotent on prKey) before starting the loop.
|
|
5
5
|
//
|
|
6
|
-
// The request body is FLAT (`{ pr | url, dependsOn?, maxRounds?, convergeOnly? }`), not
|
|
7
|
-
// `variables` envelope: this is a purpose-built operation, not a generic engine "start
|
|
8
|
-
// so it does not leak the engine's variable-map concept to callers.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
6
|
+
// The request body is FLAT (`{ pr | url, dependsOn?, maxRounds?, autoMerge?, convergeOnly? }`), not
|
|
7
|
+
// wrapped in a `variables` envelope: this is a purpose-built operation, not a generic engine "start
|
|
8
|
+
// process" call, so it does not leak the engine's variable-map concept to callers. `autoMerge` is
|
|
9
|
+
// the positive, preferred form for UI/API callers; `convergeOnly` remains a legacy negative alias.
|
|
10
|
+
// When both are present, `autoMerge` wins. The body is a `oneOf` — EXACTLY ONE of `pr` or `url` — so
|
|
11
|
+
// the runtime rejects an empty or ambiguous target at the edge (a 400 that names the allowed
|
|
12
|
+
// shapes); this delegate no longer coalesces `pr ?? url`, it just narrows the validated variant. It
|
|
13
|
+
// keeps the PR-parse guard because the reference FORMAT (owner/repo#123 or a URL) is app logic the
|
|
14
|
+
// JSON schema can't express — an unparseable reference is a 400.
|
|
13
15
|
|
|
14
16
|
import { clampRounds, MAX_ROUNDS, parsePr, submitPr } from "../app/service.ts";
|
|
15
17
|
import { defineOperation } from "../nano-generated/operations.ts";
|
|
@@ -29,9 +31,10 @@ export default defineOperation("startConvergenceLoop", async ({ body }, app) =>
|
|
|
29
31
|
}
|
|
30
32
|
const dependsOn = body.dependsOn ?? [];
|
|
31
33
|
const maxRounds = clampRounds(body.maxRounds, MAX_ROUNDS);
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
const
|
|
34
|
+
// Prefer the positive autoMerge form so an unchecked UI box is explicitly review-only. Legacy
|
|
35
|
+
// callers that omit autoMerge retain the existing convergeOnly/global-default behavior.
|
|
36
|
+
const hasAutoMerge = typeof body.autoMerge === "boolean";
|
|
37
|
+
const convergeOnly = hasAutoMerge ? body.autoMerge !== true : body.convergeOnly === true;
|
|
35
38
|
const result = await submitPr(app.data, app.engine, parsed, dependsOn, maxRounds, convergeOnly);
|
|
36
39
|
app.log.info("convergence loop started", {
|
|
37
40
|
prKey: parsed.prKey,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.190.0",
|
|
4
4
|
"description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "main.ts",
|
package/pages/home.page.json
CHANGED
|
@@ -105,8 +105,8 @@
|
|
|
105
105
|
"type": "number"
|
|
106
106
|
},
|
|
107
107
|
{
|
|
108
|
-
"key": "
|
|
109
|
-
"label": "
|
|
108
|
+
"key": "autoMerge",
|
|
109
|
+
"label": "Auto-merge \u2014 after convergence, drive the merge-loop",
|
|
110
110
|
"type": "checkbox"
|
|
111
111
|
}
|
|
112
112
|
]
|
|
@@ -149,6 +149,14 @@ test("readConvergeInput: an explicit payload.convergeOnly overrides the target d
|
|
|
149
149
|
assertEquals(r.dependsOn, ["owner/repo#5"], "non-string dependsOn entries are dropped");
|
|
150
150
|
});
|
|
151
151
|
|
|
152
|
+
test("readConvergeInput: autoMerge is the positive per-dispatch control", () => {
|
|
153
|
+
const merge = readConvergeInput("converge", { pr: "owner/repo#7", autoMerge: true }, null);
|
|
154
|
+
assertEquals(merge.convergeOnly, false);
|
|
155
|
+
|
|
156
|
+
const review = readConvergeInput("converge-merge", { pr: "owner/repo#7", autoMerge: false }, null);
|
|
157
|
+
assertEquals(review.convergeOnly, true);
|
|
158
|
+
});
|
|
159
|
+
|
|
152
160
|
test("readConvergeInput: a missing / unparseable pr fails CLOSED (a converge connector with no target PR is meaningless)", () => {
|
|
153
161
|
assertThrows(() => readConvergeInput("converge-merge", null, null), Error, "payload.pr");
|
|
154
162
|
assertThrows(() => readConvergeInput("converge-merge", {}, null), Error, "payload.pr");
|
|
@@ -76,13 +76,14 @@ export function safeStringify(value: unknown): string {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/** Parse + validate the converge connector's payload (`{ pr,
|
|
79
|
+
/** Parse + validate the converge connector's payload (`{ pr, autoMerge?, dependsOn? }`) for a
|
|
80
80
|
* `converge` / `converge-merge` / `merge-main` target. `pr` is REQUIRED and must parse to a canonical `owner/repo#N`
|
|
81
81
|
* (fail CLOSED — a converge connector with no target PR is meaningless and could never enroll).
|
|
82
|
-
* `
|
|
83
|
-
*
|
|
84
|
-
* optional list of PR refs unioned into the enrolled PR's
|
|
85
|
-
* entries are dropped; `submitPr` itself ignores unparseable
|
|
82
|
+
* `autoMerge` DEFAULTS from the target (`converge` → `false`; `converge-merge`/`merge-main` → `true`) and
|
|
83
|
+
* may be overridden per-dispatch by an explicit boolean. The deprecated `convergeOnly` alias is still
|
|
84
|
+
* accepted for compatibility. `dependsOn` is an optional list of PR refs unioned into the enrolled PR's
|
|
85
|
+
* merge-stage dependency set (only non-string entries are dropped; `submitPr` itself ignores unparseable
|
|
86
|
+
* refs). Exported for unit coverage.
|
|
86
87
|
*
|
|
87
88
|
* `pr` may be sourced three ways (issue #548), resolved by {@link resolveConvergePr} against the
|
|
88
89
|
* threaded `boundFacts` BEFORE parsing: a LITERAL `owner/repo#N`, an explicit fact REFERENCE
|
|
@@ -104,7 +105,13 @@ export function readConvergeInput(
|
|
|
104
105
|
`bound from an upstream \`agent\` node (got ${safeStringify(prValue ?? null)})`,
|
|
105
106
|
);
|
|
106
107
|
}
|
|
107
|
-
const
|
|
108
|
+
const autoMerge =
|
|
109
|
+
typeof p.autoMerge === "boolean"
|
|
110
|
+
? p.autoMerge
|
|
111
|
+
: typeof p.convergeOnly === "boolean"
|
|
112
|
+
? !p.convergeOnly
|
|
113
|
+
: !convergeOnlyForTarget(target);
|
|
114
|
+
const convergeOnly = !autoMerge;
|
|
108
115
|
const dependsOn = Array.isArray(p.dependsOn) ? p.dependsOn.filter((d): d is string => typeof d === "string") : [];
|
|
109
116
|
return { parsed, convergeOnly, dependsOn };
|
|
110
117
|
}
|