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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 109 additions & 2 deletions .github/workflows/sweep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,30 @@ on:
description: "Refresh audit state without running review or apply work"
required: false
default: "false"
proof_nudges:
description: "Run proof-nudge reminder lane for PRs waiting on real behavior proof"
required: false
default: "false"
proof_nudges_execute:
description: "Post proof-nudge comments; false runs dry-run only"
required: false
default: "false"
proof_nudges_limit:
description: "Maximum proof nudges to plan or post"
required: false
default: "10"
proof_nudges_min_age_days:
description: "Minimum age in days since review/author activity before first proof nudge"
required: false
default: "5"
proof_nudges_cooldown_days:
description: "Same-head cooldown in days between proof nudges"
required: false
default: "7"
proof_nudges_item_numbers:
description: "Optional comma-separated PR numbers to inspect first"
required: false
default: ""
schedule:
- cron: "*/5 * * * *"
# ClawHub review/apply schedules stay opt-in until the ClawSweeper app is installed there.
Expand All @@ -121,6 +145,7 @@ on:
- cron: "33 * * * *"
- cron: "48 * * * *"
- cron: "8,23,38,53 * * * *"
- cron: "41 9 * * *"

permissions:
contents: write
Expand All @@ -133,7 +158,7 @@ env:
CLAWSWEEPER_APP_CLIENT_ID: Iv23liOECG0slfuhz093

concurrency:
group: ${{ github.event_name == 'repository_dispatch' && format('clawsweeper-event-{0}-{1}', github.event.client_payload.target_repo || 'openclaw/openclaw', github.event.client_payload.item_number || github.run_id) || format('{0}-{1}', (github.event_name == 'workflow_dispatch' && github.event.inputs.apply_existing == 'true' && github.event.inputs.apply_sync_comments_only == 'true') && 'clawsweeper-comment-sync' || ((github.event_name == 'workflow_dispatch' && github.event.inputs.apply_existing == 'true') || (github.event_name == 'schedule' && (github.event.schedule == '3 * * * *' || github.event.schedule == '18 * * * *' || github.event.schedule == '33 * * * *' || github.event.schedule == '48 * * * *' || github.event.schedule == '8,23,38,53 * * * *'))) && 'clawsweeper-apply' || (github.event_name == 'workflow_dispatch' && (github.event.inputs.item_number != '' || github.event.inputs.item_numbers != '')) && format('clawsweeper-intake-exact-{0}', github.event.inputs.item_number || github.event.inputs.item_numbers) || ((github.event_name == 'workflow_dispatch' && github.event.inputs.hot_intake == 'true') || (github.event_name == 'schedule' && (github.event.schedule == '*/5 * * * *' || github.event.schedule == '2/5 * * * *'))) && 'clawsweeper-intake-v2' || ((github.event_name == 'workflow_dispatch' && github.event.inputs.audit_dashboard == 'true') || (github.event_name == 'schedule' && (github.event.schedule == '7 */6 * * *' || github.event.schedule == '12 */6 * * *' || github.event.schedule == '17 */6 * * *'))) && 'clawsweeper-audit' || 'clawsweeper-review', github.event.inputs.target_repo || github.event.client_payload.target_repo || ((github.event.schedule == '17 */6 * * *') && 'openclaw/clawsweeper' || ((github.event.schedule == '2/5 * * * *' || github.event.schedule == '22 * * * *' || github.event.schedule == '8,23,38,53 * * * *' || github.event.schedule == '12 */6 * * *') && 'openclaw/clawhub' || 'openclaw/openclaw'))) }}
group: ${{ github.event_name == 'repository_dispatch' && format('clawsweeper-event-{0}-{1}', github.event.client_payload.target_repo || 'openclaw/openclaw', github.event.client_payload.item_number || github.run_id) || format('{0}-{1}', (github.event_name == 'workflow_dispatch' && github.event.inputs.apply_existing == 'true' && github.event.inputs.apply_sync_comments_only == 'true') && 'clawsweeper-comment-sync' || ((github.event_name == 'workflow_dispatch' && github.event.inputs.apply_existing == 'true') || (github.event_name == 'schedule' && (github.event.schedule == '3 * * * *' || github.event.schedule == '18 * * * *' || github.event.schedule == '33 * * * *' || github.event.schedule == '48 * * * *' || github.event.schedule == '8,23,38,53 * * * *'))) && 'clawsweeper-apply' || ((github.event_name == 'workflow_dispatch' && github.event.inputs.proof_nudges == 'true') || (github.event_name == 'schedule' && github.event.schedule == '41 9 * * *')) && 'clawsweeper-proof-nudges' || (github.event_name == 'workflow_dispatch' && (github.event.inputs.item_number != '' || github.event.inputs.item_numbers != '')) && format('clawsweeper-intake-exact-{0}', github.event.inputs.item_number || github.event.inputs.item_numbers) || ((github.event_name == 'workflow_dispatch' && github.event.inputs.hot_intake == 'true') || (github.event_name == 'schedule' && (github.event.schedule == '*/5 * * * *' || github.event.schedule == '2/5 * * * *'))) && 'clawsweeper-intake-v2' || ((github.event_name == 'workflow_dispatch' && github.event.inputs.audit_dashboard == 'true') || (github.event_name == 'schedule' && (github.event.schedule == '7 */6 * * *' || github.event.schedule == '12 */6 * * *' || github.event.schedule == '17 */6 * * *'))) && 'clawsweeper-audit' || 'clawsweeper-review', github.event.inputs.target_repo || github.event.client_payload.target_repo || ((github.event.schedule == '17 */6 * * *') && 'openclaw/clawsweeper' || ((github.event.schedule == '2/5 * * * *' || github.event.schedule == '22 * * * *' || github.event.schedule == '8,23,38,53 * * * *' || github.event.schedule == '12 */6 * * *') && 'openclaw/clawhub' || 'openclaw/openclaw'))) }}
cancel-in-progress: ${{ github.event_name == 'repository_dispatch' }}

