Skip to content

Review event item steipete/summarize#239 #231031

Review event item steipete/summarize#239

Review event item steipete/summarize#239 #231031

Workflow file for this run

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