@nanobpm/nano-workforce 0.189.3 → 0.191.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/README.md +5 -4
- package/SPEC.md +5 -5
- package/app/agentGuide.ts +1 -1
- package/app/deliveryGraph.test.ts +447 -0
- package/app/deliveryGraph.ts +355 -0
- package/app/deliveryGraphCompiler.test.ts +1465 -3
- package/app/deliveryGraphCompiler.ts +833 -36
- package/app/deliveryGraphDispatch.test.ts +118 -2
- package/app/deliveryGraphDispatch.ts +138 -10
- package/app/deliveryGraphProposals.test.ts +243 -0
- package/app/deliveryGraphProposals.ts +130 -56
- package/app/deliveryGraphRun.test.ts +42 -2
- package/app/deliveryGraphRun.ts +46 -6
- package/app/deliveryGraphVocabulary.ts +6 -6
- package/app/deliveryRunner.test.ts +21 -0
- package/app/deliveryRunner.ts +9 -2
- package/app/readiness.test.ts +72 -0
- package/app/readiness.ts +234 -8
- package/db/migrations/113_delivery_graph_run_identity.sql +37 -0
- package/db/migrations/114_delivery_graph_proposals_stage_seq.sql +21 -0
- 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/dispatchDeliveryGraph.test.ts +255 -0
- package/operations/dispatchDeliveryGraph.ts +156 -5
- 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/app/readiness.ts
CHANGED
|
@@ -63,6 +63,11 @@ export type Backoff = "fixed" | "exponential";
|
|
|
63
63
|
// is added here without a matching vocabulary entry (AGENTS.md: no drift surfaces).
|
|
64
64
|
export const PROBE_KINDS: readonly ProbeKind[] = ["http", "command", "npm", "github-check", "capability", "pr", "epic"];
|
|
65
65
|
export const ON_TIMEOUTS: readonly OnTimeout[] = ["escalate", "fail", "continue"];
|
|
66
|
+
// The DEFAULT `onTimeout` an omitted value takes — the single source of truth `parseProbe` fills in and
|
|
67
|
+
// the compiler's display drops as an effective default (writing it explicitly is behaviourally identical
|
|
68
|
+
// to omitting it, so surfacing it would fork `semanticBpmn`/the digest from the omitted-equivalent
|
|
69
|
+
// graph; issue #778 review — thread deliveryGraphCompiler.ts:1398).
|
|
70
|
+
export const DEFAULT_ON_TIMEOUT: OnTimeout = "escalate";
|
|
66
71
|
export const BACKOFFS: readonly Backoff[] = ["fixed", "exponential"];
|
|
67
72
|
export const PR_CONDITIONS: readonly PrCondition[] = ["ready", "merged", "mergeable", "checks-green"];
|
|
68
73
|
export const EPIC_CONDITIONS: readonly EpicCondition[] = ["merged", "done"];
|
|
@@ -237,7 +242,7 @@ export function parseProbe(raw: unknown, opts?: { allowLateBoundTarget?: boolean
|
|
|
237
242
|
if (onTimeoutRaw !== "" && !isOnTimeout(onTimeoutRaw)) {
|
|
238
243
|
throw new Error(`readiness probe: invalid onTimeout '${onTimeoutRaw}' (expected ${ON_TIMEOUTS.join(", ")})`);
|
|
239
244
|
}
|
|
240
|
-
const onTimeout: OnTimeout = onTimeoutRaw === "" ?
|
|
245
|
+
const onTimeout: OnTimeout = onTimeoutRaw === "" ? DEFAULT_ON_TIMEOUT : onTimeoutRaw;
|
|
241
246
|
|
|
242
247
|
const match = isRecord(raw.match) ? parseMatch(raw.match) : undefined;
|
|
243
248
|
// A capability edge whose ref or package is blank can never resolve — fail loudly here rather than
|
|
@@ -307,7 +312,7 @@ export function parseProbe(raw: unknown, opts?: { allowLateBoundTarget?: boolean
|
|
|
307
312
|
|
|
308
313
|
// Membership guards that narrow a validated string to its union without a type assertion (the
|
|
309
314
|
// `no-unsafe-type-assertion` gate bans `as`).
|
|
310
|
-
function isProbeKind(v: string): v is ProbeKind {
|
|
315
|
+
export function isProbeKind(v: string): v is ProbeKind {
|
|
311
316
|
for (const k of PROBE_KINDS) if (k === v) return true;
|
|
312
317
|
return false;
|
|
313
318
|
}
|
|
@@ -318,7 +323,7 @@ function isOnTimeout(v: string): v is OnTimeout {
|
|
|
318
323
|
|
|
319
324
|
// `isBackoff` narrows a validated string to its union without a type assertion (the
|
|
320
325
|
// `no-unsafe-type-assertion` gate bans `as`).
|
|
321
|
-
function isBackoff(v: string): v is Backoff {
|
|
326
|
+
export function isBackoff(v: string): v is Backoff {
|
|
322
327
|
for (const b of BACKOFFS) if (b === v) return true;
|
|
323
328
|
return false;
|
|
324
329
|
}
|
|
@@ -414,7 +419,17 @@ export function normalizePoll(poll: ProbePoll | undefined): Required<ProbePoll>
|
|
|
414
419
|
const everyMs = typeof everyRaw === "number" && everyRaw >= 1 ? Math.trunc(everyRaw) : DEFAULT_EVERY_MS;
|
|
415
420
|
const timeoutMs =
|
|
416
421
|
typeof timeoutRaw === "number" && timeoutRaw >= 1 ? Math.trunc(timeoutRaw) : DEFAULT_TIMEOUT_MS;
|
|
417
|
-
|
|
422
|
+
// TRIM `backoff` to match `parseProbe` (`parsePoll` does `str(raw.backoff).trim()`): a padded
|
|
423
|
+
// `" exponential "` runs as the SAME `exponential` policy, so leaving the whitespace here would make
|
|
424
|
+
// the compiler's display path (`normalizePoll(p.poll)`) emit a spurious non-default `Poll:` line for a
|
|
425
|
+
// runtime-default poll — forking `semanticBpmn`/the digest from the omitted-equivalent graph and
|
|
426
|
+
// letting a re-stage bypass the idempotency fence to duplicate the run (issue #778 review — thread
|
|
427
|
+
// deliveryGraphCompiler.ts:1414). A whitespace-only / unrecognised value falls back to the default
|
|
428
|
+
// (the compiler display path takes raw JSON; a genuinely invalid backoff fails loudly later in
|
|
429
|
+
// `parseProbe`, so defaulting the DISPLAY is safe and keeps the effective-policy render honest).
|
|
430
|
+
const backoffTrimmed = typeof poll?.backoff === "string" ? poll.backoff.trim() : undefined;
|
|
431
|
+
const backoff = backoffTrimmed && isBackoff(backoffTrimmed) ? backoffTrimmed : DEFAULT_BACKOFF;
|
|
432
|
+
return { everyMs: Math.min(everyMs, MAX_EVERY_MS), timeoutMs, backoff };
|
|
418
433
|
}
|
|
419
434
|
|
|
420
435
|
/** The delay (ms) before the `attempt`-th retry (1-based). Fixed backoff returns `everyMs`;
|
|
@@ -1157,10 +1172,221 @@ export function redactTarget(probe: ReadinessProbe): string {
|
|
|
1157
1172
|
return `${probe.kind}:${redactString(probe.target)}`;
|
|
1158
1173
|
}
|
|
1159
1174
|
|
|
1175
|
+
/** The ONE canonical embedded-`//<userinfo>@` credential-userinfo span, shared by the redactor
|
|
1176
|
+
* ({@link redactString}, which REWRITES the span to `//***@`) and the semantic reject path
|
|
1177
|
+
* ({@link hasEmbeddedCredential}, used by `validateDeliveryGraph` to REJECT a credential-bearing
|
|
1178
|
+
* `agent.jobType`). Keeping ONE source string means the "what counts as an embedded credential" rule
|
|
1179
|
+
* can never drift between the two — the redactor must strip exactly what the validator rejects, or a
|
|
1180
|
+
* credential the validator misses would be echoed un-redacted. The userinfo class is `[^/?#]` (NOT
|
|
1181
|
+
* `[^/@\s]`, and NOT the earlier `[^/@]`/`[^/]`): it deliberately spans WHITESPACE up to the `@`, so a
|
|
1182
|
+
* malformed-but-operator-authored `//user:secret pass@host` (a literal space in the userinfo) is
|
|
1183
|
+
* caught, not left to leak verbatim into the executable `<zeebe:taskDefinition type=…>` / compiled BPMN
|
|
1184
|
+
* (issue #778 review — thread deliveryGraph.ts:570). The userinfo colon is DELIBERATELY OPTIONAL
|
|
1185
|
+
* (`[^/?#]*@`, not `[^/?#]*:[^/?#]*@`):
|
|
1186
|
+
* a passwordless, username-only `//token@host` is a bearer/OAuth token riding the userinfo and MUST be
|
|
1187
|
+
* caught too — a plain worker-routing job type never contains a `//…@` span at all (with or without a
|
|
1188
|
+
* colon), so requiring a colon would only re-open a real leak for no legitimate gain (issue #778 review
|
|
1189
|
+
* — thread readiness.ts:1170). `[^/?#]*@` is a single-quantifier match — linear, no catastrophic
|
|
1190
|
+
* backtracking.
|
|
1191
|
+
*
|
|
1192
|
+
* The userinfo class is `[^/?#]` (NOT `[^/@]`): per RFC 3986 the userinfo runs to the LAST `@` before the
|
|
1193
|
+
* authority's path, so a malformed-but-accepted multi-`@` value (`//user:pass@ss@host`) must collapse
|
|
1194
|
+
* through EVERY `@` up to the path — an earlier `[^/@]*@` stopped at the FIRST `@`, rewriting only
|
|
1195
|
+
* `//user:pass@` and leaking the `ss@host` suffix into the display artifact (issue #778 review — thread
|
|
1196
|
+
* readiness.ts:1175).
|
|
1197
|
+
*
|
|
1198
|
+
* The class also EXCLUDES `?` and `#` (NOT the earlier `[^/]`): per RFC 3986 a raw `?`/`#` starts the
|
|
1199
|
+
* query/fragment and so ENDS the authority — a `//host?token=secret@tail` carries its `@` INSIDE the
|
|
1200
|
+
* query, with no userinfo at all. An unbounded `[^/]*@` greedily consumed the `?` marker
|
|
1201
|
+
* (`//host?token=secret@` → `//***@`), so the follow-on `?query`/`#fragment` strip in {@link redactString}
|
|
1202
|
+
* found no marker and left `tail` visible. Bounding the userinfo before `?`/`#` keeps the marker in
|
|
1203
|
+
* place so the whole query/fragment (incl. any `@tail`) is redacted, while a genuine `//user:pass@host?…`
|
|
1204
|
+
* still collapses its userinfo AND has its query stripped (issue #778 review — thread readiness.ts:1199).
|
|
1205
|
+
* `[^/?#]` still excludes `/` (so it never crosses into the path), and the trailing `@` is anchored
|
|
1206
|
+
* greedily to the last one before a `/`/`?`/`#`; `[^/?#]*@` remains a single-quantifier match, so it is
|
|
1207
|
+
* linear with no catastrophic backtracking. */
|
|
1208
|
+
export const EMBEDDED_CREDENTIAL_SRC = "\\/\\/[^/?#]*@";
|
|
1209
|
+
|
|
1160
1210
|
/** Strip credential-bearing pieces from a free-form target string for logging: any `user:pass@`
|
|
1161
|
-
* userinfo and any `?query`/`#fragment` (a token often rides the query).
|
|
1211
|
+
* userinfo and any `?query`/`#fragment` (a token often rides the query). The query/fragment strip uses
|
|
1212
|
+
* `[\s\S]*` (NOT `.*$`, which cannot cross a line break) so an embedded CR/LF after the `?`/`#` — e.g.
|
|
1213
|
+
* `https://host/?token=secret\nnext` — cannot leave the token un-redacted; everything from the first
|
|
1214
|
+
* `?`/`#` to end-of-string is consumed regardless of intervening newlines. The userinfo class is
|
|
1215
|
+
* `[^/?#]` (NOT `[^/@\s]`, NOT the earlier `[^/@ ]`/`[^/@ \t]`/`[^/@]`, and NOT the later `[^/]`): ANY character smuggled
|
|
1216
|
+
* INSIDE the
|
|
1217
|
+
* userinfo up to the `@` — a raw CR/LF (`https://user:pa\nss@host`), an embedded TAB
|
|
1218
|
+
* (`https://user:pa\tss@host`), a literal SPACE (`https://user:secret pass@host`, a malformed but
|
|
1219
|
+
* operator-authored value), OR an extra raw `@` (`https://user:pass@ss@host`, whose userinfo per RFC
|
|
1220
|
+
* 3986 runs to the LAST `@`) — must not break the `//…@` match and leave the credential tail visible.
|
|
1221
|
+
* An earlier `[^/@ ]` bounded the userinfo at a SPACE to avoid over-matching prose, and `[^/@]` stopped
|
|
1222
|
+
* at the FIRST `@` (leaking a multi-`@` suffix), but both let a credential escape into an
|
|
1223
|
+
* operator-visible display artifact; since the RAW value
|
|
1224
|
+
* still reaches runtime unmodified and only the DISPLAY doc is affected, redacting more (through the
|
|
1225
|
+
* last `@` before the path) is the safe direction (issue #778 review). The `?`/`#` exclusion is the
|
|
1226
|
+
* INVERSE correction: a `[^/]` userinfo crossed a raw query/fragment delimiter — a `?token=…@tail` query
|
|
1227
|
+
* (no userinfo; the `@` rides the query) collapsed to `//***@tail`, consuming the `?` marker so the
|
|
1228
|
+
* follow-on `?query`/`#fragment` strip below found nothing and LEAKED `tail`. Bounding the userinfo at
|
|
1229
|
+
* `?`/`#` keeps the marker so the query strip redacts the whole tail (issue #778 review — thread
|
|
1230
|
+
* readiness.ts:1199). `[^/?#]*@` remains a
|
|
1231
|
+
* single-quantifier match, so it is
|
|
1232
|
+
* linear with no catastrophic backtracking. Callers that surface the result in a display artifact must
|
|
1233
|
+
* first pass it through `stripXmlInvalidChars` so an XML-forbidden control (e.g. U+000B) inside the
|
|
1234
|
+
* userinfo cannot split the `//…@` match and be re-joined at render. */
|
|
1162
1235
|
export function redactString(s: string): string {
|
|
1163
|
-
return s
|
|
1164
|
-
|
|
1165
|
-
|
|
1236
|
+
return redactEmbeddedCredential(s).replace(/[?#][\s\S]*$/, (m) => `${m[0]}***`);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/** Strip ONLY the embedded `//<userinfo>@` credential span(s) from a string, collapsing each to
|
|
1240
|
+
* `//***@` and leaving everything else (including any `?query`/`#fragment`, which may be a MEANINGFUL
|
|
1241
|
+
* opaque-token character rather than URL syntax) untouched. Shares the ONE canonical
|
|
1242
|
+
* {@link EMBEDDED_CREDENTIAL_SRC} span so "what counts as an embedded credential" can never drift from
|
|
1243
|
+
* the redactor ({@link redactString}) or the validator ({@link hasEmbeddedCredential}). A `//<userinfo>@`
|
|
1244
|
+
* span is UNAMBIGUOUSLY a credential wherever it sits — a plain opaque identifier
|
|
1245
|
+
* (`slack:#releases`, `owner/repo#42`, `pkg@version`) never contains one — so it is safe to strip
|
|
1246
|
+
* regardless of the value's URL-shape or the span's position, which is why {@link redactConnectorValue}
|
|
1247
|
+
* runs it on a NON-URL-shaped free-form value (a probe target / PR ref) whose credential rides AFTER a
|
|
1248
|
+
* prefix (`prefix //user:pass@host#42`) that the anchored {@link isUrlShaped} check would miss (issue
|
|
1249
|
+
* #778 review — thread deliveryGraph.ts:162/566). `[^/]*@` is a single-quantifier match — linear, no
|
|
1250
|
+
* catastrophic backtracking. */
|
|
1251
|
+
export function redactEmbeddedCredential(s: string): string {
|
|
1252
|
+
return s.replace(new RegExp(EMBEDDED_CREDENTIAL_SRC, "g"), "//***@");
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/** The ONE canonical embedded SCHEME-RELATIVE credential-URL token: a `//<userinfo>@authority…` run
|
|
1256
|
+
* (bounded by the next literal space) whose `//` carries userinfo. The `@` UNAMBIGUOUSLY marks the run a
|
|
1257
|
+
* URL — an opaque ref (`//host#42`, `slack:#releases`, `owner/repo#42`) never carries `//user@` — so its
|
|
1258
|
+
* `?query`/`#fragment` IS URL syntax and may hide a token, exactly like an explicit-scheme
|
|
1259
|
+
* {@link EMBEDDED_SCHEME_URL_SRC}. Distinct from the userinfo-ONLY {@link EMBEDDED_CREDENTIAL_SRC}, which
|
|
1260
|
+
* strips just the `//user:pass@` span and (correctly, for an opaque `//host#42`) LEAVES the tail: a
|
|
1261
|
+
* userinfo-bearing `//user:pass@host?token=secret` embedded after a non-URL prefix is scheme-relative,
|
|
1262
|
+
* so the anchored whole-value {@link isUrlShaped} check misses it (non-URL prefix) and the userinfo-only
|
|
1263
|
+
* strip leaks the `?token=secret` tail (issue #778 review — thread deliveryGraph.ts:188). Requiring the
|
|
1264
|
+
* `@` keeps genuine opaque `//host#42` refs (no userinfo) untouched. The token runs to the next literal
|
|
1265
|
+
* SPACE (`[^ ]*`), NOT the wider `\s` class, so it CROSSES an XML-valid internal TAB/LF/CR and still
|
|
1266
|
+
* redacts a `?query` secret sitting past it. The userinfo class is `[^/?#]` (NOT the earlier `[^/]`):
|
|
1267
|
+
* bounding it before a raw `?`/`#` stops the userinfo span crossing the query/fragment delimiter — a
|
|
1268
|
+
* `//host?token=secret@tail` (no userinfo; the `@` rides the query) no longer collapses to `//***@tail`
|
|
1269
|
+
* (leaking `tail`); it instead falls through to {@link redactEmbeddedSchemeRelativeUrl}, which strips the
|
|
1270
|
+
* whole `?query` (issue #778 review — thread readiness.ts:1199, mirroring the {@link EMBEDDED_CREDENTIAL_SRC}
|
|
1271
|
+
* fix). `[^/?#]*@` and `[^ ]*` are separated by the literal `@`
|
|
1272
|
+
* anchor, so the match stays linear with no catastrophic backtracking. */
|
|
1273
|
+
const EMBEDDED_CREDENTIAL_URL_SRC = "\\/\\/[^/?#]*@[^ ]*";
|
|
1274
|
+
|
|
1275
|
+
/** Redact each embedded SCHEME-RELATIVE credential-URL (`//<userinfo>@authority…`) token IN PLACE via
|
|
1276
|
+
* {@link redactString} — stripping its `//user:pass@` userinfo AND any `?query`/`#fragment` — while
|
|
1277
|
+
* leaving opaque `//host#42` refs (no userinfo) untouched. Lets {@link redactConnectorValue} catch a
|
|
1278
|
+
* query/fragment secret riding a credential-bearing scheme-relative URL after a non-URL prefix
|
|
1279
|
+
* (`prefix //user:pass@host?token=secret`) that the anchored {@link isUrlShaped} whole-value check AND
|
|
1280
|
+
* the userinfo-only {@link redactEmbeddedCredential} both miss (issue #778 review — thread
|
|
1281
|
+
* deliveryGraph.ts:188). Deterministic and total. */
|
|
1282
|
+
export function redactEmbeddedCredentialUrl(s: string): string {
|
|
1283
|
+
return s.replace(new RegExp(EMBEDDED_CREDENTIAL_URL_SRC, "g"), (m) => redactString(m));
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/** The ONE canonical embedded ABSOLUTE-URL token: an EXPLICIT-scheme `scheme://authority…` run bounded
|
|
1287
|
+
* by whitespace. Deliberately requires an explicit `scheme:` before the `//` (NOT the scheme-relative
|
|
1288
|
+
* `//authority` {@link isUrlShaped} also accepts): an absolute URL's `?query`/`#fragment` are
|
|
1289
|
+
* UNAMBIGUOUSLY URL syntax and may hide a token, so they are safe to strip; a scheme-relative
|
|
1290
|
+
* `//host#42` (or an opaque `slack:#releases`, `owner/repo#42`) instead carries a MEANINGFUL opaque
|
|
1291
|
+
* `#`/`?` (a `parsePrTarget` PR ref) that must survive, so it is left to the userinfo-only
|
|
1292
|
+
* {@link redactEmbeddedCredential}. The scheme may be followed by XML-attribute whitespace
|
|
1293
|
+
* (`\s*` — TAB/LF/CR/space) BEFORE the `//` authority, exactly like {@link isUrlShaped}: those are valid
|
|
1294
|
+
* XML `Char`s that `stripXmlInvalidChars` does NOT remove, so an embedded `scheme<whitespace>//authority`
|
|
1295
|
+
* after a non-URL prefix (`prefix https:\t//user:pass@host?token=secret`) is not whole-value URL-shaped,
|
|
1296
|
+
* escapes the anchored {@link isUrlShaped} check, and — without this `\s*` — was missed here too, leaving
|
|
1297
|
+
* the fallback {@link redactEmbeddedCredential} to strip only the `//…@` userinfo and LEAK the
|
|
1298
|
+
* `?token=secret` tail. Keeping this token's `scheme:` + `\s*` + `//` shape aligned with `isUrlShaped`
|
|
1299
|
+
* closes that gap (issue #778 review — thread readiness.ts:1239). The token then runs to the next literal
|
|
1300
|
+
* (`[^ ]+`), NOT the wider `\s` class, so it CROSSES an XML-valid internal TAB/LF/CR (0x09/0x0A/0x0D —
|
|
1301
|
+
* the only whitespace `stripXmlInvalidChars` keeps) and still redacts a `?query`/`#fragment` secret
|
|
1302
|
+
* sitting past it (`https://host/pa\tth?token=secret`); a `\s`-bounded token stopped at that internal
|
|
1303
|
+
* whitespace and leaked the tail (#778 review — thread deliveryGraph.ts:188). Over-redacting across an
|
|
1304
|
+
* internal newline is the safe direction. `\s*` and `[^ ]+` are separated by the literal `:`/`//`
|
|
1305
|
+
* anchors, so the match stays linear with no catastrophic backtracking. */
|
|
1306
|
+
const EMBEDDED_SCHEME_URL_SRC = "[a-z][a-z0-9+.-]*:\\s*\\/\\/[^ ]+";
|
|
1307
|
+
|
|
1308
|
+
/** Redact each embedded ABSOLUTE-URL (explicit `scheme://…`) token in a free-form value IN PLACE via
|
|
1309
|
+
* {@link redactString} — stripping that token's `user:pass@` userinfo AND `?query`/`#fragment` — while
|
|
1310
|
+
* leaving surrounding prose and any scheme-relative/opaque `//…`/`#`/`?` run untouched (those are
|
|
1311
|
+
* handled, credential-only, by {@link redactEmbeddedCredential}). Lets {@link redactConnectorValue}
|
|
1312
|
+
* catch a query/fragment secret riding an embedded absolute URL after a non-URL prefix
|
|
1313
|
+
* (`prefix https://host/path?token=secret`) that the anchored {@link isUrlShaped} whole-value check
|
|
1314
|
+
* misses (issue #778 review — thread deliveryGraph.ts:181). Deterministic and total. */
|
|
1315
|
+
export function redactEmbeddedUrl(s: string): string {
|
|
1316
|
+
return s.replace(new RegExp(EMBEDDED_SCHEME_URL_SRC, "gi"), (m) => redactString(m));
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/** The ONE canonical embedded SCHEME-RELATIVE URL token that carries a `?query`: a userinfo-LESS
|
|
1320
|
+
* `//authority…?query…` run (bounded by the next literal space). Distinct from the userinfo-bearing
|
|
1321
|
+
* {@link EMBEDDED_CREDENTIAL_URL_SRC} (which the `@` marks a URL) and the explicit-scheme
|
|
1322
|
+
* {@link EMBEDDED_SCHEME_URL_SRC}: a `senior:feature //host?token=secret` embedded after a non-URL prefix
|
|
1323
|
+
* has NO userinfo `@` and NO explicit `scheme:`, so the anchored {@link isUrlShaped} whole-value check,
|
|
1324
|
+
* {@link redactEmbeddedCredentialUrl}, and {@link redactEmbeddedUrl} ALL miss it — leaking the
|
|
1325
|
+
* `?token=secret` tail (issue #778 review — thread deliveryGraph.ts:633). A scheme-relative `//authority`
|
|
1326
|
+
* whose `?` is present is UNAMBIGUOUSLY URL syntax — an opaque ref (`owner/repo#42`, `//host#42`) uses a
|
|
1327
|
+
* `#<digits>` fragment (`parsePrTarget`), never a `//…?…` query — so its query is safe to strip while a
|
|
1328
|
+
* userinfo-less opaque `//host#42` (no `?`) is left to keep its meaningful `#42`. The token runs to the
|
|
1329
|
+
* next literal SPACE (`[^ ]*`), NOT the wider `\s` class, so it CROSSES an XML-valid internal TAB/LF/CR
|
|
1330
|
+
* and still redacts a `?query` secret sitting past it. `[^ ]*` before/after the literal `?` anchor keeps
|
|
1331
|
+
* the match linear with no catastrophic backtracking. */
|
|
1332
|
+
const EMBEDDED_SCHEME_RELATIVE_URL_SRC = "\\/\\/[^ ]*\\?[^ ]*";
|
|
1333
|
+
|
|
1334
|
+
/** Redact each embedded SCHEME-RELATIVE URL (`//authority…?query…`, no userinfo, no explicit scheme)
|
|
1335
|
+
* token IN PLACE via {@link redactString} — stripping its `?query`/`#fragment` — while leaving a
|
|
1336
|
+
* userinfo-less opaque `//host#42` PR ref (no `?`) untouched. Lets {@link redactConnectorValue} catch a
|
|
1337
|
+
* query secret riding a scheme-relative URL after a non-URL prefix (`prefix //host?token=secret`) that
|
|
1338
|
+
* the anchored {@link isUrlShaped} check, {@link redactEmbeddedCredentialUrl}, and {@link redactEmbeddedUrl}
|
|
1339
|
+
* all miss (issue #778 review — thread deliveryGraph.ts:633). Deterministic and total. */
|
|
1340
|
+
export function redactEmbeddedSchemeRelativeUrl(s: string): string {
|
|
1341
|
+
return s.replace(new RegExp(EMBEDDED_SCHEME_RELATIVE_URL_SRC, "g"), (m) => redactString(m));
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/** The canonical scheme-relative `//` authority marker — the `//` that opens an authority in ANY URL
|
|
1345
|
+
* form (absolute `scheme://`, scheme-relative `//`, credential `//user@`). A worker-routing job type
|
|
1346
|
+
* never legitimately contains it. */
|
|
1347
|
+
const SCHEME_RELATIVE_AUTHORITY_SRC = "\\/\\/";
|
|
1348
|
+
|
|
1349
|
+
/** True when `value` embeds a scheme-relative `//authority` token (see {@link SCHEME_RELATIVE_AUTHORITY_SRC}).
|
|
1350
|
+
* A plain worker-routing job type / opaque id NEVER contains `//` — so a match (whether it carries
|
|
1351
|
+
* userinfo, an explicit scheme, a `?query`, a `#fragment`, or nothing) is illegitimate in an
|
|
1352
|
+
* `agent.jobType` and must be rejected, closing the userinfo-less scheme-relative gap
|
|
1353
|
+
* (`senior:feature //host?token=secret`) that {@link hasEmbeddedCredential} (needs `@`),
|
|
1354
|
+
* {@link hasEmbeddedUrl} (needs an explicit scheme), and the anchored {@link isUrlShaped} (whole-value
|
|
1355
|
+
* only) all miss (issue #778 review — thread deliveryGraph.ts:633). */
|
|
1356
|
+
export function hasSchemeRelativeAuthority(value: string): boolean {
|
|
1357
|
+
return new RegExp(SCHEME_RELATIVE_AUTHORITY_SRC).test(value);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/** True when `value` embeds a `//<userinfo>@host` credential token (see {@link EMBEDDED_CREDENTIAL_SRC};
|
|
1361
|
+
* the userinfo colon is optional, so a passwordless `//token@host` bearer token also matches).
|
|
1362
|
+
* A plain worker-routing job type / opaque id never contains one, so a match is a credential leak to reject. */
|
|
1363
|
+
export function hasEmbeddedCredential(value: string): boolean {
|
|
1364
|
+
return new RegExp(EMBEDDED_CREDENTIAL_SRC).test(value);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/** True when `value` embeds an absolute-URL token (`scheme://authority…`; see
|
|
1368
|
+
* {@link EMBEDDED_SCHEME_URL_SRC}). A plain worker-routing job type / opaque id never contains one, so a
|
|
1369
|
+
* match is a URL whose `?query`/`#fragment` can hide a token and which — since the executable
|
|
1370
|
+
* `<zeebe:taskDefinition type=…>` carries the job type VERBATIM — would leak into the compiled BPMN.
|
|
1371
|
+
* Distinct from {@link hasEmbeddedCredential}: that catches only a `//<userinfo>@` credential span and
|
|
1372
|
+
* misses a userinfo-less `senior:feature https://host?token=secret` (issue #778 review — thread
|
|
1373
|
+
* deliveryGraph.ts:602). */
|
|
1374
|
+
export function hasEmbeddedUrl(value: string): boolean {
|
|
1375
|
+
return new RegExp(EMBEDDED_SCHEME_URL_SRC, "i").test(value);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/** The ONE canonical "is this value a URL?" classifier — a `scheme://authority` OR scheme-relative
|
|
1379
|
+
* `//authority` form, either of which can hide a credential in userinfo/query/fragment. It is the single
|
|
1380
|
+
* source of truth for URL-shape detection shared by the connector/jobType display redactor
|
|
1381
|
+
* ({@link redactString} callers in the compiler) AND the semantic validation boundary
|
|
1382
|
+
* (`validateDeliveryGraph`), so the "what counts as credential-bearing/URL-shaped" rule can never drift
|
|
1383
|
+
* between the redact path and the reject path (issue #778 review — thread deliveryGraphCompiler.ts:1606).
|
|
1384
|
+
* Classify on the TRIMMED value so leading whitespace (` //user:pass@host` — the OpenAPI edge caps
|
|
1385
|
+
* length but does not trim) cannot bypass the anchored check. The scheme may be followed by
|
|
1386
|
+
* XML-attribute whitespace (TAB/LF/CR/space) before the `//` authority: those characters are valid XML
|
|
1387
|
+
* `Char`s that `stripXmlInvalidChars` does NOT remove, so `https:\t//user:pass@host` must still classify
|
|
1388
|
+
* as URL-shaped or its credential escapes redaction as "not a URL" (issue #778 review — thread
|
|
1389
|
+
* readiness.ts:1191). Deterministic and total. */
|
|
1390
|
+
export function isUrlShaped(value: string): boolean {
|
|
1391
|
+
return /^([a-z][a-z0-9+.-]*:\s*)?\/\//i.test(value.trim());
|
|
1166
1392
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
-- Delivery Graphs: persist a LOSSLESS graph-identity fingerprint per run in a dedicated SIDE TABLE
|
|
2
|
+
-- (issue #778 review — thread dispatchDeliveryGraph.ts:332).
|
|
3
|
+
--
|
|
4
|
+
-- A run is keyed by `run_key` (an explicit operator `idempotencyKey`, else the content `digest`). The
|
|
5
|
+
-- `digest` is content-addressed over the REDACTED `semanticBpmn`, so two graphs differing ONLY in a
|
|
6
|
+
-- redacted-away secret (a URL credential, a `command` target, a match secret, a free-form connector
|
|
7
|
+
-- `payload`, …) SHARE one digest. When an operator supplies an explicit `idempotencyKey`, a re-dispatch
|
|
8
|
+
-- short-circuits onto the existing running row — but the lossy `digest` on that row cannot prove the
|
|
9
|
+
-- short-circuited run is THIS exact secret-bearing graph vs. a credential-different graph re-staged under
|
|
10
|
+
-- the same key. The dispatch door therefore refused EVERY such short-circuit with 409, which also broke
|
|
11
|
+
-- the idempotency contract for a legitimate SAME-payload retry (a lost response / double-click of the
|
|
12
|
+
-- identical proposal): it received 409 and lingered `staged` instead of short-circuiting successfully.
|
|
13
|
+
--
|
|
14
|
+
-- `graph_fingerprint` stores the run's lossless content identity — `sha256(digest \0
|
|
15
|
+
-- canonicalJson(digestInvisibleRawValues(graph)))` (the SAME identity `stableProposalRunKey` folds into
|
|
16
|
+
-- the keyless run key) — captured at launch. On an explicit-key short-circuit the door compares the
|
|
17
|
+
-- incoming graph's fingerprint against the stored one: a MATCH is a same-payload retry (allow the
|
|
18
|
+
-- short-circuit + consume the proposal); a MISMATCH — or a MISSING row (a run that predates this table)
|
|
19
|
+
-- — is unprovable, so the door refuses (the safe pre-existing 409 behaviour) and it self-heals once the
|
|
20
|
+
-- run turns over.
|
|
21
|
+
--
|
|
22
|
+
-- WHY A SIDE TABLE, not a column on `delivery_graph_runs`: that base table is projected COLUMN-FOR-COLUMN
|
|
23
|
+
-- by several drift-guarded surfaces — the `delivery_graph_runs__units` compat VIEW served from the
|
|
24
|
+
-- `delivery_units` aggregate (091/098) and the `delivery_graph_read_model`/`__tracking` VIEWs (087/096),
|
|
25
|
+
-- each of which asserts full base-column pass-through parity. This identity fingerprint is an INTERNAL
|
|
26
|
+
-- dispatch concern surfaced to no read model, so it belongs off to the side rather than threaded through
|
|
27
|
+
-- (and re-drift-guarded across) every projection.
|
|
28
|
+
--
|
|
29
|
+
-- Forward-only, additive (expand). The runner wraps each file in its own transaction — no BEGIN/COMMIT.
|
|
30
|
+
-- Numbered after 112 (the current highest on origin/main; this branch forked before 103–112 and must not
|
|
31
|
+
-- reuse a prefix main already occupies).
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS delivery_graph_run_identity (
|
|
34
|
+
run_key TEXT PRIMARY KEY,
|
|
35
|
+
graph_fingerprint TEXT NOT NULL,
|
|
36
|
+
created_at TEXT NOT NULL
|
|
37
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- A MONOTONIC per-write stage revision for delivery-graph proposals (issue #778 review, thread
|
|
2
|
+
-- app/deliveryGraphProposals.ts:245). The supersede reconcile ("exactly one live proposal per logical
|
|
3
|
+
-- graph, the LATEST stage wins") breaks a same-millisecond `updated_at` tie between two staged siblings
|
|
4
|
+
-- with a deterministic secondary key. That key used to be SQLite's `rowid` (insertion order) — but a
|
|
5
|
+
-- RE-STAGE of an existing digest is an `UPDATE` that bumps `updated_at` while LEAVING the `rowid`
|
|
6
|
+
-- unchanged, so a re-stage that lands in the same millisecond as a different digest's fresh stage could
|
|
7
|
+
-- be selected as the OLDER row (its stale, lower rowid) and be immediately superseded, inverting the
|
|
8
|
+
-- "latest stage wins" invariant this tie-break exists to uphold.
|
|
9
|
+
--
|
|
10
|
+
-- `stage_seq` fixes that: it is reassigned to `MAX(stage_seq)+1` on EVERY stage write — a fresh insert
|
|
11
|
+
-- AND a re-stage update alike — so the most-recently-written row always holds the strictly-highest value
|
|
12
|
+
-- regardless of its rowid or a coincident `updated_at`. Additive/expand (nullable-equivalent `DEFAULT 0`,
|
|
13
|
+
-- no data rewrite): existing rows read as `0` and are re-sequenced the next time they are staged.
|
|
14
|
+
ALTER TABLE delivery_graph_proposals ADD COLUMN stage_seq INTEGER NOT NULL DEFAULT 0;
|
|
15
|
+
|
|
16
|
+
-- Index `stage_seq` so the atomic `MAX(stage_seq)+1` allocation in `stageProposal` (issue #778 review,
|
|
17
|
+
-- thread app/deliveryGraphProposals.ts:237) is an O(1) reverse-index seek of the single highest value
|
|
18
|
+
-- rather than a full scan of the retained proposal history (staged + terminal rows accumulate; a bare
|
|
19
|
+
-- aggregate would grow linearly with them).
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_delivery_graph_proposals_stage_seq
|
|
21
|
+
ON delivery_graph_proposals (stage_seq);
|
|
@@ -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`.
|