Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
bump: patch
---

### Fixed

- report-outcome's hand-backs (pushback, stopped, a refused patch, a dimension skip and a turn cap with no restart left) open with `<!-- work-items:escalation lane=work-resolve-issue kind=escalated -->`, then the outcome marker, so `/work-items:attend-queue` lists the issue as escalated. The outcome-marker parser that `issue-prior`, work-unblock and the monitor signals share reads the outcome marker after that one line.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
bump: minor
---

### Fixed

- `reference/escalation-marker.md` now defines the trusted escalation authors in one place: the session's write identity, the target's `merge.agent-identity` from `docs/conventions/pr-pipeline.yaml` on its default branch, and `<lane_app_slug>[bot]` when that option is set. `/work-items:attend-queue` and `reference/item-content-trust.md` point at it, so markers the CI lanes post as the lanes App list as escalated, and a marker from any other author is reported as a suspected spoof.
- `/work-items:attend-queue` lists open pull requests carrying the human-gated label beside issues, through the github adapter's new "List pull requests" operation, since `list-items` returns issues only. A pull request row takes the answer comment and loses the human-gated label, never the autonomous-eligible role or a `work-class:` label.
20 changes: 13 additions & 7 deletions .github/scripts/work-resolve-issue/report-outcome.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,13 @@ const COMPARE_FILES_CAP = 300;
// clampReset from here.
export { clampReset };
const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?Z$/;
const MARKER = /^<!-- work-resolve-issue outcome=([a-z]+(?:-[a-z]+)*)(?: resets=(\S+))?(?: cap=(\d+))?(?: run=(\d+))?(?: branch=(resumed))? -->/;
// The attended queue's escalation marker (work-items reference
// escalation-marker.md): the first line of every comment that hands the issue
// to a person, so /work-items:attend-queue lists it as escalated. The outcome
// marker follows on the next line.
export const ESCALATION_MARKER = "<!-- work-items:escalation lane=work-resolve-issue kind=escalated -->";
const escalated = (lines) => [ESCALATION_MARKER, ...lines];
const MARKER = /^(?:<!-- work-items:escalation lane=work-resolve-issue kind=escalated -->\r?\n)?<!-- work-resolve-issue outcome=([a-z]+(?:-[a-z]+)*)(?: resets=(\S+))?(?: cap=(\d+))?(?: run=(\d+))?(?: branch=(resumed))? -->/;

// `branch=resumed` marks a resumed run that kept no branch of its own, so its
// work is still on the branch it resumed (see findPrior).
Expand All @@ -190,7 +196,7 @@ export const turnCapMarker = ({ cap, runId, onResumed = false }) =>
export const refsHoldMarker = (pr) => `<!-- work-resolve-issue refs-hold #${pr} -->`;

