@nanobpm/nano-workforce 0.102.0 → 0.102.1
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 +7 -0
- package/SPEC.md +24 -2
- package/app/failingChecksSupersede.test.ts +93 -0
- package/app/github.ts +69 -7
- package/app/mergeCiReattempt.test.ts +138 -0
- package/app/mergeRebaseArm.test.ts +5 -2
- package/package.json +1 -1
- package/resources/processes/merge-loop.bpmn +162 -88
- package/resources/prompts/fix-ci.md +32 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [0.102.1](https://github.com/nanobpm/nano-workforce/compare/v0.102.0...v0.102.1) (2026-08-19)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **merge-loop:** re-attempt merge for stale/transient CI instead of paging a human ([#348](https://github.com/nanobpm/nano-workforce/issues/348)) ([#350](https://github.com/nanobpm/nano-workforce/issues/350)) ([87ce2d6](https://github.com/nanobpm/nano-workforce/commit/87ce2d69e893b4b0f965f481d6c247fb8fcb84e8))
|
|
7
|
+
|
|
1
8
|
# [0.102.0](https://github.com/nanobpm/nano-workforce/compare/v0.101.1...v0.102.0) (2026-08-19)
|
|
2
9
|
|
|
3
10
|
|
package/SPEC.md
CHANGED
|
@@ -424,8 +424,30 @@ start ─► wait: deps merged ─► arm merge ─► wait: mergeable ─┬─
|
|
|
424
424
|
check names ride `appendPrompt`) to green the checks on the branch, then re-arms the
|
|
425
425
|
poller. It repeats while `ciFixRound < ciFixMax`
|
|
426
426
|
(`NANO_PR_MAX_CI_FIX_ROUNDS`, default 3; `0` disables). Only when the budget is
|
|
427
|
-
exhausted, the agent reports `blocked
|
|
428
|
-
|
|
427
|
+
exhausted, the agent reports `blocked` *and the PR is still blocked after a
|
|
428
|
+
ground-truth reconcile*, or the branch is in `conflict` does it fall through to the
|
|
429
|
+
human escalation path.
|
|
430
|
+
|
|
431
|
+
- **Stale / transient checks — re-attempt, don't escalate** (issue #348) — GitHub's CI
|
|
432
|
+
concurrency **cancels** a superseded workflow run while a newer run on the *identical
|
|
433
|
+
head SHA* takes over. Both land in the head's `statusCheckRollup` under the same check
|
|
434
|
+
name — the stale one stamped `CANCELLED`, the live one green. This is a
|
|
435
|
+
**CI-concurrency-cancellation drift class**, not a code defect, defended at three
|
|
436
|
+
layers so it never pages a human:
|
|
437
|
+
- **Derivation (root cause)** — the merge poller's check derivation collapses the
|
|
438
|
+
rollup to the **newest run per `(headSha, checkName)`** (`latestRunPerCheck`) before
|
|
439
|
+
classifying, so a `CANCELLED` run superseded by a newer green run is **not** counted
|
|
440
|
+
as a failing gate. The phantom `blocked` never arises, so `fix-ci` is not even armed.
|
|
441
|
+
- **Agent verdict** — when `fix-ci` pushes nothing because the failing checks are
|
|
442
|
+
stale/transient (head already green), it returns `status: "reattempt"` (with
|
|
443
|
+
`pushed: false`). That routes to `arm-merge` — the merge is simply re-queued from
|
|
444
|
+
ground truth. The prompt reserves `blocked` for a genuine human decision (a missing
|
|
445
|
+
secret, an un-fixable failure), never a self-healing PR.
|
|
446
|
+
- **Reconcile-before-escalate guard** — even a *mislabelled* `blocked` self-heals: a
|
|
447
|
+
`blocked` verdict with no push (`pushed != true`) reconciles **once** via ground
|
|
448
|
+
truth (`gw-ci-blocked` → `ci-reconcile`, which re-arms the canonical merge poller and
|
|
449
|
+
sets `ciBlockedReconciled`), and escalates only if the PR is **still** blocked on the
|
|
450
|
+
re-derived state.
|
|
429
451
|
|
|
430
452
|
- **Discovered dependency** — a `senior:fix-ci` or `senior:rebase` agent may find that
|
|
431
453
|
the PR cannot land because **another PR must merge first** (a required linked-issue
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Derivation-layer guard for the CI-concurrency-cancellation drift class (issue #348).
|
|
2
|
+
//
|
|
3
|
+
// GitHub's CI concurrency cancels a superseded workflow run while a newer run on the *identical
|
|
4
|
+
// head SHA* takes over. Both land in the head's `statusCheckRollup` under the same check name — the
|
|
5
|
+
// stale one stamped `CANCELLED`, the live one green. The merge poller's check derivation used to
|
|
6
|
+
// count that stale `CANCELLED` as a failing required check, so a PR whose head is actually green
|
|
7
|
+
// read as `blocked`, armed `senior:fix-ci`, which honestly pushed nothing and (pre-#348) returned
|
|
8
|
+
// `blocked` → a human merge-escalation for a self-healing PR.
|
|
9
|
+
//
|
|
10
|
+
// The fix collapses the rollup to the NEWEST run per check before classifying, so a superseded
|
|
11
|
+
// `CANCELLED` never counts. These are pure unit tests over the exported derivation helpers.
|
|
12
|
+
|
|
13
|
+
import { test } from "node:test";
|
|
14
|
+
import { assert, assertEquals } from "#test-assert";
|
|
15
|
+
import { allCheckNames, failingCheckNames, latestRunPerCheck } from "./github.ts";
|
|
16
|
+
|
|
17
|
+
test("a CANCELLED run superseded by a newer green run on the same head does not count as failing", () => {
|
|
18
|
+
const rollup = [
|
|
19
|
+
// The superseded run: GitHub CI concurrency cancelled it when a newer run started.
|
|
20
|
+
{ name: "engine-core", conclusion: "CANCELLED", startedAt: "2024-01-01T00:00:00Z", completedAt: "2024-01-01T00:01:00Z" },
|
|
21
|
+
// The live run on the identical head SHA: green.
|
|
22
|
+
{ name: "engine-core", conclusion: "SUCCESS", startedAt: "2024-01-01T00:02:00Z", completedAt: "2024-01-01T00:05:00Z" },
|
|
23
|
+
];
|
|
24
|
+
assertEquals(failingCheckNames(rollup), [], "the stale CANCELLED must not read as a failing gate");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("every CANCELLED-superseded required check on one head SHA is dropped (the #348 instance)", () => {
|
|
28
|
+
// The exact PR #887 evidence: four required checks each with a superseded CANCELLED + a newer
|
|
29
|
+
// green run on the same head. None must count as failing.
|
|
30
|
+
const names = ["engine-core", "engine-wasm read-model wasm32 type-check", "processos", "server"];
|
|
31
|
+
const rollup = names.flatMap((name) => [
|
|
32
|
+
{ name, conclusion: "CANCELLED", startedAt: "2024-01-01T00:00:00Z" },
|
|
33
|
+
{ name, conclusion: "SUCCESS", startedAt: "2024-01-01T00:02:00Z" },
|
|
34
|
+
]);
|
|
35
|
+
assertEquals(failingCheckNames(rollup), [], "no superseded CANCELLED may count as a failing check");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("a genuine failure on the newest run still counts (no false-negative)", () => {
|
|
39
|
+
const rollup = [
|
|
40
|
+
{ name: "engine-core", conclusion: "SUCCESS", startedAt: "2024-01-01T00:00:00Z" },
|
|
41
|
+
// Newest run genuinely failed — this must still be reported.
|
|
42
|
+
{ name: "engine-core", conclusion: "FAILURE", startedAt: "2024-01-01T00:02:00Z" },
|
|
43
|
+
];
|
|
44
|
+
assertEquals(failingCheckNames(rollup), ["engine-core"], "a real failure on the newest run must count");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("a lone CANCELLED with no superseding run still counts (nothing green replaced it)", () => {
|
|
48
|
+
const rollup = [{ name: "engine-core", conclusion: "CANCELLED", startedAt: "2024-01-01T00:00:00Z" }];
|
|
49
|
+
assertEquals(failingCheckNames(rollup), ["engine-core"], "an unsuperseded CANCELLED remains a failing gate");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("ties (missing timestamps) prefer the non-CANCELLED run so the real result wins", () => {
|
|
53
|
+
// GitHub sometimes omits run times; a superseded CANCELLED alongside a completed run must not
|
|
54
|
+
// shadow the real conclusion even when neither carries a timestamp.
|
|
55
|
+
const cancelledFirst = [
|
|
56
|
+
{ name: "server", conclusion: "CANCELLED" },
|
|
57
|
+
{ name: "server", conclusion: "SUCCESS" },
|
|
58
|
+
];
|
|
59
|
+
const successFirst = [
|
|
60
|
+
{ name: "server", conclusion: "SUCCESS" },
|
|
61
|
+
{ name: "server", conclusion: "CANCELLED" },
|
|
62
|
+
];
|
|
63
|
+
assertEquals(failingCheckNames(cancelledFirst), [], "CANCELLED-first tie resolves to the real (green) result");
|
|
64
|
+
assertEquals(failingCheckNames(successFirst), [], "success-first tie keeps the real (green) result");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("latestRunPerCheck keeps exactly one run per check name (newest)", () => {
|
|
68
|
+
const rollup = [
|
|
69
|
+
{ name: "a", conclusion: "CANCELLED", startedAt: "2024-01-01T00:00:00Z" },
|
|
70
|
+
{ name: "a", conclusion: "SUCCESS", startedAt: "2024-01-01T00:02:00Z" },
|
|
71
|
+
{ name: "b", conclusion: "FAILURE", startedAt: "2024-01-01T00:00:00Z" },
|
|
72
|
+
];
|
|
73
|
+
const latest = latestRunPerCheck(rollup);
|
|
74
|
+
assertEquals(latest.length, 2, "one run per distinct check name");
|
|
75
|
+
const a = latest.find((c) => c.name === "a");
|
|
76
|
+
assert(a && a.conclusion === "SUCCESS", "check `a` resolves to its newest (green) run");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("allCheckNames dedupes superseded reruns to a single name", () => {
|
|
80
|
+
const rollup = [
|
|
81
|
+
{ name: "engine-core", conclusion: "CANCELLED", startedAt: "2024-01-01T00:00:00Z" },
|
|
82
|
+
{ name: "engine-core", conclusion: "SUCCESS", startedAt: "2024-01-01T00:02:00Z" },
|
|
83
|
+
];
|
|
84
|
+
assertEquals(allCheckNames(rollup), ["engine-core"], "a superseded rerun must not double-list the check name");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("legacy StatusContext (state + context) supersession is handled by createdAt", () => {
|
|
88
|
+
const rollup = [
|
|
89
|
+
{ context: "ci/legacy", state: "ERROR", createdAt: "2024-01-01T00:00:00Z" },
|
|
90
|
+
{ context: "ci/legacy", state: "SUCCESS", createdAt: "2024-01-01T00:02:00Z" },
|
|
91
|
+
];
|
|
92
|
+
assertEquals(failingCheckNames(rollup), [], "a superseded legacy status context is not a failing gate");
|
|
93
|
+
});
|
package/app/github.ts
CHANGED
|
@@ -521,17 +521,78 @@ interface RollupEntry {
|
|
|
521
521
|
name?: string;
|
|
522
522
|
context?: string;
|
|
523
523
|
workflowName?: string;
|
|
524
|
+
/** CheckRun timestamps (GraphQL `statusCheckRollup`). A superseded run and the newer run that
|
|
525
|
+
* replaced it carry the same check name but different times, so they order the runs of one check.
|
|
526
|
+
* StatusContext carries `createdAt` instead. All are ISO-8601 or absent. */
|
|
527
|
+
startedAt?: string;
|
|
528
|
+
completedAt?: string;
|
|
529
|
+
createdAt?: string;
|
|
524
530
|
}
|
|
531
|
+
|
|
532
|
+
/** The canonical identity of a check across its reruns: its name (CheckRun) or context
|
|
533
|
+
* (StatusContext), falling back to its `workflowName` and finally the sentinel `"check"` when
|
|
534
|
+
* neither is present. GitHub CI concurrency can leave several runs of the SAME check on one head
|
|
535
|
+
* commit — a superseded run plus the newer run that replaced it — so this is what we group by. */
|
|
536
|
+
function checkKey(c: RollupEntry): string {
|
|
537
|
+
return c.name || c.context || c.workflowName || "check";
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** A run's ordering timestamp (newest wins): its completion, else its start, else its creation.
|
|
541
|
+
* `0` when none is present (the shape carries no time) so a timed run always outranks an untimed
|
|
542
|
+
* one. */
|
|
543
|
+
function runOrder(c: RollupEntry): number {
|
|
544
|
+
const t = c.completedAt || c.startedAt || c.createdAt;
|
|
545
|
+
if (!t) return 0;
|
|
546
|
+
const ms = Date.parse(t);
|
|
547
|
+
return Number.isNaN(ms) ? 0 : ms;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** True when a run's conclusion is `CANCELLED` — the state GitHub CI concurrency stamps on a run it
|
|
551
|
+
* supersedes with a newer run on the identical head SHA (a stale/transient cancellation, not a code
|
|
552
|
+
* defect). */
|
|
553
|
+
function isCancelled(c: RollupEntry): boolean {
|
|
554
|
+
return (c.conclusion || c.state || "").toUpperCase() === "CANCELLED";
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Collapse a head's rollup to the **newest run per check**. GitHub's CI-concurrency cancellation
|
|
558
|
+
* (issue #348) leaves BOTH a superseded run (stamped `CANCELLED`) and the newer run that replaced
|
|
559
|
+
* it on the *same head SHA*, under the same check name. Counting the stale `CANCELLED` as a failure
|
|
560
|
+
* escalates a self-healing PR whose head is actually green. The rollup is already scoped to the head
|
|
561
|
+
* commit, so grouping by check name and keeping the newest run per name yields one ground-truth
|
|
562
|
+
* conclusion per `(headSha, checkName)`. Ties (equal/absent timestamps) prefer a non-`CANCELLED`
|
|
563
|
+
* run, so a superseded cancellation never shadows the real result even when GitHub omits times. */
|
|
564
|
+
export function latestRunPerCheck(rollup: RollupEntry[]): RollupEntry[] {
|
|
565
|
+
const newest = new Map<string, RollupEntry>();
|
|
566
|
+
for (const c of rollup) {
|
|
567
|
+
const key = checkKey(c);
|
|
568
|
+
const prev = newest.get(key);
|
|
569
|
+
if (prev === undefined) {
|
|
570
|
+
newest.set(key, c);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
const dt = runOrder(c) - runOrder(prev);
|
|
574
|
+
if (dt > 0) {
|
|
575
|
+
newest.set(key, c);
|
|
576
|
+
} else if (dt === 0 && isCancelled(prev) && !isCancelled(c)) {
|
|
577
|
+
// Same/unknown time: a CANCELLED run is the superseded one — the real result wins.
|
|
578
|
+
newest.set(key, c);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return [...newest.values()];
|
|
582
|
+
}
|
|
583
|
+
|
|
525
584
|
/** Names of the checks whose result is a hard failure (as opposed to pending/success). Covers
|
|
526
585
|
* both the CheckRun shape (`conclusion` + `name`/`workflowName`) and the legacy StatusContext
|
|
527
586
|
* shape (`state` + `context`). The names are what the CI-fix agent is handed so it knows which
|
|
528
|
-
* gates to make green; `failingChecks` (the count) is derived from this list.
|
|
529
|
-
|
|
587
|
+
* gates to make green; `failingChecks` (the count) is derived from this list. Derivation is over the
|
|
588
|
+
* **newest run per check** (`latestRunPerCheck`) so a `CANCELLED` run superseded by a newer green
|
|
589
|
+
* run on the identical head SHA is not counted as a failing gate (issue #348). */
|
|
590
|
+
export function failingCheckNames(rollup: RollupEntry[]): string[] {
|
|
530
591
|
const bad = new Set(["FAILURE", "TIMED_OUT", "CANCELLED", "ACTION_REQUIRED", "STARTUP_FAILURE", "ERROR"]);
|
|
531
592
|
const names: string[] = [];
|
|
532
|
-
for (const c of rollup) {
|
|
593
|
+
for (const c of latestRunPerCheck(rollup)) {
|
|
533
594
|
const v = (c.conclusion || c.state || "").toUpperCase();
|
|
534
|
-
if (bad.has(v)) names.push(c
|
|
595
|
+
if (bad.has(v)) names.push(checkKey(c));
|
|
535
596
|
}
|
|
536
597
|
return names;
|
|
537
598
|
}
|
|
@@ -539,10 +600,11 @@ function failingCheckNames(rollup: RollupEntry[]): string[] {
|
|
|
539
600
|
/** Names of every head check present, regardless of state. Covers both the CheckRun shape
|
|
540
601
|
* (`name`/`workflowName`) and the legacy StatusContext shape (`context`). Used to test whether a
|
|
541
602
|
* repo's *required* checks are present on the head — so an unrelated always-on check (e.g.
|
|
542
|
-
* Mergify's "Merge Queue") doesn't masquerade as the required CI run having already happened.
|
|
543
|
-
|
|
603
|
+
* Mergify's "Merge Queue") doesn't masquerade as the required CI run having already happened.
|
|
604
|
+
* Deduped to the newest run per check so a superseded rerun doesn't list a check name twice. */
|
|
605
|
+
export function allCheckNames(rollup: RollupEntry[]): string[] {
|
|
544
606
|
const names: string[] = [];
|
|
545
|
-
for (const c of rollup) {
|
|
607
|
+
for (const c of latestRunPerCheck(rollup)) {
|
|
546
608
|
const name = c.name || c.context || c.workflowName;
|
|
547
609
|
if (name) names.push(name);
|
|
548
610
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Structural regression guard for the CI-concurrency-cancellation drift class (issue #348).
|
|
2
|
+
//
|
|
3
|
+
// The merge loop escalated a human whenever `senior:fix-ci` returned `blocked` — even when the
|
|
4
|
+
// failing required checks were STALE/TRANSIENT (CANCELLED runs superseded by a newer green run on
|
|
5
|
+
// the identical head SHA) and the agent honestly pushed nothing. The `fix-ci` prompt actively
|
|
6
|
+
// funnelled that self-healing case into `blocked`, and `blocked` routed straight to the merge
|
|
7
|
+
// escalation user task. A phantom-blocked, self-healing merge paged a human.
|
|
8
|
+
//
|
|
9
|
+
// The fix adds a first-class re-attempt path and a reconcile-before-escalate guard:
|
|
10
|
+
//
|
|
11
|
+
// 1. `status = "reattempt"` (a first-class fix-ci verdict for stale/transient checks) routes to
|
|
12
|
+
// `arm-merge`, re-queuing the merge from ground truth — no human, and declared explicitly
|
|
13
|
+
// beside the `f_ci_reconcile` empty-status default rather than relying on the fall-through.
|
|
14
|
+
// 2. A `blocked` verdict with no push (`pushed != true`) reconciles ONCE via ground truth
|
|
15
|
+
// (`gw-ci-blocked` → `ci-reconcile` → re-arm the poller) and escalates only if it is STILL
|
|
16
|
+
// blocked — so even a mislabelled `blocked` self-heals.
|
|
17
|
+
//
|
|
18
|
+
// Pure text assertions over the committed BPMN (no engine), matching the repo's lightweight
|
|
19
|
+
// model-guard style (see mergeEscalationQuestion.test.ts, mergeRebaseArm.test.ts).
|
|
20
|
+
|
|
21
|
+
import { test } from "node:test";
|
|
22
|
+
import { assert, assertStringIncludes } from "#test-assert";
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
|
|
25
|
+
const bpmn = readFileSync("resources/processes/merge-loop.bpmn", "utf8");
|
|
26
|
+
// Collapse whitespace so attribute-order / line-wrapping churn doesn't make the assertions brittle.
|
|
27
|
+
const flat = bpmn.replace(/\s+/g, " ");
|
|
28
|
+
|
|
29
|
+
function flowElement(id: string): string | null {
|
|
30
|
+
const re = new RegExp(
|
|
31
|
+
`<bpmn:sequenceFlow\\b[^>]*?\\bid="${id}"[^>]*?(?:/>|>(?:(?!<bpmn:sequenceFlow\\b).)*?</bpmn:sequenceFlow>)`,
|
|
32
|
+
);
|
|
33
|
+
const m = flat.match(re);
|
|
34
|
+
return m ? m[0] : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function flowHasId(id: string, source: string, target: string): boolean {
|
|
38
|
+
const el = flowElement(id);
|
|
39
|
+
if (!el) return false;
|
|
40
|
+
return el.includes(`sourceRef="${source}"`) && el.includes(`targetRef="${target}"`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function serviceTask(id: string): string | null {
|
|
44
|
+
const m = flat.match(new RegExp(`<bpmn:serviceTask\\b[^>]*\\bid="${id}"[\\s\\S]*?</bpmn:serviceTask>`));
|
|
45
|
+
return m ? m[0].replace(/"/g, '"').replace(/&/g, "&").replace(/ /g, "\n") : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
test("a first-class `reattempt` verdict re-attempts the merge (arm-merge), not escalation", () => {
|
|
49
|
+
// gw-ci-result must carry an explicit `status = "reattempt"` arm to arm-merge, declared beside
|
|
50
|
+
// (not folded into) the empty-status `f_ci_reconcile` default.
|
|
51
|
+
const gw = flat.match(/<bpmn:exclusiveGateway\b[^>]*\bid="gw-ci-result"[\s\S]*?<\/bpmn:exclusiveGateway>/);
|
|
52
|
+
assert(gw, "gw-ci-result gateway must exist");
|
|
53
|
+
assertStringIncludes(gw![0], "f_ci_reattempt", "gw-ci-result must declare the reattempt outgoing arm");
|
|
54
|
+
|
|
55
|
+
const reattempt = flowElement("f_ci_reattempt");
|
|
56
|
+
assert(reattempt, "f_ci_reattempt flow missing");
|
|
57
|
+
assert(
|
|
58
|
+
flowHasId("f_ci_reattempt", "gw-ci-result", "arm-merge"),
|
|
59
|
+
"a reattempt verdict must re-arm the merge poller (arm-merge), never escalate",
|
|
60
|
+
);
|
|
61
|
+
assertStringIncludes(reattempt!, 'status = "reattempt"', "the reattempt arm must be gated on status = reattempt");
|
|
62
|
+
// It must be an EXPLICIT labelled flow, not the empty-status default.
|
|
63
|
+
assert(!/default="f_ci_reattempt"/.test(flat), "reattempt must be an explicit arm, not the gateway default");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("a fix-ci `reattempt` result does NOT create a merge escalation", () => {
|
|
67
|
+
// No flow originating from the reattempt classification may reach the merge-escalation task.
|
|
68
|
+
assert(
|
|
69
|
+
!flowHasId("f_ci_reattempt", "gw-ci-result", "merge-esc-attempt"),
|
|
70
|
+
"reattempt must never route to merge-esc-attempt",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("a blocked-with-no-push verdict reconciles once from ground truth before escalating", () => {
|
|
75
|
+
// The `blocked` arm no longer flows straight into the escalation: it passes through gw-ci-blocked.
|
|
76
|
+
assert(
|
|
77
|
+
flowHasId("f_ci_blocked", "gw-ci-result", "gw-ci-blocked"),
|
|
78
|
+
"a blocked verdict must route through gw-ci-blocked, not straight to merge-esc-attempt",
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const gw = flat.match(/<bpmn:exclusiveGateway\b[^>]*\bid="gw-ci-blocked"[^>]*>/);
|
|
82
|
+
assert(gw, "gw-ci-blocked gateway must exist");
|
|
83
|
+
// Default is escalate (still blocked), so a missing/true reconcile flag never wedges.
|
|
84
|
+
assertStringIncludes(gw![0], 'default="f_cib_esc"', "gw-ci-blocked must default to escalation");
|
|
85
|
+
|
|
86
|
+
// The reconcile-once arm: pushed nothing AND not yet reconciled → re-derive via ci-reconcile.
|
|
87
|
+
const recon = flowElement("f_cib_recon");
|
|
88
|
+
assert(recon, "f_cib_recon flow missing");
|
|
89
|
+
assert(flowHasId("f_cib_recon", "gw-ci-blocked", "ci-reconcile"), "reconcile arm must target ci-reconcile");
|
|
90
|
+
assertStringIncludes(recon!, "pushed != true", "reconcile only when the agent pushed nothing");
|
|
91
|
+
assertStringIncludes(recon!, "ciBlockedReconciled != true", "reconcile at most once");
|
|
92
|
+
|
|
93
|
+
// The escalate arm (default): still blocked → the human merge escalation.
|
|
94
|
+
assert(flowHasId("f_cib_esc", "gw-ci-blocked", "merge-esc-attempt"), "the still-blocked arm must escalate");
|
|
95
|
+
assert(
|
|
96
|
+
!/conditionExpression/.test(flowElement("f_cib_esc") ?? ""),
|
|
97
|
+
"f_cib_esc is the default arm and must not carry a conditionExpression",
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("ci-reconcile re-arms the canonical merge poller and marks the reconcile as spent", () => {
|
|
102
|
+
const el = serviceTask("ci-reconcile");
|
|
103
|
+
assert(el, "ci-reconcile service task must exist");
|
|
104
|
+
// Reuses the canonical arm-merge worker — one poller implementation, no second poller pass.
|
|
105
|
+
assertStringIncludes(el!, 'type="pr.arm-merge"', "ci-reconcile must reuse the canonical pr.arm-merge worker");
|
|
106
|
+
// Marks the reconcile spent so the SECOND blocked (still blocked after re-derivation) escalates.
|
|
107
|
+
const outs = el!.match(/<zeebe:output\b[^>]*\/>/g) ?? [];
|
|
108
|
+
assert(
|
|
109
|
+
outs.some((t) => t.includes('target="ciBlockedReconciled"') && t.includes('source="=true"')),
|
|
110
|
+
"ci-reconcile must set ciBlockedReconciled = true so a still-blocked PR escalates on the next pass",
|
|
111
|
+
);
|
|
112
|
+
// Re-derivation flows back through the mergeable wait (re-runs the ground-truth mergeable gate).
|
|
113
|
+
assert(flowHasId("f_cib_armed", "ci-reconcile", "wait-mergeable"), "ci-reconcile must re-enter wait-mergeable");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("arm-merge clears the reconcile flag each loop so a fresh block episode gets its own reconcile", () => {
|
|
117
|
+
const el = serviceTask("arm-merge");
|
|
118
|
+
assert(el, "arm-merge service task must exist");
|
|
119
|
+
const outs = el!.match(/<zeebe:output\b[^>]*\/>/g) ?? [];
|
|
120
|
+
assert(
|
|
121
|
+
outs.some((t) => t.includes('target="ciBlockedReconciled"') && t.includes('source="=null"')),
|
|
122
|
+
"arm-merge must reset ciBlockedReconciled each loop so a later, unrelated block still reconciles once",
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("regression: a stale/transient fix-ci result can no longer page a human", () => {
|
|
127
|
+
// The old wedge: `status = "blocked"` flowing directly into merge-esc-attempt. The blocked
|
|
128
|
+
// verdict now routes through gw-ci-blocked (reconcile-before-escalate), never straight to the
|
|
129
|
+
// escalation.
|
|
130
|
+
assert(
|
|
131
|
+
!flowHasId("f_ci_blocked", "gw-ci-result", "merge-esc-attempt"),
|
|
132
|
+
"the blocked verdict must not route directly into merge-esc-attempt (the #348 phantom escalation)",
|
|
133
|
+
);
|
|
134
|
+
// The blocked arm targets the reconcile gateway; the reattempt arm re-arms the poller. Neither
|
|
135
|
+
// gw-ci-result arm may target the escalation directly.
|
|
136
|
+
assertStringIncludes(flowElement("f_ci_blocked") ?? "", 'targetRef="gw-ci-blocked"');
|
|
137
|
+
assertStringIncludes(flowElement("f_ci_reattempt") ?? "", 'targetRef="arm-merge"');
|
|
138
|
+
});
|
|
@@ -118,11 +118,14 @@ test("ci-fix result: a missing/ambiguous status reconciles from ground truth, no
|
|
|
118
118
|
flowHasId("f_ci_reconcile", "gw-ci-result", "arm-merge"),
|
|
119
119
|
"f_ci_reconcile must default gw-ci-result → arm-merge (reconcile)",
|
|
120
120
|
);
|
|
121
|
-
// Escalation reserved for the agent's explicit `blocked` verdict
|
|
121
|
+
// Escalation reserved for the agent's explicit `blocked` verdict — but now via a
|
|
122
|
+
// reconcile-before-escalate guard (issue #348): a `blocked` with no push reconciles once from
|
|
123
|
+
// ground truth, and only a still-blocked PR reaches the human escalation.
|
|
122
124
|
const ciBlocked = flat.match(/<bpmn:sequenceFlow[^>]*id="f_ci_blocked"[\s\S]*?<\/bpmn:sequenceFlow>/);
|
|
123
125
|
assert(ciBlocked, "f_ci_blocked flow missing");
|
|
124
126
|
assertStringIncludes(ciBlocked![0], 'status = "blocked"');
|
|
125
|
-
assert(hasFlow("gw-ci-result", "
|
|
127
|
+
assert(hasFlow("gw-ci-result", "gw-ci-blocked"), "blocked verdict must pass through gw-ci-blocked (reconcile-before-escalate)");
|
|
128
|
+
assert(hasFlow("gw-ci-blocked", "merge-esc-attempt"), "gw-ci-blocked → merge-esc-attempt (still blocked) missing");
|
|
126
129
|
});
|
|
127
130
|
|
|
128
131
|
test("regression: the conflict verdict passes through the rebase actor, not straight to escalation", () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.102.
|
|
3
|
+
"version": "0.102.1",
|
|
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",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
<nano:extend name="summary" type="string" optional="true" />
|
|
62
62
|
<nano:extend name="question" type="string" optional="true" />
|
|
63
63
|
<nano:extend name="dependsOn" type="string" optional="true" />
|
|
64
|
+
<nano:extend name="pushed" type="boolean" optional="true" />
|
|
64
65
|
</nano:shape>
|
|
65
66
|
<nano:shape id="RebaseIn" name="Rebase — input">
|
|
66
67
|
<nano:extend name="prKey" type="string" />
|
|
@@ -98,11 +99,13 @@
|
|
|
98
99
|
</zeebe:properties>
|
|
99
100
|
<zeebe:ioMapping>
|
|
100
101
|
<zeebe:output source="=null" target="status" />
|
|
102
|
+
<zeebe:output source="=null" target="ciBlockedReconciled" />
|
|
101
103
|
</zeebe:ioMapping>
|
|
102
104
|
</bpmn:extensionElements>
|
|
103
105
|
<bpmn:incoming>f_m_deps</bpmn:incoming>
|
|
104
106
|
<bpmn:incoming>f_m_answer</bpmn:incoming>
|
|
105
107
|
<bpmn:incoming>f_ci_fixed</bpmn:incoming>
|
|
108
|
+
<bpmn:incoming>f_ci_reattempt</bpmn:incoming>
|
|
106
109
|
<bpmn:incoming>f_reb_rebased</bpmn:incoming>
|
|
107
110
|
<bpmn:incoming>f_m_evicted</bpmn:incoming>
|
|
108
111
|
<bpmn:incoming>f_m_escReenter</bpmn:incoming>
|
|
@@ -111,6 +114,7 @@
|
|
|
111
114
|
</bpmn:serviceTask>
|
|
112
115
|
<bpmn:intermediateCatchEvent id="wait-mergeable" name="Wait: mergeable">
|
|
113
116
|
<bpmn:incoming>f_m_arm</bpmn:incoming>
|
|
117
|
+
<bpmn:incoming>f_cib_armed</bpmn:incoming>
|
|
114
118
|
<bpmn:outgoing>f_m_ready</bpmn:outgoing>
|
|
115
119
|
<bpmn:messageEventDefinition id="med_mergeReady" messageRef="Message_mergeReady" />
|
|
116
120
|
</bpmn:intermediateCatchEvent>
|
|
@@ -213,7 +217,7 @@
|
|
|
213
217
|
</zeebe:ioMapping>
|
|
214
218
|
</bpmn:extensionElements>
|
|
215
219
|
<bpmn:incoming>f_m_gBlocked</bpmn:incoming>
|
|
216
|
-
<bpmn:incoming>
|
|
220
|
+
<bpmn:incoming>f_cib_esc</bpmn:incoming>
|
|
217
221
|
<bpmn:incoming>f_reb_blocked</bpmn:incoming>
|
|
218
222
|
<bpmn:incoming>f_ci_sla</bpmn:incoming>
|
|
219
223
|
<bpmn:incoming>f_mr_giveup</bpmn:incoming>
|
|
@@ -284,9 +288,29 @@
|
|
|
284
288
|
<bpmn:incoming>f_ci_done</bpmn:incoming>
|
|
285
289
|
<bpmn:outgoing>f_ci_fixed</bpmn:outgoing>
|
|
286
290
|
<bpmn:outgoing>f_ci_wait</bpmn:outgoing>
|
|
291
|
+
<bpmn:outgoing>f_ci_reattempt</bpmn:outgoing>
|
|
287
292
|
<bpmn:outgoing>f_ci_blocked</bpmn:outgoing>
|
|
288
293
|
<bpmn:outgoing>f_ci_reconcile</bpmn:outgoing>
|
|
289
294
|
</bpmn:exclusiveGateway>
|
|
295
|
+
<bpmn:exclusiveGateway id="gw-ci-blocked" name="reconcile then escalate?" default="f_cib_esc">
|
|
296
|
+
<bpmn:incoming>f_ci_blocked</bpmn:incoming>
|
|
297
|
+
<bpmn:outgoing>f_cib_recon</bpmn:outgoing>
|
|
298
|
+
<bpmn:outgoing>f_cib_esc</bpmn:outgoing>
|
|
299
|
+
</bpmn:exclusiveGateway>
|
|
300
|
+
<bpmn:serviceTask id="ci-reconcile" name="Reconcile blocked from ground truth">
|
|
301
|
+
<bpmn:extensionElements>
|
|
302
|
+
<zeebe:taskDefinition type="pr.arm-merge" />
|
|
303
|
+
<zeebe:properties>
|
|
304
|
+
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="MergeArmIn" />
|
|
305
|
+
</zeebe:properties>
|
|
306
|
+
<zeebe:ioMapping>
|
|
307
|
+
<zeebe:output source="=null" target="status" />
|
|
308
|
+
<zeebe:output source="=true" target="ciBlockedReconciled" />
|
|
309
|
+
</zeebe:ioMapping>
|
|
310
|
+
</bpmn:extensionElements>
|
|
311
|
+
<bpmn:incoming>f_cib_recon</bpmn:incoming>
|
|
312
|
+
<bpmn:outgoing>f_cib_armed</bpmn:outgoing>
|
|
313
|
+
</bpmn:serviceTask>
|
|
290
314
|
<bpmn:exclusiveGateway id="gw-rebase" name="auto-rebase?" default="f_reb_giveup">
|
|
291
315
|
<bpmn:incoming>f_m_mRebase</bpmn:incoming>
|
|
292
316
|
<bpmn:outgoing>f_reb_go</bpmn:outgoing>
|
|
@@ -356,9 +380,17 @@
|
|
|
356
380
|
<bpmn:sequenceFlow id="f_ci_fixed" name="fixed" sourceRef="gw-ci-result" targetRef="arm-merge">
|
|
357
381
|
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=status = "fixed"</bpmn:conditionExpression>
|
|
358
382
|
</bpmn:sequenceFlow>
|
|
359
|
-
<bpmn:sequenceFlow id="f_ci_blocked" name="could not fix" sourceRef="gw-ci-result" targetRef="
|
|
383
|
+
<bpmn:sequenceFlow id="f_ci_blocked" name="could not fix" sourceRef="gw-ci-result" targetRef="gw-ci-blocked">
|
|
360
384
|
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=status = "blocked"</bpmn:conditionExpression>
|
|
361
385
|
</bpmn:sequenceFlow>
|
|
386
|
+
<bpmn:sequenceFlow id="f_ci_reattempt" name="stale/transient checks — re-attempt" sourceRef="gw-ci-result" targetRef="arm-merge">
|
|
387
|
+
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=status = "reattempt"</bpmn:conditionExpression>
|
|
388
|
+
</bpmn:sequenceFlow>
|
|
389
|
+
<bpmn:sequenceFlow id="f_cib_recon" name="pushed nothing — reconcile once" sourceRef="gw-ci-blocked" targetRef="ci-reconcile">
|
|
390
|
+
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=pushed != true and ciBlockedReconciled != true</bpmn:conditionExpression>
|
|
391
|
+
</bpmn:sequenceFlow>
|
|
392
|
+
<bpmn:sequenceFlow id="f_cib_esc" name="still blocked — escalate" sourceRef="gw-ci-blocked" targetRef="merge-esc-attempt" />
|
|
393
|
+
<bpmn:sequenceFlow id="f_cib_armed" sourceRef="ci-reconcile" targetRef="wait-mergeable" />
|
|
362
394
|
<bpmn:sequenceFlow id="f_ci_reconcile" name="no verdict — reconcile from ground truth" sourceRef="gw-ci-result" targetRef="arm-merge" />
|
|
363
395
|
<bpmn:sequenceFlow id="f_ci_wait" name="waits on another PR" sourceRef="gw-ci-result" targetRef="record-merge-dep">
|
|
364
396
|
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=status = "waiting-on-pr"</bpmn:conditionExpression>
|
|
@@ -433,7 +465,7 @@
|
|
|
433
465
|
<bpmndi:BPMNShape id="BPMNShape_wait-mergeable" bpmnElement="wait-mergeable">
|
|
434
466
|
<dc:Bounds x="552" y="102" width="36" height="36" />
|
|
435
467
|
<bpmndi:BPMNLabel>
|
|
436
|
-
<dc:Bounds x="535" y="
|
|
468
|
+
<dc:Bounds x="535" y="69" width="70" height="28" />
|
|
437
469
|
</bpmndi:BPMNLabel>
|
|
438
470
|
</bpmndi:BPMNShape>
|
|
439
471
|
<bpmndi:BPMNShape id="BPMNShape_gw-mergeable" bpmnElement="gw-mergeable" isMarkerVisible="true">
|
|
@@ -454,7 +486,7 @@
|
|
|
454
486
|
<bpmndi:BPMNShape id="BPMNShape_gw-merge-retry" bpmnElement="gw-merge-retry" isMarkerVisible="true">
|
|
455
487
|
<dc:Bounds x="1263" y="1375" width="50" height="50" />
|
|
456
488
|
<bpmndi:BPMNLabel>
|
|
457
|
-
<dc:Bounds x="
|
|
489
|
+
<dc:Bounds x="1224" y="1470" width="88" height="28" />
|
|
458
490
|
</bpmndi:BPMNLabel>
|
|
459
491
|
</bpmndi:BPMNShape>
|
|
460
492
|
<bpmndi:BPMNShape id="BPMNShape_eg-landed" bpmnElement="eg-landed" isMarkerVisible="true">
|
|
@@ -472,16 +504,16 @@
|
|
|
472
504
|
<bpmndi:BPMNShape id="BPMNShape_wait-evicted" bpmnElement="wait-evicted">
|
|
473
505
|
<dc:Bounds x="1470" y="422" width="36" height="36" />
|
|
474
506
|
<bpmndi:BPMNLabel>
|
|
475
|
-
<dc:Bounds x="
|
|
507
|
+
<dc:Bounds x="1344" y="483" width="88" height="42" />
|
|
476
508
|
</bpmndi:BPMNLabel>
|
|
477
509
|
</bpmndi:BPMNShape>
|
|
478
510
|
<bpmndi:BPMNShape id="BPMNShape_mark-merged" bpmnElement="mark-merged">
|
|
479
511
|
<dc:Bounds x="1638" y="80" width="100" height="80" />
|
|
480
512
|
</bpmndi:BPMNShape>
|
|
481
513
|
<bpmndi:BPMNShape id="BPMNShape_MergeEnd" bpmnElement="MergeEnd">
|
|
482
|
-
<dc:Bounds x="
|
|
514
|
+
<dc:Bounds x="1845" y="102" width="36" height="36" />
|
|
483
515
|
<bpmndi:BPMNLabel>
|
|
484
|
-
<dc:Bounds x="
|
|
516
|
+
<dc:Bounds x="1838" y="83" width="50" height="14" />
|
|
485
517
|
</bpmndi:BPMNLabel>
|
|
486
518
|
</bpmndi:BPMNShape>
|
|
487
519
|
<bpmndi:BPMNShape id="BPMNShape_MergeAbandoned" bpmnElement="MergeAbandoned">
|
|
@@ -491,27 +523,27 @@
|
|
|
491
523
|
</bpmndi:BPMNLabel>
|
|
492
524
|
</bpmndi:BPMNShape>
|
|
493
525
|
<bpmndi:BPMNShape id="BPMNShape_merge-esc-conflict" bpmnElement="merge-esc-conflict">
|
|
494
|
-
<dc:Bounds x="1238" y="
|
|
526
|
+
<dc:Bounds x="1238" y="2000" width="100" height="80" />
|
|
495
527
|
</bpmndi:BPMNShape>
|
|
496
528
|
<bpmndi:BPMNShape id="BPMNShape_merge-esc-attempt" bpmnElement="merge-esc-attempt">
|
|
497
|
-
<dc:Bounds x="
|
|
529
|
+
<dc:Bounds x="1638" y="1360" width="100" height="80" />
|
|
498
530
|
</bpmndi:BPMNShape>
|
|
499
531
|
<bpmndi:BPMNShape id="BPMNShape_gw-merge-escalated" bpmnElement="gw-merge-escalated" isMarkerVisible="true">
|
|
500
|
-
<dc:Bounds x="
|
|
532
|
+
<dc:Bounds x="1838" y="1375" width="50" height="50" />
|
|
501
533
|
<bpmndi:BPMNLabel>
|
|
502
|
-
<dc:Bounds x="
|
|
534
|
+
<dc:Bounds x="1826" y="1470" width="74" height="28" />
|
|
503
535
|
</bpmndi:BPMNLabel>
|
|
504
536
|
</bpmndi:BPMNShape>
|
|
505
537
|
<bpmndi:BPMNShape id="BPMNShape_wait-merge-answer" bpmnElement="wait-merge-answer">
|
|
506
|
-
<dc:Bounds x="
|
|
538
|
+
<dc:Bounds x="1988" y="1360" width="100" height="80" />
|
|
507
539
|
</bpmndi:BPMNShape>
|
|
508
540
|
<bpmndi:BPMNShape id="BPMNShape_record-merge-answer" bpmnElement="record-merge-answer">
|
|
509
|
-
<dc:Bounds x="
|
|
541
|
+
<dc:Bounds x="2188" y="1360" width="100" height="80" />
|
|
510
542
|
</bpmndi:BPMNShape>
|
|
511
543
|
<bpmndi:BPMNShape id="BPMNShape_gw-ci-fix" bpmnElement="gw-ci-fix" isMarkerVisible="true">
|
|
512
|
-
<dc:Bounds x="863" y="
|
|
544
|
+
<dc:Bounds x="863" y="2015" width="50" height="50" />
|
|
513
545
|
<bpmndi:BPMNLabel>
|
|
514
|
-
<dc:Bounds x="769" y="
|
|
546
|
+
<dc:Bounds x="769" y="2033" width="89" height="14" />
|
|
515
547
|
</bpmndi:BPMNLabel>
|
|
516
548
|
</bpmndi:BPMNShape>
|
|
517
549
|
<bpmndi:BPMNShape id="BPMNShape_fix-ci" bpmnElement="fix-ci">
|
|
@@ -523,6 +555,15 @@
|
|
|
523
555
|
<dc:Bounds x="1225" y="790" width="46" height="14" />
|
|
524
556
|
</bpmndi:BPMNLabel>
|
|
525
557
|
</bpmndi:BPMNShape>
|
|
558
|
+
<bpmndi:BPMNShape id="BPMNShape_gw-ci-blocked" bpmnElement="gw-ci-blocked" isMarkerVisible="true">
|
|
559
|
+
<dc:Bounds x="1463" y="735" width="50" height="50" />
|
|
560
|
+
<bpmndi:BPMNLabel>
|
|
561
|
+
<dc:Bounds x="1455" y="810" width="67" height="42" />
|
|
562
|
+
</bpmndi:BPMNLabel>
|
|
563
|
+
</bpmndi:BPMNShape>
|
|
564
|
+
<bpmndi:BPMNShape id="BPMNShape_ci-reconcile" bpmnElement="ci-reconcile">
|
|
565
|
+
<dc:Bounds x="1638" y="560" width="100" height="80" />
|
|
566
|
+
</bpmndi:BPMNShape>
|
|
526
567
|
<bpmndi:BPMNShape id="BPMNShape_gw-rebase" bpmnElement="gw-rebase" isMarkerVisible="true">
|
|
527
568
|
<dc:Bounds x="863" y="255" width="50" height="50" />
|
|
528
569
|
<bpmndi:BPMNLabel>
|
|
@@ -589,13 +630,21 @@
|
|
|
589
630
|
</bpmndi:BPMNEdge>
|
|
590
631
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_done" bpmnElement="f_m_done">
|
|
591
632
|
<di:waypoint x="1738" y="120" />
|
|
592
|
-
<di:waypoint x="
|
|
633
|
+
<di:waypoint x="1845" y="120" />
|
|
593
634
|
</bpmndi:BPMNEdge>
|
|
594
635
|
<bpmndi:BPMNEdge id="BPMNEdge_f_ci_giveup" bpmnElement="f_ci_giveup">
|
|
595
|
-
<di:waypoint x="913" y="
|
|
596
|
-
<di:waypoint x="1238" y="
|
|
636
|
+
<di:waypoint x="913" y="2040" />
|
|
637
|
+
<di:waypoint x="1238" y="2040" />
|
|
638
|
+
<bpmndi:BPMNLabel>
|
|
639
|
+
<dc:Bounds x="1032" y="2007" width="67" height="28" />
|
|
640
|
+
</bpmndi:BPMNLabel>
|
|
641
|
+
</bpmndi:BPMNEdge>
|
|
642
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_cib_esc" bpmnElement="f_cib_esc">
|
|
643
|
+
<di:waypoint x="1513" y="760" />
|
|
644
|
+
<di:waypoint x="1688" y="760" />
|
|
645
|
+
<di:waypoint x="1688" y="1360" />
|
|
597
646
|
<bpmndi:BPMNLabel>
|
|
598
|
-
<dc:Bounds x="
|
|
647
|
+
<dc:Bounds x="1569" y="765" width="64" height="42" />
|
|
599
648
|
</bpmndi:BPMNLabel>
|
|
600
649
|
</bpmndi:BPMNEdge>
|
|
601
650
|
<bpmndi:BPMNEdge id="BPMNEdge_f_ci_reconcile" bpmnElement="f_ci_reconcile">
|
|
@@ -612,11 +661,11 @@
|
|
|
612
661
|
<di:waypoint x="933" y="280" />
|
|
613
662
|
<di:waypoint x="933" y="305" />
|
|
614
663
|
<di:waypoint x="1326" y="305" />
|
|
615
|
-
<di:waypoint x="1326" y="
|
|
616
|
-
<di:waypoint x="1288" y="
|
|
617
|
-
<di:waypoint x="1288" y="
|
|
664
|
+
<di:waypoint x="1326" y="1980" />
|
|
665
|
+
<di:waypoint x="1288" y="1980" />
|
|
666
|
+
<di:waypoint x="1288" y="2000" />
|
|
618
667
|
<bpmndi:BPMNLabel>
|
|
619
|
-
<dc:Bounds x="1331" y="
|
|
668
|
+
<dc:Bounds x="1331" y="1646" width="67" height="28" />
|
|
620
669
|
</bpmndi:BPMNLabel>
|
|
621
670
|
</bpmndi:BPMNEdge>
|
|
622
671
|
<bpmndi:BPMNEdge id="BPMNEdge_f_reb_reconcile" bpmnElement="f_reb_reconcile">
|
|
@@ -630,9 +679,9 @@
|
|
|
630
679
|
</bpmndi:BPMNEdge>
|
|
631
680
|
<bpmndi:BPMNEdge id="BPMNEdge_f_mr_giveup" bpmnElement="f_mr_giveup">
|
|
632
681
|
<di:waypoint x="1313" y="1400" />
|
|
633
|
-
<di:waypoint x="
|
|
682
|
+
<di:waypoint x="1638" y="1400" />
|
|
634
683
|
<bpmndi:BPMNLabel>
|
|
635
|
-
<dc:Bounds x="
|
|
684
|
+
<dc:Bounds x="1432" y="1367" width="67" height="28" />
|
|
636
685
|
</bpmndi:BPMNLabel>
|
|
637
686
|
</bpmndi:BPMNEdge>
|
|
638
687
|
<bpmndi:BPMNEdge id="BPMNEdge_f_eg_landed" bpmnElement="f_eg_landed">
|
|
@@ -645,30 +694,30 @@
|
|
|
645
694
|
<di:waypoint x="1688" y="160" />
|
|
646
695
|
</bpmndi:BPMNEdge>
|
|
647
696
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_escC" bpmnElement="f_m_escC">
|
|
648
|
-
<di:waypoint x="1338" y="
|
|
649
|
-
<di:waypoint x="
|
|
650
|
-
<di:waypoint x="
|
|
697
|
+
<di:waypoint x="1338" y="2040" />
|
|
698
|
+
<di:waypoint x="2038" y="2040" />
|
|
699
|
+
<di:waypoint x="2038" y="1440" />
|
|
651
700
|
</bpmndi:BPMNEdge>
|
|
652
701
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_escA" bpmnElement="f_m_escA">
|
|
653
|
-
<di:waypoint x="
|
|
654
|
-
<di:waypoint x="
|
|
702
|
+
<di:waypoint x="1738" y="1400" />
|
|
703
|
+
<di:waypoint x="1838" y="1400" />
|
|
655
704
|
</bpmndi:BPMNEdge>
|
|
656
705
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_escReenter" bpmnElement="f_m_escReenter">
|
|
657
|
-
<di:waypoint x="
|
|
658
|
-
<di:waypoint x="
|
|
706
|
+
<di:waypoint x="1863" y="1425" />
|
|
707
|
+
<di:waypoint x="1863" y="1460" />
|
|
659
708
|
<di:waypoint x="402" y="1460" />
|
|
660
709
|
<di:waypoint x="402" y="160" />
|
|
661
710
|
<bpmndi:BPMNLabel>
|
|
662
|
-
<dc:Bounds x="
|
|
711
|
+
<dc:Bounds x="1096" y="1465" width="74" height="42" />
|
|
663
712
|
</bpmndi:BPMNLabel>
|
|
664
713
|
</bpmndi:BPMNEdge>
|
|
665
714
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_answerRecord" bpmnElement="f_m_answerRecord">
|
|
666
|
-
<di:waypoint x="
|
|
667
|
-
<di:waypoint x="
|
|
715
|
+
<di:waypoint x="2088" y="1400" />
|
|
716
|
+
<di:waypoint x="2188" y="1400" />
|
|
668
717
|
</bpmndi:BPMNEdge>
|
|
669
718
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_answer" bpmnElement="f_m_answer">
|
|
670
|
-
<di:waypoint x="
|
|
671
|
-
<di:waypoint x="
|
|
719
|
+
<di:waypoint x="2238" y="1440" />
|
|
720
|
+
<di:waypoint x="2238" y="1460" />
|
|
672
721
|
<di:waypoint x="402" y="1460" />
|
|
673
722
|
<di:waypoint x="402" y="160" />
|
|
674
723
|
</bpmndi:BPMNEdge>
|
|
@@ -678,9 +727,9 @@
|
|
|
678
727
|
<di:waypoint x="1333" y="318" />
|
|
679
728
|
<di:waypoint x="1333" y="1340" />
|
|
680
729
|
<di:waypoint x="863" y="1340" />
|
|
681
|
-
<di:waypoint x="863" y="
|
|
682
|
-
<di:waypoint x="888" y="
|
|
683
|
-
<di:waypoint x="888" y="
|
|
730
|
+
<di:waypoint x="863" y="1995" />
|
|
731
|
+
<di:waypoint x="888" y="1995" />
|
|
732
|
+
<di:waypoint x="888" y="2015" />
|
|
684
733
|
<bpmndi:BPMNLabel>
|
|
685
734
|
<dc:Bounds x="1011" y="1293" width="60" height="42" />
|
|
686
735
|
</bpmndi:BPMNLabel>
|
|
@@ -699,64 +748,62 @@
|
|
|
699
748
|
<di:waypoint x="1326" y="318" />
|
|
700
749
|
<di:waypoint x="1326" y="1340" />
|
|
701
750
|
<di:waypoint x="1218" y="1340" />
|
|
702
|
-
<di:waypoint x="1218" y="
|
|
703
|
-
<di:waypoint x="1238" y="
|
|
751
|
+
<di:waypoint x="1218" y="2020" />
|
|
752
|
+
<di:waypoint x="1238" y="2020" />
|
|
704
753
|
<bpmndi:BPMNLabel>
|
|
705
754
|
<dc:Bounds x="1236" y="862" width="85" height="14" />
|
|
706
755
|
</bpmndi:BPMNLabel>
|
|
707
756
|
</bpmndi:BPMNEdge>
|
|
708
757
|
<bpmndi:BPMNEdge id="BPMNEdge_f_ci_go" bpmnElement="f_ci_go">
|
|
709
|
-
<di:waypoint x="888" y="
|
|
710
|
-
<di:waypoint x="888" y="
|
|
711
|
-
<di:waypoint x="
|
|
712
|
-
<di:waypoint x="
|
|
758
|
+
<di:waypoint x="888" y="2065" />
|
|
759
|
+
<di:waypoint x="888" y="2085" />
|
|
760
|
+
<di:waypoint x="2308" y="2085" />
|
|
761
|
+
<di:waypoint x="2308" y="715" />
|
|
713
762
|
<di:waypoint x="1018" y="715" />
|
|
714
763
|
<di:waypoint x="1018" y="740" />
|
|
715
764
|
<di:waypoint x="1038" y="740" />
|
|
716
765
|
<bpmndi:BPMNLabel>
|
|
717
|
-
<dc:Bounds x="
|
|
766
|
+
<dc:Bounds x="2313" y="1386" width="49" height="28" />
|
|
718
767
|
</bpmndi:BPMNLabel>
|
|
719
768
|
</bpmndi:BPMNEdge>
|
|
720
|
-
<bpmndi:BPMNEdge id="
|
|
721
|
-
<di:waypoint x="
|
|
722
|
-
<di:waypoint x="
|
|
723
|
-
<di:waypoint x="
|
|
724
|
-
<di:waypoint x="1438" y="1400" />
|
|
769
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_cib_recon" bpmnElement="f_cib_recon">
|
|
770
|
+
<di:waypoint x="1488" y="735" />
|
|
771
|
+
<di:waypoint x="1488" y="600" />
|
|
772
|
+
<di:waypoint x="1638" y="600" />
|
|
725
773
|
<bpmndi:BPMNLabel>
|
|
726
|
-
<dc:Bounds x="
|
|
774
|
+
<dc:Bounds x="1513" y="460" width="67" height="56" />
|
|
727
775
|
</bpmndi:BPMNLabel>
|
|
728
776
|
</bpmndi:BPMNEdge>
|
|
729
777
|
<bpmndi:BPMNEdge id="BPMNEdge_f_ci_wait" bpmnElement="f_ci_wait">
|
|
730
778
|
<di:waypoint x="1313" y="760" />
|
|
731
|
-
<di:waypoint x="
|
|
732
|
-
<di:waypoint x="
|
|
779
|
+
<di:waypoint x="1418" y="760" />
|
|
780
|
+
<di:waypoint x="1418" y="920" />
|
|
781
|
+
<di:waypoint x="1438" y="920" />
|
|
733
782
|
<bpmndi:BPMNLabel>
|
|
734
|
-
<dc:Bounds x="
|
|
783
|
+
<dc:Bounds x="1338" y="826" width="75" height="28" />
|
|
735
784
|
</bpmndi:BPMNLabel>
|
|
736
785
|
</bpmndi:BPMNEdge>
|
|
737
786
|
<bpmndi:BPMNEdge id="BPMNEdge_f_reb_go" bpmnElement="f_reb_go">
|
|
738
787
|
<di:waypoint x="913" y="280" />
|
|
739
788
|
<di:waypoint x="933" y="280" />
|
|
740
789
|
<di:waypoint x="933" y="305" />
|
|
741
|
-
<di:waypoint x="
|
|
742
|
-
<di:waypoint x="
|
|
743
|
-
<di:waypoint x="
|
|
744
|
-
<di:waypoint x="
|
|
745
|
-
<di:waypoint x="
|
|
746
|
-
<di:waypoint x="1358" y="1035" />
|
|
747
|
-
<di:waypoint x="1018" y="1035" />
|
|
790
|
+
<di:waypoint x="1443" y="305" />
|
|
791
|
+
<di:waypoint x="1443" y="805" />
|
|
792
|
+
<di:waypoint x="1558" y="805" />
|
|
793
|
+
<di:waypoint x="1558" y="980" />
|
|
794
|
+
<di:waypoint x="1018" y="980" />
|
|
748
795
|
<di:waypoint x="1018" y="1060" />
|
|
749
796
|
<di:waypoint x="1038" y="1060" />
|
|
750
797
|
<bpmndi:BPMNLabel>
|
|
751
|
-
<dc:Bounds x="
|
|
798
|
+
<dc:Bounds x="1448" y="561" width="49" height="28" />
|
|
752
799
|
</bpmndi:BPMNLabel>
|
|
753
800
|
</bpmndi:BPMNEdge>
|
|
754
801
|
<bpmndi:BPMNEdge id="BPMNEdge_f_reb_blocked" bpmnElement="f_reb_blocked">
|
|
755
802
|
<di:waypoint x="1313" y="1080" />
|
|
756
|
-
<di:waypoint x="
|
|
757
|
-
<di:waypoint x="
|
|
803
|
+
<di:waypoint x="1688" y="1080" />
|
|
804
|
+
<di:waypoint x="1688" y="1360" />
|
|
758
805
|
<bpmndi:BPMNLabel>
|
|
759
|
-
<dc:Bounds x="
|
|
806
|
+
<dc:Bounds x="1469" y="1047" width="64" height="28" />
|
|
760
807
|
</bpmndi:BPMNLabel>
|
|
761
808
|
</bpmndi:BPMNEdge>
|
|
762
809
|
<bpmndi:BPMNEdge id="BPMNEdge_f_reb_wait" bpmnElement="f_reb_wait">
|
|
@@ -778,15 +825,15 @@
|
|
|
778
825
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_gRetry" bpmnElement="f_m_gRetry">
|
|
779
826
|
<di:waypoint x="1088" y="95" />
|
|
780
827
|
<di:waypoint x="1088" y="75" />
|
|
781
|
-
<di:waypoint x="
|
|
782
|
-
<di:waypoint x="
|
|
783
|
-
<di:waypoint x="
|
|
784
|
-
<di:waypoint x="
|
|
828
|
+
<di:waypoint x="1901" y="75" />
|
|
829
|
+
<di:waypoint x="1901" y="145" />
|
|
830
|
+
<di:waypoint x="1825" y="145" />
|
|
831
|
+
<di:waypoint x="1825" y="1445" />
|
|
785
832
|
<di:waypoint x="1263" y="1445" />
|
|
786
833
|
<di:waypoint x="1288" y="1445" />
|
|
787
834
|
<di:waypoint x="1288" y="1425" />
|
|
788
835
|
<bpmndi:BPMNLabel>
|
|
789
|
-
<dc:Bounds x="
|
|
836
|
+
<dc:Bounds x="1414" y="1412" width="78" height="28" />
|
|
790
837
|
</bpmndi:BPMNLabel>
|
|
791
838
|
</bpmndi:BPMNEdge>
|
|
792
839
|
<bpmndi:BPMNEdge id="BPMNEdge_f_eg_evicted" bpmnElement="f_eg_evicted">
|
|
@@ -797,13 +844,15 @@
|
|
|
797
844
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_gBlocked" bpmnElement="f_m_gBlocked">
|
|
798
845
|
<di:waypoint x="1088" y="95" />
|
|
799
846
|
<di:waypoint x="1088" y="75" />
|
|
800
|
-
<di:waypoint x="
|
|
801
|
-
<di:waypoint x="
|
|
802
|
-
<di:waypoint x="
|
|
803
|
-
<di:waypoint x="
|
|
804
|
-
<di:waypoint x="
|
|
847
|
+
<di:waypoint x="1901" y="75" />
|
|
848
|
+
<di:waypoint x="1901" y="145" />
|
|
849
|
+
<di:waypoint x="1825" y="145" />
|
|
850
|
+
<di:waypoint x="1825" y="1445" />
|
|
851
|
+
<di:waypoint x="1618" y="1445" />
|
|
852
|
+
<di:waypoint x="1618" y="1420" />
|
|
853
|
+
<di:waypoint x="1638" y="1420" />
|
|
805
854
|
<bpmndi:BPMNLabel>
|
|
806
|
-
<dc:Bounds x="
|
|
855
|
+
<dc:Bounds x="1837" y="153" width="53" height="14" />
|
|
807
856
|
</bpmndi:BPMNLabel>
|
|
808
857
|
</bpmndi:BPMNEdge>
|
|
809
858
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_gAbandoned" bpmnElement="f_m_gAbandoned">
|
|
@@ -822,20 +871,27 @@
|
|
|
822
871
|
<di:waypoint x="1138" y="760" />
|
|
823
872
|
<di:waypoint x="1263" y="760" />
|
|
824
873
|
</bpmndi:BPMNEdge>
|
|
874
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_ci_blocked" bpmnElement="f_ci_blocked">
|
|
875
|
+
<di:waypoint x="1313" y="760" />
|
|
876
|
+
<di:waypoint x="1463" y="760" />
|
|
877
|
+
<bpmndi:BPMNLabel>
|
|
878
|
+
<dc:Bounds x="1341" y="738" width="89" height="14" />
|
|
879
|
+
</bpmndi:BPMNLabel>
|
|
880
|
+
</bpmndi:BPMNEdge>
|
|
825
881
|
<bpmndi:BPMNEdge id="BPMNEdge_f_reb_done" bpmnElement="f_reb_done">
|
|
826
882
|
<di:waypoint x="1138" y="1080" />
|
|
827
883
|
<di:waypoint x="1263" y="1080" />
|
|
828
884
|
</bpmndi:BPMNEdge>
|
|
829
885
|
<bpmndi:BPMNEdge id="BPMNEdge_f_m_escWait" bpmnElement="f_m_escWait">
|
|
830
|
-
<di:waypoint x="
|
|
831
|
-
<di:waypoint x="
|
|
886
|
+
<di:waypoint x="1888" y="1400" />
|
|
887
|
+
<di:waypoint x="1988" y="1400" />
|
|
832
888
|
</bpmndi:BPMNEdge>
|
|
833
889
|
<bpmndi:BPMNEdge id="BPMNEdge_f_reb_sla" bpmnElement="f_reb_sla">
|
|
834
890
|
<di:waypoint x="1088" y="1138" />
|
|
835
891
|
<di:waypoint x="1088" y="1158" />
|
|
836
892
|
<di:waypoint x="1218" y="1158" />
|
|
837
|
-
<di:waypoint x="1218" y="
|
|
838
|
-
<di:waypoint x="1238" y="
|
|
893
|
+
<di:waypoint x="1218" y="2040" />
|
|
894
|
+
<di:waypoint x="1238" y="2040" />
|
|
839
895
|
<bpmndi:BPMNLabel>
|
|
840
896
|
<dc:Bounds x="1118" y="1125" width="70" height="28" />
|
|
841
897
|
</bpmndi:BPMNLabel>
|
|
@@ -845,12 +901,15 @@
|
|
|
845
901
|
<di:waypoint x="1088" y="838" />
|
|
846
902
|
<di:waypoint x="1018" y="838" />
|
|
847
903
|
<di:waypoint x="1018" y="700" />
|
|
848
|
-
<di:waypoint x="
|
|
849
|
-
<di:waypoint x="
|
|
850
|
-
<di:waypoint x="
|
|
851
|
-
<di:waypoint x="
|
|
904
|
+
<di:waypoint x="1358" y="700" />
|
|
905
|
+
<di:waypoint x="1358" y="540" />
|
|
906
|
+
<di:waypoint x="1758" y="540" />
|
|
907
|
+
<di:waypoint x="1758" y="1980" />
|
|
908
|
+
<di:waypoint x="1618" y="1980" />
|
|
909
|
+
<di:waypoint x="1618" y="1400" />
|
|
910
|
+
<di:waypoint x="1638" y="1400" />
|
|
852
911
|
<bpmndi:BPMNLabel>
|
|
853
|
-
<dc:Bounds x="
|
|
912
|
+
<dc:Bounds x="1363" y="606" width="70" height="28" />
|
|
854
913
|
</bpmndi:BPMNLabel>
|
|
855
914
|
</bpmndi:BPMNEdge>
|
|
856
915
|
<bpmndi:BPMNEdge id="BPMNEdge_f_ci_fixed" bpmnElement="f_ci_fixed">
|
|
@@ -859,9 +918,24 @@
|
|
|
859
918
|
<di:waypoint x="402" y="840" />
|
|
860
919
|
<di:waypoint x="402" y="160" />
|
|
861
920
|
<bpmndi:BPMNLabel>
|
|
862
|
-
<dc:Bounds x="
|
|
921
|
+
<dc:Bounds x="906" y="818" width="39" height="14" />
|
|
863
922
|
</bpmndi:BPMNLabel>
|
|
864
923
|
</bpmndi:BPMNEdge>
|
|
924
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_ci_reattempt" bpmnElement="f_ci_reattempt">
|
|
925
|
+
<di:waypoint x="1288" y="785" />
|
|
926
|
+
<di:waypoint x="1288" y="840" />
|
|
927
|
+
<di:waypoint x="402" y="840" />
|
|
928
|
+
<di:waypoint x="402" y="160" />
|
|
929
|
+
<bpmndi:BPMNLabel>
|
|
930
|
+
<dc:Bounds x="801" y="845" width="88" height="42" />
|
|
931
|
+
</bpmndi:BPMNLabel>
|
|
932
|
+
</bpmndi:BPMNEdge>
|
|
933
|
+
<bpmndi:BPMNEdge id="BPMNEdge_f_cib_armed" bpmnElement="f_cib_armed">
|
|
934
|
+
<di:waypoint x="1688" y="640" />
|
|
935
|
+
<di:waypoint x="1688" y="660" />
|
|
936
|
+
<di:waypoint x="570" y="660" />
|
|
937
|
+
<di:waypoint x="570" y="138" />
|
|
938
|
+
</bpmndi:BPMNEdge>
|
|
865
939
|
<bpmndi:BPMNEdge id="BPMNEdge_f_reb_rebased" bpmnElement="f_reb_rebased">
|
|
866
940
|
<di:waypoint x="1288" y="1105" />
|
|
867
941
|
<di:waypoint x="1288" y="1160" />
|
|
@@ -75,15 +75,30 @@ Return a structured result:
|
|
|
75
75
|
first, as `owner/repo#N` refs (or PR URLs), separated by commas or spaces. The
|
|
76
76
|
process records the dependency and automatically re-attempts the merge once every
|
|
77
77
|
named PR has landed.
|
|
78
|
+
- `status: "reattempt"` — you pushed **no** commit because there was **nothing to fix
|
|
79
|
+
on the branch**: the failing required checks are **stale / transient** — e.g.
|
|
80
|
+
`CANCELLED` runs superseded by a newer run on the *identical head SHA* (GitHub CI
|
|
81
|
+
concurrency), a newer run on the same SHA that is already green, or an infrastructure
|
|
82
|
+
blip — and the PR's head is actually mergeable. This is **not** a defect and **not** a
|
|
83
|
+
human decision: the merge should simply be **re-queued from ground truth**. Set
|
|
84
|
+
`pushed: false` and say in `summary` why the checks are stale (which runs, on which
|
|
85
|
+
SHA). The process re-arms the merge poller, which re-derives mergeability and merges
|
|
86
|
+
when the head is green — **no human is pulled in**. Prefer this over `blocked` for
|
|
87
|
+
every "nothing was wrong / the failure was transient / the head already looks green"
|
|
88
|
+
case.
|
|
78
89
|
- `status: "blocked"` — you could **not** fix it and it genuinely needs a human
|
|
79
90
|
**decision** (a secret, an upstream change, or a judgement call). Set `question` to a
|
|
80
91
|
concise, specific description of what is blocking and what a human must decide.
|
|
81
|
-
Reserve this for a real decision — if the PR is merely waiting on another PR
|
|
82
|
-
`waiting-on-pr
|
|
92
|
+
Reserve this for a **real** human decision — if the PR is merely waiting on another PR
|
|
93
|
+
use `waiting-on-pr`, and if the failing checks are stale/transient with nothing to fix
|
|
94
|
+
use `reattempt`. `blocked` **pages a human**, so never use it for a self-healing PR.
|
|
83
95
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
96
|
+
Report whether you pushed via `pushed` (`true` after you commit + push a fix, `false`
|
|
97
|
+
when you changed nothing). Never report `fixed` unless you actually pushed a change. If
|
|
98
|
+
nothing was wrong on the branch (the failure was transient infrastructure / stale
|
|
99
|
+
cancelled checks and the head is green), return **`reattempt`** (with `pushed: false`)
|
|
100
|
+
so the merge is simply re-attempted from ground truth — do **not** return `blocked`,
|
|
101
|
+
which would needlessly page a human.
|
|
87
102
|
|
|
88
103
|
### How to return it (the wire mechanism)
|
|
89
104
|
|
|
@@ -98,11 +113,13 @@ machine-readable result one of two ways:
|
|
|
98
113
|
|
|
99
114
|
```sh
|
|
100
115
|
# pushed a fix you believe turns the failing checks green:
|
|
101
|
-
printf '%s' '{"status":"fixed","summary":"Fixed the flaky timeout in auth.test.ts and pushed"}' > "$AGENT_RESULT_FILE"
|
|
116
|
+
printf '%s' '{"status":"fixed","pushed":true,"summary":"Fixed the flaky timeout in auth.test.ts and pushed"}' > "$AGENT_RESULT_FILE"
|
|
117
|
+
# nothing to fix — the failing checks are stale/transient (CANCELLED superseded on the same head SHA, head already green); just re-attempt the merge:
|
|
118
|
+
printf '%s' '{"status":"reattempt","pushed":false,"summary":"Failing checks are CANCELLED runs superseded by a green run on the same head SHA — nothing to fix, re-queue the merge"}' > "$AGENT_RESULT_FILE"
|
|
102
119
|
# ordering constraint — must wait for another PR to land first:
|
|
103
|
-
printf '%s' '{"status":"waiting-on-pr","summary":"Blocked by the linked-issue gate","dependsOn":"owner/repo#123"}' > "$AGENT_RESULT_FILE"
|
|
120
|
+
printf '%s' '{"status":"waiting-on-pr","pushed":false,"summary":"Blocked by the linked-issue gate","dependsOn":"owner/repo#123"}' > "$AGENT_RESULT_FILE"
|
|
104
121
|
# genuinely stuck — a human must decide:
|
|
105
|
-
printf '%s' '{"status":"blocked","summary":"CI needs an NPM_TOKEN secret I cannot set","question":"Add the NPM_TOKEN repo secret, then answer to rerun."}' > "$AGENT_RESULT_FILE"
|
|
122
|
+
printf '%s' '{"status":"blocked","pushed":false,"summary":"CI needs an NPM_TOKEN secret I cannot set","question":"Add the NPM_TOKEN repo secret, then answer to rerun."}' > "$AGENT_RESULT_FILE"
|
|
106
123
|
```
|
|
107
124
|
|
|
108
125
|
Write this file **once**, at the very end, with your final result. Keep it a flat
|
|
@@ -125,7 +142,10 @@ a non-zero exit means a genuine crash and the job is retried.
|
|
|
125
142
|
**Emitting a machine-readable result is your mandatory final step — never exit
|
|
126
143
|
silently.** It is the last thing you do on every path out of this job (including after
|
|
127
144
|
a push, or when you conclude nothing can be fixed). If you are ever unsure which status
|
|
128
|
-
applies
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
145
|
+
applies but the PR's **head looks green / the failure was transient** (stale or
|
|
146
|
+
cancelled checks, an infra blip), return **`reattempt`** (with `pushed: false`) so the
|
|
147
|
+
merge is simply re-attempted from ground truth — do **not** default to `blocked`, which
|
|
148
|
+
pages a human. Reserve `blocked` for a genuine human decision with a concrete
|
|
149
|
+
`question`. A missing result is treated by the process as *no verdict* and reconciled
|
|
150
|
+
from ground truth (it re-arms the merge poller), which is slower and drops your explicit
|
|
151
|
+
classification — emit `reattempt` explicitly rather than relying on that fall-through.
|