diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 4131c473b..8f5dec4fe 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,56 @@ 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); + // 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]' && + (issueDispatch || (pullProvenance && Boolean(selectedIssue))); } function intentContractErrors(issue, comments, pullCreatedAt) { const errors = []; @@ -1876,6 +1944,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 +1991,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 +1999,56 @@ 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); + // 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]' && + (issueDispatch || (pullProvenance && Boolean(selectedIssue))); } const prNumber = Number(process.env.INPUT_PR_NUMBER || 0); @@ -2163,6 +2299,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/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..71dbf3423 --- /dev/null +++ b/scripts/maintenance/archive-and-prune-branches.sh @@ -0,0 +1,156 @@ +#!/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 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: 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 +# -------- +# 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/" diff --git a/tests/unit/test_agent_completion_gate.py b/tests/unit/test_agent_completion_gate.py index 0cd300f63..c58715293 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'}, @@ -2880,18 +2885,36 @@ 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], - ['issue label', base, issue(1, ['mcp/agent']), 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], + // 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], + // 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]'}, 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 +2988,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 +3386,181 @@ 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 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], + // ...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, 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, false], + ['lock manifest in the pull request body, no issue', { + user: {login: 'groupthinking'}, + head: {ref: 'feature/thing'}, + labels: [], + body: '' + }, null, false], + // 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() 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(