// {outcome, resets, run, onResumed} from a comment body that opens with the lane's
// marker, or null. resets is epoch milliseconds, or null when absent or
// marker, alone or after ESCALATION_MARKER, or null. resets is epoch milliseconds, or null when absent or
// unreadable; onResumed is true for `branch=resumed`.
export function parseOutcomeMarker(body) {
const match = MARKER.exec(String(body ?? ""));
Expand Down Expand Up @@ -316,7 +322,7 @@ const bullets = (items) =>
// this report already handed on.
function handBack({ outcome, issueNumber, branch, humanLabel, runUrl, runId, onResumed = false }) {
const pushback = outcome.exit === "pushback";
const lines = [
const lines = escalated([
`<!-- work-resolve-issue outcome=${outcome.exit}${runId ? ` run=${runId}` : ""}${resumedTag(onResumed)} -->`,
pushback
? "The work-resolve-issue lane did not start this issue: it needs a person to make it clearer first."
Expand All @@ -325,7 +331,7 @@ function handBack({ outcome, issueNumber, branch, humanLabel, runUrl, runId, onR
"Why, in the run's own words:",
"",
quoted(outcome.reason || outcome.summary),
];
]);
if (outcome.notDone.length) {
lines.push("", "What the run says it did not do:", "", bullets(outcome.notDone.map(({ what }) => what)));
}
Expand Down Expand Up @@ -379,10 +385,10 @@ const RETIRED_SKIPS = new Set(["not-assessed"]);
// A run its issue dimensions skipped before any model turn: the issue goes to
// a person with the next step, and agent-ready comes off.
function dimensionSkipPlan({ reason, issueNumber, humanLabel, runUrl, runId }) {
const lines = [
const lines = escalated([
`<!-- work-resolve-issue outcome=${reason}${runId ? ` run=${runId}` : ""} -->`,
`The work-resolve-issue lane did not start this issue (\`${reason}\`). ${DIMENSION_STEPS[reason]}`,
];
]);
if (runUrl) {
lines.push("", `Run: ${runUrl}`);
}
Expand Down Expand Up @@ -461,7 +467,7 @@ function turnCapPlan({ issueNumber, branch, cap, restart, humanLabel, runId, run
exit: "turn-cap",
issueNumber,
runId,
comment: lines.join("\n"),
comment: (restart ? lines : escalated(lines)).join("\n"),
addLabels: restart ? [] : [humanLabel],
removeLabels: restart ? [] : ["agent-ready"],
};
Expand Down
55 changes: 47 additions & 8 deletions .github/scripts/work-resolve-issue/report-outcome.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import {
} from "./report-outcome.mjs";

const BRANCH = "work-resolve-issue/issue-77-123-1";
// The first line of every hand-back comment: the attended queue's escalation
// marker in the grammar of plugins/work-items/reference/escalation-marker.md.
const ESC = "<!-- work-items:escalation lane=work-resolve-issue kind=escalated -->\n";

// The run-checks job's `checks` output for `branch` at `tip`, and the ref
// read main compares that tip with. A test puts it in the outcome directory as
Expand Down Expand Up @@ -708,7 +711,7 @@ for (const [reason, step] of [
assert.deepEqual(calls.find((c) => c.method === "POST" && c.path.endsWith("/labels")).body, { labels: ["needs-human"] });
assert.ok(calls.some((c) => c.method === "DELETE" && c.path.endsWith("/labels/agent-ready")));
const comment = calls.find((c) => c.method === "POST" && c.path.endsWith("/comments")).body.body;
assert.match(comment, new RegExp(`^<!-- work-resolve-issue outcome=${reason} run=123 -->`));
assert.match(comment, new RegExp(`^${ESC}<!-- work-resolve-issue outcome=${reason} run=123 -->`));
assert.equal(parseOutcomeMarker(comment).outcome, reason);
assert.ok(comment.includes(step), comment);
assert.ok(comment.includes("https://example/o/r/actions/runs/123"), comment);
Expand Down Expand Up @@ -807,7 +810,7 @@ test("main on done opens no PR and hands the issue back when the runner's checks
assert.ok(!calls.some((c) => c.method === "POST" && c.path.endsWith("/pulls")), label);
assert.deepEqual(calls.find((c) => c.path.endsWith("/issues/77/labels")).body, { labels: ["needs-human"] }, label);
const comment = calls.find((c) => c.path.endsWith("/issues/77/comments")).body.body;
assert.ok(comment.startsWith("<!-- work-resolve-issue outcome=stopped run=123 -->"), comment);
assert.ok(comment.startsWith(ESC + "<!-- work-resolve-issue outcome=stopped run=123 -->"), comment);
},
);
}
Expand Down Expand Up @@ -946,7 +949,7 @@ test("main on a resumed done with no commit of its own and no check results hand
assert.equal(await main({ env: env(dir), fetch }), 0);
assert.ok(!calls.some((c) => c.path.endsWith("/pulls")));
const comment = calls.find((c) => c.path.endsWith("/issues/77/comments")).body.body;
assert.ok(comment.startsWith("<!-- work-resolve-issue outcome=stopped"), comment);
assert.ok(comment.startsWith(ESC + "<!-- work-resolve-issue outcome=stopped"), comment);
assert.match(comment, /resumed .* no new commit/);
assert.ok(comment.includes(RESUMED), comment);
assert.deepEqual(calls.find((c) => c.method === "POST" && c.path.endsWith("/labels")).body, { labels: ["needs-human"] });
Expand Down Expand Up @@ -1014,7 +1017,7 @@ test("main on a resumed done hands back when its branch's checks fail, moved, or
assert.equal(await main({ env: env(dir), fetch }), 0, label);
assert.ok(!calls.some((c) => c.method === "POST" && c.path.endsWith("/pulls")), label);
const comment = calls.find((c) => c.path.endsWith("/issues/77/comments")).body.body;
assert.ok(comment.startsWith("<!-- work-resolve-issue outcome=stopped"), `${label}: ${comment}`);
assert.ok(comment.startsWith(ESC + "<!-- work-resolve-issue outcome=stopped"), `${label}: ${comment}`);
assert.ok(comment.includes(RESUMED), `${label}: ${comment}`);
},
);
Expand Down Expand Up @@ -1047,7 +1050,7 @@ test("main closes a resumed branch's new PR whose head is not the tip the runner
assert.deepEqual(calls.find((c) => c.method === "PATCH" && c.path.endsWith("/pulls/90"))?.body, { state: "closed" });
assert.deepEqual(calls.find((c) => c.path.endsWith("/issues/77/labels")).body, { labels: ["needs-human"] });
const comment = calls.find((c) => c.path.endsWith("/issues/77/comments")).body.body;
assert.ok(comment.startsWith("<!-- work-resolve-issue outcome=stopped"), comment);
assert.ok(comment.startsWith(ESC + "<!-- work-resolve-issue outcome=stopped"), comment);
assert.ok(comment.includes(CHECKED_TIP), comment);
},
);
Expand Down Expand Up @@ -1346,7 +1349,7 @@ test("a run at its turn cap with no restart left hands back, naming the cap", ()
assert.equal(plan.action, "hand-back");
assert.deepEqual(plan.addLabels, ["needs-human"]);
assert.deepEqual(plan.removeLabels, ["agent-ready"]);
assert.ok(plan.comment.startsWith("<!-- work-resolve-issue outcome=turn-cap cap=75 run=555 -->\n"), plan.comment);
assert.ok(plan.comment.startsWith(ESC + "<!-- work-resolve-issue outcome=turn-cap cap=75 run=555 -->\n"), plan.comment);
assert.match(plan.comment, /turn cap 75 reached/);
}
});
Expand All @@ -1362,7 +1365,7 @@ test("a run at its turn cap that committed nothing names no branch", () => {
test("a forged turn-cap class beside a readable outcome keeps that outcome", () => {
assert.equal(capped({ outcome: done }).action, "open-pr");
const stoppedPlan = capped({ outcome: { exit: "stopped", reason: "The tests cannot run here.", title: "", summary: "" } });
assert.ok(stoppedPlan.comment.startsWith("<!-- work-resolve-issue outcome=stopped run=555 -->"), stoppedPlan.comment);
assert.ok(stoppedPlan.comment.startsWith(ESC + "<!-- work-resolve-issue outcome=stopped run=555 -->"), stoppedPlan.comment);
});

test("the turn-cap marker reads back with its run", () => {
Expand Down Expand Up @@ -1978,7 +1981,7 @@ test("hand-back markers carry run= and read back with it", () => {
{ exit: "pushback", reason: "Which file?", title: "", summary: "" },
]) {
const plan = decide({ outcome, issueNumber: 77, issueLabels: [], branch: BRANCH, changedFiles: [], runId: "555" });
assert.ok(plan.comment.startsWith(`<!-- work-resolve-issue outcome=${outcome.exit} run=555 -->\n`), plan.comment);
assert.ok(plan.comment.startsWith(`${ESC}<!-- work-resolve-issue outcome=${outcome.exit} run=555 -->\n`), plan.comment);
assert.deepEqual(parseOutcomeMarker(plan.comment), { outcome: outcome.exit, resets: null, run: "555", onResumed: false });
}
});
Expand Down Expand Up @@ -2527,3 +2530,39 @@ test("the runner records an empty result list when the activity lists no checks"
assert.equal(run.status, 0, run.stdout);
assert.deepEqual(run.checks.results, []);
});

// /work-items:attend-queue lists an issue as escalated only from a comment
// whose first line is exactly the escalation marker (work-items reference
// escalation-marker.md), so every plan that adds the human-gated label opens
// with it, and a plan that keeps agent-ready for a restart carries none.
test("every hand-back opens with the attended queue's escalation marker, and a restart carries none", () => {
const plain = { issueNumber: 77, issueLabels: [], branch: BRANCH, changedFiles: [], runId: "555" };
const handBacks = [
decide({ ...plain, outcome: { exit: "pushback", reason: "Which file?", title: "", summary: "" } }),
decide({ ...plain, outcome: { exit: "stopped", reason: "Tests failed.", title: "", summary: "" } }),
decide({ ...plain, outcome: { exit: "done", reason: "", title: "", summary: "" }, escalation: "patch-protected-path" }),
capped({ limits: { "turn-cap-restarts": 0 } }),
];
for (const plan of handBacks) {
assert.deepEqual(plan.addLabels, ["needs-human"], plan.comment);
assert.equal(plan.comment.split("\n")[0], "<!-- work-items:escalation lane=work-resolve-issue kind=escalated -->", plan.comment);
}
const restarts = [
capped(),
decideFunding({ funding: { regain: null } }),
decideFunding({ limit: { resets: Date.parse("2026-10-17T18:00:00Z") }, now: Date.parse("2026-10-17T12:00:00Z") }),
];
for (const plan of restarts) {
assert.deepEqual(plan.addLabels, [], plan.comment);
assert.ok(!plan.comment.includes("work-items:escalation"), plan.comment);
}
});

test("an outcome marker reads back after the escalation marker line, and after no other line", () => {
const outcome = "<!-- work-resolve-issue outcome=stopped run=9 -->\nWhy.";
const read = { outcome: "stopped", resets: null, run: "9", onResumed: false };
assert.deepEqual(parseOutcomeMarker(`<!-- work-items:escalation lane=work-resolve-issue kind=escalated -->\n${outcome}`), read);
assert.deepEqual(parseOutcomeMarker(`<!-- work-items:escalation lane=work-resolve-issue kind=escalated -->\r\n${outcome}`), read);
assert.equal(parseOutcomeMarker(`<!-- work-items:escalation lane=pr-unblock kind=escalated -->\n${outcome}`), null);
assert.equal(parseOutcomeMarker(`Quoting <!-- work-items:escalation lane=work-resolve-issue kind=escalated -->\n${outcome}`), null);
});
2 changes: 1 addition & 1 deletion docs/conventions/pr-pipeline/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1786,7 +1786,7 @@ every hour (and on a manual run from `main`) with no model. It reads the signals
| 6 report-only audit | the newest scheduled run of `maintenance-audit-test-selection` or `maintenance-check-upstream-drift` failed, which each does on findings | `audit:<workflow>` |
| 7 platform incident | githubstatus.com has an incident naming Actions that is open or began in the window, or shows Actions not operational | `platform:<incident>`, `platform:actions` |
| 8 throughput | no PR by `merge.agent-identity` merged for 3 hours while pr-merge's newest live sweep, at most 70 minutes old, counts candidates | `throughput-zero` |
| 9 lane stop | a comment by `merge.agent-identity` created in the window starts with a `<!-- work-resolve-issue outcome=funding\|limited\|turn-cap ... -->` marker, one issue per outcome listing each stopped issue | `lane-stop:funding`, `lane-stop:limited`, `lane-stop:turn-cap` |
| 9 lane stop | a comment by `merge.agent-identity` created in the window starts with a `<!-- work-resolve-issue outcome=funding\|limited\|turn-cap ... -->` marker, alone or after the attended queue's escalation marker line, one issue per outcome listing each stopped issue | `lane-stop:funding`, `lane-stop:limited`, `lane-stop:turn-cap` |

An issue's first body line is `<!-- factory-monitor key=<key> fp=<fingerprint> -->`; the body names
the signal, the owning area and the key, and carries the evidence inside a text fence as inert lines.
Expand Down
11 changes: 7 additions & 4 deletions docs/conventions/pr-pipeline/pr-run-activity.md
Original file line number Diff line number Diff line change
Expand Up @@ -1733,7 +1733,7 @@ listing and re-fires the issue on main's files, once per version of the two file
`probe-first`) takes no model turn and leaves no outcome, so the caller's `report-skip` job,
which needs `admit` and `pick-slot` only, runs `report-outcome` in the write file with
`dimension-skip`: it labels the issue `needs-human`, removes `agent-ready`, and comments the next
step under an `outcome=<reason>` marker. What changes from the PR flow:
step under the escalation marker and an `outcome=<reason>` marker. What changes from the PR flow:

- Step 1 also fails red unless the event is `issues`, `issue-number` is the event's issue,
`trusted-base` is `default-tip`, and `pr-number` and `dispatcher-run-id` are empty; in the read
Expand Down Expand Up @@ -1820,12 +1820,15 @@ step under an `outcome=<reason>` marker. What changes from the PR flow:
for the compare listing to show whole. It adds the same line when the issue carries `merge: human` whose newest `labeled`
event is a listed human's or the lanes App's, or cannot be read, and never for anyone else's.
The run's own summary is quoted with issue references broken, so squash-merging the PR closes only `#<n>`. On `pushback` or `stopped`, and when the outcome file is missing
or invalid, it labels the issue with the human-gated role label and comments why; pushback also
removes `agent-ready`. Each comment opens with the marker
or invalid, it labels the issue with the human-gated role label, removes `agent-ready`, and
comments why. Every comment that adds the human-gated label opens with the attended
queue's escalation marker `<!-- work-items:escalation lane=work-resolve-issue kind=escalated -->`,
so `/work-items:attend-queue` lists the issue, and then the outcome marker
`<!-- work-resolve-issue outcome=<exit> run=<run id> -->`, which `issue-prior` matches to the
branches of that run (a marker naming no run covers every branch whose tip is older than it).
Comments that keep `agent-ready` open with the outcome marker alone.
A run that wrote no readable outcome file and whose lane record has class `max-turns` is a
turn-cap stop: the comment opens with `<!-- work-resolve-issue outcome=turn-cap cap=<n> run=<run id> -->`
turn-cap stop: the comment's outcome marker is `<!-- work-resolve-issue outcome=turn-cap cap=<n> run=<run id> -->`
and says "turn cap <n> reached" and names the branch. While the turn-cap markers since a person
listed human last added `agent-ready` are fewer than `limits.turn-cap-restarts`, it keeps `agent-ready` and
adds no label, and the work-unblock sweep restarts the issue as it does after a limit stop;
Expand Down
Loading
Loading