diff --git a/.github/workflows/sweep.yml b/.github/workflows/sweep.yml index e734a47a83..9d2d506e02 100644 --- a/.github/workflows/sweep.yml +++ b/.github/workflows/sweep.yml @@ -1815,6 +1815,13 @@ jobs: with: build-script: build:all + - uses: ./.github/actions/setup-codex + if: ${{ !((github.event_name == 'repository_dispatch' && github.event.action == 'clawsweeper_hatch') || (github.event_name == 'workflow_dispatch' && github.event.inputs.apply_sync_comments_only == 'true') || (github.event_name == 'schedule' && github.event.schedule == '6,21,36,51 * * * *')) }} + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + with: + login-status: "true" + - name: Sync before applying decisions run: git pull --rebase diff --git a/prompts/repair/replacement-closeout-proof.md b/prompts/repair/replacement-closeout-proof.md new file mode 100644 index 0000000000..0049043956 --- /dev/null +++ b/prompts/repair/replacement-closeout-proof.md @@ -0,0 +1,17 @@ +You are ClawSweeper's read-only replacement closeout proof checker. + +Decide whether PR B can safely close PR A as superseded. + +Hard rules: +- You only have two decisions: `superseded` or `keep_open`. +- PR B may be user-authored and may have a different author from PR A. +- A source list or `supersedes #A` text is only a candidate signal. +- Compare the useful work generally from the compact context: title, first body excerpt, labels, file paths, file counts, timestamps, and repair provenance. +- Do not require exact patch-line equality. A replacement can cover the same behavior with different code shape. +- Return `superseded` only when PR B clearly covers PR A's useful work and PR A has no unique behavior, file concern, proof, discussion, or review point needing separate maintainer review. +- Return `keep_open` for anything else, including related PRs, incomplete proof, thin context, or uncertainty. +- Security-sensitive work is not a separate close blocker. Treat security labels, CVE/GHSA text, and ClawSweeper security markers as PR A content to compare. If PR B proves it covers that content, PR A can be `superseded`. +- Use `securityBlocked: true` only when PR A has security-sensitive content that PR B does not prove it covers. Also list that uncovered content in `uniqueSourceWork`. +- Do not ask for more context. + +Return only JSON matching the supplied schema. diff --git a/prompts/review-item.md b/prompts/review-item.md index 6d56dcf171..f53f97ee69 100644 --- a/prompts/review-item.md +++ b/prompts/review-item.md @@ -169,6 +169,14 @@ discussion: `merge-risk: 🚨 security-boundary`: 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. `merge-risk: 🚨 availability`: 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. `merge-risk: 🚨 automation`: 🚨 Merging this PR could break CI, automerge, proof capture, label sync, or automation. +Do not treat a branch being behind the current base as proof that merging the +PR will delete current-base-only files or commits. When GitHub reports the PR as +mergeable or clean and the only concern is stale base drift, describe it as +needing rebase or review refresh in `risks`, `workReason`, or `bestSolution`, +but leave `reviewFindings` and `mergeRiskLabels` focused on defects or risks +that survive the actual three-way merge result. Use deletion/drop wording for +current-base behavior only when a merge result, merge ref, conflict, or concrete +patch evidence shows that the merged PR would remove or regress it. When merge risk is present, explain it in `risks` in maintainer-facing language and make `bestSolution` the best end state. Fill `mergeRiskOptions` with 1-3 risk-specific maintainer options. Do not use a fixed menu. Each option needs a diff --git a/prompts/supersession-proof.md b/prompts/supersession-proof.md new file mode 100644 index 0000000000..066cdd9574 --- /dev/null +++ b/prompts/supersession-proof.md @@ -0,0 +1,19 @@ +You are ClawSweeper's read-only supersession proof checker. + +Decide whether PR B can safely supersede PR A. + +Hard rules: +- You only have two decisions: `superseded` or `keep_open`. +- PR B may be user-authored and may have a different author from PR A. +- Text such as `supersedes #A` is only a candidate signal. +- Compare the useful work generally from the compact context: title, first body excerpt, labels, file paths, file counts, and timestamps. +- Do not require exact patch-line equality. A replacement can cover the same behavior with different code shape. +- Return `superseded` only when PR B clearly covers PR A's useful work and PR A has no unique behavior, file concern, proof, discussion, or review point needing separate maintainer review. +- Return `keep_open` for anything else, including related PRs, incomplete proof, thin context, or uncertainty. +- Security-sensitive work is not a separate close blocker. Treat security labels, CVE/GHSA text, and ClawSweeper security markers as PR A content to compare. If PR B proves it covers that content, PR A can be `superseded`. +- Use `securityBlocked: true` only when PR A has security-sensitive content that PR B does not prove it covers. Also list that uncovered content in `uniqueSourceWork`. +- `coveredWork` must describe concrete PR A work that PR B covers. +- `uniqueSourceWork` must list any PR A behavior, file concern, proof, discussion, or review point that remains unique. Use an empty array only when none remains. +- Do not ask for more context. + +Return only JSON matching the supplied schema. diff --git a/schema/clawsweeper-supersession-proof.schema.json b/schema/clawsweeper-supersession-proof.schema.json new file mode 100644 index 0000000000..7c58bdffae --- /dev/null +++ b/schema/clawsweeper-supersession-proof.schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": false, + "required": [ + "sourceSummary", + "replacementSummary", + "coveredWork", + "uniqueSourceWork", + "securityBlocked", + "decision", + "reason" + ], + "properties": { + "sourceSummary": { + "type": "string", + "description": "Concrete summary of the useful work in PR A." + }, + "replacementSummary": { + "type": "string", + "description": "Concrete summary of the useful work in PR B." + }, + "coveredWork": { + "type": "array", + "items": { "type": "string" }, + "description": "Concrete PR A behavior, file concerns, proof, or review points that PR B covers." + }, + "uniqueSourceWork": { + "type": "array", + "items": { "type": "string" }, + "description": "Any PR A work that PR B does not cover and still needs separate review." + }, + "securityBlocked": { + "type": "boolean", + "description": "True only when PR A has security-sensitive content that PR B does not prove it covers. Security-sensitive content is not a standalone close blocker when coverage is proven." + }, + "decision": { + "type": "string", + "enum": ["superseded", "keep_open"] + }, + "reason": { + "type": "string", + "description": "Short explanation for the decision." + } + } +} diff --git a/src/clawsweeper.ts b/src/clawsweeper.ts index 7402068f7c..fddd8438c4 100644 --- a/src/clawsweeper.ts +++ b/src/clawsweeper.ts @@ -44,6 +44,12 @@ import { renderOpenClawPrSurfaceTable, type PrSurfaceFile, } from "./pr-surface-stats.js"; +import { + compactSupersessionProofView, + normalizedSupersessionProofModelResult, + parseSupersessionProofModelResult, + type SupersessionProofModelResult, +} from "./supersession-proof.js"; import { boolArg, itemNumbersArg, @@ -432,6 +438,7 @@ interface ItemContext { pullRequest?: unknown; pullFiles?: unknown[]; pullCommits?: unknown[]; + pullReviews?: unknown[]; pullReviewComments?: unknown[]; counts?: { comments: number; @@ -450,6 +457,11 @@ interface ItemContext { pullCommits?: number; pullCommitsHydrated?: number; pullCommitsTruncated?: boolean; + pullReviews?: number; + pullReviewsHydrated?: number; + pullReviewsTruncated?: boolean; + pullReviewsIncluded?: number; + pullReviewsFiltered?: number; pullReviewComments?: number; pullReviewCommentsHydrated?: number; pullReviewCommentsTruncated?: boolean; @@ -818,6 +830,12 @@ const DEFAULT_SERVICE_TIER = ""; const REVIEW_POLICY_VERSION = "2026-05-17-policy-v18"; const REVIEW_ITEM_PROMPT_PATH = join(ROOT, "prompts", "review-item.md"); const CLAWSWEEPER_DECISION_SCHEMA_PATH = join(ROOT, "schema", "clawsweeper-decision.schema.json"); +const CLAWSWEEPER_SUPERSESSION_PROOF_SCHEMA_PATH = join( + ROOT, + "schema", + "clawsweeper-supersession-proof.schema.json", +); +const SUPERSESSION_PROOF_PROMPT_PATH = join(ROOT, "prompts", "supersession-proof.md"); const REVIEW_COMMENT_MARKER_PREFIX = "