jobs:
Expand Down Expand Up @@ -542,7 +567,7 @@ jobs:

plan:
name: Plan review candidates
if: ${{ github.event_name != 'repository_dispatch' && !((github.event_name == 'workflow_dispatch' && (github.event.inputs.apply_existing == 'true' || github.event.inputs.audit_dashboard == 'true')) || (github.event_name == 'schedule' && ((github.event.schedule == '3 * * * *' || github.event.schedule == '18 * * * *' || github.event.schedule == '33 * * * *' || github.event.schedule == '48 * * * *' || github.event.schedule == '8,23,38,53 * * * *') || (github.event.schedule == '7 */6 * * *' || github.event.schedule == '12 */6 * * *' || github.event.schedule == '17 */6 * * *')))) && !(github.event_name == 'schedule' && (github.event.schedule == '2/5 * * * *' || github.event.schedule == '22 * * * *') && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') }}
if: ${{ github.event_name != 'repository_dispatch' && !((github.event_name == 'workflow_dispatch' && (github.event.inputs.apply_existing == 'true' || github.event.inputs.audit_dashboard == 'true' || github.event.inputs.proof_nudges == 'true')) || (github.event_name == 'schedule' && ((github.event.schedule == '3 * * * *' || github.event.schedule == '18 * * * *' || github.event.schedule == '33 * * * *' || github.event.schedule == '48 * * * *' || github.event.schedule == '8,23,38,53 * * * *') || (github.event.schedule == '7 */6 * * *' || github.event.schedule == '12 */6 * * *' || github.event.schedule == '17 */6 * * *') || github.event.schedule == '41 9 * * *'))) && !(github.event_name == 'schedule' && (github.event.schedule == '2/5 * * * *' || github.event.schedule == '22 * * * *') && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') }}
runs-on: ubuntu-latest
timeout-minutes: 30
outputs:
Expand Down Expand Up @@ -1588,6 +1613,88 @@ jobs:
--repo openclaw/clawsweeper-state \
--ref main || echo "Best-effort dashboard refresh dispatch failed; scheduled state dashboard will retry."

proof-nudges:
name: Proof nudges
if: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.proof_nudges == 'true') || (github.event_name == 'schedule' && github.event.schedule == '41 9 * * *' && vars.CLAWSWEEPER_PROOF_NUDGES_SCHEDULED == '1') }}
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
with:
filter: blob:none
fetch-depth: 0

- name: Resolve target repository
id: target
run: |
target_repo="${{ github.event.inputs.target_repo || vars.CLAWSWEEPER_PROOF_NUDGES_TARGET_REPO || 'openclaw/openclaw' }}"
target_owner="${target_repo%%/*}"
target_name="${target_repo#*/}"
{
echo "target_repo=$target_repo"
echo "target_repo_owner=$target_owner"
echo "target_repo_name=$target_name"
} >> "$GITHUB_OUTPUT"

- name: Create target proof-nudge token
id: target-proof-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
owner: ${{ steps.target.outputs.target_repo_owner }}
repositories: ${{ steps.target.outputs.target_repo_name }}
permission-contents: read
permission-issues: write
permission-pull-requests: read

- name: Create state token
id: state-token
uses: ./.github/actions/create-state-token
with:
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}

- uses: ./.github/actions/setup-state
with:
token: ${{ steps.state-token.outputs.token }}

- uses: ./.github/actions/setup-pnpm
with:
build-script: build:all

- name: Run proof nudges
env:
GH_TOKEN: ${{ steps.target-proof-token.outputs.token }}
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
run: |
set -euo pipefail
execute="${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.proof_nudges_execute == 'true') || (github.event_name == 'schedule' && vars.CLAWSWEEPER_PROOF_NUDGES_EXECUTE == '1') }}"
execute_arg=()
if [ "$execute" = "true" ]; then
execute_arg=(--execute)
fi
item_numbers="${{ github.event.inputs.proof_nudges_item_numbers || '' }}"
item_numbers_arg=()
if [ -n "$item_numbers" ]; then
item_numbers_arg=(--item-numbers "$item_numbers")
fi
pnpm run proof-nudges -- \
--target-repo "$TARGET_REPO" \
--limit "${{ github.event.inputs.proof_nudges_limit || vars.CLAWSWEEPER_PROOF_NUDGES_LIMIT || '10' }}" \
--min-age-days "${{ github.event.inputs.proof_nudges_min_age_days || vars.CLAWSWEEPER_PROOF_NUDGES_MIN_AGE_DAYS || '5' }}" \
--cooldown-days "${{ github.event.inputs.proof_nudges_cooldown_days || vars.CLAWSWEEPER_PROOF_NUDGES_COOLDOWN_DAYS || '7' }}" \
--report-path proof-nudge-report.json \
"${item_numbers_arg[@]}" \
"${execute_arg[@]}"

- uses: actions/upload-artifact@v7
if: ${{ always() }}
with:
name: proof-nudge-report
path: proof-nudge-report.json
if-no-files-found: ignore

apply-existing:
name: Apply close proposals
if: ${{ ((github.event_name == 'workflow_dispatch' && github.event.inputs.apply_existing == 'true') || (github.event_name == 'schedule' && (github.event.schedule == '3 * * * *' || github.event.schedule == '18 * * * *' || github.event.schedule == '33 * * * *' || github.event.schedule == '48 * * * *' || github.event.schedule == '8,23,38,53 * * * *'))) && !(github.event_name == 'schedule' && github.event.schedule == '8,23,38,53 * * * *' && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') }}
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ proof, supplied-but-not-sufficient proof, mock-only proof, and proof label
mismatches. See
[`docs/pr-proof-triage-dashboard.md`](docs/pr-proof-triage-dashboard.md).

The optional proof-nudge lane can dry-run or post polite reminder comments for
open PRs that remain blocked on `triage: needs-real-behavior-proof`. It uses
comment-body cooldown markers, never closes PRs, and keeps scheduled operation
behind default-off repository variables. See
[`docs/proof-nudges.md`](docs/proof-nudges.md).

## How It Works

ClawSweeper is split into four operational lanes:
Expand Down
80 changes: 80 additions & 0 deletions docs/proof-nudges.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# ClawSweeper Proof Nudges

Read when changing the ClawSweeper lane that reminds pull request authors to add real behavior proof.

## Scope

The proof-nudge lane is read-mostly triage hygiene. It can post a polite reminder comment on open pull requests that are stuck on `triage: needs-real-behavior-proof`, but it does not close pull requests, merge pull requests, change labels, request reviews, or modify review records.

The lane uses the latest ClawSweeper review report plus the live pull request state. It does not scrape the visible review comment for policy.

## Eligibility

A pull request is eligible only when all of these are true:

- The live item is an open pull request.
- The live pull request still has `triage: needs-real-behavior-proof`.
- The latest report still says real behavior proof blocks merge.
- The report head SHA matches the live pull request head SHA.
- The pull request is past the first-nudge age gate, defaulting to 5 days.
- The author has not commented recently and the head commit is not recent.
- There is no same-head proof nudge inside the cooldown window, defaulting to 7 days.

The lane skips maintainer-authored, bot-authored, security-sensitive, and release-style pull requests. It also skips pull requests with `proof: supplied`, `proof: sufficient`, or `proof: override`, because those need review or policy handling rather than another contributor reminder.

## Marker

Cooldown state lives in the reminder comment body:

```html
<!-- clawsweeper-proof-nudge item="123" sha="abc123" at="2026-05-18T12:00:00.000Z" v="1" -->
```

The marker records the pull request number, reviewed head SHA, timestamp, and marker version. This avoids label churn and keeps the reminder state tied to the exact head that was nudged.

## Command

Dry-run is the default:

```bash
pnpm run proof-nudges -- --target-repo openclaw/openclaw
```

Post comments only with an explicit execute flag:

```bash
pnpm run proof-nudges -- --target-repo openclaw/openclaw --execute --limit 10
```

Useful options:

- `--limit`: maximum nudges to plan or post, default `10`.
- `--processed-limit`: maximum records to inspect in one run.
- `--min-age-days`: first-nudge age gate, default `5`.
- `--cooldown-days`: same-head cooldown, default `7`.
- `--item-numbers`: comma-separated pull request numbers for a targeted dry-run or execute run.
- `--report-path`: JSON output path, default `proof-nudge-report.json`.
- `--max-runtime-ms`: optional hard runtime stop.

## Workflow Use

The ClawSweeper workflow exposes this as a manual `workflow_dispatch` lane. It defaults to dry-run. Use `proof_nudges_execute=true` only after reviewing a dry-run report.

The workflow also includes a daily scheduled lane at `41 9 * * *`, but it is off by default. This lets maintainers enable scheduled proof nudges later without another code change.

Scheduled operation uses repository variables:

- `CLAWSWEEPER_PROOF_NUDGES_SCHEDULED=1`: enable the scheduled lane. Without this, the daily schedule is skipped.
- `CLAWSWEEPER_PROOF_NUDGES_EXECUTE=1`: allow the scheduled lane to post comments. Without this, scheduled runs remain dry-run only.
- `CLAWSWEEPER_PROOF_NUDGES_TARGET_REPO`: optional target repo, default `openclaw/openclaw`.
- `CLAWSWEEPER_PROOF_NUDGES_LIMIT`: optional scheduled batch size, default `10`.
- `CLAWSWEEPER_PROOF_NUDGES_MIN_AGE_DAYS`: optional first-nudge age gate, default `5`.
- `CLAWSWEEPER_PROOF_NUDGES_COOLDOWN_DAYS`: optional same-head cooldown, default `7`.

Suggested rollout:

1. Run manually with `proof_nudges=true` and `proof_nudges_execute=false`.
2. Set `CLAWSWEEPER_PROOF_NUDGES_SCHEDULED=1` to collect scheduled dry-run reports.
3. Set `CLAWSWEEPER_PROOF_NUDGES_EXECUTE=1` only after the scheduled reports look correct.

This first version intentionally has no auto-close behavior. Any escalation after repeated proof nudges needs a separate maintainer policy decision.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"review": "node dist/clawsweeper.js review",
"apply-artifacts": "node dist/clawsweeper.js apply-artifacts",
"apply-decisions": "node dist/clawsweeper.js apply-decisions",
"proof-nudges": "node dist/clawsweeper.js proof-nudges",
"audit": "node dist/clawsweeper.js audit",
"reconcile": "node dist/clawsweeper.js reconcile",
"status": "node dist/clawsweeper.js status",
Expand Down
Loading