From 487e40fd00b77b9aae31871ea7c9374474969d3d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 00:10:47 +0000 Subject: [PATCH 1/5] test: stop asserting a CPython-version-specific rmtree detail test_cleanup_is_total_for_non_oserror_failures failed on Python 3.11.15: its premise asserted that shutil.rmtree(path, ignore_errors=True) raises ValueError on a NUL-byte path. That is an implementation detail which has changed -- ignore_errors now absorbs the non-OSError as well, so the assertion no longer holds. The contract under test is unaffected: _cleanup_download_artifacts must swallow non-OSError failures because it runs from a finally block and would otherwise replace the in-flight exception. Establish that premise against the unguarded rmtree call, which still raises, so the test proves the helper's own defensiveness rather than the stdlib's. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01YcHjCZ6pGn6A5BeeoZ6eZi --- tests/unit/test_transcript_action_workflow.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_transcript_action_workflow.py b/tests/unit/test_transcript_action_workflow.py index 7b2cb577e..f3ad2d7f8 100644 --- a/tests/unit/test_transcript_action_workflow.py +++ b/tests/unit/test_transcript_action_workflow.py @@ -1388,21 +1388,26 @@ async def failing_operation(): async def test_cleanup_is_total_for_non_oserror_failures(self): """A NUL byte in either path must not escape as ``ValueError``. - ``shutil.rmtree(..., ignore_errors=True)`` only suppresses ``OSError``: - a NUL byte makes its internal ``lstat`` raise ``ValueError``, and - ``Path.unlink`` raises the same directly. Because the helper runs from - a ``finally``, either escape would replace the in-flight exception -- - the exact defect the removal of the ``exists()`` probes fixed. No - mocking is used, so this exercises the real stdlib behaviour. + A NUL byte makes ``rmtree``'s internal ``lstat`` raise ``ValueError`` + and ``Path.unlink`` raise the same directly -- neither is an + ``OSError``, so an ``OSError``-only guard would let them through. + Because the helper runs from a ``finally``, either escape would + replace the in-flight exception -- the exact defect the removal of + the ``exists()`` probes fixed. No mocking is used, so this exercises + the real stdlib behaviour. """ nul_video = pathlib.Path("/tmp/eventrelay-nul\x00.mp4") nul_root = pathlib.Path("/tmp/eventrelay-nul\x00-dir") - # Premise: the bare calls really are not OSError-total. + # Premise: the bare calls really are not OSError-total. Assert this + # against the unguarded calls -- whether ``rmtree``'s own + # ``ignore_errors=True`` also happens to absorb a non-OSError is a + # CPython implementation detail that has changed across versions, so + # the helper must not depend on it either way. with pytest.raises(ValueError, match="null"): nul_video.unlink() with pytest.raises(ValueError, match="null"): - shutil.rmtree(nul_root, ignore_errors=True) + shutil.rmtree(nul_root) # Contract: the helper swallows both and returns normally. await TranscriptActionWorkflow._cleanup_download_artifacts( From 2fc3648a8fd906e24fe977ddc9156e4884986b43 Mon Sep 17 00:00:00 2001 From: Hayden Garvey <154503486+groupthinking@users.noreply.github.com> Date: Tue, 4 Aug 2026 06:09:43 +0000 Subject: [PATCH 2/5] fix(ci): stop arming truth gate from bare agent labels on linked issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agentTaskApplicable() unioned PR labels with linked-issue labels, so a bare agent-task/mcp-agent label applied by label automation to an issue that never declared a contract judged any closing PR an agent completion. The gate then required an Agent Run ID / Agent Login the issue never declared, yielding a permanent blocked/invalid_payload verdict no author could satisfy. Applicability now comes from PR provenance (known agent author, agent branch prefix, agent label on the PR, lock manifest) or from a genuine issue-side dispatch: an agent-task/mcp-agent label AND declared Agent Run ID + Agent Login headings. The generic agent label remains a PR-side signal only, since the snapshot job and collector never recognise it issue-side — arming from it would block permanently as linked_issue_not_agent_task with no snapshot to satisfy. The collector now emits a mislabelled_agent_task core.notice when a linked issue carries a contract label without declaring the contract, keyed on the missing contract itself (not inapplicability) so Dependabot PRs linked to valid contracts never get a false notice. Generated with [Linear](https://linear.app/myxstack/issue/GRV-196/agent-completiontruth-gate-is-permanently-unsatisfiable-for-any-pr#agent-session-8e5d62f0) Co-authored-by: linear-code[bot] <222613912+linear-code[bot]@users.noreply.github.com> --- .github/workflows/pr-checks.yml | 187 +++++++++++++++++----- tests/unit/test_agent_completion_gate.py | 192 ++++++++++++++++++++++- 2 files changed, 335 insertions(+), 44 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 4131c473b..0a1a84c98 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -618,6 +618,43 @@ jobs: return String(value || '').toLowerCase() .replace(/[^a-z0-9 ]/g, '').trim(); } + function carriesLabel(source, accepted) { + return (source || []).map(label => + typeof label === 'string' ? label : label.name + ).map(normaliseLabel).some(label => + accepted.includes(label) + ); + } + function declaresAgentContract(issue) { + function section(body, headings) { + const wanted = new Set(headings.map(normaliseLabel)); + const lines = String(body || '').split(/\r?\n/); + const output = []; + let collecting = false; + for (const line of lines) { + const heading = line.match(/^#{2,6}\s+(.+?)\s*$/); + if (heading) { + if (collecting) { + break; + } + collecting = wanted.has(normaliseLabel(heading[1])); + continue; + } + if (collecting) { + output.push(line); + } + } + return output.join('\n').trim(); + } + function declared(headings) { + const value = section( + String((issue && issue.body) || ''), headings + ).replace(/^\x60|\x60$/g, '').trim(); + return Boolean(value) && value !== '_No response_'; + } + return declared(['agent run id', 'run id']) && + declared(['agent login']); + } const login = String( pull && pull.user && pull.user.login || '' ); @@ -628,21 +665,6 @@ jobs: 'openai-codex[bot]', 'chatgpt-codex-connector[bot]' ]); - const labels = [ - ...((pull && pull.labels) || []).map(label => - typeof label === 'string' ? label : label.name - ), - ...(selectedIssue && selectedIssue.labels - ? Array.isArray(selectedIssue.labels) - ? selectedIssue.labels - : selectedIssue.labels.nodes || [] - : []).map(label => - typeof label === 'string' ? label : label.name - ) - ].map(normaliseLabel); - const agentLabel = labels.some(label => - ['agent', 'agenttask', 'mcpagent'].includes(label) - ); const agentBranch = /^(?:agent|claude|codex|copilot|jules)[/-]/i.test( String(pull && pull.head && pull.head.ref || '') @@ -651,10 +673,33 @@ jobs: //i.test( String(pull && pull.body || '') ); - return login !== 'dependabot[bot]' && ( - knownAgents.has(login) || agentBranch || agentLabel || - manifestPresent - ); + // Provenance asserted by the pull request itself. Each of these + // is a claim by the producing side that this is agent work. + const pullProvenance = knownAgents.has(login) || agentBranch || + manifestPresent || + carriesLabel((pull && pull.labels) || [], + ['agent', 'agenttask', 'mcpagent']); + // Issue-side dispatch. Label automation also applies agent task + // labels as topic tags to issues that never declared a contract, + // so the bare label is not evidence of a dispatch: it only + // counts when the issue actually declares the run id and login + // the gate goes on to require. Treating the bare label as + // sufficient made the gate permanently unsatisfiable for human + // pull requests closing such issues, because the contract they + // were measured against had never been written (#1130). Only the + // two contract labels count here -- the generic `agent` label is + // never recognised by the snapshot job or the collector, so it + // stays a pull-request-side provenance signal only. + const issueLabelSource = selectedIssue && selectedIssue.labels + ? Array.isArray(selectedIssue.labels) + ? selectedIssue.labels + : selectedIssue.labels.nodes || [] + : []; + const issueDispatch = + carriesLabel(issueLabelSource, ['agenttask', 'mcpagent']) && + declaresAgentContract(selectedIssue); + return login !== 'dependabot[bot]' && + (pullProvenance || issueDispatch); } function intentContractErrors(issue, comments, pullCreatedAt) { const errors = []; @@ -1876,6 +1921,43 @@ jobs: return String(value || '').toLowerCase() .replace(/[^a-z0-9 ]/g, '').trim(); } + function carriesLabel(source, accepted) { + return (source || []).map(label => + typeof label === 'string' ? label : label.name + ).map(normaliseLabel).some(label => + accepted.includes(label) + ); + } + function declaresAgentContract(issue) { + function section(body, headings) { + const wanted = new Set(headings.map(normaliseLabel)); + const lines = String(body || '').split(/\r?\n/); + const output = []; + let collecting = false; + for (const line of lines) { + const heading = line.match(/^#{2,6}\s+(.+?)\s*$/); + if (heading) { + if (collecting) { + break; + } + collecting = wanted.has(normaliseLabel(heading[1])); + continue; + } + if (collecting) { + output.push(line); + } + } + return output.join('\n').trim(); + } + function declared(headings) { + const value = section( + String((issue && issue.body) || ''), headings + ).replace(/^\x60|\x60$/g, '').trim(); + return Boolean(value) && value !== '_No response_'; + } + return declared(['agent run id', 'run id']) && + declared(['agent login']); + } const login = String( pull && pull.user && pull.user.login || '' ); @@ -1886,21 +1968,6 @@ jobs: 'openai-codex[bot]', 'chatgpt-codex-connector[bot]' ]); - const labels = [ - ...((pull && pull.labels) || []).map(label => - typeof label === 'string' ? label : label.name - ), - ...(selectedIssue && selectedIssue.labels - ? Array.isArray(selectedIssue.labels) - ? selectedIssue.labels - : selectedIssue.labels.nodes || [] - : []).map(label => - typeof label === 'string' ? label : label.name - ) - ].map(normaliseLabel); - const agentLabel = labels.some(label => - ['agent', 'agenttask', 'mcpagent'].includes(label) - ); const agentBranch = /^(?:agent|claude|codex|copilot|jules)[/-]/i.test( String(pull && pull.head && pull.head.ref || '') @@ -1909,10 +1976,33 @@ jobs: //i.test( String(pull && pull.body || '') ); - return login !== 'dependabot[bot]' && ( - knownAgents.has(login) || agentBranch || agentLabel || - manifestPresent - ); + // Provenance asserted by the pull request itself. Each of these + // is a claim by the producing side that this is agent work. + const pullProvenance = knownAgents.has(login) || agentBranch || + manifestPresent || + carriesLabel((pull && pull.labels) || [], + ['agent', 'agenttask', 'mcpagent']); + // Issue-side dispatch. Label automation also applies agent task + // labels as topic tags to issues that never declared a contract, + // so the bare label is not evidence of a dispatch: it only + // counts when the issue actually declares the run id and login + // the gate goes on to require. Treating the bare label as + // sufficient made the gate permanently unsatisfiable for human + // pull requests closing such issues, because the contract they + // were measured against had never been written (#1130). Only the + // two contract labels count here -- the generic `agent` label is + // never recognised by the snapshot job or the collector, so it + // stays a pull-request-side provenance signal only. + const issueLabelSource = selectedIssue && selectedIssue.labels + ? Array.isArray(selectedIssue.labels) + ? selectedIssue.labels + : selectedIssue.labels.nodes || [] + : []; + const issueDispatch = + carriesLabel(issueLabelSource, ['agenttask', 'mcpagent']) && + declaresAgentContract(selectedIssue); + return login !== 'dependabot[bot]' && + (pullProvenance || issueDispatch); } const prNumber = Number(process.env.INPUT_PR_NUMBER || 0); @@ -2163,6 +2253,27 @@ jobs: .replace(/^\x60|\x60$/g, '').trim(); const expectedAgentLogin = section(issueBody, ['agent login']) .replace(/^\x60|\x60$/g, '').trim(); + // A linked issue carrying an agent task label without declaring a + // run id and login was never dispatched to an agent -- the label + // is topic noise. Surface the mislabel so it is visible and + // correctable (#1130). Keyed on the missing contract itself, not + // on inapplicability, so pull requests that are inapplicable for + // other reasons (for example Dependabot's unconditional + // exclusion) never receive a notice falsely claiming a declared + // contract is missing. + const contractDeclared = [expectedRunId, expectedAgentLogin] + .every(value => value && value !== '_No response_'); + if (issue && !contractDeclared && + ['agenttask', 'mcpagent'].some(label => + issueLabels.has(label))) { + core.notice( + 'mislabelled_agent_task: issue #' + issue.number + + ' carries an agent task label but declares no Agent Run ID' + + ' / Agent Login. The label asserts an agent dispatch' + + ' contract; remove it from the issue if it was applied in' + + ' error.' + ); + } if (applicable && !expectedRunId) { collectionErrors.push('missing_agent_run_id'); } else if (applicable && String(manifest.run_id || '') !== expectedRunId) { diff --git a/tests/unit/test_agent_completion_gate.py b/tests/unit/test_agent_completion_gate.py index 0cd300f63..ee65d7125 100644 --- a/tests/unit/test_agent_completion_gate.py +++ b/tests/unit/test_agent_completion_gate.py @@ -2870,9 +2870,14 @@ def test_scheduled_scanner_detects_frozen_intent_changes(self): self.assertEqual(len(applicable_functions), 2) self.assertEqual(applicable_functions[0], applicable_functions[1]) applicable_assertions = r""" -const issue = (number, labels) => ({ +const CONTRACT = [ + '### Agent Login', '', '`google-labs-jules[bot]`', '', + '### Agent Run ID', '', '`run-42`' +].join('\n'); +const issue = (number, labels, body) => ({ number, - labels: {nodes: labels.map(name => ({name}))} + labels: {nodes: labels.map(name => ({name}))}, + body: body || '' }); const base = { user: {login: 'maintainer'}, @@ -2883,7 +2888,11 @@ def test_scheduled_scanner_detects_frozen_intent_changes(self): const rows = [ ['human', base, null, false], ['PR label', {...base, labels: [{name: 'agent-task'}]}, null, true], - ['issue label', base, issue(1, ['mcp/agent']), true], + // A bare agent label on the linked issue is a topic tag applied by label + // automation, not a dispatch. It only asserts agent work when the issue + // actually declares the contract the gate goes on to require (#1130). + ['issue label without contract', base, issue(1, ['mcp/agent']), false], + ['issue label with contract', base, issue(1, ['mcp/agent'], CONTRACT), true], ['branch', {...base, head: {ref: 'codex/fix'}}, null, true], ['manifest', {...base, body: ''}, null, true], ['known agent', {...base, user: {login: 'google-labs-jules[bot]'}}, null, true], @@ -2891,7 +2900,7 @@ def test_scheduled_scanner_detects_frozen_intent_changes(self): ...base, user: {login: 'dependabot[bot]'}, labels: [{name: 'agent-task'}] - }, issue(1, ['agent-task']), false] + }, issue(1, ['agent-task'], CONTRACT), false] ]; for (const [name, pull, selected, expected] of rows) { const actual = agentTaskApplicable(pull, selected); @@ -2965,8 +2974,13 @@ def test_scheduled_scanner_detects_frozen_intent_changes(self): self.assertEqual(completed.returncode, 0, completed.stderr) combined_assertions = r""" -const first = {number: 1, labels: {nodes: []}}; -const second = {number: 2, labels: {nodes: [{name: 'agent-task'}]}}; +const first = {number: 1, labels: {nodes: []}, body: ''}; +const second = { + number: 2, + labels: {nodes: [{name: 'agent-task'}]}, + body: '### Agent Login\n\n`google-labs-jules[bot]`\n\n' + + '### Agent Run ID\n\n`run-42`' +}; const multi = { user: {login: 'maintainer'}, head: {ref: 'feature/ordinary'}, @@ -3358,6 +3372,172 @@ def test_adapter_recognizes_agent_labels_scripts_and_blocking_threads(self): self.assertIn("!explicitlyNonBehavioral", workflow) self.assertIn("VADE-RECOMMENDATION", workflow) + def test_agent_applicability_requires_provenance_or_declared_contract(self): + """A bare `agent-task` label on a linked issue is not a dispatch. + + Label automation applies `agent-task` and `mcp/agent` as topic tags to + issues that were never created from the agent task template. Before + this guard the gate unioned pull request labels with linked issue + labels, so any pull request closing such an issue was judged an agent + completion and then measured against a contract the issue had never + declared -- producing a permanent ``blocked``/``invalid_payload`` + verdict that no author could satisfy (#1130). + """ + + workflow = self._workflow() + applicable = _javascript_functions( + workflow, + "function agentTaskApplicable(", + ) + self.assertEqual(len(applicable), 2) + + assertions = r""" +const CONTRACT = [ + '### Agent Login', + '', + '`google-labs-jules[bot]`', + '', + '### Agent Run ID', + '', + '`run-42`' +].join('\n'); +const human = { + user: {login: 'groupthinking'}, + head: {ref: 'feature/thing'}, + labels: [], + body: 'Fixes #7' +}; +const rows = [ + // The regression this guard exists for: a human pull request closing an + // issue that automation mislabelled `agent-task` without a contract. + ['mislabelled linked issue', { + user: {login: 'groupthinking'}, + head: {ref: 'fix/agentic-workflow-noop-terminal-state-1091'}, + labels: [{name: 'documentation'}, {name: 'ci/cd'}], + body: 'Fixes #1091' + }, { + number: 1091, + labels: [{name: 'agent-task'}, {name: 'bug'}], + body: '## Summary\nSomething broke.\n' + }, false], + // A genuine issue-side dispatch is still gated. + ['declared contract', human, { + number: 7, labels: [{name: 'agent-task'}], body: CONTRACT + }, true], + ['contract via graphql label nodes', human, { + number: 7, labels: {nodes: [{name: 'agent-task'}]}, body: CONTRACT + }, true], + // Only the two contract labels arm the issue side. The generic `agent` + // label is never recognised by the snapshot job or the collector, so an + // issue carrying it (even with contract headings) must not arm the gate: + // it would be permanently blocked as linked_issue_not_agent_task with no + // snapshot to satisfy. + ['generic agent label on issue', human, { + number: 7, labels: [{name: 'agent'}], body: CONTRACT + }, false], + // Unfilled issue form fields render as the placeholder, not a contract. + ['no response placeholder', human, { + number: 7, + labels: [{name: 'agent-task'}], + body: '### Agent Login\n\n_No response_\n\n' + + '### Agent Run ID\n\n_No response_\n' + }, false], + ['half declared contract', human, { + number: 7, + labels: [{name: 'agent-task'}], + body: '### Agent Login\n\n`jules`\n' + }, false], + ['unlabelled issue with contract', human, { + number: 7, labels: [{name: 'bug'}], body: CONTRACT + }, false], + // Pull request provenance stands alone: an agent producing work against a + // contract-less issue is still applicable, and therefore still blocked. + ['known agent author', { + user: {login: 'google-labs-jules[bot]'}, + head: {ref: 'feature/thing'}, labels: [], body: '' + }, {number: 7, labels: [{name: 'agent-task'}], body: '## Summary\n'}, true], + ['agent branch prefix', { + user: {login: 'groupthinking'}, + head: {ref: 'jules/thing'}, labels: [], body: '' + }, null, true], + ['agent label on the pull request', { + user: {login: 'groupthinking'}, + head: {ref: 'feature/thing'}, + labels: [{name: 'agent'}], body: '' + }, null, true], + ['lock manifest in the pull request body', { + user: {login: 'groupthinking'}, + head: {ref: 'feature/thing'}, + labels: [], + body: '' + }, null, true], + // Dependabot is excluded regardless of every other signal. + ['dependabot', { + user: {login: 'dependabot[bot]'}, + head: {ref: 'jules/bump'}, + labels: [{name: 'agent-task'}], body: '' + }, {number: 7, labels: [{name: 'agent-task'}], body: CONTRACT}, false], + ['plain human pull request', human, { + number: 7, labels: [{name: 'bug'}], body: '' + }, false], + ['no linked issue', human, null, false] +]; +for (const [name, pull, issue, expected] of rows) { + const actual = agentTaskApplicable(pull, issue); + if (actual !== expected) { + throw new Error(`${name}: ${actual} !== ${expected}`); + } +} +""" + completed = subprocess.run( + ["node", "-e", applicable[0] + assertions], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(completed.returncode, 0, completed.stderr) + + def test_agent_applicability_copies_stay_identical(self): + """The scheduled sweep and the per-pull-request collector must agree. + + Both jobs publish to the same commit status context, so divergent + applicability logic would let one job block what the other skips. The + contract check is nested inside ``agentTaskApplicable`` so the + function stays self-contained for the ``node -e`` extraction harness + above. + """ + + workflow = self._workflow() + copies = _javascript_functions(workflow, "function agentTaskApplicable(") + self.assertEqual(len(copies), 2) + self.assertEqual(copies[0], copies[1]) + self.assertIn("function declaresAgentContract(", copies[0]) + + def test_mislabelled_agent_task_is_reported_rather_than_blocked(self): + """The mislabel is surfaced as an annotation, never as a gate reason. + + ``agent_completion_gate.evaluate`` short-circuits to + ``not_applicable`` before it reads ``collection_errors``, so a + diagnostic pushed there would be silently discarded. It is emitted + with ``core.notice`` instead, and it is keyed on the missing contract + itself rather than on inapplicability, so pull requests that are + inapplicable for unrelated reasons (Dependabot's unconditional + exclusion) never receive a notice falsely claiming a declared + contract is missing. + """ + + workflow = self._workflow() + + self.assertIn("mislabelled_agent_task", workflow) + self.assertNotIn( + "collectionErrors.push('mislabelled_agent_task')", + workflow, + ) + notice = workflow[workflow.index("mislabelled_agent_task") - 600:] + self.assertIn("core.notice(", notice[:800]) + self.assertIn("!contractDeclared", notice[:800]) + self.assertNotIn("!applicable", notice[:800]) + def test_workflow_does_not_approve_or_merge(self): workflow = self._workflow() From 1484ad33584629c0e98c570adcb07f5fadd40c96 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 00:41:08 +0000 Subject: [PATCH 3/5] fix(ci): require a linked issue before arming the truth gate The truth gate scores a pull request against the frozen intent snapshot on its linked issue. That snapshot is only ever written by snapshot-agent-task-intent, which runs on `issues` events alone -- it never runs on pull_request_target. So a pull request with no linked issue has no snapshot, no declared agent_login and no declared run_id, which means policy.agent_login, policy.run_id and issue.number can never be populated and the verdict is permanently `invalid_payload` no matter what the author does. Arming that unsatisfiable state from a branch-name prefix made the check red on pull requests that never had a contract to satisfy. It is red on merged commits too: #1368, the current tip of main, merged with agent-completion/truth-gate/pr-1368 failing on exactly this. A check that is red on everything gates nothing and buries real failures, which is the failure mode agent-completion-enforcement.yml already warns about in its own comments. Pull-side provenance now arms the gate only when a linked issue exists to verify against. With none there is nothing to measure, so the verdict is not_applicable rather than blocked. This is not an escape hatch: a pull request that links a dispatched issue is gated exactly as before, and the requirement to bind a pull request to a focused issue at all is separately owned by the `Canonical issue and evidence` check, which states a requirement an author can actually meet. Consolidates the two competing open implementations of this fix. #1364's commit is cherry-picked here with authorship intact; #1154 carried the same intent but had drifted to 117 files and 14k lines of unrelated changes. Both should close in favour of this. Full unit suite: 8079 passed, 0 failed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01YcHjCZ6pGn6A5BeeoZ6eZi --- .github/workflows/pr-checks.yml | 50 +++++++++++++++++++++++- tests/unit/test_agent_completion_gate.py | 47 ++++++++++++++++------ 2 files changed, 83 insertions(+), 14 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 0a1a84c98..8f5dec4fe 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -698,8 +698,31 @@ jobs: const issueDispatch = carriesLabel(issueLabelSource, ['agenttask', 'mcpagent']) && declaresAgentContract(selectedIssue); + // Pull-side provenance says who produced the branch. It is not + // evidence that a dispatch contract exists to measure that + // branch against. The gate scores a pull request against the + // frozen intent snapshot on its linked issue, and that snapshot + // is only ever written by `snapshot-agent-task-intent`, which + // runs on `issues` events alone. With no linked issue there is + // no snapshot, no declared run id and no declared login, so + // `policy.agent_login`, `policy.run_id` and `issue.number` are + // all unsatisfiable and the verdict is permanently + // `invalid_payload` regardless of what the author does. A + // branch named `claude/...` is a naming convention, not a + // dispatch. Arming on it alone is what made this check red on + // pull requests that never had a contract to satisfy -- including + // #1368, which merged with this status failing. + // + // So provenance arms the gate only once a linked issue exists to + // verify against; with none, there is nothing to measure and the + // verdict is `not_applicable`. This does not create an escape + // hatch: a pull request that links a dispatched issue is still + // fully gated, and requiring a pull request to bind to a focused + // issue at all is separately owned by `Canonical issue and + // evidence`, which states a requirement an author can actually + // meet. return login !== 'dependabot[bot]' && - (pullProvenance || issueDispatch); + (issueDispatch || (pullProvenance && Boolean(selectedIssue))); } function intentContractErrors(issue, comments, pullCreatedAt) { const errors = []; @@ -2001,8 +2024,31 @@ jobs: const issueDispatch = carriesLabel(issueLabelSource, ['agenttask', 'mcpagent']) && declaresAgentContract(selectedIssue); + // Pull-side provenance says who produced the branch. It is not + // evidence that a dispatch contract exists to measure that + // branch against. The gate scores a pull request against the + // frozen intent snapshot on its linked issue, and that snapshot + // is only ever written by `snapshot-agent-task-intent`, which + // runs on `issues` events alone. With no linked issue there is + // no snapshot, no declared run id and no declared login, so + // `policy.agent_login`, `policy.run_id` and `issue.number` are + // all unsatisfiable and the verdict is permanently + // `invalid_payload` regardless of what the author does. A + // branch named `claude/...` is a naming convention, not a + // dispatch. Arming on it alone is what made this check red on + // pull requests that never had a contract to satisfy -- including + // #1368, which merged with this status failing. + // + // So provenance arms the gate only once a linked issue exists to + // verify against; with none, there is nothing to measure and the + // verdict is `not_applicable`. This does not create an escape + // hatch: a pull request that links a dispatched issue is still + // fully gated, and requiring a pull request to bind to a focused + // issue at all is separately owned by `Canonical issue and + // evidence`, which states a requirement an author can actually + // meet. return login !== 'dependabot[bot]' && - (pullProvenance || issueDispatch); + (issueDispatch || (pullProvenance && Boolean(selectedIssue))); } const prNumber = Number(process.env.INPUT_PR_NUMBER || 0); diff --git a/tests/unit/test_agent_completion_gate.py b/tests/unit/test_agent_completion_gate.py index ee65d7125..c58715293 100644 --- a/tests/unit/test_agent_completion_gate.py +++ b/tests/unit/test_agent_completion_gate.py @@ -2885,17 +2885,31 @@ def test_scheduled_scanner_detects_frozen_intent_changes(self): labels: [], body: '' }; +const PLAIN = issue(1, [], ''); const rows = [ ['human', base, null, false], - ['PR label', {...base, labels: [{name: 'agent-task'}]}, null, true], // A bare agent label on the linked issue is a topic tag applied by label // automation, not a dispatch. It only asserts agent work when the issue // actually declares the contract the gate goes on to require (#1130). ['issue label without contract', base, issue(1, ['mcp/agent']), false], ['issue label with contract', base, issue(1, ['mcp/agent'], CONTRACT), true], - ['branch', {...base, head: {ref: 'codex/fix'}}, null, true], - ['manifest', {...base, body: ''}, null, true], - ['known agent', {...base, user: {login: 'google-labs-jules[bot]'}}, null, true], + // Pull-side provenance identifies the producer; it does not supply a + // contract to score against. With no linked issue there is no intent + // snapshot, so `policy.agent_login`, `policy.run_id` and `issue.number` + // can never be populated and the verdict is permanently `invalid_payload`. + // Each of these arms the gate only once an issue exists to verify against. + ['PR label, no issue', {...base, labels: [{name: 'agent-task'}]}, null, false], + ['PR label + issue', {...base, labels: [{name: 'agent-task'}]}, PLAIN, true], + ['branch, no issue', {...base, head: {ref: 'codex/fix'}}, null, false], + ['branch + issue', {...base, head: {ref: 'codex/fix'}}, PLAIN, true], + ['manifest, no issue', + {...base, body: ''}, null, false], + ['manifest + issue', + {...base, body: ''}, PLAIN, true], + ['known agent, no issue', + {...base, user: {login: 'google-labs-jules[bot]'}}, null, false], + ['known agent + issue', + {...base, user: {login: 'google-labs-jules[bot]'}}, PLAIN, true], ['dependabot excluded', { ...base, user: {login: 'dependabot[bot]'}, @@ -3450,27 +3464,36 @@ def test_agent_applicability_requires_provenance_or_declared_contract(self): ['unlabelled issue with contract', human, { number: 7, labels: [{name: 'bug'}], body: CONTRACT }, false], - // Pull request provenance stands alone: an agent producing work against a - // contract-less issue is still applicable, and therefore still blocked. + // Pull request provenance applies against a linked issue -- an agent + // producing work against a contract-less issue is still applicable, and + // therefore still blocked. ['known agent author', { user: {login: 'google-labs-jules[bot]'}, head: {ref: 'feature/thing'}, labels: [], body: '' }, {number: 7, labels: [{name: 'agent-task'}], body: '## Summary\n'}, true], - ['agent branch prefix', { + // ...but with no linked issue there is no intent snapshot and no declared + // run id or login, so the required payload fields are unsatisfiable and the + // gate would block permanently rather than ever reaching a verdict. These + // are `not_applicable`, not violations. + ['agent branch prefix, no issue', { user: {login: 'groupthinking'}, head: {ref: 'jules/thing'}, labels: [], body: '' - }, null, true], - ['agent label on the pull request', { + }, null, false], + ['agent branch prefix with issue', { + user: {login: 'groupthinking'}, + head: {ref: 'jules/thing'}, labels: [], body: '' + }, {number: 7, labels: [], body: ''}, true], + ['agent label on the pull request, no issue', { user: {login: 'groupthinking'}, head: {ref: 'feature/thing'}, labels: [{name: 'agent'}], body: '' - }, null, true], - ['lock manifest in the pull request body', { + }, null, false], + ['lock manifest in the pull request body, no issue', { user: {login: 'groupthinking'}, head: {ref: 'feature/thing'}, labels: [], body: '' - }, null, true], + }, null, false], // Dependabot is excluded regardless of every other signal. ['dependabot', { user: {login: 'dependabot[bot]'}, From 4a6f2f7bbe02ad8f8701e03426a514b95de9f5f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 01:03:53 +0000 Subject: [PATCH 4/5] chore(maintenance): audit all 337 remote branches and add a prune script Classifies every remote branch using the one signal that stays honest after the secret-purge force-push: whether it shares any ancestry with `main`. $ git merge-base origin/main origin/ (empty) 275 of 337 branches return empty -- they predate the rewrite and no rebase recovers them. The signals the branch-cleanup harness normally leans on all mislead here, so they are deliberately not used: * `git merge-tree` calls these orphans a CLEAN merge; unrelated trees do not textually conflict, they would clobber. * A two-dot diff against an empty merge base silently degrades to a working-tree diff, which is why a two-line Dependabot bump measures as 111 files / 15,650 lines. * The purge rewrote committer dates, so every branch reads as under 30 days old and no staleness threshold ever fires. Running the stock harness on this repo produced 263 REVIEW off those bad signals. The ancestry test resolves the same set into: KEEP-OPEN-PR 28 REVIEW-SHARED 29 real shared ancestry, no open PR -- not pruned CLOSE-MERGED 1 tip is an ancestor of main CLOSE-ORPHANED 275 The script prunes only the last two groups (276 branches) and archive-tags each one first, verifying every tag is on the remote before deleting anything. It defaults to a dry run. That dry run caught the audit classifying `main` itself as CLOSE-MERGED -- `git merge-base --is-ancestor origin/main origin/main` is trivially true. The row is removed, and a protected-ref guard plus a default-branch check now abort regardless of what the CSV contains. Not executed here: this session's credentials are scoped to one branch and tag creation fails with HTTP 403, so the archive tags cannot be written. Deleting without them would remove the only durable recovery path, so nothing was deleted. Run the script with tag-write credentials. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01YcHjCZ6pGn6A5BeeoZ6eZi --- docs/branch-audit-2026-08-05.csv | 334 ++++++++++++++++++ .../maintenance/archive-and-prune-branches.sh | 153 ++++++++ 2 files changed, 487 insertions(+) create mode 100644 docs/branch-audit-2026-08-05.csv create mode 100755 scripts/maintenance/archive-and-prune-branches.sh diff --git a/docs/branch-audit-2026-08-05.csv b/docs/branch-audit-2026-08-05.csv new file mode 100644 index 000000000..7c098ee50 --- /dev/null +++ b/docs/branch-audit-2026-08-05.csv @@ -0,0 +1,334 @@ +branch,verdict,last_author_date +claude/determined-maxwell-dbco8k,CLOSE-MERGED,2026-08-03 +agent-harden-api-cost-outbox-17376027973963893260,CLOSE-ORPHANED,2026-07-24 +agent/add-playwright-k6-readiness-gates-12086133390999099551,CLOSE-ORPHANED,2026-07-24 +agent/autonomous-repository-governance-11618302243648674141,CLOSE-ORPHANED,2026-07-23 +agent/harden-api-cost-outbox,CLOSE-ORPHANED,2026-07-27 +agent/harden-api-cost-outbox-17376027973963893260,CLOSE-ORPHANED,2026-07-25 +agent/harden-api-cost-outbox-4026591943861956227,CLOSE-ORPHANED,2026-07-23 +agent/sop-orchestration-framework-12765657140344224400,CLOSE-ORPHANED,2026-07-23 +agent/suppress-noop-runs-reporting-5600344477059750791,CLOSE-ORPHANED,2026-07-23 +bolt-agentflow-visualizer-optimize-4496922033576160387,CLOSE-ORPHANED,2026-07-25 +bolt-fix-database-optimizer-n1-queries-14843617805558882255,CLOSE-ORPHANED,2026-07-14 +bolt-interactive-transcript-search-opt-14916651575670311014,CLOSE-ORPHANED,2026-07-21 +bolt-memoize-segment-row-14076716723355865204,CLOSE-ORPHANED,2026-07-12 +bolt-optimize-call-stacks-and-allocations-6836399447387675120,CLOSE-ORPHANED,2026-07-26 +bolt-optimize-interactive-transcript-4719841486477851193,CLOSE-ORPHANED,2026-07-24 +bolt-optimize-interactive-transcript-4719841486477851193-7466394128762780856,CLOSE-ORPHANED,2026-07-23 +bolt-optimize-interactive-transcript-5311554956858795087,CLOSE-ORPHANED,2026-07-24 +bolt-optimize-interactive-transcript-filtering-12565678381754441902,CLOSE-ORPHANED,2026-07-21 +bolt-optimize-math-spread-10940801643914955491,CLOSE-ORPHANED,2026-08-01 +bolt-optimize-tolowercase-17536213582151471881,CLOSE-ORPHANED,2026-07-21 +bolt/fix-batch-query-n1-bottleneck-16664381751066617032,CLOSE-ORPHANED,2026-07-12 +bolt/frontend-string-alloc-optimizations-17876779194115511565,CLOSE-ORPHANED,2026-07-23 +bolt/optimize-text-filtering-15414235492514692486,CLOSE-ORPHANED,2026-07-24 +bolt/optimize-transcript-filtering-15405883277453463883,CLOSE-ORPHANED,2026-07-23 +bolt/optimize-viewbox-computation-16758650612686925146,CLOSE-ORPHANED,2026-07-27 +claude/add-deterministic-agent-completion-truth-gate,CLOSE-ORPHANED,2026-07-24 +claude/agent-login-fix,CLOSE-ORPHANED,2026-07-24 +claude/dazzling-edison-95u1m4,CLOSE-ORPHANED,2026-07-17 +claude/dazzling-edison-ymkr67,CLOSE-ORPHANED,2026-07-17 +claude/determined-maxwell-06popt,CLOSE-ORPHANED,2026-06-29 +claude/determined-maxwell-0ium0k,CLOSE-ORPHANED,2026-07-27 +claude/determined-maxwell-0n3r27,CLOSE-ORPHANED,2026-08-01 +claude/determined-maxwell-11jydn,CLOSE-ORPHANED,2026-07-12 +claude/determined-maxwell-13gas7,CLOSE-ORPHANED,2026-07-21 +claude/determined-maxwell-1419pg,CLOSE-ORPHANED,2026-08-01 +claude/determined-maxwell-14jgh5,CLOSE-ORPHANED,2026-07-14 +claude/determined-maxwell-1y1im5,CLOSE-ORPHANED,2026-07-18 +claude/determined-maxwell-3vgs17,CLOSE-ORPHANED,2026-07-09 +claude/determined-maxwell-3z56dm,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-4dxv0e,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-4k7sox,CLOSE-ORPHANED,2026-07-31 +claude/determined-maxwell-4ky0tz,CLOSE-ORPHANED,2026-07-14 +claude/determined-maxwell-4l1ghl,CLOSE-ORPHANED,2026-07-17 +claude/determined-maxwell-4nb64a,CLOSE-ORPHANED,2026-07-02 +claude/determined-maxwell-4thx4d,CLOSE-ORPHANED,2026-07-10 +claude/determined-maxwell-5593h9,CLOSE-ORPHANED,2026-07-02 +claude/determined-maxwell-5c11cx,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-5k80n4,CLOSE-ORPHANED,2026-07-20 +claude/determined-maxwell-5t91o2,CLOSE-ORPHANED,2026-08-02 +claude/determined-maxwell-61nzcv,CLOSE-ORPHANED,2026-07-12 +claude/determined-maxwell-6rg53d,CLOSE-ORPHANED,2026-07-09 +claude/determined-maxwell-6ssl0l,CLOSE-ORPHANED,2026-07-27 +claude/determined-maxwell-7c9b1r,CLOSE-ORPHANED,2026-08-01 +claude/determined-maxwell-7l7uv0,CLOSE-ORPHANED,2026-07-23 +claude/determined-maxwell-7p4us0,CLOSE-ORPHANED,2026-08-01 +claude/determined-maxwell-7vrh8v,CLOSE-ORPHANED,2026-06-28 +claude/determined-maxwell-7wu0ng,CLOSE-ORPHANED,2026-07-17 +claude/determined-maxwell-7xje6x,CLOSE-ORPHANED,2026-07-28 +claude/determined-maxwell-81rne1,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-82cm3w,CLOSE-ORPHANED,2026-08-01 +claude/determined-maxwell-8p5n7u,CLOSE-ORPHANED,2026-07-17 +claude/determined-maxwell-92049h,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-9cogca,CLOSE-ORPHANED,2026-08-01 +claude/determined-maxwell-9erquc,CLOSE-ORPHANED,2026-07-18 +claude/determined-maxwell-9ld7rc,CLOSE-ORPHANED,2026-07-15 +claude/determined-maxwell-9qcq2r,CLOSE-ORPHANED,2026-07-06 +claude/determined-maxwell-9qx6ue,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-9wecpk,CLOSE-ORPHANED,2026-07-12 +claude/determined-maxwell-a6apnm,CLOSE-ORPHANED,2026-07-03 +claude/determined-maxwell-aacju9,CLOSE-ORPHANED,2026-07-04 +claude/determined-maxwell-ayuxew,CLOSE-ORPHANED,2026-07-14 +claude/determined-maxwell-bfoiqr,CLOSE-ORPHANED,2026-07-12 +claude/determined-maxwell-bloicm,CLOSE-ORPHANED,2026-07-17 +claude/determined-maxwell-bmfsgm,CLOSE-ORPHANED,2026-07-02 +claude/determined-maxwell-bowgqi,CLOSE-ORPHANED,2026-07-15 +claude/determined-maxwell-brk70z,CLOSE-ORPHANED,2026-07-12 +claude/determined-maxwell-cmkdxq,CLOSE-ORPHANED,2026-07-12 +claude/determined-maxwell-dczolv,CLOSE-ORPHANED,2026-07-17 +claude/determined-maxwell-e0iuif,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-e1ur8w,CLOSE-ORPHANED,2026-07-12 +claude/determined-maxwell-e8uv5v,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-e9hha1,CLOSE-ORPHANED,2026-07-17 +claude/determined-maxwell-en9kru,CLOSE-ORPHANED,2026-07-06 +claude/determined-maxwell-ey01vy,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-f02xls,CLOSE-ORPHANED,2026-07-12 +claude/determined-maxwell-fuumwi,CLOSE-ORPHANED,2026-07-06 +claude/determined-maxwell-g31z0u,CLOSE-ORPHANED,2026-07-18 +claude/determined-maxwell-g6w2ay,CLOSE-ORPHANED,2026-08-01 +claude/determined-maxwell-gfxhok,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-gvupbt,CLOSE-ORPHANED,2026-07-12 +claude/determined-maxwell-h7gz7l,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-h904ct,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-hli561,CLOSE-ORPHANED,2026-07-03 +claude/determined-maxwell-hu0rea,CLOSE-ORPHANED,2026-07-12 +claude/determined-maxwell-i68kve,CLOSE-ORPHANED,2026-08-01 +claude/determined-maxwell-ilpxq0,CLOSE-ORPHANED,2026-07-23 +claude/determined-maxwell-iox2ok,CLOSE-ORPHANED,2026-07-17 +claude/determined-maxwell-ip1f59,CLOSE-ORPHANED,2026-07-06 +claude/determined-maxwell-jm8plv,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-kaypem,CLOSE-ORPHANED,2026-07-08 +claude/determined-maxwell-kdbbzs,CLOSE-ORPHANED,2026-07-03 +claude/determined-maxwell-klo4xc,CLOSE-ORPHANED,2026-07-21 +claude/determined-maxwell-kpfd1c,CLOSE-ORPHANED,2026-07-12 +claude/determined-maxwell-l4kzow,CLOSE-ORPHANED,2026-07-12 +claude/determined-maxwell-ldvlx6,CLOSE-ORPHANED,2026-06-29 +claude/determined-maxwell-ldye72,CLOSE-ORPHANED,2026-07-15 +claude/determined-maxwell-lxth4b,CLOSE-ORPHANED,2026-07-17 +claude/determined-maxwell-mhgrjd,CLOSE-ORPHANED,2026-07-29 +claude/determined-maxwell-mij7u9,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-msnh15,CLOSE-ORPHANED,2026-07-04 +claude/determined-maxwell-n1dpmx,CLOSE-ORPHANED,2026-07-15 +claude/determined-maxwell-nb1zz3,CLOSE-ORPHANED,2026-06-29 +claude/determined-maxwell-nbnstz,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-o4am5t,CLOSE-ORPHANED,2026-07-29 +claude/determined-maxwell-omtz6j,CLOSE-ORPHANED,2026-08-01 +claude/determined-maxwell-ostftd,CLOSE-ORPHANED,2026-07-17 +claude/determined-maxwell-p4s1gj,CLOSE-ORPHANED,2026-07-11 +claude/determined-maxwell-p5akc5,CLOSE-ORPHANED,2026-08-02 +claude/determined-maxwell-p6cak6,CLOSE-ORPHANED,2026-07-11 +claude/determined-maxwell-p9v5qd,CLOSE-ORPHANED,2026-07-17 +claude/determined-maxwell-pcaeli,CLOSE-ORPHANED,2026-07-06 +claude/determined-maxwell-pduakf,CLOSE-ORPHANED,2026-07-15 +claude/determined-maxwell-ptx784,CLOSE-ORPHANED,2026-07-27 +claude/determined-maxwell-pzoqq8,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-q4p6rk,CLOSE-ORPHANED,2026-07-04 +claude/determined-maxwell-qfkbc0,CLOSE-ORPHANED,2026-07-27 +claude/determined-maxwell-qkp0n8,CLOSE-ORPHANED,2026-07-31 +claude/determined-maxwell-r5qzbp,CLOSE-ORPHANED,2026-08-01 +claude/determined-maxwell-r838un,CLOSE-ORPHANED,2026-07-06 +claude/determined-maxwell-rn9d53,CLOSE-ORPHANED,2026-07-15 +claude/determined-maxwell-s49460,CLOSE-ORPHANED,2026-07-15 +claude/determined-maxwell-sdzggo,CLOSE-ORPHANED,2026-07-31 +claude/determined-maxwell-shjn00,CLOSE-ORPHANED,2026-07-06 +claude/determined-maxwell-t6hfkk,CLOSE-ORPHANED,2026-07-12 +claude/determined-maxwell-tbufwf,CLOSE-ORPHANED,2026-07-12 +claude/determined-maxwell-u7kfre,CLOSE-ORPHANED,2026-07-14 +claude/determined-maxwell-ubh3hf,CLOSE-ORPHANED,2026-07-06 +claude/determined-maxwell-ubv6b6,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-udy1ne,CLOSE-ORPHANED,2026-07-17 +claude/determined-maxwell-uql65l,CLOSE-ORPHANED,2026-07-28 +claude/determined-maxwell-uybuwc,CLOSE-ORPHANED,2026-07-17 +claude/determined-maxwell-vkacey,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-vupf5j,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-w20w27,CLOSE-ORPHANED,2026-07-11 +claude/determined-maxwell-xyhnam,CLOSE-ORPHANED,2026-07-07 +claude/determined-maxwell-yre7nw,CLOSE-ORPHANED,2026-08-01 +claude/determined-maxwell-z1fkxw,CLOSE-ORPHANED,2026-07-23 +claude/evaluate-unused-folders,CLOSE-ORPHANED,2026-07-03 +claude/explore-codebase-implementation-plan,CLOSE-ORPHANED,2026-06-19 +claude/help-github-docs-page,CLOSE-ORPHANED,2026-06-18 +claude/prompt-start-subagents-for-action,CLOSE-ORPHANED,2026-07-07 +claude/repo-architecture-review-Aewa3,CLOSE-ORPHANED,2026-06-29 +claude/review-session-history-tips,CLOSE-ORPHANED,2026-06-21 +coderabbitai/utg/c89b9b7,CLOSE-ORPHANED,2026-07-03 +coderabbitai/utg/e30d1ce,CLOSE-ORPHANED,2026-07-03 +codex/establish-autonomous-governance,CLOSE-ORPHANED,2026-07-20 +codex/myx-51-proxy-hardening,CLOSE-ORPHANED,2026-07-03 +codex/pr877-postmerge-remediation,CLOSE-ORPHANED,2026-07-27 +codex/pr877-postmerge-remediation-10186041658105794493,CLOSE-ORPHANED,2026-07-23 +codex/restore-google-oauth-config,CLOSE-ORPHANED,2026-07-20 +codex/uvai-studio-realtime,CLOSE-ORPHANED,2026-06-08 +copilot/29934228828,CLOSE-ORPHANED,2026-07-22 +copilot/9tavevksgvhued9nuvqyvtrpadnd,CLOSE-ORPHANED,2026-07-07 +copilot/complete-repo,CLOSE-ORPHANED,2026-06-28 +copilot/develop,CLOSE-ORPHANED,2026-07-22 +copilot/explore-codebase-optimizations,CLOSE-ORPHANED,2026-07-27 +copilot/find-uvai-projects-and-test-repos,CLOSE-ORPHANED,2026-06-19 +copilot/fix-agent-completion-enforcement,CLOSE-ORPHANED,2026-07-24 +copilot/fix-api-integration-issue,CLOSE-ORPHANED,2026-07-19 +copilot/fix-commit-and-merge,CLOSE-ORPHANED,2026-07-22 +copilot/fix-copilot-action-error,CLOSE-ORPHANED,2026-07-22 +copilot/fix-copilot-failures,CLOSE-ORPHANED,2026-07-23 +copilot/fix-event-relay-issues,CLOSE-ORPHANED,2026-07-22 +copilot/fix-failing-actions,CLOSE-ORPHANED,2026-07-22 +copilot/fix-misconfiguration-issue,CLOSE-ORPHANED,2026-07-17 +copilot/fix-payload-parameter-issue,CLOSE-ORPHANED,2026-07-31 +copilot/fix-placeholder-responses,CLOSE-ORPHANED,2026-07-07 +copilot/fix-web-lockfile-issue,CLOSE-ORPHANED,2026-07-07 +copilot/fix-with-copilot,CLOSE-ORPHANED,2026-07-22 +copilot/fix-with-copilot-again,CLOSE-ORPHANED,2026-07-22 +copilot/fix-with-copilot-another-one,CLOSE-ORPHANED,2026-07-22 +copilot/inspect-and-resolve-issue,CLOSE-ORPHANED,2026-07-17 +copilot/jules-5495098003656491952-178ff50c,CLOSE-ORPHANED,2026-07-23 +copilot/list-latest-open-pull-requests,CLOSE-ORPHANED,2026-06-28 +copilot/loop-build-next-item-on-plan,CLOSE-ORPHANED,2026-06-22 +copilot/performance-investigation,CLOSE-ORPHANED,2026-07-23 +copilot/refactorhybrid-infra-v2-another-one,CLOSE-ORPHANED,2026-07-07 +copilot/reference-29387579436,CLOSE-ORPHANED,2026-07-15 +copilot/replace-placeholder-send-request-implementations,CLOSE-ORPHANED,2026-07-07 +copilot/replace-placeholder-with-openai-anthropic-and-gemi,CLOSE-ORPHANED,2026-07-06 +copilot/resolve-reference-29551032718,CLOSE-ORPHANED,2026-07-17 +copilot/set-up-load-testing-environment,CLOSE-ORPHANED,2026-07-07 +copilot/update-event-relay-commit,CLOSE-ORPHANED,2026-07-12 +copilot/upgrade-sentry-nextjs-version,CLOSE-ORPHANED,2026-07-07 +copilot/wire-stainless-sdk-into-api-calls,CLOSE-ORPHANED,2026-07-07 +execution-verify-google-oauth-in-vercel-production-pr-grv-88-95ec,CLOSE-ORPHANED,2026-07-28 +feat-use-crypto-uuid-mcp-extractors-1792204326380299787,CLOSE-ORPHANED,2026-07-09 +feat/sentry-nextjs,CLOSE-ORPHANED,2026-06-16 +feat/vera-platform,CLOSE-ORPHANED,2026-06-19 +feature/implement-mcp-orchestrator-execution-7365924146265499223,CLOSE-ORPHANED,2026-07-27 +fix-api-cost-notifications-17019008512322578877,CLOSE-ORPHANED,2026-07-17 +fix-sql-injection-694727443155089718,CLOSE-ORPHANED,2026-07-21 +fix-sql-injection-innovation-dashboard-2441357465350856795,CLOSE-ORPHANED,2026-07-21 +fix-ssl-verification-14087089150067492378,CLOSE-ORPHANED,2026-07-21 +fix-ssl-verification-8459614219617171993,CLOSE-ORPHANED,2026-07-14 +fix/agentic-workflow-noop-terminal-state-1091,CLOSE-ORPHANED,2026-07-31 +fix/auth-google-env-precedence,CLOSE-ORPHANED,2026-07-28 +fix/brace-expansion-override-floors,CLOSE-ORPHANED,2026-07-30 +fix/centralized-transcript-proxy-1087,CLOSE-ORPHANED,2026-07-31 +fix/drop-phantom-python-jose,CLOSE-ORPHANED,2026-08-01 +fix/pipeline-transcript-abort-fallback,CLOSE-ORPHANED,2026-07-29 +fix/ralph-max-unclosed-demo-verification,CLOSE-ORPHANED,2026-06-19 +fix/sql-injection-uvai-mcp-3635811407062686435,CLOSE-ORPHANED,2026-07-21 +fix/ssl-context-vulnerability-339192204152313471,CLOSE-ORPHANED,2026-07-14 +fix/unified-ai-sdk-retry-logic-9463596296568156311,CLOSE-ORPHANED,2026-07-17 +fix/video-processor-pipeline-stubs,CLOSE-ORPHANED,2026-06-20 +fix/ytdlp-download-errors,CLOSE-ORPHANED,2026-07-14 +groupthinking-fix-agent-gate-applicability,CLOSE-ORPHANED,2026-08-01 +groupthinking-fix-apps-web-lockfile-drift,CLOSE-ORPHANED,2026-07-30 +groupthinking-fix-gitleaks-lockfile-false-positive,CLOSE-ORPHANED,2026-08-01 +groupthinking-fix-proxy-credential-leakage-and-vacuous,CLOSE-ORPHANED,2026-07-31 +groupthinking-fix-truth-gate-and-one-click-deploy,CLOSE-ORPHANED,2026-08-01 +groupthinking-issue-triage-priority-fixes,CLOSE-ORPHANED,2026-08-01 +groupthinking-patch-1,CLOSE-ORPHANED,2026-07-22 +jules-10052537357683796690-4b414bf2,CLOSE-ORPHANED,2026-07-22 +jules-12356548241704752495-3bde6b1d,CLOSE-ORPHANED,2026-07-27 +jules-14621095376073933587-506584da,CLOSE-ORPHANED,2026-07-21 +jules-15243187445261469621-ffdb089e,CLOSE-ORPHANED,2026-07-28 +jules-16114574054034633582-24106047,CLOSE-ORPHANED,2026-07-17 +jules-16956857968710046401-f9d5380b,CLOSE-ORPHANED,2026-07-27 +jules-17777940839649608045-490ff2bd,CLOSE-ORPHANED,2026-07-27 +jules-1892762060881911102-099062c8,CLOSE-ORPHANED,2026-07-17 +jules-2208226896713623970-489e4637,CLOSE-ORPHANED,2026-07-21 +jules-2315565067636360607-33125800,CLOSE-ORPHANED,2026-07-14 +jules-2699333998357810880-b5bf4fe2,CLOSE-ORPHANED,2026-07-21 +jules-348407815784084824-9d2bde61,CLOSE-ORPHANED,2026-07-14 +jules-3490404254746687522-e8d41571,CLOSE-ORPHANED,2026-07-21 +jules-5495098003656491952-178ff50c,CLOSE-ORPHANED,2026-07-23 +jules-6360156794036608515-c0e89156,CLOSE-ORPHANED,2026-08-03 +jules-7350580235812023711-6bbd4f1a,CLOSE-ORPHANED,2026-07-28 +jules-7793714024128700837-68e8da47,CLOSE-ORPHANED,2026-07-21 +jules-7975870365122853693-d93780a1,CLOSE-ORPHANED,2026-06-21 +jules-8463579903470851495-195fc0b5,CLOSE-ORPHANED,2026-07-29 +jules-9638972698930112439-d2c4ab7c,CLOSE-ORPHANED,2026-07-07 +jules-bolt-interactive-transcript-2241025088926726400,CLOSE-ORPHANED,2026-07-17 +jules-performance-grok-async-13181522220485004404,CLOSE-ORPHANED,2026-07-17 +jules-refactor-cloud-endpoints-15392513722284013717,CLOSE-ORPHANED,2026-07-12 +jules-security-md5-mitigation-9776717496752801111,CLOSE-ORPHANED,2026-07-12 +jules/resolve-draft-audit-17235039707557018585,CLOSE-ORPHANED,2026-07-15 +palette-a11y-improvements-8526728425873563248,CLOSE-ORPHANED,2026-07-11 +palette-add-aria-label-search-input-11565478109907226861,CLOSE-ORPHANED,2026-07-14 +palette/a11y-video-generator-15541785465389385126,CLOSE-ORPHANED,2026-07-11 +palette/dashboard-a11y-focus-13522420836634214970,CLOSE-ORPHANED,2026-08-01 +palette/dashboard-accessibility-8216739090300641563,CLOSE-ORPHANED,2026-07-27 +ralph-max-final2,CLOSE-ORPHANED,2026-06-18 +ralph-max-final3,CLOSE-ORPHANED,2026-06-18 +ralph-max-final4,CLOSE-ORPHANED,2026-06-18 +refactor-skill-registry-di-5907113418432710467,CLOSE-ORPHANED,2026-07-14 +refactor-stream-handler-9233370667506742960,CLOSE-ORPHANED,2026-07-12 +sentinel-fix-error-leakage-1121709532317009963,CLOSE-ORPHANED,2026-08-01 +sentinel-fix-infodisclosure-codegenerator-4053805493819656242,CLOSE-ORPHANED,2026-07-19 +sentinel-security-fix-md5-6305435416460063457,CLOSE-ORPHANED,2026-07-17 +test-index-analysis-grade-5389899992828549898,CLOSE-ORPHANED,2026-07-07 +test/billing-chat-gating-1116,CLOSE-ORPHANED,2026-07-30 +test/weight-persistence-15252431174567993674,CLOSE-ORPHANED,2026-07-21 +testing-and-production-launch-prep-4613142473161012757,CLOSE-ORPHANED,2026-07-21 +v0/ultrathinking-14b534ae,CLOSE-ORPHANED,2026-07-08 +v0/ultrathinking-2b862801,CLOSE-ORPHANED,2026-06-29 +v0/ultrathinking-48361bf4,CLOSE-ORPHANED,2026-06-29 +v0/ultrathinking-588aba59,CLOSE-ORPHANED,2026-06-02 +v0/ultrathinking-6aaf1beb,CLOSE-ORPHANED,2026-06-29 +v0/ultrathinking-6aaf1beb-2,CLOSE-ORPHANED,2026-06-29 +v0/ultrathinking-8ff3a2ce,CLOSE-ORPHANED,2026-06-17 +v0/ultrathinking-9fd5992d,CLOSE-ORPHANED,2026-07-09 +v0/ultrathinking-a2a8b20e,CLOSE-ORPHANED,2026-07-03 +v0/ultrathinking-c81b3cdf,CLOSE-ORPHANED,2026-07-11 +addressing-issues-grv-194-a1c2,KEEP-OPEN-PR,2026-08-04 +bolt/optimize-workflow-latest-iso-5209698121814795090,KEEP-OPEN-PR,2026-08-02 +claude/agentharden-api-cost-outbox,KEEP-OPEN-PR,2026-08-04 +claude/automation-repository-drift-report,KEEP-OPEN-PR,2026-08-04 +claude/determined-maxwell-j5mdyu,KEEP-OPEN-PR,2026-07-21 +claude/determined-maxwell-j8w4lt,KEEP-OPEN-PR,2026-08-02 +claude/determined-maxwell-raq7mg,KEEP-OPEN-PR,2026-08-02 +claude/determined-maxwell-rswptp,KEEP-OPEN-PR,2026-08-04 +claude/event-relay-blockers-1k020k,KEEP-OPEN-PR,2026-08-05 +dependabot/github_actions/actions/checkout-7.0.1,KEEP-OPEN-PR,2026-08-04 +dependabot/github_actions/github/gh-aw-actions/setup-0.84.0,KEEP-OPEN-PR,2026-08-04 +dependabot/npm_and_yarn/apps/web/openai-7.1.0,KEEP-OPEN-PR,2026-08-04 +dependabot/npm_and_yarn/openai-7.1.0,KEEP-OPEN-PR,2026-08-04 +execution-enforce-cloud-tasks-authentication-before-grv-198-fba7,KEEP-OPEN-PR,2026-08-04 +fix-auth-gate-fail-open,KEEP-OPEN-PR,2026-08-02 +fix-flaky-chat-billing-test,KEEP-OPEN-PR,2026-08-02 +fix/dockerfile-production-hardening-1121,KEEP-OPEN-PR,2026-08-01 +groupthinking-bigquery-export-coverage,KEEP-OPEN-PR,2026-08-02 +groupthinking-fix-brace-expansion-override-floors,KEEP-OPEN-PR,2026-08-02 +groupthinking-skill-dispatch-regression-tests,KEEP-OPEN-PR,2026-08-02 +jules-refactor-inline-errors-8256439203984646776,KEEP-OPEN-PR,2026-08-02 +one-click-deploysh-is-non-functional-stale-precheck-grv-195-77d4,KEEP-OPEN-PR,2026-08-04 +perf-metrics_recorded-overcounts-non-numeric-samples-grv-308-efbc,KEEP-OPEN-PR,2026-08-04 +perf/blocking-io-executor-isolation-1234,KEEP-OPEN-PR,2026-08-02 +perf/status-cache-glob-offload-1231,KEEP-OPEN-PR,2026-08-02 +sentinel/fix-code-gen-info-disclosure-4711205736159086816,KEEP-OPEN-PR,2026-08-02 +test/dns-gate-vacuity-914,KEEP-OPEN-PR,2026-08-02 +test/transcript-search-regression-908,KEEP-OPEN-PR,2026-08-02 +agent-completiontruth-gate-is-permanently-unsatisfiable-grv-196-8883,REVIEW-SHARED,2026-08-04 +claude/determined-maxwell-03lus6,REVIEW-SHARED,2026-08-02 +claude/determined-maxwell-2frkmc,REVIEW-SHARED,2026-08-01 +claude/determined-maxwell-33gxdg,REVIEW-SHARED,2026-08-04 +claude/determined-maxwell-4gzjlq,REVIEW-SHARED,2026-08-04 +claude/determined-maxwell-56ir7p,REVIEW-SHARED,2026-08-02 +claude/determined-maxwell-6q4s34,REVIEW-SHARED,2026-08-02 +claude/determined-maxwell-70t6tf,REVIEW-SHARED,2026-08-03 +claude/determined-maxwell-7ljegg,REVIEW-SHARED,2026-08-03 +claude/determined-maxwell-9t5kou,REVIEW-SHARED,2026-08-02 +claude/determined-maxwell-a9n93o,REVIEW-SHARED,2026-08-04 +claude/determined-maxwell-b1npnt,REVIEW-SHARED,2026-08-02 +claude/determined-maxwell-c3g14j,REVIEW-SHARED,2026-08-02 +claude/determined-maxwell-ef6pqu,REVIEW-SHARED,2026-08-02 +claude/determined-maxwell-g9x8i1,REVIEW-SHARED,2026-08-04 +claude/determined-maxwell-gbxcgj,REVIEW-SHARED,2026-08-02 +claude/determined-maxwell-gy6tub,REVIEW-SHARED,2026-08-03 +claude/determined-maxwell-ihwn4o,REVIEW-SHARED,2026-08-02 +claude/determined-maxwell-l04xha,REVIEW-SHARED,2026-08-02 +claude/determined-maxwell-le4432,REVIEW-SHARED,2026-08-02 +claude/determined-maxwell-lv8mlc,REVIEW-SHARED,2026-08-02 +claude/determined-maxwell-u91xgx,REVIEW-SHARED,2026-08-01 +claude/determined-maxwell-vunivy,REVIEW-SHARED,2026-08-04 +claude/determined-maxwell-x3ui2m,REVIEW-SHARED,2026-08-03 +claude/determined-maxwell-xqcnit,REVIEW-SHARED,2026-08-02 +codex/fix-brace-expansion-oom-vulnerability,REVIEW-SHARED,2026-08-04 +fix/truth-gate-surface-collection-errors,REVIEW-SHARED,2026-08-03 +groupthinking-secure-cloud-ai-local-image-paths,REVIEW-SHARED,2026-08-03 +land-pr-1270-fixsecurity-neutralize-crlf-in-rendered-grv-295-ffc0,REVIEW-SHARED,2026-08-04 diff --git a/scripts/maintenance/archive-and-prune-branches.sh b/scripts/maintenance/archive-and-prune-branches.sh new file mode 100755 index 000000000..c533640c6 --- /dev/null +++ b/scripts/maintenance/archive-and-prune-branches.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# +# archive-and-prune-branches.sh — archive-tag then delete the branches the +# 2026-08-05 audit found unrecoverable, and nothing else. +# +# WHY THIS EXISTS AS A SCRIPT RATHER THAN AN APPLIED CHANGE +# --------------------------------------------------------- +# The audit ran in a Claude Code session whose git credentials are scoped to a +# single feature branch. Creating the archive tags failed there: +# +# $ git push origin refs/tags/archive/addressing-issues-grv-194-a1c2 +# error: RPC failed; HTTP 403 +# +# Branch deletion appeared to be permitted (a --dry-run succeeded), but +# deleting without the archive tags in place would remove the only durable +# recovery path, so nothing was deleted. Run this with credentials that can +# write tags. +# +# WHAT THE VERDICTS MEAN +# ---------------------- +# `main` was force-pushed for a secret purge (see CLAUDE.md -> Repo Hygiene). +# Branches created before that share NO ancestry with today's `main`: +# +# $ git merge-base origin/main origin/ +# (empty) +# +# That emptiness is the verdict signal, and it is the one that stays honest +# after a rewrite. The usual signals do not: +# * `git merge-tree` reports these orphans as merging CLEAN (unrelated trees +# do not textually conflict -- they would clobber). +# * A two-dot diff against an empty merge base silently degrades to a +# working-tree diff, which is why a two-line Dependabot bump measures as +# 111 files / 15,650 lines. +# * The purge rewrote committer dates, so every branch looks "recent" and +# staleness thresholds never fire. +# +# KEEP-OPEN-PR 28 open PR -- never touched by this script +# REVIEW-SHARED 29 real shared ancestry, no open PR -- NOT deleted here +# CLOSE-MERGED 2 tip is an ancestor of main -- nothing to lose +# CLOSE-ORPHANED 275 no common ancestor with main -- no rebase recovers them +# +# Only CLOSE-MERGED and CLOSE-ORPHANED are pruned: 277 branches. +# +# RECOVERY +# -------- +# git push origin archive/:refs/heads/ +# GitHub also restores deleted branches through the UI for ~90 days. +# +# USAGE +# ./scripts/maintenance/archive-and-prune-branches.sh # dry run (default) +# ./scripts/maintenance/archive-and-prune-branches.sh --execute # tag, verify, then delete + +set -euo pipefail + +CSV="${CSV:-docs/branch-audit-2026-08-05.csv}" +REMOTE="${REMOTE:-origin}" +BATCH="${BATCH:-15}" +EXECUTE=0 +[ "${1:-}" = "--execute" ] && EXECUTE=1 + +[ -f "$CSV" ] || { echo "missing $CSV" >&2; exit 1; } + +# Protected refs are filtered here and re-checked below. The audit that +# produced this CSV originally classified `main` itself as CLOSE-MERGED -- +# `git merge-base --is-ancestor origin/main origin/main` is trivially true -- +# which would have deleted the default branch. The row is gone from the CSV, +# and this guard makes the mistake unrepeatable no matter what the CSV says. +PROTECTED_RE='^(main|master|HEAD)$' + +mapfile -t TARGETS < <( + awk -F, 'NR>1 && ($2=="CLOSE-ORPHANED" || $2=="CLOSE-MERGED"){print $1}' "$CSV" \ + | grep -Ev "$PROTECTED_RE" +) + +for b in "${TARGETS[@]}"; do + if [[ "$b" =~ $PROTECTED_RE ]]; then + echo "ABORT: protected branch '$b' reached the delete list." >&2 + exit 1 + fi +done + +DEFAULT_REF="$(git symbolic-ref -q "refs/remotes/$REMOTE/HEAD" 2>/dev/null || true)" +DEFAULT_BRANCH="${DEFAULT_REF##*/}" +if [ -n "$DEFAULT_BRANCH" ]; then + for b in "${TARGETS[@]}"; do + if [ "$b" = "$DEFAULT_BRANCH" ]; then + echo "ABORT: '$b' is $REMOTE's default branch." >&2 + exit 1 + fi + done +fi + +echo "branches selected for pruning: ${#TARGETS[@]}" +if [ "${#TARGETS[@]}" -eq 0 ]; then echo "nothing to do"; exit 0; fi + +if [ "$EXECUTE" -eq 0 ]; then + echo + echo "DRY RUN -- nothing will change. Re-run with --execute to apply." + echo "First 10 targets:" + printf ' %s\n' "${TARGETS[@]:0:10}" + echo " ..." + echo + echo "Each target would get: git tag archive/ origin/; push tag; push --delete " + exit 0 +fi + +# --- phase 1: create and push archive tags ------------------------------- +echo "== phase 1: archive tags ==" +for b in "${TARGETS[@]}"; do + git tag -f "archive/$b" "refs/remotes/$REMOTE/$b" >/dev/null +done + +pending=() +for b in "${TARGETS[@]}"; do pending+=("refs/tags/archive/$b"); done + +for ((i = 0; i < ${#pending[@]}; i += BATCH)); do + chunk=("${pending[@]:i:BATCH}") + if ! git push "$REMOTE" "${chunk[@]}" >/dev/null 2>&1; then + for ref in "${chunk[@]}"; do + git push "$REMOTE" "$ref" >/dev/null 2>&1 || echo " tag push FAILED: $ref" >&2 + done + fi +done + +# --- phase 2: verify every tag landed before deleting anything ----------- +echo "== phase 2: verify ==" +git ls-remote --tags "$REMOTE" 'refs/tags/archive/*' \ + | sed 's/\^{}//' | awk '{print $2}' | sed 's#refs/tags/##' | sort -u > /tmp/.archived_ok + +missing=0 +for b in "${TARGETS[@]}"; do + grep -qxF "archive/$b" /tmp/.archived_ok || { echo " NOT ARCHIVED: $b" >&2; missing=$((missing + 1)); } +done + +if [ "$missing" -gt 0 ]; then + echo "ABORT: $missing branch(es) have no archive tag on $REMOTE. Nothing deleted." >&2 + exit 1 +fi +echo " all ${#TARGETS[@]} archive tags confirmed on $REMOTE" + +# --- phase 3: delete ------------------------------------------------------ +echo "== phase 3: delete ==" +for ((i = 0; i < ${#TARGETS[@]}; i += BATCH)); do + chunk=("${TARGETS[@]:i:BATCH}") + if ! git push "$REMOTE" --delete "${chunk[@]}" >/dev/null 2>&1; then + for b in "${chunk[@]}"; do + git push "$REMOTE" --delete "$b" >/dev/null 2>&1 || echo " delete FAILED: $b" >&2 + done + fi +done + +echo "done. recover any branch with:" +echo " git push $REMOTE archive/:refs/heads/" From 10113efe27fad21e5a73b20225805d7d2208b453 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 01:15:52 +0000 Subject: [PATCH 5/5] docs(maintenance): correct prune-script header counts to match the CSV The header summary read "CLOSE-MERGED 2 / 277 branches" while the shipped docs/branch-audit-2026-08-05.csv has 1 CLOSE-MERGED and 276 prunable. The header was written before the `main` row was dropped from the CSV and was never updated. Comment only -- the selection logic already read from the CSV, so the script was correctly selecting 276 the whole time. Verified: header, CSV tally, and the script's own dry-run count now all agree at 276. Reported by the Vercel review bot on #1377. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01YcHjCZ6pGn6A5BeeoZ6eZi --- scripts/maintenance/archive-and-prune-branches.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/maintenance/archive-and-prune-branches.sh b/scripts/maintenance/archive-and-prune-branches.sh index c533640c6..71dbf3423 100755 --- a/scripts/maintenance/archive-and-prune-branches.sh +++ b/scripts/maintenance/archive-and-prune-branches.sh @@ -36,10 +36,13 @@ # # KEEP-OPEN-PR 28 open PR -- never touched by this script # REVIEW-SHARED 29 real shared ancestry, no open PR -- NOT deleted here -# CLOSE-MERGED 2 tip is an ancestor of main -- nothing to lose +# CLOSE-MERGED 1 tip is an ancestor of main -- nothing to lose # CLOSE-ORPHANED 275 no common ancestor with main -- no rebase recovers them # -# Only CLOSE-MERGED and CLOSE-ORPHANED are pruned: 277 branches. +# Only CLOSE-MERGED and CLOSE-ORPHANED are pruned: 276 branches. +# +# (An earlier draft of this header read "2 / 277". It was written before the +# `main` row was dropped from the CSV -- see the protected-ref note below.) # # RECOVERY # --------