Review event item steipete/summarize#239 #231031
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: ClawSweeper | |
| run-name: ${{ (github.event_name == 'schedule' && (github.event.schedule == '4/15 * * * *' || github.event.schedule == '41 * * * *' || github.event.schedule == '37 */6 * * *')) && 'Fan out ClawSweeper targets' || (github.event_name == 'schedule' && github.event.schedule == '13 8,20 * * *') && 'Retry failed Codex reviews' || (github.event_name == 'repository_dispatch' && github.event.action == 'clawsweeper_target_sweep' && github.event.client_payload.hot_intake == 'true') && format('Review hot target repo {0}', github.event.client_payload.target_repo || 'openclaw/openclaw') || (github.event_name == 'repository_dispatch' && github.event.action == 'clawsweeper_target_sweep') && format('Review target repo {0}', github.event.client_payload.target_repo || 'openclaw/openclaw') || github.event_name == 'repository_dispatch' && format('Review event item {0}#{1}', github.event.client_payload.target_repo || 'openclaw/openclaw', github.event.client_payload.item_number || '?') || ((github.event_name == 'workflow_dispatch' && github.event.inputs.apply_existing == 'true' && github.event.inputs.apply_sync_comments_only == 'true') || (github.event_name == 'schedule' && github.event.schedule == '6,21,36,51 * * * *')) && 'Sync Codex review comments' || ((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 * * * *'))) && 'Apply ClawSweeper closures' || ((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 * * *'))) && 'Audit ClawSweeper state' || ((github.event_name == 'workflow_dispatch' && github.event.inputs.hot_intake == 'true') || (github.event_name == 'schedule' && (github.event.schedule == '*/5 * * * *' || github.event.schedule == '2/5 * * * *'))) && 'Review hot ClawSweeper items' || 'Review ClawSweeper items' }} | |
| on: | |
| repository_dispatch: | |
| types: [clawsweeper_item, clawsweeper_target_sweep] | |
| workflow_dispatch: | |
| inputs: | |
| target_repo: | |
| description: "Repository to sweep" | |
| required: false | |
| default: "openclaw/openclaw" | |
| apply_existing: | |
| description: "Apply existing proposed close decisions without rerunning Codex; skips items changed since review" | |
| required: false | |
| default: "false" | |
| apply_limit: | |
| description: "Maximum existing proposed items to close in apply-existing mode" | |
| required: false | |
| default: "20" | |
| apply_min_age_days: | |
| description: "Minimum item age in days before apply-existing can close it" | |
| required: false | |
| default: "0" | |
| apply_min_age_minutes: | |
| description: "Optional minute-level minimum item age before apply-existing can close it" | |
| required: false | |
| default: "" | |
| apply_kind: | |
| description: "Item kind to close in apply-existing mode: issue, pull_request, or all" | |
| required: false | |
| default: "all" | |
| apply_close_reasons: | |
| description: "Close reasons enabled in apply-existing mode, comma-separated or all" | |
| required: false | |
| default: "all" | |
| apply_stale_min_age_days: | |
| description: "Minimum item age in days before age-gated stale or mostly-implemented closes" | |
| required: false | |
| default: "60" | |
| apply_item_numbers: | |
| description: "Optional comma-separated item numbers to sync/apply first" | |
| required: false | |
| default: "" | |
| apply_sync_comments_only: | |
| description: "Only sync durable review comments; do not close items" | |
| required: false | |
| default: "false" | |
| apply_comment_sync_min_age_days: | |
| description: "Minimum age in days before comment-only sync rewrites an existing review comment" | |
| required: false | |
| default: "7" | |
| apply_close_delay_ms: | |
| description: "Delay after each close/comment pair to avoid GitHub secondary write throttling" | |
| required: false | |
| default: "2000" | |
| apply_progress_every: | |
| description: "Log apply progress every N processed records, plus every close" | |
| required: false | |
| default: "10" | |
| apply_checkpoint_size: | |
| description: "Fresh closes per checkpoint commit in apply-existing mode" | |
| required: false | |
| default: "50" | |
| batch_size: | |
| description: "Items per worker job" | |
| required: false | |
| default: "3" | |
| codex_timeout_ms: | |
| description: "Per-item Codex timeout in milliseconds" | |
| required: false | |
| default: "600000" | |
| shard_count: | |
| description: "Parallel shards (capped by config/automation-limits.json)" | |
| required: false | |
| default: "39" | |
| item_number: | |
| description: "Optional single issue/PR number to review" | |
| required: false | |
| default: "" | |
| item_numbers: | |
| description: "Optional comma-separated issue/PR numbers to review" | |
| required: false | |
| default: "" | |
| additional_prompt: | |
| description: "Optional one-off instructions for the selected review item(s)" | |
| required: false | |
| default: "" | |
| hot_intake: | |
| description: "Run the low-latency intake lane for new/active issues and PRs" | |
| required: false | |
| default: "false" | |
| apply_after_review: | |
| description: "After publishing selected review artifacts, immediately apply safe close proposals for those items" | |
| required: false | |
| default: "false" | |
| apply_after_review_close_reasons: | |
| description: "Close reasons enabled for immediate post-review apply" | |
| required: false | |
| default: "implemented_on_main,duplicate_or_superseded,low_signal_unmergeable_pr" | |
| apply_after_review_min_age_minutes: | |
| description: "Minute-level item age floor for immediate post-review apply" | |
| required: false | |
| default: "0" | |
| audit_dashboard: | |
| description: "Refresh audit state without running review or apply work" | |
| required: false | |
| default: "false" | |
| schedule: | |
| - cron: "*/5 * * * *" | |
| # ClawHub review/apply schedules stay opt-in until the ClawSweeper app is installed there. | |
| - cron: "2/5 * * * *" | |
| - cron: "7 */6 * * *" | |
| - cron: "12 */6 * * *" | |
| - cron: "17 */6 * * *" | |
| - cron: "1/5 * * * *" | |
| - cron: "22 * * * *" | |
| - cron: "3 * * * *" | |
| - cron: "18 * * * *" | |
| - cron: "33 * * * *" | |
| - cron: "48 * * * *" | |
| - cron: "8,23,38,53 * * * *" | |
| - cron: "6,21,36,51 * * * *" | |
| - cron: "4/15 * * * *" | |
| - cron: "41 * * * *" | |
| - cron: "37 */6 * * *" | |
| - cron: "13 8,20 * * *" | |
| permissions: | |
| contents: write | |
| actions: write | |
| issues: write | |
| pull-requests: write | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| CLAWSWEEPER_APP_CLIENT_ID: Iv23liOECG0slfuhz093 | |
| concurrency: | |
| group: ${{ (github.event_name == 'schedule' && (github.event.schedule == '4/15 * * * *' || github.event.schedule == '41 * * * *' || github.event.schedule == '37 */6 * * *')) && format('clawsweeper-target-fanout-{0}', github.event.schedule || github.run_id) || 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 == 'schedule' && github.event.schedule == '13 8,20 * * *') && 'clawsweeper-failed-review-retry' || ((github.event_name == 'workflow_dispatch' && github.event.inputs.apply_existing == 'true' && github.event.inputs.apply_sync_comments_only == 'true') || (github.event_name == 'schedule' && github.event.schedule == '6,21,36,51 * * * *')) && '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'))) }} | |
| cancel-in-progress: ${{ github.event_name == 'repository_dispatch' }} | |
| jobs: | |
| event-disabled-target: | |
| name: Skip disabled target event | |
| if: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.target_repo == 'openclaw/clawhub' && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Explain skipped event | |
| run: | | |
| echo "Skipping ${{ github.event.client_payload.target_repo }}#${{ github.event.client_payload.item_number || '?' }}." | |
| echo "Set CLAWSWEEPER_ENABLE_CLAWHUB=1 after installing the ClawSweeper GitHub App on openclaw/clawhub." | |
| event-review-apply: | |
| name: Review, comment, and apply event item | |
| if: ${{ github.event_name == 'repository_dispatch' && github.event.action != 'clawsweeper_target_sweep' && !(github.event.client_payload.target_repo == 'openclaw/clawhub' && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 120 | |
| concurrency: | |
| group: clawsweeper-event-review-${{ github.event.client_payload.target_repo || 'openclaw/openclaw' }}-${{ github.event.client_payload.item_number || github.run_id }} | |
| cancel-in-progress: true | |
| permissions: | |
| actions: write | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| filter: blob:none | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Resolve event payload | |
| id: target | |
| run: | | |
| set -euo pipefail | |
| target_repo="${{ github.event.client_payload.target_repo || 'openclaw/openclaw' }}" | |
| target_branch="${{ github.event.client_payload.target_branch || 'main' }}" | |
| item_number="${{ github.event.client_payload.item_number || '' }}" | |
| item_kind="${{ github.event.client_payload.item_kind || '' }}" | |
| if ! printf '%s' "$target_repo" | grep -Eq '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'; then | |
| echo "Invalid target_repo: $target_repo" >&2 | |
| exit 1 | |
| fi | |
| if ! printf '%s' "$target_branch" | grep -Eq '^[A-Za-z0-9_.\/-]+$'; then | |
| echo "Invalid target_branch: $target_branch" >&2 | |
| exit 1 | |
| fi | |
| if ! printf '%s' "$item_number" | grep -Eq '^[0-9]+$'; then | |
| echo "Invalid item_number: $item_number" >&2 | |
| exit 1 | |
| fi | |
| case "$item_kind" in | |
| ""|"issue"|"pull_request") ;; | |
| *) | |
| echo "Invalid item_kind: $item_kind" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| target_owner="${target_repo%%/*}" | |
| target_name="${target_repo#*/}" | |
| target_checkout_dir="$target_name" | |
| if [ "$target_repo" = "${{ github.repository }}" ]; then | |
| target_checkout_dir="${target_name}-target" | |
| fi | |
| { | |
| echo "target_repo=$target_repo" | |
| echo "target_repo_owner=$target_owner" | |
| echo "target_repo_name=$target_name" | |
| echo "target_branch=$target_branch" | |
| echo "target_checkout_dir=$target_checkout_dir" | |
| echo "item_number=$item_number" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Create target read token | |
| id: target-read-token | |
| continue-on-error: true | |
| 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: read | |
| permission-pull-requests: read | |
| - name: Create target write token | |
| id: target-write-token | |
| continue-on-error: true | |
| 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: write | |
| permission-issues: write | |
| permission-pull-requests: write | |
| - name: React to target item review start | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$GH_TOKEN" | |
| react() { | |
| local content="$1" | |
| local err | |
| err="$(mktemp)" | |
| if gh api -X POST \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions" \ | |
| -f content="$content" 2>"$err" >/dev/null; then | |
| echo "Added $content reaction to $TARGET_REPO#$ITEM_NUMBER." | |
| elif grep -qi "HTTP 422\\|already exists" "$err"; then | |
| echo "$content reaction already exists on $TARGET_REPO#$ITEM_NUMBER." | |
| else | |
| cat "$err" >&2 | |
| return 1 | |
| fi | |
| rm -f "$err" | |
| } | |
| react "eyes" | |
| - uses: ./.github/actions/setup-pnpm | |
| id: setup-pnpm | |
| with: | |
| build-script: build:all | |
| - uses: ./.github/actions/setup-media-proof-tools | |
| - name: Mark re-review command in progress | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| COMMAND_STATUS_MARKER: ${{ github.event.client_payload.command_status_marker || '' }} | |
| STATUS_COMMENT_ID: ${{ github.event.client_payload.status_comment_id || '' }} | |
| RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| pnpm run repair:update-command-status -- \ | |
| --repo "$TARGET_REPO" \ | |
| --item-number "$ITEM_NUMBER" \ | |
| --marker "$COMMAND_STATUS_MARKER" \ | |
| --status-comment-id "$STATUS_COMMENT_ID" \ | |
| --state "Review in progress" \ | |
| --detail "Targeted re-review run started; Codex is reviewing the item." \ | |
| --run-url "$RUN_URL" \ | |
| --wait-ms 120000 | |
| - uses: ./.github/actions/setup-codex | |
| env: | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| with: | |
| login-status: "true" | |
| - uses: actions/cache@v5 | |
| with: | |
| path: ${{ steps.target.outputs.target_checkout_dir }}-cache.git | |
| key: ${{ steps.target.outputs.target_repo_name }}-event-git-${{ runner.os }}-${{ github.run_id }} | |
| restore-keys: | | |
| ${{ steps.target.outputs.target_repo_name }}-event-git-${{ runner.os }}- | |
| ${{ steps.target.outputs.target_repo_name }}-git-${{ runner.os }}- | |
| - name: Check out target repository | |
| run: | | |
| set -euo pipefail | |
| url="https://github.com/${{ steps.target.outputs.target_repo }}.git" | |
| cache_dir="${{ steps.target.outputs.target_checkout_dir }}-cache.git" | |
| checkout_dir="${{ steps.target.outputs.target_checkout_dir }}" | |
| target_branch="${{ steps.target.outputs.target_branch }}" | |
| if [ -d "$cache_dir" ]; then | |
| git -C "$cache_dir" remote set-url origin "$url" | |
| git -C "$cache_dir" config remote.origin.promisor true | |
| git -C "$cache_dir" config remote.origin.partialclonefilter blob:none | |
| if [ -f "$cache_dir/shallow" ]; then | |
| cache_fetch=(git -C "$cache_dir" fetch --prune --unshallow --filter=blob:none origin "$target_branch") | |
| else | |
| cache_fetch=(git -C "$cache_dir" fetch --prune --filter=blob:none origin "$target_branch") | |
| fi | |
| if ! "${cache_fetch[@]}"; then | |
| echo "::warning::Cached target repository fetch failed; rebuilding cache." | |
| rm -rf "$cache_dir" | |
| git clone --bare --filter=blob:none --single-branch --branch "$target_branch" "$url" "$cache_dir" | |
| fi | |
| else | |
| git clone --bare --filter=blob:none --single-branch --branch "$target_branch" "$url" "$cache_dir" | |
| fi | |
| if ! git clone --reference-if-able "$cache_dir" --dissociate --filter=blob:none --branch "$target_branch" --single-branch "$url" "$checkout_dir"; then | |
| echo "::warning::Cached target checkout failed; retrying without cache reference." | |
| rm -rf "$checkout_dir" "$cache_dir" | |
| git clone --bare --filter=blob:none --single-branch --branch "$target_branch" "$url" "$cache_dir" | |
| git clone --filter=blob:none --branch "$target_branch" --single-branch "$url" "$checkout_dir" | |
| fi | |
| git -C "$checkout_dir" rev-parse --short HEAD | |
| - name: Review exact event item | |
| id: review-exact-event-item | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| ADDITIONAL_PROMPT: ${{ github.event.client_payload.additional_prompt || '' }} | |
| CLAWSWEEPER_RELATED_GITHUB_SEARCH: ${{ vars.CLAWSWEEPER_RELATED_GITHUB_SEARCH || '1' }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$GH_TOKEN" | |
| additional_prompt_arg=() | |
| if [ -n "$ADDITIONAL_PROMPT" ]; then | |
| additional_prompt_arg=(--additional-prompt "$ADDITIONAL_PROMPT") | |
| fi | |
| timeout --kill-after=30s 12m pnpm run review -- \ | |
| --target-repo "${{ steps.target.outputs.target_repo }}" \ | |
| --target-dir "${{ steps.target.outputs.target_checkout_dir }}" \ | |
| --artifact-dir artifacts/event \ | |
| --batch-size 1 \ | |
| --max-pages 1 \ | |
| --codex-model gpt-5.5 \ | |
| --codex-reasoning-effort high \ | |
| --codex-sandbox danger-full-access \ | |
| --codex-timeout-ms 600000 \ | |
| --item-numbers "${{ steps.target.outputs.item_number }}" \ | |
| --readonly-openclaw \ | |
| --shard-index 0 \ | |
| --shard-count 1 \ | |
| "${additional_prompt_arg[@]}" | |
| - 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 }} | |
| fetch-depth: 1 | |
| - name: Publish event result and apply safe close | |
| id: publish-event-result | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| REPO_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| CLOSE_REASONS: ${{ github.event.client_payload.apply_close_reasons || 'implemented_on_main,duplicate_or_superseded,low_signal_unmergeable_pr' }} | |
| MIN_AGE_MINUTES: ${{ github.event.client_payload.apply_min_age_minutes || '0' }} | |
| run: pnpm run repair:publish-event-result | |
| - name: Route synced ClawSweeper verdict | |
| id: route-synced-verdict | |
| timeout-minutes: 4 | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| CLAWSWEEPER_DISPATCH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| CLAWSWEEPER_MUTATION_TOKEN_SOURCE: clawsweeper-app | |
| CLAWSWEEPER_ALLOW_MERGE: ${{ vars.CLAWSWEEPER_ALLOW_MERGE || '0' }} | |
| CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES: ${{ vars.CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES || '180' }} | |
| CLAWSWEEPER_COMMENT_MAX_COMMENTS: ${{ vars.CLAWSWEEPER_COMMENT_MAX_COMMENTS || '1000' }} | |
| run: | | |
| set -euo pipefail | |
| pnpm run repair:comment-router -- \ | |
| --write-report \ | |
| --repo "$TARGET_REPO" \ | |
| --item-number "$ITEM_NUMBER" \ | |
| --lookback-minutes "$CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES" \ | |
| --max-comments "$CLAWSWEEPER_COMMENT_MAX_COMMENTS" \ | |
| --execute | |
| - name: Mark re-review complete | |
| if: ${{ always() && steps.setup-pnpm.outcome == 'success' }} | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| COMMAND_STATUS_MARKER: ${{ github.event.client_payload.command_status_marker || '' }} | |
| STATUS_COMMENT_ID: ${{ github.event.client_payload.status_comment_id || '' }} | |
| RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| REVIEW_OUTCOME: ${{ steps.review-exact-event-item.outcome }} | |
| PUBLISH_OUTCOME: ${{ steps.publish-event-result.outcome }} | |
| ROUTE_OUTCOME: ${{ steps.route-synced-verdict.outcome }} | |
| run: | | |
| state="Failed" | |
| detail="The targeted re-review did not finish cleanly. Check the workflow run for details." | |
| if [ "$REVIEW_OUTCOME" = "cancelled" ]; then | |
| state="Superseded" | |
| detail="A newer re-review for this item started before this run finished, so GitHub cancelled this older run. Check the latest ClawSweeper run for the current result." | |
| fi | |
| if [ "$REVIEW_OUTCOME" = "success" ] && [ "$PUBLISH_OUTCOME" = "success" ] && [ "$ROUTE_OUTCOME" = "success" ]; then | |
| state="Complete" | |
| detail="The targeted re-review finished, the durable review comment was updated, and the synced verdict was routed." | |
| fi | |
| pnpm run repair:update-command-status -- \ | |
| --repo "$TARGET_REPO" \ | |
| --item-number "$ITEM_NUMBER" \ | |
| --marker "$COMMAND_STATUS_MARKER" \ | |
| --status-comment-id "$STATUS_COMMENT_ID" \ | |
| --state "$state" \ | |
| --detail "$detail" \ | |
| --run-url "$RUN_URL" | |
| - name: Commit event comment router ledger | |
| if: ${{ !cancelled() && steps.setup-pnpm.outcome == 'success' }} | |
| run: | | |
| pnpm run repair:publish-main -- \ | |
| --message "chore: record ClawSweeper event comment routing" \ | |
| --path results/comment-router.json \ | |
| --path results/comment-router-latest.json \ | |
| --path jobs \ | |
| --rebase-strategy theirs | |
| - name: Detect waiting event repair dispatches | |
| id: waiting-event-repair-dispatches | |
| if: ${{ success() }} | |
| run: | | |
| set -euo pipefail | |
| count="0" | |
| if [ -f results/comment-router-latest.json ]; then | |
| count="$(pnpm run --silent workflow -- count-command-actions --report results/comment-router-latest.json --action dispatch_repair --status waiting)" | |
| fi | |
| echo "count=$count" >> "$GITHUB_OUTPUT" | |
| - name: Retry waiting event repair dispatches | |
| if: ${{ steps.waiting-event-repair-dispatches.outputs.count != '' && steps.waiting-event-repair-dispatches.outputs.count != '0' }} | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| CLAWSWEEPER_DISPATCH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| CLAWSWEEPER_MUTATION_TOKEN_SOURCE: clawsweeper-app | |
| CLAWSWEEPER_ALLOW_MERGE: ${{ vars.CLAWSWEEPER_ALLOW_MERGE || '0' }} | |
| CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES: ${{ vars.CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES || '180' }} | |
| CLAWSWEEPER_COMMENT_MAX_COMMENTS: ${{ vars.CLAWSWEEPER_COMMENT_MAX_COMMENTS || '1000' }} | |
| run: | | |
| set -euo pipefail | |
| echo "Retrying ${{ steps.waiting-event-repair-dispatches.outputs.count }} waiting repair dispatch(es) after publishing router state." | |
| pnpm run repair:comment-router -- \ | |
| --write-report \ | |
| --repo "$TARGET_REPO" \ | |
| --item-number "$ITEM_NUMBER" \ | |
| --lookback-minutes "$CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES" \ | |
| --max-comments "$CLAWSWEEPER_COMMENT_MAX_COMMENTS" \ | |
| --execute | |
| - name: Commit event comment router retry ledger | |
| if: ${{ !cancelled() && steps.setup-pnpm.outcome == 'success' && steps.waiting-event-repair-dispatches.outputs.count != '' && steps.waiting-event-repair-dispatches.outputs.count != '0' }} | |
| run: | | |
| pnpm run repair:publish-main -- \ | |
| --message "chore: record ClawSweeper event repair dispatch retry" \ | |
| --path results/comment-router.json \ | |
| --path results/comment-router-latest.json \ | |
| --path jobs \ | |
| --rebase-strategy theirs | |
| - name: React to target item completion | |
| if: ${{ success() }} | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| ITEM_NUMBER: ${{ steps.target.outputs.item_number }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$GH_TOKEN" | |
| add_completion_reaction() { | |
| local err | |
| err="$(mktemp)" | |
| if gh api -X POST \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions" \ | |
| -f content="+1" 2>"$err" >/dev/null; then | |
| echo "Added +1 reaction to $TARGET_REPO#$ITEM_NUMBER." | |
| elif grep -qi "HTTP 422\\|already exists" "$err"; then | |
| echo "+1 reaction already exists on $TARGET_REPO#$ITEM_NUMBER." | |
| else | |
| cat "$err" >&2 | |
| rm -f "$err" | |
| exit 1 | |
| fi | |
| rm -f "$err" | |
| } | |
| remove_own_eyes_reactions() { | |
| local ids | |
| local err | |
| err="$(mktemp)" | |
| if ! ids="$(gh api -X GET \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions" \ | |
| -f content="eyes" \ | |
| -F per_page=100 \ | |
| --paginate \ | |
| --jq '.[] | select(.content == "eyes") | select(.user.login == "clawsweeper" or .user.login == "clawsweeper[bot]" or .user.login == "openclaw-clawsweeper[bot]") | .id' 2>"$err")"; then | |
| cat "$err" >&2 | |
| rm -f "$err" | |
| return 0 | |
| fi | |
| rm -f "$err" | |
| while IFS= read -r reaction_id; do | |
| [ -n "$reaction_id" ] || continue | |
| err="$(mktemp)" | |
| if gh api -X DELETE \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions/$reaction_id" 2>"$err" >/dev/null; then | |
| echo "Removed eyes reaction $reaction_id from $TARGET_REPO#$ITEM_NUMBER." | |
| else | |
| cat "$err" >&2 | |
| fi | |
| rm -f "$err" | |
| done <<< "$ids" | |
| } | |
| add_completion_reaction | |
| remove_own_eyes_reactions | |
| target-fanout: | |
| name: Fan out target repository sweeps | |
| if: ${{ github.event_name == 'schedule' && (github.event.schedule == '4/15 * * * *' || github.event.schedule == '41 * * * *' || github.event.schedule == '37 */6 * * *') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| actions: write | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| filter: blob:none | |
| fetch-depth: 0 | |
| - 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:repair | |
| - name: Create OpenClaw inventory token | |
| id: openclaw-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: openclaw | |
| - name: Create steipete inventory token | |
| id: steipete-token | |
| continue-on-error: true | |
| 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: steipete | |
| - name: Dispatch selected targets | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| CLAWSWEEPER_DISPATCH_TOKEN: ${{ github.token }} | |
| CLAWSWEEPER_INVENTORY_TOKEN_OPENCLAW: ${{ steps.openclaw-token.outputs.token }} | |
| CLAWSWEEPER_INVENTORY_TOKEN_STEIPETE: ${{ steps.steipete-token.outputs.token || '__public__' }} | |
| FANOUT_MODE: ${{ github.event.schedule == '41 * * * *' && 'normal-review' || (github.event.schedule == '37 */6 * * *' && 'audit' || 'hot-intake') }} | |
| FANOUT_LIMIT: ${{ github.event.schedule == '41 * * * *' && '6' || (github.event.schedule == '37 */6 * * *' && '12' || '6') }} | |
| run: | | |
| set -euo pipefail | |
| pnpm run target-fanout -- \ | |
| --mode "$FANOUT_MODE" \ | |
| --limit "$FANOUT_LIMIT" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --workflow sweep.yml \ | |
| --ref main | |
| - name: Publish fanout cursor | |
| run: | | |
| pnpm run repair:publish-main -- \ | |
| --message "chore: update target fanout cursor" \ | |
| --path results/target-fanout-cursors \ | |
| --rebase-strategy theirs | |
| plan: | |
| name: Plan review candidates | |
| if: ${{ (github.event_name != 'repository_dispatch' || github.event.action == 'clawsweeper_target_sweep') && !((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 == '6,21,36,51 * * * *') || (github.event.schedule == '7 */6 * * *' || github.event.schedule == '12 */6 * * *' || github.event.schedule == '17 */6 * * *') || (github.event.schedule == '4/15 * * * *' || github.event.schedule == '41 * * * *' || github.event.schedule == '37 */6 * * *') || github.event.schedule == '13 8,20 * * *'))) && !(github.event_name == 'schedule' && (github.event.schedule == '2/5 * * * *' || github.event.schedule == '22 * * * *') && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') && !(github.event_name == 'repository_dispatch' && github.event.client_payload.target_repo == 'openclaw/clawhub' && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| outputs: | |
| batch_size: ${{ steps.mode.outputs.batch_size }} | |
| codex_timeout_ms: ${{ steps.mode.outputs.codex_timeout_ms }} | |
| hot_intake: ${{ steps.mode.outputs.hot_intake }} | |
| matrix: ${{ steps.select.outputs.matrix }} | |
| max_pages: ${{ steps.mode.outputs.max_pages }} | |
| min_active_shards: ${{ steps.mode.outputs.min_active_shards }} | |
| min_backfill_review_age_minutes: ${{ steps.mode.outputs.min_backfill_review_age_minutes }} | |
| planned_count: ${{ steps.select.outputs.planned_count }} | |
| planned_capacity: ${{ steps.select.outputs.planned_capacity }} | |
| planned_item_numbers: ${{ steps.select.outputs.planned_item_numbers }} | |
| planned_shards: ${{ steps.select.outputs.planned_shards }} | |
| active_codex_target: ${{ steps.select.outputs.active_codex_target }} | |
| due_backlog: ${{ steps.select.outputs.due_backlog }} | |
| oldest_unreviewed_at: ${{ steps.select.outputs.oldest_unreviewed_at }} | |
| capacity_reason: ${{ steps.select.outputs.capacity_reason }} | |
| shard_count: ${{ steps.mode.outputs.shard_count }} | |
| target_checkout_dir: ${{ steps.target.outputs.target_checkout_dir }} | |
| target_branch: ${{ steps.target.outputs.target_branch }} | |
| target_repo: ${{ steps.target.outputs.target_repo }} | |
| target_repo_name: ${{ steps.target.outputs.target_repo_name }} | |
| target_repo_owner: ${{ steps.target.outputs.target_repo_owner }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| path: clawsweeper | |
| filter: blob:none | |
| fetch-depth: 0 | |
| - name: Resolve target repository | |
| id: target | |
| run: | | |
| target_repo="${{ github.event.inputs.target_repo || github.event.client_payload.target_repo || '' }}" | |
| target_branch="${{ github.event.client_payload.target_branch || 'main' }}" | |
| if [ -z "$target_repo" ]; then | |
| case "${{ github.event.schedule || '' }}" in | |
| "2/5 * * * *"|"22 * * * *"|"8,23,38,53 * * * *"|"12 */6 * * *") | |
| target_repo="openclaw/clawhub" | |
| ;; | |
| *) | |
| target_repo="openclaw/openclaw" | |
| ;; | |
| esac | |
| fi | |
| if ! printf '%s' "$target_branch" | grep -Eq '^[A-Za-z0-9_.\/-]+$'; then | |
| echo "Invalid target_branch: $target_branch" >&2 | |
| exit 1 | |
| fi | |
| target_owner="${target_repo%%/*}" | |
| target_name="${target_repo#*/}" | |
| target_checkout_dir="$target_name" | |
| if [ "$target_repo" = "${{ github.repository }}" ]; then | |
| target_checkout_dir="${target_name}-target" | |
| fi | |
| { | |
| echo "target_repo=$target_repo" | |
| echo "target_repo_owner=$target_owner" | |
| echo "target_repo_name=$target_name" | |
| echo "target_branch=$target_branch" | |
| echo "target_checkout_dir=$target_checkout_dir" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Create target read token | |
| id: target-read-token | |
| continue-on-error: true | |
| 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: read | |
| permission-pull-requests: read | |
| - name: Create state token | |
| id: state-token | |
| uses: ./clawsweeper/.github/actions/create-state-token | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| - uses: ./clawsweeper/.github/actions/setup-state | |
| with: | |
| token: ${{ steps.state-token.outputs.token }} | |
| worktree-path: clawsweeper | |
| fetch-depth: 1 | |
| - uses: ./clawsweeper/.github/actions/setup-pnpm | |
| with: | |
| working-directory: clawsweeper | |
| build-script: build:all | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: clawsweeper-runtime-dist | |
| path: clawsweeper/dist | |
| if-no-files-found: error | |
| retention-days: 1 | |
| - name: Publish planning-started status | |
| 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 == '6,21,36,51 * * * *'))) && !((github.event_name == 'workflow_dispatch' && github.event.inputs.hot_intake == 'true' && github.event.inputs.item_number == '' && github.event.inputs.item_numbers == '') || (github.event_name == 'schedule' && (github.event.schedule == '*/5 * * * *' || github.event.schedule == '2/5 * * * *'))) }} | |
| continue-on-error: true | |
| working-directory: clawsweeper | |
| run: | | |
| target_slug="${{ steps.target.outputs.target_repo }}" | |
| target_slug="${target_slug//\//-}" | |
| pnpm run status -- \ | |
| --target-repo "${{ steps.target.outputs.target_repo }}" \ | |
| --state "Planning review" \ | |
| --detail "Planner is scanning GitHub for the next review candidates. Candidate counts and shard details will be posted after planning completes." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| timeout 20s pnpm run repair:publish-main -- \ | |
| --message "chore: mark sweep planning started" \ | |
| --path "results/sweep-status/${target_slug}.json" \ | |
| --rebase-strategy theirs || echo "::warning::Skipped slow planning-started dashboard publish so candidate selection can start." | |
| - id: mode | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| limit() { | |
| pnpm --dir clawsweeper run --silent workflow -- limit "$1" | |
| } | |
| worker_limit() { | |
| pnpm --dir clawsweeper run --silent workflow -- worker-limit "$@" | |
| } | |
| active_runs_json() { | |
| { | |
| for run_status in in_progress pending queued waiting requested; do | |
| gh api "repos/${{ github.repository }}/actions/runs?per_page=100&status=${run_status}" \ | |
| --jq '.workflow_runs[] | {databaseId:.id, workflowName:.name, displayTitle:.display_title, status:.status, createdAt:.created_at, updatedAt:.updated_at}' 2>/dev/null \ | |
| || true | |
| done | |
| } | jq -s 'unique_by(.databaseId)' | |
| } | |
| ACTIVE_RUNS_JSON="$(active_runs_json)" | |
| STALE_QUEUED_CUTOFF="$(date -u -d '6 hours ago' '+%Y-%m-%dT%H:%M:%SZ')" | |
| active_run_count() { | |
| printf '%s' "$ACTIVE_RUNS_JSON" \ | |
| | WORKFLOW_NAME="$1" STALE_QUEUED_CUTOFF="$STALE_QUEUED_CUTOFF" jq '[.[] | select(.workflowName == env.WORKFLOW_NAME) | select(.status == "in_progress" or ((.status == "pending" or .status == "queued" or .status == "waiting" or .status == "requested") and ((.updatedAt // .createdAt // "") >= env.STALE_QUEUED_CUTOFF)))] | length' 2>/dev/null \ | |
| || printf '0' | |
| } | |
| active_sweep_exact_count() { | |
| printf '%s' "$ACTIVE_RUNS_JSON" \ | |
| | STALE_QUEUED_CUTOFF="$STALE_QUEUED_CUTOFF" jq '[.[] | select(.workflowName == "ClawSweeper") | select((.status == "in_progress" or ((.status == "pending" or .status == "queued" or .status == "waiting" or .status == "requested") and ((.updatedAt // .createdAt // "") >= env.STALE_QUEUED_CUTOFF))) and (.displayTitle | startswith("Review event item ")))] | length' 2>/dev/null \ | |
| || printf '0' | |
| } | |
| active_sweep_background_workers() { | |
| local runs total id title status active_shards | |
| total=0 | |
| runs="$(printf '%s' "$ACTIVE_RUNS_JSON" \ | |
| | CURRENT_RUN_ID="${GITHUB_RUN_ID:-0}" STALE_QUEUED_CUTOFF="$STALE_QUEUED_CUTOFF" jq -r '.[] | select((.databaseId | tostring) != env.CURRENT_RUN_ID) | select(.workflowName == "ClawSweeper") | select(.status == "in_progress" or ((.status == "pending" or .status == "queued" or .status == "waiting" or .status == "requested") and ((.updatedAt // .createdAt // "") >= env.STALE_QUEUED_CUTOFF))) | select(.displayTitle == "Review ClawSweeper items" or .displayTitle == "Review hot ClawSweeper items" or (.displayTitle | startswith("Review target repo ")) or (.displayTitle | startswith("Review hot target repo "))) | [.databaseId, .displayTitle, .status] | @tsv' 2>/dev/null || true)" | |
| while IFS=$'\t' read -r id title status; do | |
| if [ -z "$id" ]; then | |
| continue | |
| fi | |
| active_shards=0 | |
| if [ "$status" = "in_progress" ]; then | |
| active_shards="$(gh run view "$id" --repo "${{ github.repository }}" --json jobs 2>/dev/null \ | |
| | jq '[.jobs[]? | select(.name | startswith("Review shard ")) | select(.status == "in_progress" or .status == "pending" or .status == "queued" or .status == "waiting" or .status == "requested")] | length' 2>/dev/null \ | |
| || printf '0')" | |
| fi | |
| if ! [[ "$active_shards" =~ ^[0-9]+$ ]]; then | |
| active_shards=0 | |
| fi | |
| # Planning, publish, queued, and not-yet-expanded matrix runs have not | |
| # created shard jobs yet. Reserve their quiet lane size so a second | |
| # planner cannot over-allocate before the first matrix expands. | |
| if [ "$active_shards" -lt 1 ]; then | |
| if [ "$title" = "Review hot ClawSweeper items" ] || [[ "$title" == Review\ hot\ target\ repo\ * ]]; then | |
| active_shards="$(limit review_shards.hot_intake_default)" | |
| else | |
| active_shards="$(limit review_shards.normal_default)" | |
| fi | |
| fi | |
| total=$((total + active_shards)) | |
| done <<< "$runs" | |
| printf '%s' "$total" | |
| } | |
| exact_item_shards="$(limit review_shards.exact_item_default)" | |
| normal_active_floor="$(limit review_shards.normal_active_floor)" | |
| hard_shard_cap="$(limit review_shards.hard_cap)" | |
| commit_page_size="$(limit commit_review.page_size_default)" | |
| active_critical_workers="$(( $(active_run_count "repair cluster worker") + $(active_sweep_exact_count) ))" | |
| active_background_workers="$(( $(active_run_count "ClawSweeper Commit Review") * commit_page_size + $(active_sweep_background_workers) ))" | |
| hot_intake_shards="$(worker_limit hot_intake --active-critical "$active_critical_workers" --active-background "$active_background_workers")" | |
| normal_shards="$(worker_limit normal_review --active-critical "$active_critical_workers" --active-background "$active_background_workers")" | |
| hot_intake="${{ ((github.event_name == 'repository_dispatch' && github.event.client_payload.hot_intake == 'true') || (github.event_name == 'workflow_dispatch' && github.event.inputs.hot_intake == 'true') || (github.event_name == 'schedule' && (github.event.schedule == '*/5 * * * *' || github.event.schedule == '2/5 * * * *'))) && 'true' || 'false' }}" | |
| exact_item="${{ github.event.client_payload.item_number || github.event.inputs.item_number || github.event.inputs.item_numbers || '' }}" | |
| target_repo="${{ steps.target.outputs.target_repo }}" | |
| if [ "$hot_intake" = "true" ] && [ -n "$exact_item" ]; then | |
| batch_size="1" | |
| shard_count="$exact_item_shards" | |
| max_pages="1" | |
| elif [ "$hot_intake" = "true" ]; then | |
| batch_size="1" | |
| shard_count="${{ github.event.client_payload.shard_count || '' }}" | |
| if [ -z "$shard_count" ]; then | |
| shard_count="$hot_intake_shards" | |
| fi | |
| max_pages="10" | |
| min_active_shards="0" | |
| min_backfill_review_age_minutes="360" | |
| else | |
| if [ "${{ github.event_name }}" = "schedule" ]; then | |
| batch_size="1" | |
| else | |
| batch_size="${{ github.event.client_payload.batch_size || github.event.inputs.batch_size || '3' }}" | |
| fi | |
| if [ "$target_repo" = "openclaw/openclaw" ]; then | |
| min_active_shards="$normal_active_floor" | |
| else | |
| min_active_shards="0" | |
| fi | |
| shard_count="${{ github.event.client_payload.shard_count || github.event.inputs.shard_count || '' }}" | |
| if [ -z "$shard_count" ]; then | |
| shard_count="$normal_shards" | |
| fi | |
| max_pages="250" | |
| min_backfill_review_age_minutes="360" | |
| fi | |
| if [ "$hot_intake" = "true" ] && [ -n "$exact_item" ]; then | |
| min_active_shards="0" | |
| min_backfill_review_age_minutes="360" | |
| fi | |
| if ! [[ "$shard_count" =~ ^[0-9]+$ ]]; then | |
| shard_count="$normal_shards" | |
| fi | |
| if [ "$shard_count" -gt "$hard_shard_cap" ]; then | |
| shard_count="$hard_shard_cap" | |
| fi | |
| if [ -z "$exact_item" ]; then | |
| lane_shard_cap="$normal_shards" | |
| if [ "$hot_intake" = "true" ]; then | |
| lane_shard_cap="$hot_intake_shards" | |
| fi | |
| if ! [[ "$lane_shard_cap" =~ ^[0-9]+$ ]] || [ "$lane_shard_cap" -lt 1 ]; then | |
| lane_shard_cap="1" | |
| fi | |
| if [ "$shard_count" -gt "$lane_shard_cap" ]; then | |
| echo "::notice::Capping broad background review shards from $shard_count to scheduler allowance $lane_shard_cap." | |
| shard_count="$lane_shard_cap" | |
| fi | |
| fi | |
| { | |
| echo "batch_size=$batch_size" | |
| echo "codex_timeout_ms=${{ github.event.client_payload.codex_timeout_ms || github.event.inputs.codex_timeout_ms || '600000' }}" | |
| echo "hot_intake=$hot_intake" | |
| echo "max_pages=$max_pages" | |
| echo "min_active_shards=$min_active_shards" | |
| echo "min_backfill_review_age_minutes=$min_backfill_review_age_minutes" | |
| echo "shard_count=$shard_count" | |
| } >> "$GITHUB_OUTPUT" | |
| - id: select | |
| working-directory: clawsweeper | |
| env: | |
| SHARD_COUNT: ${{ steps.mode.outputs.shard_count }} | |
| BATCH_SIZE: ${{ steps.mode.outputs.batch_size }} | |
| HOT_INTAKE: ${{ steps.mode.outputs.hot_intake }} | |
| ITEM_NUMBER: ${{ github.event.inputs.item_number || '' }} | |
| ITEM_NUMBERS: ${{ github.event.inputs.item_numbers || github.event.client_payload.item_number || '' }} | |
| MAX_PAGES: ${{ steps.mode.outputs.max_pages }} | |
| MIN_ACTIVE_SHARDS: ${{ steps.mode.outputs.min_active_shards }} | |
| MIN_BACKFILL_REVIEW_AGE_MINUTES: ${{ steps.mode.outputs.min_backfill_review_age_minutes }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| GH_TOKEN: ${{ steps.target-read-token.outputs.token || github.token }} | |
| run: | | |
| set -euo pipefail | |
| item_arg=() | |
| if [ -n "$ITEM_NUMBER" ]; then | |
| item_arg=(--item-number "$ITEM_NUMBER") | |
| fi | |
| if [ -n "$ITEM_NUMBERS" ]; then | |
| item_arg+=("--item-numbers" "$ITEM_NUMBERS") | |
| fi | |
| hot_intake_arg=() | |
| if [ "$HOT_INTAKE" = "true" ]; then | |
| hot_intake_arg=(--hot-intake) | |
| fi | |
| pnpm run --silent plan -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --batch-size "$BATCH_SIZE" \ | |
| --max-pages "$MAX_PAGES" \ | |
| --shard-count "$SHARD_COUNT" \ | |
| --codex-model gpt-5.5 \ | |
| --codex-reasoning-effort high \ | |
| --codex-sandbox danger-full-access \ | |
| --min-active-shards "$MIN_ACTIVE_SHARDS" \ | |
| --min-backfill-review-age-minutes "$MIN_BACKFILL_REVIEW_AGE_MINUTES" \ | |
| "${hot_intake_arg[@]}" \ | |
| "${item_arg[@]}" > plan.json | |
| pnpm run --silent workflow -- plan-output \ | |
| --plan plan.json \ | |
| --batch-size "$BATCH_SIZE" \ | |
| --shard-count "$SHARD_COUNT" >> "$GITHUB_OUTPUT" | |
| cat plan.json | |
| - name: Publish planning status | |
| if: ${{ (github.event_name != 'workflow_dispatch' || github.event.inputs.apply_existing != 'true') && (steps.mode.outputs.hot_intake != 'true' || github.event.inputs.item_number != '' || github.event.inputs.item_numbers != '') }} | |
| continue-on-error: true | |
| working-directory: clawsweeper | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| target_slug="$TARGET_REPO" | |
| target_slug="${target_slug//\//-}" | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "${{ steps.mode.outputs.hot_intake == 'true' && 'Hot intake in progress' || 'Review in progress' }}" \ | |
| --detail "${{ steps.mode.outputs.hot_intake == 'true' && 'Hot intake planned' || 'Planned' }} ${{ steps.select.outputs.planned_count }} items across ${{ steps.select.outputs.planned_shards }} shards. Capacity is ${{ steps.select.outputs.planned_capacity }} items; due backlog scanned is ${{ steps.select.outputs.due_backlog }}. Capacity reason: ${{ steps.select.outputs.capacity_reason }}. Review shards are starting; publish will merge artifacts when they finish." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ | |
| --planned-count "${{ steps.select.outputs.planned_count }}" \ | |
| --planned-capacity "${{ steps.select.outputs.planned_capacity }}" \ | |
| --planned-shards "${{ steps.select.outputs.planned_shards }}" \ | |
| --active-codex "${{ steps.select.outputs.active_codex_target }}" \ | |
| --due-backlog "${{ steps.select.outputs.due_backlog }}" \ | |
| --oldest-unreviewed-at "${{ steps.select.outputs.oldest_unreviewed_at }}" \ | |
| --capacity-reason "${{ steps.select.outputs.capacity_reason }}" | |
| timeout 20s pnpm run repair:publish-main -- \ | |
| --message "chore: mark sweep review in progress" \ | |
| --path "results/sweep-status/${target_slug}.json" \ | |
| --rebase-strategy theirs || echo "::warning::Skipped slow in-progress dashboard publish so review shards can start." | |
| review: | |
| name: Review shard ${{ matrix.shard }} | |
| needs: plan | |
| runs-on: ${{ vars.CLAWSWEEPER_REVIEW_RUNNER || 'ubuntu-latest' }} | |
| timeout-minutes: 75 | |
| continue-on-error: true | |
| permissions: | |
| contents: read | |
| issues: read | |
| pull-requests: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJson(needs.plan.outputs.matrix) }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| path: clawsweeper | |
| filter: blob:none | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Create target review token | |
| id: target-read-token | |
| continue-on-error: true | |
| 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: ${{ needs.plan.outputs.target_repo_owner }} | |
| repositories: ${{ needs.plan.outputs.target_repo_name }} | |
| permission-contents: read | |
| permission-issues: write | |
| permission-pull-requests: read | |
| - name: Create target Codex inspection token | |
| id: codex-inspection-token | |
| continue-on-error: true | |
| 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: ${{ needs.plan.outputs.target_repo_owner }} | |
| repositories: ${{ needs.plan.outputs.target_repo_name }} | |
| permission-contents: read | |
| permission-issues: read | |
| permission-pull-requests: read | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: clawsweeper-runtime-dist | |
| path: clawsweeper/dist | |
| - uses: ./clawsweeper/.github/actions/setup-codex | |
| env: | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| with: | |
| login-status: "true" | |
| - uses: ./clawsweeper/.github/actions/setup-media-proof-tools | |
| - uses: actions/cache@v5 | |
| with: | |
| path: ${{ needs.plan.outputs.target_checkout_dir }}-cache.git | |
| key: ${{ needs.plan.outputs.target_repo_name }}-git-${{ runner.os }}-${{ github.run_id }} | |
| restore-keys: | | |
| ${{ needs.plan.outputs.target_repo_name }}-git-${{ runner.os }}- | |
| - name: Check out target repository | |
| run: | | |
| set -euo pipefail | |
| url="https://github.com/${{ needs.plan.outputs.target_repo }}.git" | |
| cache_dir="${{ needs.plan.outputs.target_checkout_dir }}-cache.git" | |
| checkout_dir="${{ needs.plan.outputs.target_checkout_dir }}" | |
| target_branch="${{ needs.plan.outputs.target_branch }}" | |
| if [ -d "$cache_dir" ]; then | |
| git -C "$cache_dir" remote set-url origin "$url" | |
| git -C "$cache_dir" config remote.origin.promisor true | |
| git -C "$cache_dir" config remote.origin.partialclonefilter blob:none | |
| if [ -f "$cache_dir/shallow" ]; then | |
| cache_fetch=(git -C "$cache_dir" fetch --prune --unshallow --filter=blob:none origin "$target_branch") | |
| else | |
| cache_fetch=(git -C "$cache_dir" fetch --prune --filter=blob:none origin "$target_branch") | |
| fi | |
| if ! "${cache_fetch[@]}"; then | |
| echo "::warning::Cached target repository fetch failed; rebuilding cache." | |
| rm -rf "$cache_dir" | |
| git clone --bare --filter=blob:none --single-branch --branch "$target_branch" "$url" "$cache_dir" | |
| fi | |
| else | |
| git clone --bare --filter=blob:none --single-branch --branch "$target_branch" "$url" "$cache_dir" | |
| fi | |
| if ! git clone --reference-if-able "$cache_dir" --dissociate --filter=blob:none --branch "$target_branch" --single-branch "$url" "$checkout_dir"; then | |
| echo "::warning::Cached target checkout failed; retrying without cache reference." | |
| rm -rf "$checkout_dir" "$cache_dir" | |
| git clone --bare --filter=blob:none --single-branch --branch "$target_branch" "$url" "$cache_dir" | |
| git clone --filter=blob:none --branch "$target_branch" --single-branch "$url" "$checkout_dir" | |
| fi | |
| git -C "$checkout_dir" rev-parse --short HEAD | |
| - name: Mark shard start | |
| id: shard-start | |
| run: echo "started_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT" | |
| - name: Review shard | |
| id: review-shard | |
| continue-on-error: true | |
| working-directory: clawsweeper | |
| env: | |
| GH_TOKEN: ${{ steps.target-read-token.outputs.token || github.token }} | |
| CLAWSWEEPER_PROOF_INSPECTION_TOKEN: ${{ steps.codex-inspection-token.outputs.token || github.token }} | |
| ADDITIONAL_PROMPT: ${{ github.event.inputs.additional_prompt || github.event.client_payload.additional_prompt || '' }} | |
| run: | | |
| hot_intake_arg=() | |
| if [ "${{ needs.plan.outputs.hot_intake }}" = "true" ]; then | |
| hot_intake_arg=(--hot-intake) | |
| fi | |
| additional_prompt_arg=() | |
| if [ -n "$ADDITIONAL_PROMPT" ]; then | |
| additional_prompt_arg=(--additional-prompt "$ADDITIONAL_PROMPT") | |
| fi | |
| codex_timeout_seconds=$(((${{ needs.plan.outputs.codex_timeout_ms }} + 999) / 1000)) | |
| review_timeout_seconds=$(((codex_timeout_seconds + 180) * ${{ needs.plan.outputs.batch_size }})) | |
| if [ "$review_timeout_seconds" -lt 300 ]; then | |
| review_timeout_seconds=300 | |
| fi | |
| if [ "$review_timeout_seconds" -gt 4200 ]; then | |
| review_timeout_seconds=4200 | |
| fi | |
| echo "::notice::Review shard timeout is ${review_timeout_seconds}s for batch size ${{ needs.plan.outputs.batch_size }} and per-item Codex timeout ${codex_timeout_seconds}s." | |
| timeout --kill-after=30s "${review_timeout_seconds}s" node dist/clawsweeper.js review \ | |
| --target-repo "${{ needs.plan.outputs.target_repo }}" \ | |
| --target-dir "../${{ needs.plan.outputs.target_checkout_dir }}" \ | |
| --artifact-dir ../review-artifacts/shard-${{ matrix.shard }} \ | |
| --batch-size ${{ needs.plan.outputs.batch_size }} \ | |
| --max-pages ${{ needs.plan.outputs.max_pages }} \ | |
| --codex-model gpt-5.5 \ | |
| --codex-reasoning-effort high \ | |
| --codex-sandbox danger-full-access \ | |
| --codex-timeout-ms ${{ needs.plan.outputs.codex_timeout_ms }} \ | |
| --item-numbers "${{ matrix.item_numbers }}" \ | |
| "${hot_intake_arg[@]}" \ | |
| --readonly-openclaw \ | |
| --shard-index ${{ matrix.shard }} \ | |
| --shard-count ${{ needs.plan.outputs.planned_shards }} \ | |
| "${additional_prompt_arg[@]}" | |
| - name: Record shard metrics | |
| if: always() | |
| env: | |
| ITEM_NUMBERS: ${{ matrix.item_numbers }} | |
| REVIEW_OUTCOME: ${{ steps.review-shard.outcome || 'not_started' }} | |
| STARTED_AT: ${{ steps.shard-start.outputs.started_at || '' }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p review-artifacts/metrics | |
| jq -n \ | |
| --arg shard "${{ matrix.shard }}" \ | |
| --arg item_numbers "$ITEM_NUMBERS" \ | |
| --arg review_outcome "$REVIEW_OUTCOME" \ | |
| --arg started_at "$STARTED_AT" \ | |
| --arg completed_at "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \ | |
| --arg target_repo "$TARGET_REPO" \ | |
| '{ | |
| shard: ($shard | tonumber), | |
| item_numbers: $item_numbers, | |
| review_outcome: $review_outcome, | |
| started_at: $started_at, | |
| completed_at: $completed_at, | |
| target_repo: $target_repo | |
| }' > review-artifacts/metrics/shard-${{ matrix.shard }}.json | |
| - name: Record failed review shard | |
| if: ${{ failure() || steps.review-shard.outcome == 'failure' }} | |
| run: | | |
| mkdir -p review-artifacts/failed-shards | |
| cat > review-artifacts/failed-shards/shard-${{ matrix.shard }}.json <<JSON | |
| { | |
| "shard": ${{ matrix.shard }}, | |
| "item_numbers": "${{ matrix.item_numbers }}" | |
| } | |
| JSON | |
| - uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: review-shard-${{ matrix.shard }} | |
| path: | | |
| review-artifacts/shard-${{ matrix.shard }}/*.md | |
| if-no-files-found: ignore | |
| - uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: review-failed-shard-${{ matrix.shard }} | |
| path: review-artifacts/failed-shards/shard-${{ matrix.shard }}.json | |
| if-no-files-found: ignore | |
| - uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: review-metrics-${{ matrix.shard }} | |
| path: review-artifacts/metrics/shard-${{ matrix.shard }}.json | |
| if-no-files-found: ignore | |
| publish: | |
| name: Publish review artifacts | |
| needs: [plan, review] | |
| if: ${{ always() && needs.plan.result == 'success' && needs.review.result != 'cancelled' && needs.plan.outputs.target_repo != '' && (github.event_name != 'repository_dispatch' || github.event.action == 'clawsweeper_target_sweep') && !((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 == '6,21,36,51 * * * *') || (github.event.schedule == '7 */6 * * *' || github.event.schedule == '12 */6 * * *' || github.event.schedule == '17 */6 * * *')))) }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| filter: blob:none | |
| fetch-depth: 0 | |
| - name: Create target write token | |
| id: target-write-token | |
| continue-on-error: true | |
| 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: ${{ needs.plan.outputs.target_repo_owner }} | |
| repositories: ${{ needs.plan.outputs.target_repo_name }} | |
| permission-contents: write | |
| permission-issues: write | |
| permission-pull-requests: write | |
| - 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 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| pattern: review-shard-* | |
| path: artifacts | |
| merge-multiple: true | |
| - uses: actions/download-artifact@v8 | |
| continue-on-error: true | |
| with: | |
| pattern: review-metrics-* | |
| path: review-metrics | |
| merge-multiple: true | |
| - name: Sync before applying artifacts | |
| run: git pull --rebase | |
| - name: Apply review artifacts | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| EXACT_ITEM: ${{ github.event.client_payload.item_number || github.event.inputs.item_number || github.event.inputs.item_numbers || '' }} | |
| HOT_INTAKE: ${{ needs.plan.outputs.hot_intake }} | |
| MAX_PAGES: ${{ needs.plan.outputs.max_pages }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| run: | | |
| apply_artifacts_args=(--target-repo "$TARGET_REPO" --artifact-dir artifacts --skip-dashboard --skip-reconcile) | |
| publish_state="Review publish complete" | |
| reviewed_count="$(find artifacts -type f -name '*.md' | wc -l | tr -d ' ')" | |
| metric_count="$(find review-metrics -type f -name '*.json' 2>/dev/null | wc -l | tr -d ' ')" | |
| non_success_metric_count="$(find review-metrics -type f -name '*.json' -print0 2>/dev/null | xargs -0 -r jq -r 'select(.review_outcome != "success") | .shard' | wc -l | tr -d ' ')" | |
| publish_detail="Merged ${reviewed_count} review artifacts for run ${{ github.run_id }}. Captured ${metric_count} shard metrics; ${non_success_metric_count} shards reported non-success review status. Folder reconciliation moved tracked files to match current GitHub open/closed state." | |
| if [ "$HOT_INTAKE" = "true" ] && [ -z "$EXACT_ITEM" ]; then | |
| apply_artifacts_args+=(--max-pages "$MAX_PAGES" --skip-reconcile) | |
| publish_state="Hot intake publish complete" | |
| publish_detail="Merged ${reviewed_count} hot intake artifacts for run ${{ github.run_id }} without full folder reconciliation. Captured ${metric_count} shard metrics; ${non_success_metric_count} shards reported non-success review status." | |
| fi | |
| pnpm run apply-artifacts -- "${apply_artifacts_args[@]}" | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "$publish_state" \ | |
| --detail "$publish_detail" \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ | |
| --planned-count "${{ needs.plan.outputs.planned_count }}" \ | |
| --planned-capacity "${{ needs.plan.outputs.planned_capacity }}" \ | |
| --planned-shards "${{ needs.plan.outputs.planned_shards }}" \ | |
| --active-codex "0" \ | |
| --due-backlog "${{ needs.plan.outputs.due_backlog }}" \ | |
| --oldest-unreviewed-at "${{ needs.plan.outputs.oldest_unreviewed_at }}" \ | |
| --capacity-reason "${{ needs.plan.outputs.capacity_reason }}" | |
| - name: Commit review records | |
| env: | |
| EXACT_ITEM: ${{ github.event.client_payload.item_number || github.event.inputs.item_number || github.event.inputs.item_numbers || '' }} | |
| GH_TOKEN: ${{ github.token }} | |
| HOT_INTAKE: ${{ needs.plan.outputs.hot_intake }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| run: | | |
| target_slug="$TARGET_REPO" | |
| target_slug="${target_slug//\//-}" | |
| if [ "$HOT_INTAKE" = "true" ] && [ -z "$EXACT_ITEM" ]; then | |
| echo "Skipping full reconcile for broad hot-intake publish." | |
| else | |
| pnpm run reconcile -- --target-repo "$TARGET_REPO" --skip-closed-at | |
| fi | |
| pnpm run repair:publish-main -- \ | |
| --message "chore: update sweep records" \ | |
| --path "records/${target_slug}" \ | |
| --path "results/sweep-status/${target_slug}.json" \ | |
| --rebase-strategy theirs | |
| - name: Dispatch reproducible bug implementation candidates | |
| if: ${{ success() && needs.plan.outputs.target_repo == 'openclaw/openclaw' && vars.CLAWSWEEPER_AUTO_IMPLEMENT_REPRO_BUGS == '1' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| ENABLED: ${{ vars.CLAWSWEEPER_AUTO_IMPLEMENT_REPRO_BUGS == '1' && 'true' || 'false' }} | |
| MAX_DISPATCH: ${{ vars.CLAWSWEEPER_AUTO_IMPLEMENT_MAX_DISPATCH_PER_SWEEP || '' }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$MAX_DISPATCH" ]; then | |
| MAX_DISPATCH="$(pnpm run --silent workflow -- limit issue_implementation.dispatches_per_sweep_default)" | |
| fi | |
| candidate_output="$(pnpm run --silent repair:issue-implementation-intake -- candidates \ | |
| --enabled "$ENABLED" \ | |
| --candidate-kind strict_bug \ | |
| --target-repo "$TARGET_REPO" \ | |
| --artifact-dir artifacts)" | |
| echo "$candidate_output" | |
| candidates_json="$(CANDIDATE_OUTPUT="$candidate_output" node <<'NODE' | |
| const output = JSON.parse(process.env.CANDIDATE_OUTPUT || "{}"); | |
| console.log(JSON.stringify(output.candidates || [])); | |
| NODE | |
| )" | |
| if [ -z "$candidates_json" ] || [ "$candidates_json" = "[]" ]; then | |
| echo "No strict reproducible bug implementation candidates." | |
| exit 0 | |
| fi | |
| CANDIDATES_JSON="$candidates_json" MAX_DISPATCH="$MAX_DISPATCH" node <<'NODE' > /tmp/issue-implementation-candidates.tsv | |
| const candidates = JSON.parse(process.env.CANDIDATES_JSON || "[]"); | |
| const limit = Math.max(0, Number(process.env.MAX_DISPATCH || "0")); | |
| for (const candidate of candidates.slice(0, limit)) { | |
| console.log([ | |
| candidate.item_number, | |
| candidate.report_path, | |
| candidate.report_url, | |
| ].join("\t")); | |
| } | |
| NODE | |
| if [ ! -s /tmp/issue-implementation-candidates.tsv ]; then | |
| echo "No candidates remained after dispatch cap." | |
| exit 0 | |
| fi | |
| while IFS=$'\t' read -r item_number report_path report_url; do | |
| echo "Dispatching reproducible bug implementation intake for $TARGET_REPO#$item_number" | |
| gh workflow run repair-issue-implementation-intake.yml \ | |
| --ref main \ | |
| -f enabled=true \ | |
| -f target_repo="$TARGET_REPO" \ | |
| -f item_number="$item_number" \ | |
| -f candidate_kind=strict_bug \ | |
| -f report_path="$report_path" \ | |
| -f report_url="$report_url" | |
| done < /tmp/issue-implementation-candidates.tsv | |
| - name: Dispatch vision-fit implementation candidates | |
| if: ${{ success() && needs.plan.outputs.target_repo == 'openclaw/openclaw' && vars.CLAWSWEEPER_AUTO_IMPLEMENT_VISION_FIT == '1' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| ENABLED: ${{ vars.CLAWSWEEPER_AUTO_IMPLEMENT_VISION_FIT == '1' && 'true' || 'false' }} | |
| MAX_DISPATCH: ${{ vars.CLAWSWEEPER_AUTO_IMPLEMENT_VISION_FIT_MAX_DISPATCH_PER_SWEEP || vars.CLAWSWEEPER_AUTO_IMPLEMENT_MAX_DISPATCH_PER_SWEEP || '' }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$MAX_DISPATCH" ]; then | |
| MAX_DISPATCH="$(pnpm run --silent workflow -- limit issue_implementation.dispatches_per_sweep_default)" | |
| fi | |
| candidate_output="$(pnpm run --silent repair:issue-implementation-intake -- candidates \ | |
| --enabled "$ENABLED" \ | |
| --candidate-kind vision_fit \ | |
| --target-repo "$TARGET_REPO" \ | |
| --artifact-dir artifacts)" | |
| echo "$candidate_output" | |
| candidates_json="$(CANDIDATE_OUTPUT="$candidate_output" node <<'NODE' | |
| const output = JSON.parse(process.env.CANDIDATE_OUTPUT || "{}"); | |
| console.log(JSON.stringify(output.candidates || [])); | |
| NODE | |
| )" | |
| if [ -z "$candidates_json" ] || [ "$candidates_json" = "[]" ]; then | |
| echo "No vision-fit implementation candidates." | |
| exit 0 | |
| fi | |
| CANDIDATES_JSON="$candidates_json" MAX_DISPATCH="$MAX_DISPATCH" node <<'NODE' > /tmp/vision-fit-implementation-candidates.tsv | |
| const candidates = JSON.parse(process.env.CANDIDATES_JSON || "[]"); | |
| const limit = Math.max(0, Number(process.env.MAX_DISPATCH || "0")); | |
| for (const candidate of candidates.slice(0, limit)) { | |
| console.log([ | |
| candidate.item_number, | |
| candidate.report_path, | |
| candidate.report_url, | |
| ].join("\t")); | |
| } | |
| NODE | |
| if [ ! -s /tmp/vision-fit-implementation-candidates.tsv ]; then | |
| echo "No vision-fit candidates remained after dispatch cap." | |
| exit 0 | |
| fi | |
| while IFS=$'\t' read -r item_number report_path report_url; do | |
| echo "Dispatching vision-fit implementation intake for $TARGET_REPO#$item_number" | |
| gh workflow run repair-issue-implementation-intake.yml \ | |
| --ref main \ | |
| -f enabled=true \ | |
| -f target_repo="$TARGET_REPO" \ | |
| -f item_number="$item_number" \ | |
| -f candidate_kind=vision_fit \ | |
| -f report_path="$report_path" \ | |
| -f report_url="$report_url" | |
| done < /tmp/vision-fit-implementation-candidates.tsv | |
| - name: Dispatch background review comment sync | |
| if: ${{ success() && steps.target-write-token.outputs.token != '' && needs.plan.outputs.hot_intake != 'true' && (github.event_name != 'repository_dispatch' || github.event.action == 'clawsweeper_target_sweep') && (github.event_name != 'workflow_dispatch' || (github.event.inputs.item_number == '' && github.event.inputs.item_numbers == '')) }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| item_numbers="$(pnpm run --silent workflow -- artifact-item-numbers --artifact-dir artifacts)" | |
| if [ -z "$item_numbers" ]; then | |
| echo "No review artifacts to sync comments for." | |
| exit 0 | |
| fi | |
| for attempt in 1 2 3; do | |
| if gh workflow run sweep.yml \ | |
| --ref main \ | |
| -f target_repo="$TARGET_REPO" \ | |
| -f apply_existing=true \ | |
| -f apply_sync_comments_only=true \ | |
| -f apply_item_numbers="$item_numbers" \ | |
| -f apply_limit=0 \ | |
| -f apply_comment_sync_min_age_days=7 \ | |
| -f apply_progress_every=25; then | |
| echo "Dispatched background comment sync for item numbers: $item_numbers" | |
| exit 0 | |
| fi | |
| sleep "$((attempt * 10))" | |
| done | |
| echo "::warning::Unable to dispatch background comment sync after three attempts; apply/comment-sync backstops can pick it up later." | |
| - uses: ./.github/actions/setup-codex | |
| if: ${{ success() && steps.target-write-token.outputs.token != '' && (((github.event_name == 'repository_dispatch' && github.event.action != 'clawsweeper_target_sweep') || github.event.inputs.item_number != '' || github.event.inputs.item_numbers != '') || github.event.inputs.apply_after_review == 'true') }} | |
| env: | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| - name: Sync selected review comments | |
| if: ${{ success() && steps.target-write-token.outputs.token != '' && ((github.event_name == 'repository_dispatch' && github.event.action != 'clawsweeper_target_sweep') || github.event.inputs.item_number != '' || github.event.inputs.item_numbers != '') }} | |
| timeout-minutes: 15 | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$GH_TOKEN" | |
| target_slug="$TARGET_REPO" | |
| target_slug="${target_slug//\//-}" | |
| item_numbers="$(pnpm run --silent workflow -- artifact-item-numbers --artifact-dir artifacts)" | |
| if [ -z "$item_numbers" ]; then | |
| echo "No review artifacts to sync comments for." | |
| exit 0 | |
| fi | |
| git pull --rebase | |
| pnpm run apply-decisions -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --skip-dashboard \ | |
| --item-numbers "$item_numbers" \ | |
| --sync-comments-only \ | |
| --apply-kind all \ | |
| --limit 0 \ | |
| --processed-limit 1000 \ | |
| --max-runtime-ms 720000 \ | |
| --comment-sync-min-age-days 7 \ | |
| --progress-every 25 | |
| synced_count="$(pnpm run --silent workflow -- count-actions --report apply-report.json --action review_comment_synced)" | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "Review comments checked" \ | |
| --detail "Checked selected durable Codex review comments and synced missing or stale comments. Synced: $synced_count. Item numbers: $item_numbers." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ | |
| --planned-count "${{ needs.plan.outputs.planned_count }}" \ | |
| --planned-capacity "${{ needs.plan.outputs.planned_capacity }}" \ | |
| --planned-shards "${{ needs.plan.outputs.planned_shards }}" \ | |
| --active-codex "0" \ | |
| --due-backlog "${{ needs.plan.outputs.due_backlog }}" \ | |
| --oldest-unreviewed-at "${{ needs.plan.outputs.oldest_unreviewed_at }}" \ | |
| --capacity-reason "${{ needs.plan.outputs.capacity_reason }}" | |
| pnpm run repair:publish-main -- \ | |
| --message "chore: sync selected review comments" \ | |
| --path "records/${target_slug}" \ | |
| --path apply-report.json \ | |
| --path "results/sweep-status/${target_slug}.json" \ | |
| --rebase-strategy theirs | |
| - name: Apply selected safe close proposals | |
| if: ${{ success() && steps.target-write-token.outputs.token != '' && github.event.inputs.apply_after_review == 'true' }} | |
| timeout-minutes: 30 | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| TARGET_REPO: ${{ needs.plan.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| test -n "$GH_TOKEN" | |
| target_slug="$TARGET_REPO" | |
| target_slug="${target_slug//\//-}" | |
| item_numbers="$(pnpm run --silent workflow -- artifact-item-numbers --artifact-dir artifacts)" | |
| if [ -z "$item_numbers" ]; then | |
| echo "No review artifacts to apply." | |
| exit 0 | |
| fi | |
| item_count="$(pnpm run --silent workflow -- count-csv --items "$item_numbers")" | |
| close_reasons="${{ github.event.inputs.apply_after_review_close_reasons || 'implemented_on_main,duplicate_or_superseded,low_signal_unmergeable_pr' }}" | |
| min_age_minutes="${{ github.event.inputs.apply_after_review_min_age_minutes || '0' }}" | |
| git pull --rebase | |
| pnpm run apply-decisions -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --skip-dashboard \ | |
| --item-numbers "$item_numbers" \ | |
| --apply-kind all \ | |
| --apply-close-reasons "$close_reasons" \ | |
| --stale-min-age-days 30 \ | |
| --limit "$item_count" \ | |
| --processed-limit "$((item_count * 10))" \ | |
| --min-age-minutes "$min_age_minutes" \ | |
| --close-delay-ms 1000 \ | |
| --comment-sync-min-age-days 0 \ | |
| --progress-every 1 | |
| closed_count="$(pnpm run --silent workflow -- count-actions --report apply-report.json --action closed)" | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "Immediate apply checked" \ | |
| --detail "Checked selected safe close proposals right after review publish. Closed: $closed_count/$item_count. Close reasons enabled: $close_reasons. Item numbers: $item_numbers." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ | |
| --planned-count "${{ needs.plan.outputs.planned_count }}" \ | |
| --planned-capacity "${{ needs.plan.outputs.planned_capacity }}" \ | |
| --planned-shards "${{ needs.plan.outputs.planned_shards }}" \ | |
| --active-codex "0" \ | |
| --due-backlog "${{ needs.plan.outputs.due_backlog }}" \ | |
| --oldest-unreviewed-at "${{ needs.plan.outputs.oldest_unreviewed_at }}" \ | |
| --capacity-reason "${{ needs.plan.outputs.capacity_reason }}" | |
| pnpm run repair:publish-main -- \ | |
| --message "chore: apply selected sweep decisions" \ | |
| --path "records/${target_slug}" \ | |
| --path apply-report.json \ | |
| --path "results/sweep-status/${target_slug}.json" \ | |
| --rebase-strategy theirs | |
| - name: Continue sweep | |
| if: ${{ success() && needs.plan.outputs.planned_count == needs.plan.outputs.planned_capacity && github.event_name != 'repository_dispatch' && (github.event_name != 'workflow_dispatch' || (github.event.inputs.item_number == '' && github.event.inputs.item_numbers == '')) }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| for attempt in 1 2 3; do | |
| if gh workflow run sweep.yml \ | |
| --ref main \ | |
| -f apply_existing=false \ | |
| -f hot_intake="${{ needs.plan.outputs.hot_intake }}" \ | |
| -f target_repo="${{ needs.plan.outputs.target_repo }}" \ | |
| -f batch_size="${{ needs.plan.outputs.batch_size }}" \ | |
| -f shard_count="${{ needs.plan.outputs.shard_count }}" \ | |
| -f codex_timeout_ms="${{ needs.plan.outputs.codex_timeout_ms }}"; then | |
| exit 0 | |
| fi | |
| sleep "$((attempt * 10))" | |
| done | |
| echo "::warning::Unable to dispatch the next sweep after three attempts; the scheduled backstop will pick it up." | |
| recover-review-failures: | |
| name: Recover failed review shards | |
| needs: [plan, review, publish] | |
| if: ${{ always() && needs.plan.result == 'success' && needs.review.result != 'skipped' && !contains(github.event.inputs.additional_prompt || '', '[clawsweeper-recovery-attempt=1]') && needs.plan.outputs.planned_item_numbers != '' && github.event_name != 'repository_dispatch' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| actions: write | |
| contents: read | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| id: failed-shards | |
| continue-on-error: true | |
| with: | |
| pattern: review-failed-shard-* | |
| path: failed-review-shards | |
| merge-multiple: true | |
| - name: Requeue planned review items once | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| ADDITIONAL_PROMPT: ${{ github.event.inputs.additional_prompt || '' }} | |
| MATRIX_JSON: ${{ needs.plan.outputs.matrix }} | |
| run: | | |
| set -euo pipefail | |
| recovery_prompt="[clawsweeper-recovery-attempt=1]" | |
| if [ -n "$ADDITIONAL_PROMPT" ]; then | |
| recovery_prompt="$(printf '%s\n\n%s' "$ADDITIONAL_PROMPT" "$recovery_prompt")" | |
| fi | |
| failed_shards="" | |
| if [ -d failed-review-shards ]; then | |
| failed_shards="$(find failed-review-shards -type f -name 'shard-*.json' -print0 \ | |
| | xargs -0 -r jq -r '.shard // empty' \ | |
| | paste -sd, -)" | |
| fi | |
| if [ -z "$failed_shards" ]; then | |
| failed_shards="$( | |
| gh run view "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" --json jobs --jq ' | |
| .jobs[] | |
| | select((.conclusion == "failure" or .conclusion == "cancelled") and (.name | startswith("Review shard "))) | |
| | .name | |
| | sub("^Review shard "; "") | |
| ' | paste -sd, - | |
| )" | |
| fi | |
| if [ -z "$failed_shards" ]; then | |
| echo "No failed review shard jobs found; nothing to requeue." | |
| exit 0 | |
| fi | |
| item_numbers="$( | |
| jq -r --arg failed_shards ",$failed_shards," ' | |
| [ | |
| .[] | |
| | . as $matrix_entry | |
| | select($failed_shards | contains("," + ($matrix_entry.shard | tostring) + ",")) | |
| | $matrix_entry.item_numbers | |
| | split(",")[] | |
| | select(test("^[0-9]+$")) | |
| ] | |
| | unique | |
| | join(",") | |
| ' <<<"$MATRIX_JSON" | |
| )" | |
| if [ -z "$item_numbers" ]; then | |
| echo "Failed shards had no item numbers to requeue: $failed_shards" | |
| exit 0 | |
| fi | |
| recovery_shard_count="$( | |
| jq -r --arg failed_shards ",$failed_shards," ' | |
| [ | |
| .[] | |
| | . as $matrix_entry | |
| | select($failed_shards | contains("," + ($matrix_entry.shard | tostring) + ",")) | |
| ] | |
| | length | |
| | if . < 1 then 1 else . end | |
| ' <<<"$MATRIX_JSON" | |
| )" | |
| args=( | |
| workflow run sweep.yml | |
| --repo "$GITHUB_REPOSITORY" | |
| --ref main | |
| -f apply_existing=false | |
| -f hot_intake=false | |
| -f target_repo="${{ needs.plan.outputs.target_repo }}" | |
| -f batch_size="${{ needs.plan.outputs.batch_size }}" | |
| -f shard_count="$recovery_shard_count" | |
| -f codex_timeout_ms="${{ needs.plan.outputs.codex_timeout_ms }}" | |
| -f item_numbers="$item_numbers" | |
| -f additional_prompt="$recovery_prompt" | |
| ) | |
| echo "Requeueing failed review shards $failed_shards with item numbers: $item_numbers" | |
| gh "${args[@]}" | |
| retry-failed-reviews: | |
| name: Retry failed Codex reviews | |
| if: ${{ github.event_name == 'schedule' && github.event.schedule == '13 8,20 * * *' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| actions: write | |
| contents: write | |
| issues: read | |
| pull-requests: read | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| filter: blob:none | |
| fetch-depth: 0 | |
| - 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: Plan or dispatch failed-review retries | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TARGET_REPO: openclaw/openclaw | |
| DRY_RUN: ${{ vars.CLAWSWEEPER_FAILED_REVIEW_RETRY_ENABLED == '1' && 'false' || 'true' }} | |
| RETRY_LIMIT: ${{ vars.CLAWSWEEPER_FAILED_REVIEW_RETRY_LIMIT || '3' }} | |
| RETRY_MAX_ATTEMPTS: ${{ vars.CLAWSWEEPER_FAILED_REVIEW_RETRY_MAX_ATTEMPTS || '2' }} | |
| RETRY_COOLDOWN_MINUTES: ${{ vars.CLAWSWEEPER_FAILED_REVIEW_RETRY_COOLDOWN_MINUTES || '45' }} | |
| CODEX_TIMEOUT_MS: ${{ vars.CLAWSWEEPER_FAILED_REVIEW_RETRY_CODEX_TIMEOUT_MS || vars.CLAWSWEEPER_CODEX_TIMEOUT_MS || '600000' }} | |
| run: | | |
| set -euo pipefail | |
| git pull --rebase | |
| dry_run_arg=() | |
| if [ "$DRY_RUN" = "true" ]; then | |
| dry_run_arg=(--dry-run) | |
| echo "::notice::Failed-review retry is running in dry-run mode. Set CLAWSWEEPER_FAILED_REVIEW_RETRY_ENABLED=1 to dispatch exact-item retries." | |
| fi | |
| pnpm run retry-failed-reviews -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --limit "$RETRY_LIMIT" \ | |
| --max-attempts "$RETRY_MAX_ATTEMPTS" \ | |
| --cooldown-minutes "$RETRY_COOLDOWN_MINUTES" \ | |
| --codex-timeout-ms "$CODEX_TIMEOUT_MS" \ | |
| --workflow-repo "$GITHUB_REPOSITORY" \ | |
| --workflow-ref main \ | |
| --report-path artifacts/failed-review-retry-report.json \ | |
| "${dry_run_arg[@]}" | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: failed-review-retry-report | |
| path: artifacts/failed-review-retry-report.json | |
| if-no-files-found: error | |
| retention-days: 14 | |
| - name: Publish failed-review retry state | |
| if: ${{ vars.CLAWSWEEPER_FAILED_REVIEW_RETRY_ENABLED == '1' }} | |
| run: | | |
| pnpm run repair:publish-main -- \ | |
| --message "chore: update failed review retry state" \ | |
| --path records/openclaw-openclaw \ | |
| --rebase-strategy theirs | |
| audit-dashboard: | |
| name: Audit state | |
| if: ${{ (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 * * *')) }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| 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 || github.event.client_payload.target_repo || '' }}" | |
| if [ -z "$target_repo" ]; then | |
| case "${{ github.event.schedule || '' }}" in | |
| "12 */6 * * *") | |
| target_repo="openclaw/clawhub" | |
| ;; | |
| "17 */6 * * *") | |
| target_repo="openclaw/clawsweeper" | |
| ;; | |
| *) | |
| target_repo="openclaw/openclaw" | |
| ;; | |
| esac | |
| fi | |
| 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 read token | |
| id: target-read-token | |
| continue-on-error: true | |
| 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: read | |
| permission-pull-requests: read | |
| - name: Select target read token | |
| id: target-token | |
| env: | |
| APP_TOKEN: ${{ steps.target-read-token.outputs.token }} | |
| FALLBACK_TOKEN: ${{ github.token }} | |
| run: | | |
| if [ -n "$APP_TOKEN" ]; then | |
| echo "Using ClawSweeper App token for audit reads." | |
| echo "token=$APP_TOKEN" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Using workflow token fallback for public audit reads." | |
| echo "token=$FALLBACK_TOKEN" >> "$GITHUB_OUTPUT" | |
| fi | |
| - 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: Refresh Audit Health | |
| env: | |
| GH_TOKEN: ${{ steps.target-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| git pull --rebase | |
| reconcile_json="$(pnpm run --silent reconcile -- --target-repo "$TARGET_REPO" --max-pages 250 --skip-closed-at)" | |
| echo "$reconcile_json" | |
| moved_to_closed="$(jq -r '.movedToClosed' <<<"$reconcile_json")" | |
| moved_to_items="$(jq -r '.movedToItems' <<<"$reconcile_json")" | |
| removed_stale_closed_copies="$(jq -r '.removedStaleClosedCopies' <<<"$reconcile_json")" | |
| pnpm run audit -- --target-repo "$TARGET_REPO" --max-pages 250 --sample-limit 25 --output /tmp/clawsweeper-audit.json --update-dashboard | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "Audit finished" \ | |
| --detail "Reconciled durable $TARGET_REPO records before audit: moved ${moved_to_closed} closed records, restored ${moved_to_items} reopened records, removed ${removed_stale_closed_copies} stale archived copies. Refreshed audit state from a full live scan." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| - name: Commit Audit Health | |
| env: | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| target_slug="$TARGET_REPO" | |
| target_slug="${target_slug//\//-}" | |
| pnpm run repair:publish-main -- \ | |
| --message "chore: update sweep audit state" \ | |
| --path README.md \ | |
| --path "records/${target_slug}" \ | |
| --path "results/audit/${target_slug}.json" \ | |
| --path "results/sweep-status/${target_slug}.json" \ | |
| --rebase-strategy theirs | |
| - name: Refresh state dashboard | |
| env: | |
| GH_TOKEN: ${{ steps.state-token.outputs.token }} | |
| run: | | |
| gh workflow run dashboard.yml \ | |
| --repo openclaw/clawsweeper-state \ | |
| --ref main || echo "Best-effort dashboard refresh dispatch failed; scheduled state dashboard will retry." | |
| 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.schedule == '6,21,36,51 * * * *'))) && !(github.event_name == 'repository_dispatch' && github.event.client_payload.target_repo == 'openclaw/clawhub' && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') && !(github.event_name == 'schedule' && github.event.schedule == '8,23,38,53 * * * *' && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 360 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| filter: blob:none | |
| fetch-depth: 0 | |
| - name: Resolve target repository | |
| id: target | |
| run: | | |
| target_repo="${{ github.event_name == 'repository_dispatch' && github.event.client_payload.target_repo || github.event.inputs.target_repo || '' }}" | |
| if [ -z "$target_repo" ]; then | |
| case "${{ github.event.schedule || '' }}" in | |
| "8,23,38,53 * * * *") | |
| target_repo="openclaw/clawhub" | |
| ;; | |
| *) | |
| target_repo="openclaw/openclaw" | |
| ;; | |
| esac | |
| fi | |
| 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 write token | |
| id: target-write-token | |
| continue-on-error: true | |
| 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: write | |
| permission-issues: write | |
| permission-pull-requests: write | |
| - 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: Sync before applying decisions | |
| run: git pull --rebase | |
| - name: Reconcile before apply preselect | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| item_numbers="${{ github.event_name == 'repository_dispatch' && github.event.client_payload.item_number || github.event.inputs.apply_item_numbers || '' }}" | |
| if [ "$item_numbers" = "__cursor__" ]; then | |
| item_numbers="" | |
| fi | |
| reconcile_args=(--target-repo "$TARGET_REPO" --skip-closed-at) | |
| if [ -n "$item_numbers" ]; then | |
| reconcile_args+=(--item-numbers "$item_numbers") | |
| fi | |
| pnpm run reconcile -- "${reconcile_args[@]}" | |
| - name: Preselect apply work that can need Codex | |
| id: apply-preselect | |
| env: | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| run: | | |
| set -euo pipefail | |
| min_age_days="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_min_age_days || '0' }}" | |
| min_age_minutes="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_min_age_minutes || '' }}" | |
| apply_kind="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_kind || 'all' }}" | |
| apply_close_reasons="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_close_reasons || 'all' }}" | |
| stale_min_age_days="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_stale_min_age_days || '60' }}" | |
| item_numbers="${{ github.event_name == 'repository_dispatch' && github.event.client_payload.item_number || github.event.inputs.apply_item_numbers || '' }}" | |
| sync_open_pr_batch="${{ github.event_name == 'schedule' && github.event.schedule == '6,21,36,51 * * * *' && 'true' || 'false' }}" | |
| if [ "$item_numbers" = "__cursor__" ]; then | |
| sync_open_pr_batch="true" | |
| item_numbers="" | |
| fi | |
| sync_comments_only="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_sync_comments_only || 'false' }}" | |
| if [ "$sync_open_pr_batch" = "true" ]; then | |
| sync_comments_only="true" | |
| apply_kind="pull_request" | |
| fi | |
| needs_codex=false | |
| if [ "$sync_comments_only" = "true" ]; then | |
| if [ "$sync_open_pr_batch" = "true" ]; then | |
| target_slug="$TARGET_REPO" | |
| target_slug="${target_slug//\//-}" | |
| cursor_path="results/comment-sync-cursors/${target_slug}.json" | |
| sync_batch_size="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_limit || '25' }}" | |
| batch_env=".artifacts/apply-preselect-comment-sync-batch.env" | |
| mkdir -p "$(dirname "$batch_env")" | |
| pnpm run --silent workflow -- comment-sync-batch \ | |
| --target-repo "$TARGET_REPO" \ | |
| --apply-kind "$apply_kind" \ | |
| --batch-size "$sync_batch_size" \ | |
| --cursor-path "$cursor_path" > "$batch_env" | |
| batch_count="$(awk -F= '$1 == "count" { print $2 }' "$batch_env")" | |
| fi | |
| else | |
| proof_args=( | |
| --target-repo "$TARGET_REPO" | |
| --apply-kind "$apply_kind" | |
| --apply-close-reasons "$apply_close_reasons" | |
| --stale-min-age-days "$stale_min_age_days" | |
| --min-age-days "$min_age_days" | |
| --min-age-minutes "$min_age_minutes" | |
| ) | |
| if [ -n "$item_numbers" ]; then | |
| proof_args+=(--item-numbers "$item_numbers") | |
| fi | |
| selected="$(pnpm run --silent workflow -- proposed-pr-close-coverage-item-numbers "${proof_args[@]}")" | |
| if [ -n "$selected" ]; then | |
| needs_codex=true | |
| fi | |
| fi | |
| echo "needs_codex=$needs_codex" >> "$GITHUB_OUTPUT" | |
| - uses: ./.github/actions/setup-codex | |
| if: ${{ steps.apply-preselect.outputs.needs_codex == 'true' }} | |
| env: | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| - name: Apply unchanged proposed decisions with checkpoints | |
| env: | |
| GH_TOKEN: ${{ steps.target-write-token.outputs.token }} | |
| TARGET_REPO: ${{ steps.target.outputs.target_repo }} | |
| CLAWSWEEPER_PUBLISH_THROTTLE_STATUS: "true" | |
| CLAWSWEEPER_RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| CLAWSWEEPER_THROTTLE_STATUS_MIN_WAIT_MS: "60000" | |
| CLAWSWEEPER_THROTTLE_STATUS_MIN_INTERVAL_MS: "120000" | |
| run: | | |
| test -n "$GH_TOKEN" | |
| limit="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_limit || '20' }}" | |
| min_age_days="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_min_age_days || '0' }}" | |
| min_age_minutes="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_min_age_minutes || '' }}" | |
| apply_kind="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_kind || 'all' }}" | |
| apply_close_reasons="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_close_reasons || 'all' }}" | |
| stale_min_age_days="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_stale_min_age_days || '60' }}" | |
| close_delay_ms="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_close_delay_ms || '2000' }}" | |
| progress_every="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_progress_every || '10' }}" | |
| checkpoint_size="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_checkpoint_size || '50' }}" | |
| sync_batch_size="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_limit || '25' }}" | |
| item_numbers="${{ github.event_name == 'repository_dispatch' && github.event.client_payload.item_number || github.event.inputs.apply_item_numbers || '' }}" | |
| sync_open_pr_batch="${{ github.event_name == 'schedule' && github.event.schedule == '6,21,36,51 * * * *' && 'true' || 'false' }}" | |
| if [ "$item_numbers" = "__cursor__" ]; then | |
| sync_open_pr_batch="true" | |
| item_numbers="" | |
| fi | |
| sync_comments_only="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_sync_comments_only || 'false' }}" | |
| comment_sync_min_age_days="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_comment_sync_min_age_days || '7' }}" | |
| target_slug="$TARGET_REPO" | |
| target_slug="${target_slug//\//-}" | |
| cursor_path="results/comment-sync-cursors/${target_slug}.json" | |
| next_cursor="" | |
| if [ "$sync_open_pr_batch" = "true" ]; then | |
| sync_comments_only="true" | |
| apply_kind="pull_request" | |
| comment_sync_min_age_days="0" | |
| fi | |
| sync_comments_arg=() | |
| if [ "$sync_comments_only" = "true" ]; then | |
| sync_comments_arg=(--sync-comments-only) | |
| fi | |
| min_age_minutes_arg=() | |
| if [ -n "$min_age_minutes" ]; then | |
| min_age_minutes_arg=(--min-age-minutes "$min_age_minutes") | |
| fi | |
| mkdir -p .artifacts/apply-reports | |
| publish_changes() { | |
| message="$1" | |
| shift | |
| publish_args=(--message "$message" --rebase-strategy apply-records) | |
| for path in "$@"; do | |
| publish_args+=(--path "$path") | |
| done | |
| pnpm run repair:publish-main -- "${publish_args[@]}" | |
| } | |
| publish_status() { | |
| message="$1" | |
| if ! publish_changes "$message" results/sweep-status; then | |
| echo "Best-effort status update failed: $message" | |
| git restore results/sweep-status || true | |
| fi | |
| } | |
| reconcile_args=(--target-repo "$TARGET_REPO" --skip-closed-at) | |
| if [ -n "$item_numbers" ]; then | |
| reconcile_args+=(--item-numbers "$item_numbers") | |
| fi | |
| pnpm run reconcile -- "${reconcile_args[@]}" | |
| if [ "$sync_open_pr_batch" = "true" ] && [ -z "$item_numbers" ]; then | |
| batch_env=".artifacts/comment-sync-batch.env" | |
| pnpm run --silent workflow -- comment-sync-batch \ | |
| --target-repo "$TARGET_REPO" \ | |
| --apply-kind "$apply_kind" \ | |
| --batch-size "$sync_batch_size" \ | |
| --cursor-path "$cursor_path" > "$batch_env" | |
| cat "$batch_env" | |
| item_numbers="$(awk -F= '$1 == "item_numbers" { print $2 }' "$batch_env")" | |
| next_cursor="$(awk -F= '$1 == "next_cursor" { print $2 }' "$batch_env")" | |
| batch_count="$(awk -F= '$1 == "count" { print $2 }' "$batch_env")" | |
| if [ "${batch_count:-0}" -eq 0 ]; then | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "Apply comments idle" \ | |
| --detail "No open PR review records were available for cursor-based comment sync. Cursor remains ${next_cursor:-0}." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| publish_status "chore: update idle sweep comment sync status" | |
| { | |
| echo "APPLY_CLOSED_TOTAL=0" | |
| echo "APPLY_LIMIT=1" | |
| echo "APPLY_MIN_AGE_DAYS=$min_age_days" | |
| echo "APPLY_MIN_AGE_MINUTES=$min_age_minutes" | |
| echo "APPLY_KIND=$apply_kind" | |
| echo "APPLY_CLOSE_REASONS=$apply_close_reasons" | |
| echo "APPLY_STALE_MIN_AGE_DAYS=$stale_min_age_days" | |
| echo "APPLY_CLOSE_DELAY_MS=$close_delay_ms" | |
| echo "APPLY_PROGRESS_EVERY=$progress_every" | |
| echo "APPLY_CHECKPOINT_SIZE=$checkpoint_size" | |
| echo "APPLY_ITEM_NUMBERS=" | |
| echo "APPLY_TARGET_REPO=$TARGET_REPO" | |
| echo "APPLY_SYNC_COMMENTS_ONLY=true" | |
| echo "APPLY_SYNC_OPEN_PR_BATCH=true" | |
| echo "APPLY_COMMENT_SYNC_MIN_AGE_DAYS=$comment_sync_min_age_days" | |
| echo "APPLY_NOOP=true" | |
| } >> "$GITHUB_ENV" | |
| exit 0 | |
| fi | |
| echo "Selected cursor-based comment sync batch: $item_numbers" | |
| fi | |
| if [ "$sync_comments_only" != "true" ] && [ -z "$item_numbers" ]; then | |
| item_numbers="$(pnpm run --silent workflow -- proposed-item-numbers \ | |
| --target-repo "$TARGET_REPO" \ | |
| --apply-kind "$apply_kind" \ | |
| --apply-close-reasons "$apply_close_reasons" \ | |
| --stale-min-age-days "$stale_min_age_days" \ | |
| --min-age-days "$min_age_days" \ | |
| --min-age-minutes "$min_age_minutes")" | |
| if [ -n "$item_numbers" ]; then | |
| proposed_count="$(pnpm run --silent workflow -- count-csv --items "$item_numbers")" | |
| if [ "$proposed_count" -lt "$limit" ]; then | |
| limit="$proposed_count" | |
| fi | |
| echo "Auto-selected $proposed_count proposed close(s): $item_numbers" | |
| fi | |
| fi | |
| item_numbers_arg=() | |
| if [ -n "$item_numbers" ]; then | |
| item_numbers_arg=(--item-numbers "$item_numbers") | |
| fi | |
| if [ "$sync_comments_only" != "true" ] && [ -z "$item_numbers" ]; then | |
| echo "No unchanged high-confidence close proposals are awaiting apply. Scheduled apply wakes every 15 minutes and exits without scanning unrelated keep-open records when there is no close work." | |
| { | |
| echo "APPLY_CLOSED_TOTAL=0" | |
| echo "APPLY_LIMIT=1" | |
| echo "APPLY_MIN_AGE_DAYS=$min_age_days" | |
| echo "APPLY_MIN_AGE_MINUTES=$min_age_minutes" | |
| echo "APPLY_KIND=$apply_kind" | |
| echo "APPLY_CLOSE_REASONS=$apply_close_reasons" | |
| echo "APPLY_STALE_MIN_AGE_DAYS=$stale_min_age_days" | |
| echo "APPLY_CLOSE_DELAY_MS=$close_delay_ms" | |
| echo "APPLY_PROGRESS_EVERY=$progress_every" | |
| echo "APPLY_CHECKPOINT_SIZE=$checkpoint_size" | |
| echo "APPLY_ITEM_NUMBERS=" | |
| echo "APPLY_SYNC_COMMENTS_ONLY=false" | |
| echo "APPLY_COMMENT_SYNC_MIN_AGE_DAYS=$comment_sync_min_age_days" | |
| echo "APPLY_NOOP=true" | |
| } >> "$GITHUB_ENV" | |
| exit 0 | |
| fi | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "Apply in progress" \ | |
| --detail "Starting apply/comment-sync run for up to $limit fresh $apply_kind closes. Close reasons: $apply_close_reasons. Existing Codex automated review comments are updated in place when closing or when comment-only sync is stale by ${comment_sync_min_age_days} day(s); checkpoints commit every $checkpoint_size fresh closes; close delay is ${close_delay_ms}ms; sync-comments-only=$sync_comments_only; item numbers=${item_numbers:-all}." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| if ! publish_changes "chore: mark sweep apply in progress" records results/sweep-status; then | |
| echo "Best-effort apply start status update failed" | |
| git restore results/sweep-status || true | |
| fi | |
| closed_total=0 | |
| checkpoint=0 | |
| if [ "$sync_comments_only" = "true" ]; then | |
| checkpoint=1 | |
| echo "::group::Apply checkpoint $checkpoint" | |
| echo "Syncing durable review comments only for item numbers: ${item_numbers:-all}" | |
| pnpm run apply-decisions -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --skip-dashboard \ | |
| --limit 0 \ | |
| --min-age-days "$min_age_days" \ | |
| "${min_age_minutes_arg[@]}" \ | |
| --apply-kind "$apply_kind" \ | |
| --apply-close-reasons "$apply_close_reasons" \ | |
| --stale-min-age-days "$stale_min_age_days" \ | |
| --close-delay-ms "$close_delay_ms" \ | |
| --progress-every "$progress_every" \ | |
| --processed-limit "$((checkpoint_size * 20))" \ | |
| --comment-sync-min-age-days "$comment_sync_min_age_days" \ | |
| "${item_numbers_arg[@]}" \ | |
| "${sync_comments_arg[@]}" | |
| cp apply-report.json ".artifacts/apply-reports/apply-report-$checkpoint.json" | |
| result_count="$(pnpm run --silent workflow -- count-report --report ".artifacts/apply-reports/apply-report-$checkpoint.json")" | |
| synced_count="$(pnpm run --silent workflow -- count-actions --report ".artifacts/apply-reports/apply-report-$checkpoint.json" --action review_comment_synced)" | |
| if [ "$sync_open_pr_batch" = "true" ]; then | |
| pnpm run workflow -- write-comment-sync-cursor \ | |
| --cursor-path "$cursor_path" \ | |
| --next-cursor "$next_cursor" \ | |
| --target-repo "$TARGET_REPO" | |
| fi | |
| publish_changes "chore: sync sweep review comments checkpoint $checkpoint" records apply-report.json results/comment-sync-cursors | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "Apply comments synced" \ | |
| --detail "Comment-only apply checkpoint $checkpoint finished. Synced durable review comments: $synced_count. Result records: $result_count. Item numbers: ${item_numbers:-all}. Next cursor: ${next_cursor:-none}." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| publish_status "chore: update sweep comment sync status" | |
| echo "::endgroup::" | |
| fi | |
| while [ "$closed_total" -lt "$limit" ]; do | |
| if [ "$sync_comments_only" = "true" ]; then | |
| break | |
| fi | |
| remaining=$((limit - closed_total)) | |
| chunk_limit="$checkpoint_size" | |
| if [ "$remaining" -lt "$chunk_limit" ]; then | |
| chunk_limit="$remaining" | |
| fi | |
| checkpoint=$((checkpoint + 1)) | |
| echo "::group::Apply checkpoint $checkpoint" | |
| echo "Applying up to $chunk_limit fresh closes; $closed_total/$limit already closed in this run" | |
| CLAWSWEEPER_APPLY_CHECKPOINT="$checkpoint" pnpm run apply-decisions -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --skip-dashboard \ | |
| --limit "$chunk_limit" \ | |
| --min-age-days "$min_age_days" \ | |
| "${min_age_minutes_arg[@]}" \ | |
| --apply-kind "$apply_kind" \ | |
| --apply-close-reasons "$apply_close_reasons" \ | |
| --stale-min-age-days "$stale_min_age_days" \ | |
| --close-delay-ms "$close_delay_ms" \ | |
| --progress-every "$progress_every" \ | |
| --processed-limit "$((chunk_limit * 20))" \ | |
| --comment-sync-min-age-days "$comment_sync_min_age_days" \ | |
| "${item_numbers_arg[@]}" \ | |
| "${sync_comments_arg[@]}" | |
| cp apply-report.json ".artifacts/apply-reports/apply-report-$checkpoint.json" | |
| pnpm run workflow -- merge-apply-reports --dir .artifacts/apply-reports --output apply-report.json | |
| closed_in_chunk="$(pnpm run --silent workflow -- count-actions --report ".artifacts/apply-reports/apply-report-$checkpoint.json" --action closed)" | |
| result_count="$(pnpm run --silent workflow -- count-report --report ".artifacts/apply-reports/apply-report-$checkpoint.json")" | |
| closed_total=$((closed_total + closed_in_chunk)) | |
| echo "Checkpoint $checkpoint result_count=$result_count closed_in_chunk=$closed_in_chunk closed_total=$closed_total/$limit" | |
| publish_changes "chore: apply sweep decisions checkpoint $checkpoint" records apply-report.json | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "Apply in progress" \ | |
| --detail "Checkpoint $checkpoint finished. Fresh closes in checkpoint: $closed_in_chunk. Total fresh closes in this run: $closed_total/$limit. Result records in checkpoint: $result_count, including durable review comment syncs." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| publish_status "chore: update sweep apply checkpoint $checkpoint status" | |
| echo "::endgroup::" | |
| if [ "$result_count" -eq 0 ]; then | |
| echo "No more applicable proposed closes found." | |
| break | |
| fi | |
| if [ "$closed_in_chunk" -eq 0 ]; then | |
| echo "Checkpoint made no fresh closes; stopping to avoid a retry loop." | |
| break | |
| fi | |
| done | |
| pnpm run status -- \ | |
| --target-repo "$TARGET_REPO" \ | |
| --state "Apply finished" \ | |
| --detail "Apply/comment-sync run finished with $closed_total fresh closes out of requested limit $limit. See apply-report.json for per-item results." \ | |
| --run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| publish_status "chore: mark sweep apply finished" | |
| { | |
| echo "APPLY_CLOSED_TOTAL=$closed_total" | |
| echo "APPLY_LIMIT=$limit" | |
| echo "APPLY_MIN_AGE_DAYS=$min_age_days" | |
| echo "APPLY_MIN_AGE_MINUTES=$min_age_minutes" | |
| echo "APPLY_KIND=$apply_kind" | |
| echo "APPLY_CLOSE_REASONS=$apply_close_reasons" | |
| echo "APPLY_STALE_MIN_AGE_DAYS=$stale_min_age_days" | |
| echo "APPLY_CLOSE_DELAY_MS=$close_delay_ms" | |
| echo "APPLY_PROGRESS_EVERY=$progress_every" | |
| echo "APPLY_CHECKPOINT_SIZE=$checkpoint_size" | |
| echo "APPLY_ITEM_NUMBERS=$item_numbers" | |
| echo "APPLY_TARGET_REPO=$TARGET_REPO" | |
| echo "APPLY_SYNC_COMMENTS_ONLY=$sync_comments_only" | |
| echo "APPLY_SYNC_OPEN_PR_BATCH=$sync_open_pr_batch" | |
| echo "APPLY_COMMENT_SYNC_MIN_AGE_DAYS=$comment_sync_min_age_days" | |
| } >> "$GITHUB_ENV" | |
| - name: Commit apply results | |
| run: | | |
| if [ "${APPLY_NOOP:-false}" = "true" ]; then | |
| echo "No proposed closes were ready; no apply results to commit." | |
| exit 0 | |
| fi | |
| target_slug="$APPLY_TARGET_REPO" | |
| target_slug="${target_slug//\//-}" | |
| publish_args=( | |
| --message "chore: apply sweep decisions" | |
| --path "records/${target_slug}" | |
| --path apply-report.json | |
| --path "results/sweep-status/${target_slug}.json" | |
| --rebase-strategy apply-records | |
| ) | |
| if [ "${APPLY_SYNC_OPEN_PR_BATCH:-false}" = "true" ]; then | |
| publish_args+=(--path results/comment-sync-cursors) | |
| fi | |
| pnpm run repair:publish-main -- "${publish_args[@]}" | |
| - name: Continue apply sweep | |
| if: ${{ success() }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| if [ "${APPLY_NOOP:-false}" = "true" ]; then | |
| echo "No proposed closes were ready; not queueing another apply run." | |
| exit 0 | |
| fi | |
| if [ "${APPLY_CLOSED_TOTAL:-0}" -lt "${APPLY_LIMIT:-0}" ]; then | |
| echo "Apply closed ${APPLY_CLOSED_TOTAL:-0}/${APPLY_LIMIT:-0}; not queueing another apply run." | |
| exit 0 | |
| fi | |
| if [ "${APPLY_SYNC_COMMENTS_ONLY:-false}" = "true" ]; then | |
| echo "Comment-only apply run finished; not queueing another apply run." | |
| exit 0 | |
| fi | |
| run_workflow_with_retry() { | |
| local label="$1" | |
| shift | |
| for attempt in 1 2 3; do | |
| if gh workflow run sweep.yml "$@"; then | |
| return 0 | |
| fi | |
| echo "::warning::Failed to queue ${label} on attempt ${attempt}; retrying." | |
| sleep "$((attempt * 10))" | |
| done | |
| echo "::warning::Unable to queue ${label} after three attempts; scheduled runs will backstop it." | |
| return 0 | |
| } | |
| echo "Apply reached requested limit; queueing another apply run." | |
| run_workflow_with_retry "apply continuation" \ | |
| --ref main \ | |
| -f apply_existing=true \ | |
| -f target_repo="$APPLY_TARGET_REPO" \ | |
| -f apply_limit="$APPLY_LIMIT" \ | |
| -f apply_min_age_days="$APPLY_MIN_AGE_DAYS" \ | |
| -f apply_min_age_minutes="$APPLY_MIN_AGE_MINUTES" \ | |
| -f apply_kind="$APPLY_KIND" \ | |
| -f apply_close_reasons="$APPLY_CLOSE_REASONS" \ | |
| -f apply_stale_min_age_days="$APPLY_STALE_MIN_AGE_DAYS" \ | |
| -f apply_item_numbers="$APPLY_ITEM_NUMBERS" \ | |
| -f apply_sync_comments_only="$APPLY_SYNC_COMMENTS_ONLY" \ | |
| -f apply_comment_sync_min_age_days="$APPLY_COMMENT_SYNC_MIN_AGE_DAYS" \ | |
| -f apply_close_delay_ms="$APPLY_CLOSE_DELAY_MS" \ | |
| -f apply_progress_every="$APPLY_PROGRESS_EVERY" \ | |
| -f apply_checkpoint_size="$APPLY_CHECKPOINT_SIZE" | |
| - name: Queue review backstops | |
| if: ${{ success() && steps.target.outputs.target_repo == 'openclaw/openclaw' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| hot_intake_shards="$(pnpm run --silent workflow -- limit review_shards.hot_intake_default)" | |
| normal_shards="$(pnpm run --silent workflow -- limit review_shards.normal_default)" | |
| runs_json="$(gh run list --repo "${{ github.repository }}" --limit 100 --json workflowName,displayTitle,status,createdAt,updatedAt)" | |
| eval "$( | |
| RUNS_JSON="$runs_json" node <<'NODE' | |
| const runs = JSON.parse(process.env.RUNS_JSON || "[]"); | |
| const now = Date.now(); | |
| const staleQueuedMs = 6 * 60 * 60 * 1000; | |
| const active = new Set(["in_progress", "pending", "queued", "waiting", "requested"]); | |
| const queued = new Set(["pending", "queued", "waiting", "requested"]); | |
| function activeRun(run) { | |
| if (!active.has(String(run.status))) return false; | |
| if (!queued.has(String(run.status))) return true; | |
| const lastChangedAt = Date.parse(String(run.updatedAt || run.createdAt || "")); | |
| return !Number.isFinite(lastChangedAt) || now - lastChangedAt <= staleQueuedMs; | |
| } | |
| function recent(title, windowMs) { | |
| return runs.some((run) => { | |
| if (run.workflowName !== "ClawSweeper") return false; | |
| if (run.displayTitle !== title) return false; | |
| if (activeRun(run)) return true; | |
| const createdAt = Date.parse(String(run.createdAt || "")); | |
| return Number.isFinite(createdAt) && now - createdAt < windowMs; | |
| }); | |
| } | |
| console.log(`HOT_RECENT=${recent("Review hot ClawSweeper items", 10 * 60 * 1000) ? "true" : "false"}`); | |
| console.log(`REVIEW_RECENT=${recent("Review ClawSweeper items", 70 * 60 * 1000) ? "true" : "false"}`); | |
| NODE | |
| )" | |
| run_workflow_with_retry() { | |
| local label="$1" | |
| shift | |
| for attempt in 1 2 3; do | |
| if gh workflow run sweep.yml "$@"; then | |
| return 0 | |
| fi | |
| echo "::warning::Failed to queue ${label} on attempt ${attempt}; retrying." | |
| sleep "$((attempt * 10))" | |
| done | |
| echo "::warning::Unable to queue ${label} after three attempts; scheduled runs will backstop it." | |
| return 0 | |
| } | |
| if [ "$HOT_RECENT" != "true" ]; then | |
| echo "No recent hot-intake run found; queueing backstop." | |
| run_workflow_with_retry "hot review backstop" \ | |
| --ref main \ | |
| -f apply_existing=false \ | |
| -f hot_intake=true \ | |
| -f target_repo=openclaw/openclaw \ | |
| -f batch_size=1 \ | |
| -f shard_count="$hot_intake_shards" \ | |
| -f codex_timeout_ms=600000 | |
| fi | |
| if [ "$REVIEW_RECENT" != "true" ]; then | |
| echo "No recent normal review run found; queueing backstop." | |
| run_workflow_with_retry "normal review backstop" \ | |
| --ref main \ | |
| -f apply_existing=false \ | |
| -f hot_intake=false \ | |
| -f target_repo=openclaw/openclaw \ | |
| -f batch_size=3 \ | |
| -f shard_count="$normal_shards" \ | |
| -f codex_timeout_ms=600000 | |
| fi |