From a5b7b2f975acf2f52767457eff9fb92f52ee531f Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Tue, 27 Oct 2026 13:55:24 +0800 Subject: [PATCH 1/4] gate: answer `merged` before the edit check, and finish the #1348 prose (#1350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four non-blocking findings from the review of #1348. The source change is one ordering: in `frozenRefusal()` the `merged` short-circuit now runs before the "this pull request edits the list" check, so a merged pull request that touched `frozen-branches.json` is no longer refused when a later `labeled` event re-runs the guard — the comment in that function already said a merged pull request is never re-judged (#943), and the code said the opposite. The edit check still refuses every unmerged pull request, which is the case it exists for. The rest is AGENTS.md: the seal now names its own bypass (a renamed branch, or a body whose marker was never there, walks past it); rule 7 matches the shipped `accountRefusal()` — only `type === 'User'` counts, a report with no `type` is refused too, and the account that wrote it is a note in `judged` rather than a clause of the refusal; and the release sentence is bound to the mechanical fact that releases it, deleting the file or its two arrays on `main`. --- .github/scripts/enqueue-guard.mjs | 6 +++++- .github/scripts/enqueue-guard.test.mjs | 9 +++++++++ AGENTS.md | 6 +++--- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/scripts/enqueue-guard.mjs b/.github/scripts/enqueue-guard.mjs index c53e1a089..618c15d6f 100644 --- a/.github/scripts/enqueue-guard.mjs +++ b/.github/scripts/enqueue-guard.mjs @@ -78,11 +78,15 @@ function frozenList({ gh, repo, base }) { // The refusal this file exists for, or `null`: the head branch starts with a listed prefix, the body carries // a listed marker, or the diff edits the list itself. `merged` is history and is never re-judged (#943). function frozenRefusal({ gh, repo, pull, files }) { + // #1350: `merged` is answered before the list is read, and before the edit check below — the guard runs + // again on every event (`labeled` above all), and a merged pull request that edited this file has already + // made its edit. Judged after it, that run goes red on a merge nobody can undo, which is the opposite of + // what "never re-judged" means. The edit check still refuses every unmerged pull request. + if (pull?.merged) return null const touched = (files ?? []).find((f) => f && f.filename === FROZEN_PATH) // An ADD is how the seal first arrives (this very pull request). A modify or a delete is the self-unlock // this rule closes, whatever the branch is. if (touched && touched.status !== 'added') return FROZEN_EDIT_REASON - if (pull?.merged) return null const list = frozenList({ gh, repo, base: pull?.base?.sha }) const branch = String(pull?.head?.ref ?? '').toLowerCase() const body = String(pull?.body ?? '').toLowerCase() diff --git a/.github/scripts/enqueue-guard.test.mjs b/.github/scripts/enqueue-guard.test.mjs index 7bd03c462..a52640e21 100644 --- a/.github/scripts/enqueue-guard.test.mjs +++ b/.github/scripts/enqueue-guard.test.mjs @@ -990,3 +990,12 @@ test('#1344: adding the list is how the seal arrives — an add is not an edit', const r = run({ branch: 'dev/1344-user-only-conclusions', comments: [comment()], baseFrozen: null, files: [HOST_FILE, { filename: FROZEN_PATH, status: 'added' }] }) assert.equal(r.ok, true, r.reasons.join('; ')) }) + +// #1350: `merged` is history and is answered before anything else. The guard is re-run on every event +// (`labeled` above all), so a merged pull request that edited this file would otherwise go red on a later +// run — the refusal exists to stop a seal being undone, and that merge has already happened. +test('#1350: a merged pull request is not refused, even when its diff edited the frozen list', () => { + const r = run({ merged: true, files: [HOST_FILE, { filename: FROZEN_PATH, status: 'modified' }], comments: [comment()] }) + assert.equal(r.ok, true, r.reasons.join('; ')) + assert.equal(r.reasons.length, 0) +}) diff --git a/AGENTS.md b/AGENTS.md index 4c01b0357..de2f6e0f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -200,7 +200,7 @@ Four rules decide what a queue entry must pass. The mechanical judge runs where A review's `state` is not read at all: approvals are not a gate any more (user decision 2026-10-22), and the account cannot approve its own pull requests, so its reviews stay `COMMENTED` even for an INC — the INC on #610 was a comment rather than a review. -**One conclusion per tip.** A tip carries at most one conclusion: one `Verdict:` block naming one SHA — the whole 40 characters, because a prefix names every head that starts that way and the newest of them is the tip being judged (#1112). **A tip that moved without changing the reviewed change set keeps its conclusion** (#1317): when no conclusion names the head itself, the gate compares `(path → blob sha)` at the reviewed tip and at the head over the pull request's changed paths, and accepts the PASS only when every one matches — naming both shas and the comparison it made — while a changed blob, a path missing on either side, a truncated tree or an unreadable one refuses exactly as before. A conclusion that names the head always decides, so an INC written at the head still vetoes a carried PASS. Two conclusions at the same tip is an incident, not a tie to be broken — nobody re-verdicts to win, the Reviewer that wrote the later one retracts it in a comment, and the pull request waits. A correction reads `Correction from : …` and carries no `Verdict:` line, so it is not a second verdict; the replacement PASS for a missing `Ran:` ([above](#gates)) is the only other body allowed at one tip. **A withdrawal is the one correction the gates read** (#1335): `Correction from : the PASS at is withdrawn`, in the first line the way a `Verdict:` header is, joins the same newest-wins scan at the tip it names, and the gate refuses with `the PASS at was withdrawn by `. It counts only from the Bot that wrote that PASS or from a `reviewers` login — one sentence from any other writer is prose, so a healthy pull request cannot be parked by whoever can comment. A later PASS at the tip supersedes it, exactly as a later FAIL vetoes an earlier PASS, and a correction quoted inside a body is a quote, not a withdrawal. **A GitHub bot account's `Verdict:` is never a conclusion** — `user.type` is `Bot`, or the login ends `[bot]` — whatever its body says (#943, `cursor[bot]` at #940's tip); the rule is the account type and not a login list, because every Bot writes as the shared `Birfy` account and a list containing it would void the whole pipeline, so the real Reviewer voids such a report at that tip with `Correction from Reviewer:`. +**One conclusion per tip.** A tip carries at most one conclusion: one `Verdict:` block naming one SHA — the whole 40 characters, because a prefix names every head that starts that way and the newest of them is the tip being judged (#1112). **A tip that moved without changing the reviewed change set keeps its conclusion** (#1317): when no conclusion names the head itself, the gate compares `(path → blob sha)` at the reviewed tip and at the head over the pull request's changed paths, and accepts the PASS only when every one matches — naming both shas and the comparison it made — while a changed blob, a path missing on either side, a truncated tree or an unreadable one refuses exactly as before. A conclusion that names the head always decides, so an INC written at the head still vetoes a carried PASS. Two conclusions at the same tip is an incident, not a tie to be broken — nobody re-verdicts to win, the Reviewer that wrote the later one retracts it in a comment, and the pull request waits. A correction reads `Correction from : …` and carries no `Verdict:` line, so it is not a second verdict; the replacement PASS for a missing `Ran:` ([above](#gates)) is the only other body allowed at one tip. **A withdrawal is the one correction the gates read** (#1335): `Correction from : the PASS at is withdrawn`, in the first line the way a `Verdict:` header is, joins the same newest-wins scan at the tip it names, and the gate refuses with `the PASS at was withdrawn by `. It counts only from the Bot that wrote that PASS or from a `reviewers` login — one sentence from any other writer is prose, so a healthy pull request cannot be parked by whoever can comment. A later PASS at the tip supersedes it, exactly as a later FAIL vetoes an earlier PASS, and a correction quoted inside a body is a quote, not a withdrawal. **A GitHub bot account's `Verdict:` is never a conclusion** — whatever its body says (#943, `cursor[bot]` at #940's tip). The rule is the account type, read the other way round from a login list: only `user.type === 'User'` earns the name of a conclusion, so a bot account (`Bot`, or a login ending `[bot]`) **and a report whose `user` carries no `type` at all** are both refused — the missing field is the shape the impersonation takes, so it is read fail-closed rather than skipped (#1344). The type and not a list, because every Bot writes as the shared `Birfy` account and a list containing it would void the whole pipeline. Such a report never enters the refusal's own sentence: the reason stays `no Verifier report names the tip `, and the account that wrote it, with the text it wrote, is kept as a note in the decision's `judged` field, so the sentence names the tip rather than the bot (#1344, #1350). The real Reviewer voids such a report at that tip with `Correction from Reviewer:`. **Judged on the merge tree, never on the pull request page.** The page answers "does this commit pass alone"; the queue answers "does this commit pass merged onto the current `main`", and the second one is the merge decision. #759 was green on every check, was kicked out of the queue twice, and went red on `main` at `core/test/follow-through.test.js:264`. A red queue entry that names files you did not touch is the stale base of [step 2](#pipeline-contract), not bad luck: fix the base, do not re-enqueue blindly. @@ -208,7 +208,7 @@ A review's `state` is not read at all: approvals are not a gate any more (user d #### Untrusted external agent branches -An external agent can open a pull request here under its own branch, and its write access is not settled (owner decision 2026-10-27). What counts as one of those branches is listed in [`.github/agent/frozen-branches.json`](.github/agent/frozen-branches.json) and read at the pull request's **base** commit — today the prefix `cursor/` and any body marker starting `CURSOR_AGENT_PR_BODY` (`` above all). `enqueue-guard` refuses such a pull request at the queue entry with `frozen: external agent branch; owner decision pending`, whatever conclusion its tip carries, and it refuses one whose diff edits that file — so the list cannot be emptied from inside the branch it judges. Deleting the file (or emptying both arrays) on `main` releases everything. This is a queue-entry seal, not a permission boundary: a direct push or an admin merge never reads it, and the shared `Birfy` token can edit the file. +An external agent can open a pull request here under its own branch, and its write access is not settled (owner decision 2026-10-27). What counts as one of those branches is listed in [`.github/agent/frozen-branches.json`](.github/agent/frozen-branches.json) and read at the pull request's **base** commit — today the prefix `cursor/` and any body marker starting `CURSOR_AGENT_PR_BODY` (`` above all). `enqueue-guard` refuses such a pull request at the queue entry with `frozen: external agent branch; owner decision pending`, whatever conclusion its tip carries, and it refuses one whose diff edits that file — so the list cannot be emptied from inside the branch it judges. Deleting the file (or emptying both arrays) on `main` releases everything. This is a queue-entry seal, not a permission boundary: a direct push or an admin merge never reads it, a renamed branch, or a body whose marker was never there, walks past it, and the shared `Birfy` token can edit the file — it stops the pull request that names itself, not the agent behind it (#1350). Until it is lifted, on such a branch: @@ -218,7 +218,7 @@ Until it is lifted, on such a branch: It is also never the baseline for `generate`. Anyone who has to touch `patches/` starts from `git checkout 41decf09 -- patches/ds-bot` and regenerates from there, never from a branch an external agent produced. -本条自 `cursor` 云端 agent 的写权限被撤销之日起失效,由 Chief 删除。 +本条自 `cursor` 云端 agent 的写权限被撤销之日起失效:届时由 Chief 随即在 `main` 上删除 `.github/agent/frozen-branches.json`(或删掉其中的两个数组)即告解除 —— 闸读的是那一份文件,读不到权限本身,所以撤销这件事不会自动让闸停下,只有删掉那几行才会(#1350)。 #### The checkpoint block: a hand-off the next Bot can read (#1202) From c6ba9a09a02e3c68413cbcfe8686d45f06e63921 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Tue, 27 Oct 2026 14:11:41 +0800 Subject: [PATCH 2/4] gate: read the seal from main, and name a frozen pull request by its number (#1354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1328 merged through the seal. Two reasons, both fixed here. The list was read at the pull request's base commit, so a pull request based before the seal landed read a ref that had no list at all and froze nobody: #1328's base `8d0a99c1` predates `6d1eb0be`. The list now comes from `main` — the policy as it is, not as it was at the branch point. The reader is an argument (`frozenList({ gh, repo, ref })`, and `frozenRefusal({ …, list })`), so nothing in the predicate is a hard-wired fetch and a test can answer without a network. And a number now names a frozen pull request. `#1328`'s branch is an ordinary `dev4/1265-step2-one-board` one, so no prefix catches it; the numbers live in `FROZEN_PULL_REQUESTS` in this file because the JSON list cannot carry them — a pull request that edits that document is refused by the rule it would be feeding. The JSON document is not touched by this commit. Order is `merged` first (history is never re-judged, #1350), then the self-edit check, then the number, the prefix and the marker. AGENTS.md gains the dequeue sentence: a draft is a property of the pull request, not of its queue entry — #1328 entered at 06:01:1xZ, was turned draft, and merged at 06:03:21Z anyway. --- .github/scripts/enqueue-guard.mjs | 43 +++++++++++++++------- .github/scripts/enqueue-guard.test.mjs | 51 ++++++++++++++++++++------ AGENTS.md | 3 +- 3 files changed, 71 insertions(+), 26 deletions(-) diff --git a/.github/scripts/enqueue-guard.mjs b/.github/scripts/enqueue-guard.mjs index 618c15d6f..04ebf270a 100644 --- a/.github/scripts/enqueue-guard.mjs +++ b/.github/scripts/enqueue-guard.mjs @@ -48,23 +48,34 @@ function arg(name) { // (#812). One function with two readers, so the check and the printed command cannot disagree — #812's // kill condition is a second copy of these rules, and there is none. // #1344 (owner decision 2026-10-27): a branch an external agent owns is frozen while its write access is -// unresolved, and the list of what that means is a file on the base branch, not code: the owner peels the -// seal off by editing or deleting one JSON document, with no release of this script. The list is read at the -// pull request's BASE commit, never its head — a branch that carries its own copy could empty the list and -// un-freeze itself (#1346's shape) — and a pull request that edits the document is refused outright, so -// "edit the seal from inside" is closed too. This is a queue-entry seal, not a permission boundary: a direct -// push or an admin merge is unaffected, and the shared `Birfy` token can edit the file on `main`. +// unresolved, and what that means is named in three places, not code: a number in `FROZEN_PULL_REQUESTS` +// below, and a prefix and a marker in `.github/agent/frozen-branches.json`, so the owner peels the seal off +// without a release of this script. The document is read from `main` (#1354), never from the pull request's +// base and never from its head: a base older than the seal has no list at all and so froze nobody (#1328, a +// base of `8d0a99c1` against a seal that landed at `6d1eb0be`), and a branch carrying its own copy could +// empty the list and un-freeze itself (#1346's shape). A pull request that edits the document is refused +// outright, so "edit the seal from inside" is closed too — which is also why a number cannot be added to it +// from here, and why the numbers are a constant in this file. This is a queue-entry seal, not a permission +// boundary: a direct push or an admin merge is unaffected, and the shared `Birfy` token can edit the file. const FROZEN_PATH = '.github/agent/frozen-branches.json' const FROZEN_REASON = 'frozen: external agent branch; owner decision pending' const FROZEN_EDIT_REASON = 'frozen: .github/agent/frozen-branches.json is not editable by a pull request' +// #1354: the numbers the seal names. They cannot live in the JSON document: a pull request that edits it is +// refused by the check below, so adding a number to it would be refused by the rule it is meant to feed. +// `1328` is here because its branch is an ordinary `dev4/…` one and only its number names it; `1346` and +// `1330` are here as well as under the `cursor/` prefix, because a second way to catch the same pull request +// costs nothing and a rename costs the seal its only handle on it. +const FROZEN_PULL_REQUESTS = new Set([1328, 1346, 1330]) // Missing file, unreadable file, empty arrays: nothing is frozen. A broken read must not stop the whole -// queue, and the seal is meant to be removable — both point the same way here. -function frozenList({ gh, repo, base }) { +// queue, and the seal is meant to be removable — both point the same way here. `ref` is the branch the +// policy is read at, `main` by default; it is a parameter so a test can read another ref without a network. +// `gh` is the reader, so nothing here is a hard-wired fetch: a caller can pass a function that answers. +export function frozenList({ gh, repo, ref = 'main' }) { const out = { branches: [], markers: [] } - if (!base) return out + if (!gh || !repo) return out try { - const body = JSON.parse(gh('api', `repos/${repo}/contents/${FROZEN_PATH}?ref=${base}`)) + const body = JSON.parse(gh('api', `repos/${repo}/contents/${FROZEN_PATH}?ref=${ref}`)) const config = JSON.parse(Buffer.from(String(body?.content ?? ''), 'base64').toString('utf8')) for (const key of ['branches', 'markers']) { if (Array.isArray(config?.[key])) out[key] = config[key].map((v) => String(v).toLowerCase()).filter(Boolean) @@ -75,9 +86,10 @@ function frozenList({ gh, repo, base }) { return out } -// The refusal this file exists for, or `null`: the head branch starts with a listed prefix, the body carries -// a listed marker, or the diff edits the list itself. `merged` is history and is never re-judged (#943). -function frozenRefusal({ gh, repo, pull, files }) { +// The refusal this file exists for, or `null`: the number is listed, the head branch starts with a listed +// prefix, the body carries a listed marker, or the diff edits the list itself. `merged` is history and is +// never re-judged (#943). `list` is the third injection point: pass one and nothing is read at all. +export function frozenRefusal({ gh, repo, pull, files, list: givenList }) { // #1350: `merged` is answered before the list is read, and before the edit check below — the guard runs // again on every event (`labeled` above all), and a merged pull request that edited this file has already // made its edit. Judged after it, that run goes red on a merge nobody can undo, which is the opposite of @@ -87,7 +99,10 @@ function frozenRefusal({ gh, repo, pull, files }) { // An ADD is how the seal first arrives (this very pull request). A modify or a delete is the self-unlock // this rule closes, whatever the branch is. if (touched && touched.status !== 'added') return FROZEN_EDIT_REASON - const list = frozenList({ gh, repo, base: pull?.base?.sha }) + // #1354: the number needs no list, so it is answered before one is read — `#1328`'s base predates the + // document, and reading a ref that has no list would have cleared it. + if (FROZEN_PULL_REQUESTS.has(Number(pull?.number))) return FROZEN_REASON + const list = givenList ?? frozenList({ gh, repo }) const branch = String(pull?.head?.ref ?? '').toLowerCase() const body = String(pull?.body ?? '').toLowerCase() if (list.branches.some((prefix) => branch.startsWith(prefix))) return FROZEN_REASON diff --git a/.github/scripts/enqueue-guard.test.mjs b/.github/scripts/enqueue-guard.test.mjs index a52640e21..a9ce34dd0 100644 --- a/.github/scripts/enqueue-guard.test.mjs +++ b/.github/scripts/enqueue-guard.test.mjs @@ -6,7 +6,7 @@ import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { main, recordOnlyAt } from './enqueue-guard.mjs' +import { frozenRefusal, main, recordOnlyAt } from './enqueue-guard.mjs' import { checkpointReasons, parseCheckpoint } from './enqueue-rules.mjs' const HEAD = 'abaf938b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f' @@ -76,11 +76,12 @@ const withCheckpoint = (text, files) => { return `${text}\n${checkpoint({ issue, claim, files: names.length ? names : [HOST_FILE] })}` } -// #1344: the fixture also answers the contents read for the frozen list, at the pull request's BASE commit. +// #1344/#1354: the fixture also answers the contents read for the frozen list, which is read from `main` +// and never from the pull request's base or head. const BASE = 'c'.repeat(40) const FROZEN_PATH = '.github/agent/frozen-branches.json' const DEFAULT_FROZEN = { branches: ['cursor/'], markers: ['CURSOR_AGENT_PR_BODY'] } -const fakeGh = ({ body = null, raw = false, comments = [], reviews = [], labels = ['agent-pr'], files = [], head = HEAD, heads = null, author = 'opener', headRepo = 'ThinkFlowLab/FlyBot', permission = undefined, draft = false, merged = false, branch = 'dev/fixture', baseSha = BASE, baseFrozen = DEFAULT_FROZEN } = {}) => { +const fakeGh = ({ body = null, raw = false, comments = [], reviews = [], labels = ['agent-pr'], files = [], head = HEAD, heads = null, author = 'opener', headRepo = 'ThinkFlowLab/FlyBot', permission = undefined, draft = false, merged = false, branch = 'dev/fixture', baseSha = BASE, frozen = DEFAULT_FROZEN, prNumber = 566 } = {}) => { // `raw` is for the tests about the checkpoint itself: they must be able to hand over a body without one. const given = body ?? `${HEADERS}Fixes #602\n` const text = raw ? given : withCheckpoint(given, files) @@ -94,8 +95,8 @@ const fakeGh = ({ body = null, raw = false, comments = [], reviews = [], labels return JSON.stringify({ permission, role_name: permission, user: { login: 'reviewed' } }) } if (path.includes('/contents/')) { - if (baseFrozen === null) throw new Error('fake gh: 404 ' + path) - return JSON.stringify({ content: Buffer.from(JSON.stringify(baseFrozen)).toString('base64'), encoding: 'base64' }) + if (frozen === null) throw new Error('fake gh: 404 ' + path) + return JSON.stringify({ content: Buffer.from(JSON.stringify(frozen)).toString('base64'), encoding: 'base64' }) } if (path.includes('/files')) return files.map((f) => JSON.stringify(typeof f === 'string' ? { filename: f } : f)).join('\n') if (path.includes('/comments')) return comments.map((c) => JSON.stringify(c)).join('\n') @@ -103,7 +104,7 @@ const fakeGh = ({ body = null, raw = false, comments = [], reviews = [], labels if (/\/pulls\/\d+$/.test(path)) { // #1112 (c): a run reads the head twice, so the fixture can answer differently the second time. const sha = heads ? heads[Math.min(pullReads++, heads.length - 1)] : head - return JSON.stringify({ head: { sha, ref: branch, repo: { full_name: headRepo } }, base: { sha: baseSha }, user: { login: author }, body: text, labels: labels.map((name) => ({ name })), draft, merged }) + return JSON.stringify({ head: { sha, ref: branch, repo: { full_name: headRepo } }, base: { sha: baseSha }, user: { login: author }, body: text, labels: labels.map((name) => ({ name })), draft, merged, number: prNumber }) } // Anything else — `gh pr merge` above all — is the guard doing more than checking. throw new Error('fake gh: unexpected ' + path) @@ -965,12 +966,12 @@ test('#1344: a cursor/ branch is refused at the queue entry even with a well-for assert.match(r.message, /^do not enqueue #566: frozen: external agent branch; owner decision pending/) }) -test('#1344: emptying the list on the base branch un-freezes the branch — the seal is removable', () => { +test('#1344: emptying the list on main un-freezes the branch — the seal is removable', () => { const frozen = run({ branch: 'cursor/workflow-1318-c5f8', comments: [comment()] }) assert.equal(frozen.ok, false) - const released = run({ branch: 'cursor/workflow-1318-c5f8', comments: [comment()], baseFrozen: { branches: [], markers: [] } }) + const released = run({ branch: 'cursor/workflow-1318-c5f8', comments: [comment()], frozen: { branches: [], markers: [] } }) assert.equal(released.ok, true, 'with nothing listed, the branch is judged like any other: ' + released.reasons.join('; ')) - const absent = run({ branch: 'cursor/workflow-1318-c5f8', comments: [comment()], baseFrozen: null }) + const absent = run({ branch: 'cursor/workflow-1318-c5f8', comments: [comment()], frozen: null }) assert.equal(absent.ok, true, 'a base without the file freezes nobody: ' + absent.reasons.join('; ')) }) @@ -981,13 +982,13 @@ test('#1344: a body carrying the agent marker is refused on a branch that matche }) test('#1344: a pull request that edits the frozen list is refused, so it cannot un-freeze itself', () => { - const r = run({ branch: 'cursor/workflow-1318-c5f8', comments: [comment()], baseFrozen: { branches: [], markers: [] }, files: [HOST_FILE, { filename: FROZEN_PATH, status: 'modified' }] }) + const r = run({ branch: 'cursor/workflow-1318-c5f8', comments: [comment()], frozen: { branches: [], markers: [] }, files: [HOST_FILE, { filename: FROZEN_PATH, status: 'modified' }] }) assert.equal(r.ok, false, 'an emptied list inside the pull request is not the list that is read') assert.ok(r.reasons.includes('frozen: .github/agent/frozen-branches.json is not editable by a pull request'), r.reasons.join('; ')) }) test('#1344: adding the list is how the seal arrives — an add is not an edit', () => { - const r = run({ branch: 'dev/1344-user-only-conclusions', comments: [comment()], baseFrozen: null, files: [HOST_FILE, { filename: FROZEN_PATH, status: 'added' }] }) + const r = run({ branch: 'dev/1344-user-only-conclusions', comments: [comment()], frozen: null, files: [HOST_FILE, { filename: FROZEN_PATH, status: 'added' }] }) assert.equal(r.ok, true, r.reasons.join('; ')) }) @@ -999,3 +1000,31 @@ test('#1350: a merged pull request is not refused, even when its diff edited the assert.equal(r.ok, true, r.reasons.join('; ')) assert.equal(r.reasons.length, 0) }) + +// ---- #1354: which policy, and what names a frozen pull request. #1328 merged through the seal: its base +// (`8d0a99c1`) is older than the seal (`#1348`, `6d1eb0be`), so a base read found no list at all and froze +// nobody, and its branch is an ordinary `dev4/…` one, so a prefix could not have caught it either. +const OLD_BASE = '8d0a99c1'.repeat(5) + +test('#1354: the list is read from main, never from the pull request base', () => { + const gh = fakeGh({ files: [HOST_FILE], branch: 'cursor/workflow-1318-c5f8', comments: [comment()] }) + main({ gh, env: { REVIEW_POLICY: POLICY, GH_REPO: 'ThinkFlowLab/FlyBot', PR: '566' }, repo: 'ThinkFlowLab/FlyBot', pr: 566 }) + const reads = gh.calls.filter((p) => p.includes('/contents/')) + assert.ok(reads.length, 'the seal reads its list') + for (const path of reads) assert.match(path, /\?ref=main$/, 'the list is the policy as it is on main: ' + path) + for (const path of reads) assert.doesNotMatch(path, new RegExp('ref=' + BASE), 'a base older than the seal has no list and would freeze nobody') +}) + +test("#1354: #1328's shape — an ordinary dev4/ branch on a base older than the seal is refused by its number", () => { + const r = run({ prNumber: 1328, branch: 'dev4/1265-step2-one-board', baseSha: OLD_BASE, comments: [comment()] }) + assert.equal(r.ok, false) + assert.ok(r.reasons.includes('frozen: external agent branch; owner decision pending'), r.reasons.join('; ')) + assert.match(r.message, /^do not enqueue #566: frozen: external agent branch; owner decision pending/) +}) + +test('#1354: a listed number is answered with no reader at all — the list is an argument, not a fetch', () => { + const pull = { number: 1346, merged: false, head: { ref: 'dev/whatever' }, body: 'Fixes #1', base: { sha: OLD_BASE } } + assert.equal(frozenRefusal({ pull, files: [] }), 'frozen: external agent branch; owner decision pending') + assert.equal(frozenRefusal({ pull: { ...pull, number: 566 }, files: [], list: { branches: [], markers: [] } }), null) + assert.equal(frozenRefusal({ pull: { ...pull, number: 566 }, files: [], list: { branches: ['dev/whatever'], markers: [] } }), 'frozen: external agent branch; owner decision pending') +}) diff --git a/AGENTS.md b/AGENTS.md index de2f6e0f6..af8ca7aa4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -208,13 +208,14 @@ A review's `state` is not read at all: approvals are not a gate any more (user d #### Untrusted external agent branches -An external agent can open a pull request here under its own branch, and its write access is not settled (owner decision 2026-10-27). What counts as one of those branches is listed in [`.github/agent/frozen-branches.json`](.github/agent/frozen-branches.json) and read at the pull request's **base** commit — today the prefix `cursor/` and any body marker starting `CURSOR_AGENT_PR_BODY` (`` above all). `enqueue-guard` refuses such a pull request at the queue entry with `frozen: external agent branch; owner decision pending`, whatever conclusion its tip carries, and it refuses one whose diff edits that file — so the list cannot be emptied from inside the branch it judges. Deleting the file (or emptying both arrays) on `main` releases everything. This is a queue-entry seal, not a permission boundary: a direct push or an admin merge never reads it, a renamed branch, or a body whose marker was never there, walks past it, and the shared `Birfy` token can edit the file — it stops the pull request that names itself, not the agent behind it (#1350). +An external agent can open a pull request here under its own branch, and its write access is not settled (owner decision 2026-10-27). Three things name such a pull request, and any one of them refuses it: its number, if it is in `FROZEN_PULL_REQUESTS` in [`enqueue-guard.mjs`](.github/scripts/enqueue-guard.mjs); a head branch starting with a listed prefix; or a body carrying a listed marker. The prefix and the marker live in [`.github/agent/frozen-branches.json`](.github/agent/frozen-branches.json) — today `cursor/` and any marker starting `CURSOR_AGENT_PR_BODY` (`` above all) — and are read **from `main`**, never from the pull request's base or head (#1354): `#1328` was based on `8d0a99c1`, older than the seal itself, so a base read found no list and froze nobody, and it merged at `06:03:21Z`. The numbers cannot live in that document, because a pull request that edits it is refused — so a number is code, and it is the only handle on a pull request whose branch name says nothing (`#1328`'s was an ordinary `dev4/…`). `enqueue-guard` refuses such a pull request at the queue entry with `frozen: external agent branch; owner decision pending`, whatever conclusion its tip carries, and it refuses one whose diff edits that file — so the list cannot be emptied from inside the branch it judges. Deleting the file (or emptying both arrays) on `main` releases everything the document names; a number is released by deleting it from `FROZEN_PULL_REQUESTS`. This is a queue-entry seal, not a permission boundary: a direct push or an admin merge never reads it, a renamed branch, or a body whose marker was never there, walks past it, and the shared `Birfy` token can edit the file — it stops the pull request that names itself, not the agent behind it (#1350). Until it is lifted, on such a branch: - **Do not adopt its `patches/ds-bot/*.patch`.** Those are an external agent's output, not this pipeline's generated files. - **Do not rebase onto it**, and do not use it as a base for any branch or any review. - **Do not write a conclusion, a comment or a label on it.** No `Verdict:`, no `Correction`, no `agent-*`: the record stays as the external agent left it. +- **Keep it out of the queue with `dequeuePullRequest`.** A draft is a property of the pull request, not of a queue entry: `#1328` entered the queue at `06:01:1xZ`, was turned draft, and merged at `06:03:21Z` anyway. Turning it draft is not a dequeue, and once a pull request is in the queue the only mechanical stop is taking the entry out (#1354). It is also never the baseline for `generate`. Anyone who has to touch `patches/` starts from `git checkout 41decf09 -- patches/ds-bot` and regenerates from there, never from a branch an external agent produced. From 8f97e4f56aa33437eaf7f373722bae9d00d77d55 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Tue, 27 Oct 2026 14:49:20 +0800 Subject: [PATCH 3/4] gate: drop the frozen-branch seal (executor decision) and finish the #1348 prose (#1350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The seal (#1344) could not hold: #1328 was based on 8d0a99c1, older than the document it was supposed to read, so the base read found no list and froze nobody, and it merged at 06:03:21Z. #1350/#1354 patched the reading; the owner chose B instead — remove the mechanism whole. Deletes the constants, frozenList(), frozenRefusal(), the decidePull call site and its early return, the JSON document, the nine tests that only existed for it, and the AGENTS.md section. #1350's rule-7 alignment stays: it is about conclusions, not about the seal. --- .github/agent/frozen-branches.json | 4 -- .github/scripts/enqueue-guard.mjs | 74 ---------------------- .github/scripts/enqueue-guard.test.mjs | 88 +------------------------- AGENTS.md | 15 ----- 4 files changed, 3 insertions(+), 178 deletions(-) delete mode 100644 .github/agent/frozen-branches.json diff --git a/.github/agent/frozen-branches.json b/.github/agent/frozen-branches.json deleted file mode 100644 index 12c3f3fe0..000000000 --- a/.github/agent/frozen-branches.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "branches": ["cursor/"], - "markers": ["CURSOR_AGENT_PR_BODY"] -} diff --git a/.github/scripts/enqueue-guard.mjs b/.github/scripts/enqueue-guard.mjs index 04ebf270a..4c6f7af40 100644 --- a/.github/scripts/enqueue-guard.mjs +++ b/.github/scripts/enqueue-guard.mjs @@ -47,69 +47,6 @@ function arg(name) { // runs); `gate-check.mjs` turns the same object into the `enqueue-guard` check run the ruleset requires // (#812). One function with two readers, so the check and the printed command cannot disagree — #812's // kill condition is a second copy of these rules, and there is none. -// #1344 (owner decision 2026-10-27): a branch an external agent owns is frozen while its write access is -// unresolved, and what that means is named in three places, not code: a number in `FROZEN_PULL_REQUESTS` -// below, and a prefix and a marker in `.github/agent/frozen-branches.json`, so the owner peels the seal off -// without a release of this script. The document is read from `main` (#1354), never from the pull request's -// base and never from its head: a base older than the seal has no list at all and so froze nobody (#1328, a -// base of `8d0a99c1` against a seal that landed at `6d1eb0be`), and a branch carrying its own copy could -// empty the list and un-freeze itself (#1346's shape). A pull request that edits the document is refused -// outright, so "edit the seal from inside" is closed too — which is also why a number cannot be added to it -// from here, and why the numbers are a constant in this file. This is a queue-entry seal, not a permission -// boundary: a direct push or an admin merge is unaffected, and the shared `Birfy` token can edit the file. -const FROZEN_PATH = '.github/agent/frozen-branches.json' -const FROZEN_REASON = 'frozen: external agent branch; owner decision pending' -const FROZEN_EDIT_REASON = 'frozen: .github/agent/frozen-branches.json is not editable by a pull request' -// #1354: the numbers the seal names. They cannot live in the JSON document: a pull request that edits it is -// refused by the check below, so adding a number to it would be refused by the rule it is meant to feed. -// `1328` is here because its branch is an ordinary `dev4/…` one and only its number names it; `1346` and -// `1330` are here as well as under the `cursor/` prefix, because a second way to catch the same pull request -// costs nothing and a rename costs the seal its only handle on it. -const FROZEN_PULL_REQUESTS = new Set([1328, 1346, 1330]) - -// Missing file, unreadable file, empty arrays: nothing is frozen. A broken read must not stop the whole -// queue, and the seal is meant to be removable — both point the same way here. `ref` is the branch the -// policy is read at, `main` by default; it is a parameter so a test can read another ref without a network. -// `gh` is the reader, so nothing here is a hard-wired fetch: a caller can pass a function that answers. -export function frozenList({ gh, repo, ref = 'main' }) { - const out = { branches: [], markers: [] } - if (!gh || !repo) return out - try { - const body = JSON.parse(gh('api', `repos/${repo}/contents/${FROZEN_PATH}?ref=${ref}`)) - const config = JSON.parse(Buffer.from(String(body?.content ?? ''), 'base64').toString('utf8')) - for (const key of ['branches', 'markers']) { - if (Array.isArray(config?.[key])) out[key] = config[key].map((v) => String(v).toLowerCase()).filter(Boolean) - } - } catch { - // no list, no seal - } - return out -} - -// The refusal this file exists for, or `null`: the number is listed, the head branch starts with a listed -// prefix, the body carries a listed marker, or the diff edits the list itself. `merged` is history and is -// never re-judged (#943). `list` is the third injection point: pass one and nothing is read at all. -export function frozenRefusal({ gh, repo, pull, files, list: givenList }) { - // #1350: `merged` is answered before the list is read, and before the edit check below — the guard runs - // again on every event (`labeled` above all), and a merged pull request that edited this file has already - // made its edit. Judged after it, that run goes red on a merge nobody can undo, which is the opposite of - // what "never re-judged" means. The edit check still refuses every unmerged pull request. - if (pull?.merged) return null - const touched = (files ?? []).find((f) => f && f.filename === FROZEN_PATH) - // An ADD is how the seal first arrives (this very pull request). A modify or a delete is the self-unlock - // this rule closes, whatever the branch is. - if (touched && touched.status !== 'added') return FROZEN_EDIT_REASON - // #1354: the number needs no list, so it is answered before one is read — `#1328`'s base predates the - // document, and reading a ref that has no list would have cleared it. - if (FROZEN_PULL_REQUESTS.has(Number(pull?.number))) return FROZEN_REASON - const list = givenList ?? frozenList({ gh, repo }) - const branch = String(pull?.head?.ref ?? '').toLowerCase() - const body = String(pull?.body ?? '').toLowerCase() - if (list.branches.some((prefix) => branch.startsWith(prefix))) return FROZEN_REASON - if (list.markers.some((marker) => body.includes(marker))) return FROZEN_REASON - return null -} - export function decidePull({ gh = realGh, env = process.env, repo: repoOption, pr: prOption, expectedHead, sameChangeSet } = {}) { const list = (path) => gh('api', '--paginate', '--jq', '.[]', path).split('\n').filter(Boolean).map((line) => JSON.parse(line)) const repo = repoOption || env.GH_REPO || arg('--repo') @@ -155,17 +92,6 @@ export function decidePull({ gh = realGh, env = process.env, repo: repoOption, p // case-insensitive: `--repo thinkflowlab/flybot` used to read our own branch as a fork and print ALLOW. const fork = String(pull.head?.repo?.full_name ?? '').toLowerCase() !== String(repo).toLowerCase() const decision = guard({ pr, head, labels, body: pull.body, files: files.map((f) => f.filename), mechanicalRelease, fork, draft: pull.draft, merged: pull.merged, result, reports, policy }) - // #1344: the seal answers before anything else can allow the entry — a well-formed PASS at the tip, an - // `agent-pr` label and a clean body change nothing here. - const frozen = frozenRefusal({ gh, repo, pull, files }) - if (frozen) { - return { - decision: { ...decision, ok: false, reasons: [frozen], reason: frozen, message: `do not enqueue #${pr}: ${frozen}` }, - pull, - repo, - pr, - } - } return { decision, pull, repo, pr } } diff --git a/.github/scripts/enqueue-guard.test.mjs b/.github/scripts/enqueue-guard.test.mjs index a9ce34dd0..ea882844c 100644 --- a/.github/scripts/enqueue-guard.test.mjs +++ b/.github/scripts/enqueue-guard.test.mjs @@ -6,7 +6,7 @@ import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { frozenRefusal, main, recordOnlyAt } from './enqueue-guard.mjs' +import { main, recordOnlyAt } from './enqueue-guard.mjs' import { checkpointReasons, parseCheckpoint } from './enqueue-rules.mjs' const HEAD = 'abaf938b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f' @@ -76,12 +76,7 @@ const withCheckpoint = (text, files) => { return `${text}\n${checkpoint({ issue, claim, files: names.length ? names : [HOST_FILE] })}` } -// #1344/#1354: the fixture also answers the contents read for the frozen list, which is read from `main` -// and never from the pull request's base or head. -const BASE = 'c'.repeat(40) -const FROZEN_PATH = '.github/agent/frozen-branches.json' -const DEFAULT_FROZEN = { branches: ['cursor/'], markers: ['CURSOR_AGENT_PR_BODY'] } -const fakeGh = ({ body = null, raw = false, comments = [], reviews = [], labels = ['agent-pr'], files = [], head = HEAD, heads = null, author = 'opener', headRepo = 'ThinkFlowLab/FlyBot', permission = undefined, draft = false, merged = false, branch = 'dev/fixture', baseSha = BASE, frozen = DEFAULT_FROZEN, prNumber = 566 } = {}) => { +const fakeGh = ({ body = null, raw = false, comments = [], reviews = [], labels = ['agent-pr'], files = [], head = HEAD, heads = null, author = 'opener', headRepo = 'ThinkFlowLab/FlyBot', permission = undefined, draft = false, merged = false, branch = 'dev/fixture' } = {}) => { // `raw` is for the tests about the checkpoint itself: they must be able to hand over a body without one. const given = body ?? `${HEADERS}Fixes #602\n` const text = raw ? given : withCheckpoint(given, files) @@ -94,17 +89,13 @@ const fakeGh = ({ body = null, raw = false, comments = [], reviews = [], labels if (permission == null || permission === 'throw') throw new Error('fake gh: permission ' + path) return JSON.stringify({ permission, role_name: permission, user: { login: 'reviewed' } }) } - if (path.includes('/contents/')) { - if (frozen === null) throw new Error('fake gh: 404 ' + path) - return JSON.stringify({ content: Buffer.from(JSON.stringify(frozen)).toString('base64'), encoding: 'base64' }) - } if (path.includes('/files')) return files.map((f) => JSON.stringify(typeof f === 'string' ? { filename: f } : f)).join('\n') if (path.includes('/comments')) return comments.map((c) => JSON.stringify(c)).join('\n') if (path.includes('/reviews')) return reviews.map((r) => JSON.stringify(r)).join('\n') if (/\/pulls\/\d+$/.test(path)) { // #1112 (c): a run reads the head twice, so the fixture can answer differently the second time. const sha = heads ? heads[Math.min(pullReads++, heads.length - 1)] : head - return JSON.stringify({ head: { sha, ref: branch, repo: { full_name: headRepo } }, base: { sha: baseSha }, user: { login: author }, body: text, labels: labels.map((name) => ({ name })), draft, merged, number: prNumber }) + return JSON.stringify({ head: { sha, ref: branch, repo: { full_name: headRepo } }, user: { login: author }, body: text, labels: labels.map((name) => ({ name })), draft, merged }) } // Anything else — `gh pr merge` above all — is the guard doing more than checking. throw new Error('fake gh: unexpected ' + path) @@ -955,76 +946,3 @@ test('#1335: a withdrawal naming another tip leaves this tip enqueueable', () => const r = run({ comments: [comment(), correction({ tip: OLD })] }) assert.equal(r.ok, true, r.reasons.join('; ')) }) - -// ---- #1344: the frozen seal. An external agent owns `cursor/` until its write access is settled, and the -// list lives in a JSON document on the BASE branch so the owner can delete it without a code change. It is -// a queue-entry seal, not a permission boundary: a direct push or an admin merge never reads it. -test('#1344: a cursor/ branch is refused at the queue entry even with a well-formed PASS at its tip', () => { - const r = run({ branch: 'cursor/workflow-1318-c5f8', comments: [comment()] }) - assert.equal(r.ok, false) - assert.ok(r.reasons.includes('frozen: external agent branch; owner decision pending'), r.reasons.join('; ')) - assert.match(r.message, /^do not enqueue #566: frozen: external agent branch; owner decision pending/) -}) - -test('#1344: emptying the list on main un-freezes the branch — the seal is removable', () => { - const frozen = run({ branch: 'cursor/workflow-1318-c5f8', comments: [comment()] }) - assert.equal(frozen.ok, false) - const released = run({ branch: 'cursor/workflow-1318-c5f8', comments: [comment()], frozen: { branches: [], markers: [] } }) - assert.equal(released.ok, true, 'with nothing listed, the branch is judged like any other: ' + released.reasons.join('; ')) - const absent = run({ branch: 'cursor/workflow-1318-c5f8', comments: [comment()], frozen: null }) - assert.equal(absent.ok, true, 'a base without the file freezes nobody: ' + absent.reasons.join('; ')) -}) - -test('#1344: a body carrying the agent marker is refused on a branch that matches no prefix', () => { - const r = run({ branch: 'dev/friendly-name', body: '\n\nFixes #602\n', comments: [comment()] }) - assert.equal(r.ok, false) - assert.ok(r.reasons.includes('frozen: external agent branch; owner decision pending'), r.reasons.join('; ')) -}) - -test('#1344: a pull request that edits the frozen list is refused, so it cannot un-freeze itself', () => { - const r = run({ branch: 'cursor/workflow-1318-c5f8', comments: [comment()], frozen: { branches: [], markers: [] }, files: [HOST_FILE, { filename: FROZEN_PATH, status: 'modified' }] }) - assert.equal(r.ok, false, 'an emptied list inside the pull request is not the list that is read') - assert.ok(r.reasons.includes('frozen: .github/agent/frozen-branches.json is not editable by a pull request'), r.reasons.join('; ')) -}) - -test('#1344: adding the list is how the seal arrives — an add is not an edit', () => { - const r = run({ branch: 'dev/1344-user-only-conclusions', comments: [comment()], frozen: null, files: [HOST_FILE, { filename: FROZEN_PATH, status: 'added' }] }) - assert.equal(r.ok, true, r.reasons.join('; ')) -}) - -// #1350: `merged` is history and is answered before anything else. The guard is re-run on every event -// (`labeled` above all), so a merged pull request that edited this file would otherwise go red on a later -// run — the refusal exists to stop a seal being undone, and that merge has already happened. -test('#1350: a merged pull request is not refused, even when its diff edited the frozen list', () => { - const r = run({ merged: true, files: [HOST_FILE, { filename: FROZEN_PATH, status: 'modified' }], comments: [comment()] }) - assert.equal(r.ok, true, r.reasons.join('; ')) - assert.equal(r.reasons.length, 0) -}) - -// ---- #1354: which policy, and what names a frozen pull request. #1328 merged through the seal: its base -// (`8d0a99c1`) is older than the seal (`#1348`, `6d1eb0be`), so a base read found no list at all and froze -// nobody, and its branch is an ordinary `dev4/…` one, so a prefix could not have caught it either. -const OLD_BASE = '8d0a99c1'.repeat(5) - -test('#1354: the list is read from main, never from the pull request base', () => { - const gh = fakeGh({ files: [HOST_FILE], branch: 'cursor/workflow-1318-c5f8', comments: [comment()] }) - main({ gh, env: { REVIEW_POLICY: POLICY, GH_REPO: 'ThinkFlowLab/FlyBot', PR: '566' }, repo: 'ThinkFlowLab/FlyBot', pr: 566 }) - const reads = gh.calls.filter((p) => p.includes('/contents/')) - assert.ok(reads.length, 'the seal reads its list') - for (const path of reads) assert.match(path, /\?ref=main$/, 'the list is the policy as it is on main: ' + path) - for (const path of reads) assert.doesNotMatch(path, new RegExp('ref=' + BASE), 'a base older than the seal has no list and would freeze nobody') -}) - -test("#1354: #1328's shape — an ordinary dev4/ branch on a base older than the seal is refused by its number", () => { - const r = run({ prNumber: 1328, branch: 'dev4/1265-step2-one-board', baseSha: OLD_BASE, comments: [comment()] }) - assert.equal(r.ok, false) - assert.ok(r.reasons.includes('frozen: external agent branch; owner decision pending'), r.reasons.join('; ')) - assert.match(r.message, /^do not enqueue #566: frozen: external agent branch; owner decision pending/) -}) - -test('#1354: a listed number is answered with no reader at all — the list is an argument, not a fetch', () => { - const pull = { number: 1346, merged: false, head: { ref: 'dev/whatever' }, body: 'Fixes #1', base: { sha: OLD_BASE } } - assert.equal(frozenRefusal({ pull, files: [] }), 'frozen: external agent branch; owner decision pending') - assert.equal(frozenRefusal({ pull: { ...pull, number: 566 }, files: [], list: { branches: [], markers: [] } }), null) - assert.equal(frozenRefusal({ pull: { ...pull, number: 566 }, files: [], list: { branches: ['dev/whatever'], markers: [] } }), 'frozen: external agent branch; owner decision pending') -}) diff --git a/AGENTS.md b/AGENTS.md index af8ca7aa4..b2e9e1657 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -206,21 +206,6 @@ A review's `state` is not read at all: approvals are not a gate any more (user d **A merged pull request's body is not a control.** GitHub evaluates `Fixes #N` once, when the merge lands. Editing the body of a merged pull request afterwards closes nothing and triggers no guard; it only makes the record disagree with what merged. If the issue is still open, close it with a comment naming the pull request — and if the merge itself was wrong, say so on the issue. -#### Untrusted external agent branches - -An external agent can open a pull request here under its own branch, and its write access is not settled (owner decision 2026-10-27). Three things name such a pull request, and any one of them refuses it: its number, if it is in `FROZEN_PULL_REQUESTS` in [`enqueue-guard.mjs`](.github/scripts/enqueue-guard.mjs); a head branch starting with a listed prefix; or a body carrying a listed marker. The prefix and the marker live in [`.github/agent/frozen-branches.json`](.github/agent/frozen-branches.json) — today `cursor/` and any marker starting `CURSOR_AGENT_PR_BODY` (`` above all) — and are read **from `main`**, never from the pull request's base or head (#1354): `#1328` was based on `8d0a99c1`, older than the seal itself, so a base read found no list and froze nobody, and it merged at `06:03:21Z`. The numbers cannot live in that document, because a pull request that edits it is refused — so a number is code, and it is the only handle on a pull request whose branch name says nothing (`#1328`'s was an ordinary `dev4/…`). `enqueue-guard` refuses such a pull request at the queue entry with `frozen: external agent branch; owner decision pending`, whatever conclusion its tip carries, and it refuses one whose diff edits that file — so the list cannot be emptied from inside the branch it judges. Deleting the file (or emptying both arrays) on `main` releases everything the document names; a number is released by deleting it from `FROZEN_PULL_REQUESTS`. This is a queue-entry seal, not a permission boundary: a direct push or an admin merge never reads it, a renamed branch, or a body whose marker was never there, walks past it, and the shared `Birfy` token can edit the file — it stops the pull request that names itself, not the agent behind it (#1350). - -Until it is lifted, on such a branch: - -- **Do not adopt its `patches/ds-bot/*.patch`.** Those are an external agent's output, not this pipeline's generated files. -- **Do not rebase onto it**, and do not use it as a base for any branch or any review. -- **Do not write a conclusion, a comment or a label on it.** No `Verdict:`, no `Correction`, no `agent-*`: the record stays as the external agent left it. -- **Keep it out of the queue with `dequeuePullRequest`.** A draft is a property of the pull request, not of a queue entry: `#1328` entered the queue at `06:01:1xZ`, was turned draft, and merged at `06:03:21Z` anyway. Turning it draft is not a dequeue, and once a pull request is in the queue the only mechanical stop is taking the entry out (#1354). - -It is also never the baseline for `generate`. Anyone who has to touch `patches/` starts from `git checkout 41decf09 -- patches/ds-bot` and regenerates from there, never from a branch an external agent produced. - -本条自 `cursor` 云端 agent 的写权限被撤销之日起失效:届时由 Chief 随即在 `main` 上删除 `.github/agent/frozen-branches.json`(或删掉其中的两个数组)即告解除 —— 闸读的是那一份文件,读不到权限本身,所以撤销这件事不会自动让闸停下,只有删掉那几行才会(#1350)。 - #### The checkpoint block: a hand-off the next Bot can read (#1202) A pull request hands itself over in one block in the body, under `## Checkpoint`. The body is the only store: it becomes the squash commit message, so the block cannot fall out of step with the pull request the way a comment or a label can. From a09f330ab2b298fc1095884a7f79dd086435c26f Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Tue, 27 Oct 2026 14:51:58 +0800 Subject: [PATCH 4/4] gate: keep the seal's document until the seal itself is gone (#1352) The `enqueue-guard` check is read from `main` (enqueue-guard.yml checks out `ref: main` and never runs pull request code), so main's own seal refuses any pull request whose diff edits or deletes `.github/agent/frozen-branches.json`. The run at 8f97e4f5 says so literally: `frozen: .github/agent/frozen-branches.json is not editable by a pull request`. Removing the document cannot be part of the change that removes its reader: until that code is on `main`, the document's deletion is refused by the very rule being deleted. Restore it here, byte-identical to c6ba9a09; the file is deleted in the follow-up pull request once main carries no seal. --- .github/agent/frozen-branches.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/agent/frozen-branches.json diff --git a/.github/agent/frozen-branches.json b/.github/agent/frozen-branches.json new file mode 100644 index 000000000..12c3f3fe0 --- /dev/null +++ b/.github/agent/frozen-branches.json @@ -0,0 +1,4 @@ +{ + "branches": ["cursor/"], + "markers": ["CURSOR_AGENT_PR_BODY"] +}