Skip to content

Review exact item openclaw/openclaw#22438 rev 4 head na #975991

Review exact item openclaw/openclaw#22438 rev 4 head na

Review exact item openclaw/openclaw#22438 rev 4 head na #975991

Workflow file for this run

name: ClawSweeper
run-name: >-
${{
(github.event_name == 'schedule' &&
(github.event.schedule == '4/20 * * * *' ||
github.event.schedule == '41/10 * * * *' ||
github.event.schedule == '37 */6 * * *')) &&
'Fan out ClawSweeper targets' ||
(github.event_name == 'schedule' && github.event.schedule == '13 * * * *') &&
'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' && github.event.client_payload.queue_lease_id != '') &&
github.event.client_payload.source_action == 'scheduled_hot_intake' &&
format('Review scheduled hot item {0} rev {1}', github.event.client_payload.queue_claim.item_key || github.event.client_payload.item_key || '?', github.event.client_payload.queue_claim.lease_revision || github.event.client_payload.lease_revision || '?') ||
(github.event_name == 'repository_dispatch' && github.event.client_payload.queue_lease_id != '') &&
github.event.client_payload.source_action == 'scheduled_normal_backfill' &&
format('Review scheduled normal item {0} rev {1}', github.event.client_payload.queue_claim.item_key || github.event.client_payload.item_key || '?', github.event.client_payload.queue_claim.lease_revision || github.event.client_payload.lease_revision || '?') ||
(github.event_name == 'repository_dispatch' && github.event.client_payload.queue_lease_id != '') &&
format('Review exact item {0} rev {1} head {2}', github.event.client_payload.queue_claim.item_key || github.event.client_payload.item_key || '?', github.event.client_payload.queue_claim.lease_revision || github.event.client_payload.lease_revision || '?', github.event.client_payload.queue_claim.source_head_sha || github.event.client_payload.source_head_sha || 'na') ||
(github.event_name == 'repository_dispatch' && github.event.client_payload.dispatch_key != '') &&
format('Review event item {0}#{1} [{2}]', github.event.client_payload.target_repo || 'openclaw/openclaw', github.event.client_payload.item_number || '?', github.event.client_payload.dispatch_key) ||
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 * * * *')) &&
format('Sync Codex review comments for {0}', github.event.inputs.target_repo || 'openclaw/openclaw') ||
(github.event_name == 'workflow_dispatch' &&
github.event.inputs.apply_existing == 'true' &&
github.event.inputs.apply_sync_comments_only != 'true' &&
(github.event.inputs.apply_item_numbers != '' ||
github.event.inputs.apply_limit != '40' ||
github.event.inputs.apply_min_age_days != '0' ||
github.event.inputs.apply_min_age_minutes != '' ||
github.event.inputs.apply_kind != 'all' ||
(github.event.inputs.apply_close_reasons != '' && github.event.inputs.apply_close_reasons != 'all') ||
github.event.inputs.apply_stale_min_age_days != '60' ||
github.event.inputs.apply_close_delay_ms != '2000' ||
github.event.inputs.apply_checkpoint_size != '40' ||
github.event.inputs.apply_comment_sync_min_age_days != '7')) &&
format('Apply custom ClawSweeper closures for {0}', github.event.inputs.target_repo || 'openclaw/openclaw') ||
((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 * * * *'))) &&
format('Apply default ClawSweeper closures for {0}', github.event.inputs.target_repo || ((github.event.schedule == '8,23,38,53 * * * *') && 'openclaw/clawhub' || 'openclaw/openclaw')) ||
(github.event_name == 'workflow_dispatch' && startsWith(github.event.inputs.item_numbers, 'router-')) &&
format('Review event item {0}#{1} [{2}]', github.event.inputs.target_repo || 'openclaw/openclaw', github.event.inputs.item_number || '?', github.event.inputs.item_numbers) ||
(github.event_name == 'workflow_dispatch' &&
github.event.inputs.item_number != '' &&
github.event.inputs.item_numbers != '') &&
format('Review event items {0}#{1},{2} [shards={3}]', github.event.inputs.target_repo || 'openclaw/openclaw', github.event.inputs.item_number, github.event.inputs.item_numbers, (github.event.inputs.hot_intake == 'true' && '1' || github.event.inputs.shard_count || '89')) ||
(github.event_name == 'workflow_dispatch' &&
github.event.inputs.item_number != '' &&
github.event.inputs.item_numbers == '') &&
format('Review event item {0}#{1}', github.event.inputs.target_repo || 'openclaw/openclaw', github.event.inputs.item_number) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.item_numbers != '') &&
format('Review event items {0}#{1} [shards={2}]', github.event.inputs.target_repo || 'openclaw/openclaw', github.event.inputs.item_numbers, (github.event.inputs.hot_intake == 'true' && '1' || github.event.inputs.shard_count || '89')) ||
((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"
target_branch:
description: "Target repository branch to review"
required: false
default: ""
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: "40"
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: "Optional apply-existing close-reason override; defaults to the shared automatic-close policy"
required: false
default: ""
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_checkpoint_size:
description: "Fresh closes per checkpoint commit in apply-existing mode"
required: false
default: "40"
batch_size:
description: "Items per worker job"
required: false
default: "3"
codex_timeout_ms:
description: "Per-item Codex timeout in milliseconds"
required: false
default: "1200000"
shard_count:
description: "Parallel shards (capped by config/automation-limits.json)"
required: false
default: "89"
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: "Optional immediate-close reason override; defaults to the shared automatic-close policy"
required: false
default: ""
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 * * * *"
# Temporary containment: run hot fleet fanout every 20 minutes while trusted
# ClawSweeper-owned activity can feed unchanged items back into hot review.
# Restore a faster cadence only after that loop is fixed and quota telemetry
# confirms the higher demand is safe.
- cron: "4/20 * * * *"
# Normal backfill every 10 minutes instead of hourly, for the same reason.
- cron: "41/10 * * * *"
- cron: "37 */6 * * *"
- cron: "13 * * * *"
permissions:
contents: write
actions: write
issues: write
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
CLAWSWEEPER_AUTO_CLOSE_REASONS: ${{ vars.CLAWSWEEPER_AUTO_CLOSE_REASONS || 'all' }}
CLAWSWEEPER_UNCONFIRMED_PRODUCT_DIRECTION_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_UNCONFIRMED_PRODUCT_DIRECTION_CLOSE_ENABLED || 'false' }}
CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED || 'false' }}
CLAWSWEEPER_STALE_VERSION_BUG_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_STALE_VERSION_BUG_CLOSE_ENABLED || 'false' }}
CLAWSWEEPER_OBSOLETE_FIX_PR_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_OBSOLETE_FIX_PR_CLOSE_ENABLED || 'false' }}
CLAWSWEEPER_AUTHOR_PR_BUDGET: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET || '15' }}
CLAWSWEEPER_AUTHOR_PR_BUDGET_MAX_CLOSES_PER_RUN: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET_MAX_CLOSES_PER_RUN || '5' }}
CLAWSWEEPER_IDEA_REVIVAL_REACTIONS: ${{ vars.CLAWSWEEPER_IDEA_REVIVAL_REACTIONS || '5' }}
CLAWSWEEPER_APP_CLIENT_ID: Iv23liOECG0slfuhz093
CLAWSWEEPER_MODEL: internal
CLAWSWEEPER_RUNNER: ${{ vars.CLAWSWEEPER_RUNNER || 'codex' }}
CLAWSWEEPER_OPENCLAW_MODEL: ${{ secrets.CLAWSWEEPER_OPENCLAW_MODEL }}
CLAWSWEEPER_OPENCLAW_PROVIDERS_JSON: ${{ secrets.CLAWSWEEPER_OPENCLAW_PROVIDERS_JSON }}
CLAWSWEEPER_OPENCLAW_OPENAI_KEY: ${{ vars.CLAWSWEEPER_RUNNER == 'openclaw' && secrets.OPENAI_API_KEY || '' }}
ANTHROPIC_API_KEY: ${{ vars.CLAWSWEEPER_RUNNER == 'openclaw' && secrets.ANTHROPIC_API_KEY || '' }}
GEMINI_API_KEY: ${{ vars.CLAWSWEEPER_RUNNER == 'openclaw' && secrets.GEMINI_API_KEY || '' }}
KIMI_API_KEY: ${{ vars.CLAWSWEEPER_RUNNER == 'openclaw' && secrets.KIMI_API_KEY || '' }}
OPENROUTER_API_KEY: ${{ vars.CLAWSWEEPER_RUNNER == 'openclaw' && secrets.OPENROUTER_API_KEY || '' }}
concurrency:
group: ${{ (github.event_name == 'schedule' && (github.event.schedule == '4/20 * * * *' || github.event.schedule == '41/10 * * * *' || 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.queue_lease_id || github.event.client_payload.item_number || github.run_id) || format('{0}-{1}', (github.event_name == 'schedule' && github.event.schedule == '13 * * * *') && '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.inputs.apply_item_numbers == '__cursor__' && github.event.inputs.apply_kind == 'all' && github.event.inputs.apply_comment_sync_min_age_days == '0' && github.event.inputs.apply_limit == '40' && github.event.inputs.apply_min_age_days == '0' && github.event.inputs.apply_min_age_minutes == '' && (github.event.inputs.apply_close_reasons == '' || github.event.inputs.apply_close_reasons == (vars.CLAWSWEEPER_AUTO_CLOSE_REASONS || 'all')) && github.event.inputs.apply_stale_min_age_days == '60' && github.event.inputs.apply_close_delay_ms == '2000' && github.event.inputs.apply_checkpoint_size == '40' && 'clawsweeper-comment-sync' || format('clawsweeper-comment-sync-{0}', github.run_id)) || (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') && format('clawsweeper-apply-{0}', github.run_id) || (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.apply_existing != 'true') && format('clawsweeper-operator-dispatch-{0}', github.run_id) || ((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: false
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."
legacy-event-queue-intake:
name: Queue legacy exact-review event
if: ${{ github.event_name == 'repository_dispatch' && github.event.action != 'clawsweeper_target_sweep' && github.event.client_payload.queue_lease_id == '' && !(github.event.client_payload.target_repo == 'openclaw/clawhub' && vars.CLAWSWEEPER_ENABLE_CLAWHUB != '1') }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Enqueue legacy event through the durable control plane
env:
CLIENT_PAYLOAD: ${{ toJson(github.event.client_payload) }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
run: |
set -euo pipefail
test -n "$CLAWSWEEPER_WEBHOOK_SECRET"
queue_url="${QUEUE_URL%/}"
mapfile -d '' -t legacy_intake_fields < <(node <<'NODE'
const payload = JSON.parse(process.env.CLIENT_PAYLOAD || "{}");
const itemKind = payload.item_kind === "pull_request" ? "pull_request" : "issue";
const queueClaim = payload.queue_claim && typeof payload.queue_claim === "object"
? payload.queue_claim
: {};
const installationId = Number(queueClaim.installation_id ?? payload.installation_id);
const sourceHeadSha = String(
queueClaim.source_head_sha ?? payload.source_head_sha ?? "",
)
.trim()
.toLowerCase();
const sourceBaseSha = String(
queueClaim.source_base_sha ?? payload.source_base_sha ?? "",
)
.trim()
.toLowerCase();
const sourceIsDraft = queueClaim.source_is_draft ?? payload.source_is_draft;
const sourceContentRevision = String(
queueClaim.source_content_revision ?? payload.source_content_revision ?? "",
)
.trim()
.toLowerCase();
const ingressFingerprint = String(payload.ingress_fingerprint || "").trim().toLowerCase();
const targetDispatcherIngress =
payload.ingress_route === "target_dispatcher" &&
itemKind === "pull_request" &&
(payload.source_event === "pull_request" || payload.source_event === "pull_request_target") &&
/^[0-9a-f]{64}$/.test(ingressFingerprint);
process.stdout.write(
`${String(payload.target_repo || "openclaw/openclaw").trim()}\0${String(payload.target_branch || "").trim()}\0${itemKind === "pull_request" && payload.source_event === "pull_request" && payload.source_action === "edited" && Number.isInteger(installationId) && installationId > 0 && /^[0-9a-f]{40}$/.test(sourceHeadSha) && /^[0-9a-f]{40}$/.test(sourceBaseSha) && typeof sourceIsDraft === "boolean" && /^[0-9a-f]{64}$/.test(sourceContentRevision) && !targetDispatcherIngress ? "1" : "0"}\0`,
);
NODE
)
if [ "${#legacy_intake_fields[@]}" -ne 3 ]; then
echo "Invalid legacy intake field count." >&2
exit 1
fi
target_repo="${legacy_intake_fields[0]}"
target_branch="${legacy_intake_fields[1]}"
use_source_authority="${legacy_intake_fields[2]}"
if ! printf '%s' "$target_repo" | grep -Eq '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'; then
echo "Invalid legacy target repository: $target_repo" >&2
exit 1
fi
if [ -n "$target_branch" ] && ! printf '%s' "$target_branch" | grep -Eq '^[A-Za-z0-9_.\/-]+$'; then
echo "Invalid legacy target branch for $target_repo: $target_branch" >&2
exit 1
fi
queue_path="/internal/exact-review/enqueue"
if [ -z "$target_branch" ]; then
queue_path="/internal/exact-review/branch-authority"
elif [ "$use_source_authority" = "1" ]; then
queue_path="/internal/exact-review/source-authority"
fi
payload="$(TARGET_REPO="$target_repo" TARGET_BRANCH="$target_branch" USE_SOURCE_AUTHORITY="$use_source_authority" node <<'NODE'
const payload = JSON.parse(process.env.CLIENT_PAYLOAD || "{}");
const itemKind = payload.item_kind === "pull_request" ? "pull_request" : "issue";
const sourceEvent =
payload.source_event === "pull_request" || payload.source_event === "pull_request_target"
? "pull_request"
: "issues";
const queueClaim = payload.queue_claim && typeof payload.queue_claim === "object"
? payload.queue_claim
: {};
const reviewOptions = payload.review_options && typeof payload.review_options === "object"
? payload.review_options
: {};
const dispatchKey = String(payload.dispatch_key || "").trim();
const ingressFingerprint = String(payload.ingress_fingerprint || "").trim().toLowerCase();
const ingress =
payload.ingress_route === "target_dispatcher" &&
itemKind === "pull_request" &&
sourceEvent === "pull_request" &&
/^[0-9a-f]{64}$/.test(ingressFingerprint)
? { route: "target_dispatcher", fingerprint: ingressFingerprint }
: undefined;
const sourceHeadSha = String(queueClaim.source_head_sha ?? payload.source_head_sha ?? "").trim().toLowerCase();
const sourceBaseSha = String(queueClaim.source_base_sha ?? payload.source_base_sha ?? "").trim().toLowerCase();
const sourceIsDraft = queueClaim.source_is_draft ?? payload.source_is_draft;
const sourceContentRevision = String(queueClaim.source_content_revision ?? payload.source_content_revision ?? "").trim().toLowerCase();
const sourceUpdatedAt = String(queueClaim.source_updated_at ?? payload.source_updated_at ?? "").trim();
const installationId = Number(queueClaim.installation_id ?? payload.installation_id);
const targetBranch = String(process.env.TARGET_BRANCH || "").trim();
const sourceAuthorityRequired = process.env.USE_SOURCE_AUTHORITY === "1";
process.stdout.write(
JSON.stringify({
delivery_id: dispatchKey
? `router:${dispatchKey}`
: `legacy:${process.env.GITHUB_RUN_ID}:${process.env.GITHUB_RUN_ATTEMPT}`,
...(Number.isInteger(installationId) && installationId > 0
? { installation_id: installationId }
: {}),
...(!targetBranch && sourceAuthorityRequired
? { source_authority_required: true }
: {}),
decision: {
targetRepo: process.env.TARGET_REPO,
...(targetBranch ? { targetBranch } : {}),
itemNumber: Number(payload.item_number),
itemKind,
sourceEvent,
sourceAction: payload.source_action || "legacy_dispatch",
supersedesInProgress: payload.supersedes_in_progress === true,
...(typeof payload.source_delivery_id === "string" && payload.source_delivery_id
? { sourceDeliveryId: payload.source_delivery_id }
: {}),
...(/^[0-9a-f]{40}$/.test(sourceHeadSha)
? { sourceHeadSha }
: {}),
...(/^[0-9a-f]{40}$/.test(sourceBaseSha)
? { sourceBaseSha }
: {}),
...(typeof sourceIsDraft === "boolean" ? { sourceIsDraft } : {}),
...(/^[0-9a-f]{64}$/.test(sourceContentRevision)
? { sourceContentRevision }
: {}),
...(sourceUpdatedAt && Number.isFinite(Date.parse(sourceUpdatedAt))
? { sourceUpdatedAt }
: {}),
...(Number.isFinite(
Number(queueClaim.codex_timeout_ms ?? reviewOptions.codex_timeout_ms ?? payload.codex_timeout_ms),
)
? {
codexTimeoutMs: Number(
queueClaim.codex_timeout_ms ?? reviewOptions.codex_timeout_ms ?? payload.codex_timeout_ms,
),
}
: {}),
...(Number.isFinite(
Number(
queueClaim.media_proof_timeout_ms ??
reviewOptions.media_proof_timeout_ms ??
payload.media_proof_timeout_ms,
),
)
? {
mediaProofTimeoutMs: Number(
queueClaim.media_proof_timeout_ms ??
reviewOptions.media_proof_timeout_ms ??
payload.media_proof_timeout_ms,
),
}
: {}),
...(Object.hasOwn(payload, "command_status_marker")
? { commandStatusMarker: payload.command_status_marker }
: {}),
...(Object.hasOwn(payload, "status_comment_id")
? { statusCommentId: payload.status_comment_id }
: {}),
...(Object.hasOwn(payload, "additional_prompt")
? { additionalPrompt: payload.additional_prompt }
: {}),
},
...(ingress ? { ingress } : {}),
}),
);
NODE
)"
signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')"
curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST \
--header "content-type: application/json" \
--header "x-clawsweeper-exact-review-signature: $signature" \
--data "$payload" \
"$queue_url$queue_path" >/dev/null
event-review-apply:
name: Review exact event item
if: ${{ github.event_name == 'repository_dispatch' && github.event.action != 'clawsweeper_target_sweep' && github.event.client_payload.queue_lease_id != '' && github.event.client_payload.source_action != 'exact_review_artifact_publish' && github.event.client_payload.source_action != 'exact_review_command_acknowledgement' }}
runs-on: ${{ vars.CLAWSWEEPER_REVIEW_RUNNER || 'ubuntu-latest' }}
timeout-minutes: 120
concurrency:
group: clawsweeper-event-review-${{ github.event.client_payload.queue_claim.item_key || github.event.client_payload.item_key || github.run_id }}
cancel-in-progress: false
permissions:
actions: write
checks: read
contents: read
issues: read
pull-requests: read
statuses: read
steps:
- name: Claim exact-review queue lease
id: claim-exact-review-queue
env:
DISPATCH_PAYLOAD: ${{ toJSON(github.event.client_payload) }}
ITEM_KEY: ${{ github.event.client_payload.queue_claim.item_key || github.event.client_payload.item_key }}
QUEUE_LEASE_ID: ${{ github.event.client_payload.queue_lease_id }}
QUEUE_LEASE_REVISION: ${{ github.event.client_payload.queue_claim.lease_revision || github.event.client_payload.lease_revision }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
set -euo pipefail
test -n "$QUEUE_LEASE_ID"
printf 'claimed=false\ndecision={}\n' >> "$GITHUB_OUTPUT"
queue_url="${QUEUE_URL%/}"
payload="$(node -e '
const runAttempt = Number(process.env.RUN_ATTEMPT);
if (!Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1);
const itemKey = String(process.env.ITEM_KEY || "").trim();
const rawLeaseRevision = String(process.env.QUEUE_LEASE_REVISION || "").trim();
const hasTuple = Boolean(itemKey || rawLeaseRevision);
const leaseRevision = Number(rawLeaseRevision);
if (hasTuple && (!itemKey || !Number.isInteger(leaseRevision) || leaseRevision < 1)) {
process.exit(1);
}
process.stdout.write(JSON.stringify({
lease_id: process.env.QUEUE_LEASE_ID,
...(hasTuple ? { item_key: itemKey, lease_revision: leaseRevision } : {}),
run_id: process.env.GITHUB_RUN_ID,
run_attempt: runAttempt,
}));
')"
response_file="$(mktemp)"
error_file="$(mktemp)"
trap 'rm -f "$response_file" "$error_file"' EXIT
for attempt in 1 2 3; do
: > "$response_file"
: > "$error_file"
if status="$(curl --silent --show-error --connect-timeout 5 --max-time 20 \
--output "$response_file" \
--write-out '%{http_code}' \
--request POST \
--header "content-type: application/json" \
--data "$payload" \
"$queue_url/internal/exact-review/claim" 2>"$error_file")"; then
response="$(<"$response_file")"
if [ "$status" = "409" ]; then
conflict_reason="$(RESPONSE="$response" node <<'NODE'
const response = JSON.parse(process.env.RESPONSE || "{}");
const safeConflicts = new Set([
"lease_not_active",
"lease_already_claimed",
"lease_decision_unavailable",
"stale_run_attempt",
]);
if (!safeConflicts.has(response.error)) process.exit(1);
process.stdout.write(response.error);
NODE
)" || {
echo "Unexpected exact-review lease conflict: $response" >&2
exit 1
}
echo "::notice::Skipping exact review because lease claim lost safely: $conflict_reason"
exit 0
fi
if [ "$status" != "200" ]; then
if [[ "$status" != 5* ]]; then
echo "Exact-review lease claim returned HTTP $status: $response" >&2
exit 1
fi
elif RESPONSE="$response" node <<'NODE'
const fs = require("node:fs");
const response = JSON.parse(process.env.RESPONSE || "{}");
const dispatch = JSON.parse(process.env.DISPATCH_PAYLOAD || "{}");
const requestedItemKey = String(process.env.ITEM_KEY || "").trim();
const requestedLeaseRevision = Number(process.env.QUEUE_LEASE_REVISION);
const responseProtocol = Number(response.protocol_version || 1);
if (responseProtocol !== 1 && responseProtocol !== 2) process.exit(1);
if (response.claimed !== true) process.exit(1);
const reviewOptions =
dispatch.review_options && typeof dispatch.review_options === "object"
? dispatch.review_options
: {};
const legacyDecision = {
targetRepo: String(dispatch.target_repo || ""),
targetBranch: String(dispatch.target_branch || "main"),
itemNumber: Number(dispatch.item_number),
itemKind: String(dispatch.item_kind || ""),
sourceEvent: String(dispatch.source_event || ""),
sourceAction: String(dispatch.source_action || "legacy_dispatch"),
supersedesInProgress: dispatch.supersedes_in_progress === true,
...(/^[0-9a-f]{40}$/.test(String(dispatch.queue_claim?.source_head_sha || dispatch.source_head_sha || "").trim().toLowerCase())
? { sourceHeadSha: String(dispatch.queue_claim?.source_head_sha || dispatch.source_head_sha).trim().toLowerCase() }
: {}),
...(Number.isFinite(Number(reviewOptions.codex_timeout_ms))
? { codexTimeoutMs: Number(reviewOptions.codex_timeout_ms) }
: {}),
...(Number.isFinite(Number(reviewOptions.media_proof_timeout_ms))
? { mediaProofTimeoutMs: Number(reviewOptions.media_proof_timeout_ms) }
: {}),
...(Object.hasOwn(reviewOptions, "command_status_marker")
? { commandStatusMarker: reviewOptions.command_status_marker }
: {}),
...(Object.hasOwn(reviewOptions, "status_comment_id")
? { statusCommentId: reviewOptions.status_comment_id }
: {}),
...(Object.hasOwn(reviewOptions, "additional_prompt")
? { additionalPrompt: reviewOptions.additional_prompt }
: {}),
};
const decision =
response.decision && typeof response.decision === "object"
? response.decision
: legacyDecision;
const targetRepo = String(decision?.targetRepo || "");
const itemNumber = Number(decision?.itemNumber);
const itemKey = `${targetRepo}#${itemNumber}`;
const leaseRevision =
responseProtocol === 2
? Number(response.lease_revision)
: Number(
response.revision ||
response.lease_revision ||
process.env.QUEUE_LEASE_REVISION,
);
const claimGeneration = Number(response.claim_generation);
const repeatRevision = response.repeat_revision;
if (
!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(targetRepo) ||
!Number.isInteger(itemNumber) ||
itemNumber < 1 ||
(decision.itemKind !== "issue" && decision.itemKind !== "pull_request") ||
(decision.sourceEvent !== "issues" && decision.sourceEvent !== "pull_request") ||
typeof decision.sourceAction !== "string" ||
!decision.sourceAction
) {
process.exit(1);
}
if (response.item_key && response.item_key !== itemKey) process.exit(1);
if (requestedItemKey && requestedItemKey !== itemKey) process.exit(1);
if (
responseProtocol === 2 &&
(!response.decision ||
typeof response.decision !== "object" ||
response.item_key !== requestedItemKey ||
response.lease_revision !== requestedLeaseRevision ||
!Number.isInteger(claimGeneration) ||
claimGeneration < 1 ||
typeof repeatRevision !== "boolean")
) {
process.exit(1);
}
const output = [
"claimed=true",
`lease_id=${process.env.QUEUE_LEASE_ID}`,
`item_key=${itemKey}`,
`lease_revision=${Number.isInteger(leaseRevision) ? leaseRevision : ""}`,
`claim_generation=${responseProtocol === 2 ? claimGeneration : ""}`,
`repeat_revision=${responseProtocol === 2 ? repeatRevision : false}`,
`protocol_version=${responseProtocol}`,
`decision=${JSON.stringify(decision)}`,
];
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${output.join("\n")}\n`);
NODE
then
exit 0
else
echo "Exact-review lease claim returned an invalid success payload." >&2
exit 1
fi
else
cat "$error_file" >&2
fi
if [ "$attempt" -lt 3 ]; then
sleep "$((attempt * 5))"
fi
done
exit 1
- uses: actions/checkout@v7
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' }}
with:
filter: blob:none
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup-action-ledger
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' }}
continue-on-error: true
- name: Resolve event payload
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' }}
id: target
env:
CLAIM_DECISION: ${{ steps.claim-exact-review-queue.outputs.decision }}
CLAWHUB_ENABLED: ${{ vars.CLAWSWEEPER_ENABLE_CLAWHUB }}
CONFIGURED_CODEX_TIMEOUT_MS: ${{ vars.CLAWSWEEPER_CODEX_TIMEOUT_MS || '1200000' }}
run: |
set -euo pipefail
node <<'NODE'
const fs = require("node:fs");
const decision = JSON.parse(process.env.CLAIM_DECISION || "{}");
const targetRepo = String(decision.targetRepo || "");
const itemNumber = Number(decision.itemNumber);
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(targetRepo)) process.exit(1);
if (!Number.isInteger(itemNumber) || itemNumber < 1) process.exit(1);
// The review-start lease permits at most two hours, including its ten-minute
// cushion. The 120-minute job leaves 64 minutes for target checkout/setup and
// artifact/publication finalizers after this cap and the 11-minute review reserve.
const maxExactReviewCodexTimeoutMs = 2_700_000;
const configuredValue = Number(process.env.CONFIGURED_CODEX_TIMEOUT_MS);
const configuredTimeout =
Number.isInteger(configuredValue) && configuredValue > 0
? Math.min(maxExactReviewCodexTimeoutMs, configuredValue)
: 1_200_000;
const adaptiveValue = Number(decision.codexTimeoutMs);
const adaptiveTimeout =
Number.isInteger(adaptiveValue) && adaptiveValue > 0
? Math.min(1_800_000, Math.max(600_000, adaptiveValue))
: 0;
const mediaValue = Number(decision.mediaProofTimeoutMs);
const mediaTimeout =
Number.isInteger(mediaValue) && mediaValue > 0 ? Math.min(480_000, mediaValue) : 0;
const [owner, name] = targetRepo.split("/");
const targetSlug = targetRepo.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, "-");
const checkoutDir = targetRepo === process.env.GITHUB_REPOSITORY ? `${name}-target` : name;
const hasCommandContext = Boolean(decision.commandStatusMarker || decision.statusCommentId);
const targetEnabled =
targetRepo !== "openclaw/clawhub" || process.env.CLAWHUB_ENABLED === "1";
const output = {
target_repo: targetRepo,
target_repo_owner: owner,
target_repo_name: name,
target_slug: targetSlug,
target_checkout_dir: checkoutDir,
item_number: itemNumber,
codex_timeout_ms: Math.min(
maxExactReviewCodexTimeoutMs,
Math.max(configuredTimeout, adaptiveTimeout),
),
media_proof_timeout_ms: mediaTimeout,
has_command_context: hasCommandContext,
target_enabled: targetEnabled,
};
fs.appendFileSync(
process.env.GITHUB_OUTPUT,
`${Object.entries(output)
.map(([key, value]) => `${key}=${value}`)
.join("\n")}\n`,
);
NODE
- name: Skip disabled claimed target
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.target.outputs.target_enabled == 'false' }}
run: echo "Completing the claimed exact-review lease without target access because this target is disabled."
- name: Create target read token
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.target.outputs.target_enabled == 'true' }}
id: target-read-token
continue-on-error: true
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
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-checks: read
permission-contents: read
permission-issues: read
permission-pull-requests: read
permission-statuses: read
- name: Check live target item state
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.target.outputs.target_enabled == 'true' }}
id: live-item
env:
CLAIM_DECISION: ${{ steps.claim-exact-review-queue.outputs.decision }}
GH_TOKEN: ${{ steps.target.outputs.target_repo == 'openclaw/openclaw' && github.token || steps.target-read-token.outputs.token }}
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.target.outputs.item_number }}
CLAIM_TARGET_BRANCH: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).targetBranch }}
run: |
set -euo pipefail
test -n "$GH_TOKEN"
target_branch="$CLAIM_TARGET_BRANCH"
branch_is_usable() {
local branch="$1"
[ -n "$branch" ] &&
printf '%s' "$branch" | grep -Eq '^[A-Za-z0-9_.\/-]+$' &&
! printf '%s' "$branch" | grep -Eq '^[0-9]+$' &&
[[ "$branch" != *".."* ]]
}
if ! branch_is_usable "$target_branch"; then
queued_target_branch="$target_branch"
branch_error="$(mktemp)"
if ! target_branch="$(gh api "repos/$TARGET_REPO" --jq '.default_branch // empty' 2>"$branch_error")" ||
! branch_is_usable "$target_branch"; then
echo "::warning::Unable to resolve a usable default branch for $TARGET_REPO after queued target branch '$queued_target_branch' was rejected; releasing the claim for retry."
cat "$branch_error" >&2
rm -f "$branch_error"
retry_at="$(node -e 'process.stdout.write(new Date(Date.now() + 5 * 60_000).toISOString())')"
{
echo "admission_retry=true"
echo "retry_at=$retry_at"
echo "proceed=false"
echo "terminal_noop=false"
echo "terminal_missing=false"
echo "guarded_open=false"
echo "guarded_open_action="
} >> "$GITHUB_OUTPUT"
exit 0
fi
rm -f "$branch_error"
echo "::warning::Resolved invalid queued target branch '$queued_target_branch' to default branch '$target_branch' for $TARGET_REPO."
fi
effective_decision="$(CLAIM_DECISION="$CLAIM_DECISION" TARGET_BRANCH="$target_branch" node <<'NODE'
const decision = JSON.parse(process.env.CLAIM_DECISION || "{}");
decision.targetBranch = process.env.TARGET_BRANCH;
process.stdout.write(JSON.stringify(decision));
NODE
)"
{
echo "admission_retry=false"
echo "target_branch=$target_branch"
echo "decision=$effective_decision"
} >> "$GITHUB_OUTPUT"
live_item_error="$(mktemp)"
if ! live_item="$(gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER" 2>"$live_item_error")"; then
if grep -Eqi 'HTTP 404|Not Found' "$live_item_error" && gh api "repos/$TARGET_REPO" >/dev/null; then
rm -f "$live_item_error"
echo "proceed=false" >> "$GITHUB_OUTPUT"
echo "terminal_noop=false" >> "$GITHUB_OUTPUT"
echo "terminal_missing=true" >> "$GITHUB_OUTPUT"
echo "guarded_open=false" >> "$GITHUB_OUTPUT"
echo "guarded_open_action=" >> "$GITHUB_OUTPUT"
echo "::notice::Completing $TARGET_REPO#$ITEM_NUMBER because the repository is accessible but the item is missing."
exit 0
fi
if grep -Eqi 'rate limit exceeded|secondary rate limit|HTTP 429' "$live_item_error"; then
cat "$live_item_error" >&2
rm -f "$live_item_error"
retry_at="$(node -e 'process.stdout.write(new Date(Date.now() + 20 * 60_000).toISOString())')"
echo "::notice::GitHub throttled the live-item check for $TARGET_REPO#$ITEM_NUMBER; releasing the claim for retry at $retry_at without spending failure budget."
{
echo "admission_retry=true"
echo "retry_kind=throttle"
echo "retry_at=$retry_at"
echo "proceed=false"
echo "terminal_noop=false"
echo "terminal_missing=false"
echo "guarded_open=false"
echo "guarded_open_action="
} >> "$GITHUB_OUTPUT"
exit 0
fi
cat "$live_item_error" >&2
rm -f "$live_item_error"
exit 1
fi
rm -f "$live_item_error"
live_state="$(jq -r '.state' <<< "$live_item")"
live_locked="$(jq -r '.locked == true' <<< "$live_item")"
item_kind="$(jq -r 'if .pull_request then "pull_request" else "issue" end' <<< "$live_item")"
echo "item_kind=$item_kind" >> "$GITHUB_OUTPUT"
if [ "$live_state" = "open" ] && [ "$live_locked" != "true" ]; then
live_head_sha=""
if [ "$item_kind" = "pull_request" ]; then
if ! live_head_sha="$(gh api "repos/$TARGET_REPO/pulls/$ITEM_NUMBER" --jq '.head.sha')"; then
echo "::warning::Unable to read the live pull request head; preserving normal review admission."
live_head_sha=""
fi
fi
comments_error="$(mktemp)"
if live_comments="$(gh api --paginate --slurp "repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments?per_page=100" 2>"$comments_error" | jq 'add')"; then
classification_output="$(mktemp)"
if CLAIM_DECISION="$CLAIM_DECISION" LIVE_ITEM="$live_item" LIVE_COMMENTS="$live_comments" LIVE_HEAD_SHA="$live_head_sha" GITHUB_OUTPUT="$classification_output" \
node scripts/classify-scheduled-review-noop.ts; then
cat "$classification_output" >> "$GITHUB_OUTPUT"
if grep -qx 'scheduled_noop=true' "$classification_output"; then
reason="$(sed -n 's/^scheduled_noop_reason=//p' "$classification_output")"
rm -f "$classification_output" "$comments_error"
{
echo "proceed=false"
echo "terminal_noop=false"
echo "scheduled_semantic_noop=true"
echo "terminal_missing=false"
echo "guarded_open=false"
echo "guarded_open_action="
} >> "$GITHUB_OUTPUT"
echo "::notice::Completing $TARGET_REPO#$ITEM_NUMBER as a scheduled no-op before target checkout because $reason."
exit 0
fi
else
echo "::warning::Scheduled no-op classification failed; preserving normal review admission."
fi
rm -f "$classification_output"
else
echo "::warning::Unable to read comments for scheduled no-op classification; preserving normal review admission."
cat "$comments_error" >&2
fi
rm -f "$comments_error"
fi
case "$live_state" in
open)
echo "scheduled_semantic_noop=false" >> "$GITHUB_OUTPUT"
echo "terminal_noop=false" >> "$GITHUB_OUTPUT"
echo "terminal_missing=false" >> "$GITHUB_OUTPUT"
if [ "$live_locked" = "true" ]; then
echo "proceed=false" >> "$GITHUB_OUTPUT"
echo "guarded_open=true" >> "$GITHUB_OUTPUT"
echo "guarded_open_action=skipped_locked_conversation" >> "$GITHUB_OUTPUT"
echo "::notice::Completing $TARGET_REPO#$ITEM_NUMBER without Codex because the open conversation is locked."
else
echo "proceed=true" >> "$GITHUB_OUTPUT"
echo "guarded_open=false" >> "$GITHUB_OUTPUT"
echo "guarded_open_action=" >> "$GITHUB_OUTPUT"
fi
;;
closed)
echo "scheduled_semantic_noop=false" >> "$GITHUB_OUTPUT"
echo "proceed=false" >> "$GITHUB_OUTPUT"
echo "terminal_noop=true" >> "$GITHUB_OUTPUT"
echo "terminal_missing=false" >> "$GITHUB_OUTPUT"
echo "guarded_open=false" >> "$GITHUB_OUTPUT"
echo "guarded_open_action=" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping terminal $TARGET_REPO#$ITEM_NUMBER because it is already closed."
;;
*)
echo "Unexpected live state for $TARGET_REPO#$ITEM_NUMBER: $live_state" >&2
exit 1
;;
esac
- name: Create target write token
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outcome == 'success' && steps.live-item.outputs.scheduled_semantic_noop != 'true' && steps.live-item.outputs.admission_retry != 'true' }}
id: target-write-token
continue-on-error: true
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
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-checks: read
permission-contents: write
permission-issues: write
permission-pull-requests: write
permission-statuses: read
- name: React to target item review start
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' }}
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
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outcome == 'success' && steps.live-item.outputs.scheduled_semantic_noop != 'true' && steps.live-item.outputs.admission_retry != 'true' }}
with:
build-script: ${{ steps.live-item.outputs.proceed == 'true' && 'build:all' || 'build:repair' }}
- uses: actions/cache@v6
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' }}
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
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' }}
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.live-item.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 re-review command in progress
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' }}
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: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).commandStatusMarker || '' }}
STATUS_COMMENT_ID: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).statusCommentId || '' }}
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 "The exact-review queue leased this run; Codex is reviewing the item." \
--run-url "$RUN_URL" \
--wait-ms 120000
- uses: ./.github/actions/setup-codex
if: ${{ env.CLAWSWEEPER_RUNNER != 'openclaw' && steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' }}
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CLAWSWEEPER_INTERNAL_MODEL: ${{ secrets.CLAWSWEEPER_MODEL }}
with:
login-status: "true"
- uses: ./.github/actions/setup-openclaw
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' }}
- name: Reserve exact review lease
id: reserve-exact-review-lease
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' }}
env:
GH_TOKEN: ${{ steps.target-write-token.outputs.token }}
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.target.outputs.item_number }}
CODEX_TIMEOUT_MS: ${{ steps.target.outputs.codex_timeout_ms }}
MEDIA_PROOF_TIMEOUT_MS: ${{ steps.target.outputs.media_proof_timeout_ms }}
EXACT_REVIEW_ITEM_KEY: ${{ steps.claim-exact-review-queue.outputs.item_key }}
EXACT_REVIEW_LEASE_ID: ${{ steps.claim-exact-review-queue.outputs.lease_id }}
EXACT_REVIEW_LEASE_REVISION: ${{ steps.claim-exact-review-queue.outputs.lease_revision }}
EXACT_REVIEW_CLAIM_GENERATION: ${{ steps.claim-exact-review-queue.outputs.claim_generation }}
EXACT_REVIEW_SOURCE_HEAD_SHA: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).sourceHeadSha || '' }}
EXACT_REVIEW_QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
run: |
set -euo pipefail
test -n "$GH_TOKEN"
if ! [[ "$CODEX_TIMEOUT_MS" =~ ^[0-9]+$ ]] || [ "$CODEX_TIMEOUT_MS" -lt 1 ]; then
echo "Invalid Codex timeout: $CODEX_TIMEOUT_MS" >&2
exit 1
fi
if ! [[ "$MEDIA_PROOF_TIMEOUT_MS" =~ ^[0-9]+$ ]] || [ "$MEDIA_PROOF_TIMEOUT_MS" -lt 0 ]; then
echo "Invalid media proof timeout: $MEDIA_PROOF_TIMEOUT_MS" >&2
exit 1
fi
codex_timeout_seconds=$(((CODEX_TIMEOUT_MS + 999) / 1000))
media_preprocessing_reserve_seconds=480
review_timeout_ms=$(((codex_timeout_seconds + media_preprocessing_reserve_seconds + 180) * 1000))
reservation=""
reservation_error="$(mktemp)"
trap 'rm -f "$reservation_error"' EXIT
for attempt in 1 2 3 4 5; do
: > "$reservation_error"
if reservation="$(pnpm run --silent reserve-review-lease -- \
--target-repo "$TARGET_REPO" \
--item-number "$ITEM_NUMBER" \
--review-timeout-ms "$review_timeout_ms" 2>"$reservation_error")"; then
break
else
reserve_exit=$?
fi
if grep -Eqi 'rate limit exceeded|secondary rate limit|HTTP 429' "$reservation_error"; then
retry_at="$(date -u -d "@$(( $(date -u +%s) + 1200 ))" +%Y-%m-%dT%H:%M:%SZ)"
echo "::notice::GitHub throttled the exact-review reservation; deferring until $retry_at so the durable queue retries without spending failure budget."
reservation="{\"status\":\"held\",\"retryAt\":\"$retry_at\",\"retryKind\":\"throttle\"}"
break
fi
if ! grep -Eqi '(exact-review queue authority changed|review revision changed|could not (confirm|identify).+review lease).+retry required' "$reservation_error"; then
cat "$reservation_error" >&2
exit "$reserve_exit"
fi
if [ "$attempt" -eq 5 ]; then
echo "::notice::Exact-review reservation contention did not settle after 5 attempts; queue authority moved on, so this run is a successful no-op."
reservation='{"status":"superseded"}'
break
fi
delay_seconds=$((attempt * 2 + RANDOM % 4))
echo "::notice::Exact-review reservation contention on attempt $attempt/5; retrying in ${delay_seconds}s."
sleep "$delay_seconds"
done
echo "$reservation"
RESERVATION="$reservation" node <<'NODE'
const fs = require("node:fs");
const reservation = JSON.parse(process.env.RESERVATION || "{}");
const append = (key, value) => fs.appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`);
if (reservation.status === "posted") {
const owner = String(reservation.owner || "");
const commentId = Number(reservation.commentId);
if (!/^[a-zA-Z0-9._-]{1,200}$/.test(owner) || !Number.isInteger(commentId) || commentId <= 0) {
process.exit(1);
}
append("status", "posted");
append("owner", owner);
append("comment_id", String(commentId));
process.exit(0);
}
if (reservation.status === "held") {
const retryAt = String(reservation.retryAt || "");
if (!Number.isFinite(Date.parse(retryAt))) process.exit(1);
const retryKind =
reservation.retryKind === "throttle" ? "throttle" : "coordination";
append("status", "held");
append("retry_kind", retryKind);
append("retry_at", new Date(retryAt).toISOString());
process.exit(0);
}
if (reservation.status === "superseded") {
append("status", "superseded");
process.exit(0);
}
process.exit(1);
NODE
# At-least-once queue delivery can overlap an exact-head review that is already
# running. The durable queue owns retry, so lease contention is deferred work.
- name: Defer exact review while same-head lease is held
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && (steps.reserve-exact-review-lease.outputs.status == 'held' || steps.reserve-exact-review-lease.outputs.status == 'superseded') }}
env:
RESERVATION_STATUS: ${{ steps.reserve-exact-review-lease.outputs.status }}
RETRY_AT: ${{ steps.reserve-exact-review-lease.outputs.retry_at }}
run: |
if [ "$RESERVATION_STATUS" = "superseded" ]; then
echo "::notice::Exact-review queue authority moved to another worker; this run completed as a successful no-op."
else
echo "::notice::Another exact-head review is already active; retry deferred until $RETRY_AT."
fi
- name: Review exact event item
id: review-exact-event-item
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' && steps.reserve-exact-review-lease.outputs.status == 'posted' }}
continue-on-error: true
env:
# Review hydration is the largest read stream (~30 calls/review); it
# stays on the App installation bucket so the per-repo Actions pool
# keeps headroom for the comment-router and support lanes.
GH_TOKEN: ${{ steps.target-read-token.outputs.token }}
CLAWSWEEPER_PROOF_INSPECTION_TOKEN: ${{ steps.target-read-token.outputs.token }}
ADDITIONAL_PROMPT: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).additionalPrompt || '' }}
CLAWSWEEPER_RELATED_GITHUB_SEARCH: ${{ vars.CLAWSWEEPER_RELATED_GITHUB_SEARCH || '1' }}
EXACT_REVIEW_ITEM_KEY: ${{ steps.claim-exact-review-queue.outputs.item_key }}
EXACT_REVIEW_LEASE_ID: ${{ steps.claim-exact-review-queue.outputs.lease_id }}
EXACT_REVIEW_LEASE_REVISION: ${{ steps.claim-exact-review-queue.outputs.lease_revision }}
EXACT_REVIEW_CLAIM_GENERATION: ${{ steps.claim-exact-review-queue.outputs.claim_generation }}
EXACT_REVIEW_SOURCE_HEAD_SHA: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).sourceHeadSha || '' }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
REVIEW_LEASE_OWNER: ${{ steps.reserve-exact-review-lease.outputs.owner }}
REVIEW_LEASE_COMMENT_ID: ${{ steps.reserve-exact-review-lease.outputs.comment_id }}
SOURCE_ACTION: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).sourceAction || '' }}
run: |
set -euo pipefail
test -n "$GH_TOKEN"
heartbeat_pid=""
review_pid=""
review_pgid=""
superseded_marker="$RUNNER_TEMP/exact-review-superseded-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
rm -f "$superseded_marker"
terminate_review_group() {
[ -n "$review_pgid" ] || return 0
kill -TERM -- "-$review_pgid" 2>/dev/null || true
}
wait_for_review_group() {
[ -n "$review_pgid" ] || return 0
for _ in {1..30}; do
if ! kill -0 -- "-$review_pgid" 2>/dev/null; then
return 0
fi
sleep 1
done
kill -KILL -- "-$review_pgid" 2>/dev/null || true
for _ in {1..50}; do
if ! kill -0 -- "-$review_pgid" 2>/dev/null; then
return 0
fi
sleep 0.1
done
echo "::error::Exact-review process group $review_pgid did not terminate."
return 1
}
heartbeat_loop() {
while true; do
heartbeat_status="$(curl --silent --connect-timeout 5 --max-time 20 \
--output /dev/null \
--write-out '%{http_code}' \
--request POST \
--header "content-type: application/json" \
--data-binary "$heartbeat_payload" \
"${QUEUE_URL%/}/internal/exact-review/heartbeat" 2>/dev/null)" || heartbeat_status=""
if [ "$heartbeat_status" = "409" ]; then
echo "::notice::Exact-review heartbeat lost its lease tuple."
: > "$superseded_marker"
terminate_review_group
return
fi
sleep 60
done
}
start_heartbeat() {
# Heartbeat authenticates by the full lease tuple (same capability model as
# claim/complete), so this Codex-adjacent step never sees the webhook secret.
heartbeat_payload="$(node -e '
const leaseRevision = Number(process.env.EXACT_REVIEW_LEASE_REVISION);
const claimGeneration = Number(process.env.EXACT_REVIEW_CLAIM_GENERATION);
const runAttempt = Number(process.env.GITHUB_RUN_ATTEMPT);
const sourceHeadSha = String(process.env.EXACT_REVIEW_SOURCE_HEAD_SHA || "").trim().toLowerCase();
if (!process.env.EXACT_REVIEW_ITEM_KEY || !process.env.EXACT_REVIEW_LEASE_ID) process.exit(1);
if (!Number.isInteger(leaseRevision) || leaseRevision < 1) process.exit(1);
if (!Number.isInteger(claimGeneration) || claimGeneration < 1) process.exit(1);
if (!Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1);
if (sourceHeadSha && !/^[0-9a-f]{40}$/.test(sourceHeadSha)) process.exit(1);
process.stdout.write(JSON.stringify({
item_key: process.env.EXACT_REVIEW_ITEM_KEY,
lease_id: process.env.EXACT_REVIEW_LEASE_ID,
lease_revision: leaseRevision,
claim_generation: claimGeneration,
run_id: process.env.GITHUB_RUN_ID,
run_attempt: runAttempt,
...(sourceHeadSha ? { source_head_sha: sourceHeadSha } : {}),
}));
')" || return 0
heartbeat_loop &
heartbeat_pid=$!
}
cleanup_heartbeat() {
[ -n "$heartbeat_pid" ] || return 0
kill "$heartbeat_pid" 2>/dev/null || true
wait "$heartbeat_pid" 2>/dev/null || true
}
mark_finalizing() {
finalizing_payload="$(HEARTBEAT_PAYLOAD="$heartbeat_payload" node -e '
const payload = JSON.parse(process.env.HEARTBEAT_PAYLOAD || "{}");
process.stdout.write(JSON.stringify({ ...payload, phase: "finalizing" }));
')" || return 1
for attempt in 1 2 3; do
finalizing_status="$(curl --silent --connect-timeout 5 --max-time 20 \
--output /dev/null \
--write-out '%{http_code}' \
--request POST \
--header "content-type: application/json" \
--data-binary "$finalizing_payload" \
"${QUEUE_URL%/}/internal/exact-review/heartbeat" 2>/dev/null)" || finalizing_status=""
if [ "$finalizing_status" = "200" ]; then
return 0
fi
if [ "$finalizing_status" = "409" ]; then
echo "::notice::Exact-review finalization lost its lease tuple."
: > "$superseded_marker"
return 0
fi
if [ "$attempt" -lt 3 ]; then
sleep "$((attempt * 5))"
fi
done
echo "::error::Exact-review lease could not enter finalization."
return 1
}
trap cleanup_heartbeat EXIT
codex_timeout_ms="${{ steps.target.outputs.codex_timeout_ms }}"
if ! [[ "$codex_timeout_ms" =~ ^[0-9]+$ ]] || [ "$codex_timeout_ms" -lt 1 ]; then
echo "Invalid Codex timeout: $codex_timeout_ms" >&2
exit 1
fi
additional_prompt_arg=()
if [ -n "$ADDITIONAL_PROMPT" ]; then
additional_prompt_arg=(--additional-prompt "$ADDITIONAL_PROMPT")
fi
codex_timeout_seconds=$(((codex_timeout_ms + 999) / 1000))
media_proof_timeout_ms="${{ steps.target.outputs.media_proof_timeout_ms }}"
media_proof_timeout_seconds=$(((media_proof_timeout_ms + 999) / 1000))
media_preprocessing_reserve_seconds=480
review_timeout_seconds=$((codex_timeout_seconds + media_preprocessing_reserve_seconds + 180))
echo "::notice::Exact event review timeout is ${review_timeout_seconds}s for per-item Codex timeout ${codex_timeout_seconds}s, reserved media preprocessing ${media_preprocessing_reserve_seconds}s, and detected media allowance ${media_proof_timeout_seconds}s."
set +e
setsid timeout --kill-after=30s "${review_timeout_seconds}s" 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 internal \
--codex-reasoning-effort high \
--codex-sandbox read-only \
--codex-timeout-ms "$codex_timeout_ms" \
--item-numbers "${{ steps.target.outputs.item_number }}" \
--readonly-openclaw \
--skip-start-comment \
--review-lease-owner "$REVIEW_LEASE_OWNER" \
--review-lease-comment-id "$REVIEW_LEASE_COMMENT_ID" \
--shard-index 0 \
--shard-count 1 \
--review-source-action "$SOURCE_ACTION" \
"${additional_prompt_arg[@]}" &
review_pid=$!
review_pgid=$review_pid
start_heartbeat
wait "$review_pid"
review_exit_code=$?
cleanup_heartbeat
if [ "$review_exit_code" -eq 0 ] && [ ! -f "$superseded_marker" ]; then
mark_finalizing || review_exit_code=1
fi
if [ -f "$superseded_marker" ]; then
wait_for_review_group
echo "superseded=true" >> "$GITHUB_OUTPUT"
echo "exit_code=0" >> "$GITHUB_OUTPUT"
echo "::notice::Exact-review generation stopped because another worker owns the newer queue authority; completing this run as a successful no-op."
exit 0
fi
set -e
echo "exit_code=$review_exit_code" >> "$GITHUB_OUTPUT"
if [ "$review_exit_code" -ne 0 ]; then
exit "$review_exit_code"
fi
coordination_held_path="artifacts/event/coordination-held.json"
if [ -f "$coordination_held_path" ]; then
retry_at="$(node -e '
const data = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8"));
const value = String(data.retry_at || "").trim();
const timestamp = Date.parse(value);
if (!value || !Number.isFinite(timestamp)) process.exit(1);
process.stdout.write(new Date(timestamp).toISOString());
' "$coordination_held_path")"
echo "retry_kind=coordination" >> "$GITHUB_OUTPUT"
echo "retry_at=$retry_at" >> "$GITHUB_OUTPUT"
echo "::notice::Review coordination is held until $retry_at."
fi
report_path="artifacts/event/${{ steps.target.outputs.item_number }}.md"
if [ ! -f "$report_path" ]; then
live_state="$(gh api "repos/${{ steps.target.outputs.target_repo }}/issues/${{ steps.target.outputs.item_number }}" --jq .state)"
if [ "$live_state" != "closed" ]; then
echo "Exact review produced no artifact for open item ${{ steps.target.outputs.target_repo }}#${{ steps.target.outputs.item_number }}." >&2
exit 1
fi
echo "::notice::Exact review produced no artifact because the item closed during review."
echo "terminal_during_review=true" >> "$GITHUB_OUTPUT"
fi
- name: Finalize exact event action ledger
if: ${{ always() && steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' }}
continue-on-error: true
env:
REVIEW_EXIT_CODE: ${{ steps.review-exact-event-item.outputs.exit_code || '' }}
REVIEW_OUTCOME: ${{ steps.review-exact-event-item.outcome || 'not_started' }}
REVIEW_SUPERSEDED: ${{ steps.review-exact-event-item.outputs.superseded || 'false' }}
run: |
args=()
if [ "$REVIEW_SUPERSEDED" = "true" ] || [ "$REVIEW_OUTCOME" = "cancelled" ] || [ "$REVIEW_EXIT_CODE" = "130" ] || [ "$REVIEW_EXIT_CODE" = "143" ]; then
args=(--interrupt-open-attempts --reason cancelled)
elif [ "$REVIEW_EXIT_CODE" = "124" ] || [ "$REVIEW_EXIT_CODE" = "137" ]; then
args=(--interrupt-open-attempts --reason timeout)
elif [ "$REVIEW_OUTCOME" != "success" ]; then
args=(--interrupt-open-attempts --reason workflow_failed)
fi
pnpm run --silent finalize-action-events -- "${args[@]}"
- name: Create exact review artifact bundle
id: create-exact-review-bundle
if: ${{ always() && steps.claim-exact-review-queue.outputs.claimed == 'true' && !cancelled() && steps.target.outputs.target_enabled == 'true' && steps.live-item.outcome == 'success' && steps.live-item.outputs.scheduled_semantic_noop != 'true' && steps.live-item.outputs.admission_retry != 'true' && steps.setup-pnpm.outcome == 'success' && steps.review-exact-event-item.outputs.superseded != 'true' && (steps.live-item.outputs.proceed != 'true' || (steps.review-exact-event-item.outcome == 'success' && steps.review-exact-event-item.outputs.retry_at == '')) }}
env:
EXACT_REVIEW_ACTION_LEDGER_ROOT: ${{ env.CLAWSWEEPER_ACTION_LEDGER_OUTPUT_ROOT }}
EXACT_REVIEW_BUNDLE_DIR: .artifacts/exact-review-bundle
EXACT_REVIEW_CLAIM_GENERATION: ${{ steps.claim-exact-review-queue.outputs.claim_generation }}
EXACT_REVIEW_DECISION: ${{ steps.live-item.outputs.decision }}
EXACT_REVIEW_GENERATION_ATTEMPT: ${{ github.run_attempt }}
EXACT_REVIEW_ITEM_KEY: ${{ steps.claim-exact-review-queue.outputs.item_key }}
EXACT_REVIEW_ITEM_KIND: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).itemKind }}
EXACT_REVIEW_ITEM_NUMBER: ${{ steps.target.outputs.item_number }}
EXACT_REVIEW_LEASE_REVISION: ${{ steps.claim-exact-review-queue.outputs.lease_revision }}
EXACT_REVIEW_LIVE_GUARDED_OPEN: ${{ steps.live-item.outputs.guarded_open }}
EXACT_REVIEW_LIVE_PROCEEDED: ${{ steps.review-exact-event-item.outputs.terminal_during_review == 'true' && 'false' || steps.live-item.outputs.proceed }}
EXACT_REVIEW_LIVE_TERMINAL_MISSING: ${{ steps.live-item.outputs.terminal_missing }}
EXACT_REVIEW_LIVE_TERMINAL_NOOP: ${{ steps.review-exact-event-item.outputs.terminal_during_review == 'true' && 'true' || steps.live-item.outputs.terminal_noop }}
EXACT_REVIEW_PRODUCER_JOB: event-review-apply
EXACT_REVIEW_PROTOCOL_VERSION: ${{ steps.claim-exact-review-queue.outputs.protocol_version }}
EXACT_REVIEW_REPORT_PATH: artifacts/event/${{ steps.target.outputs.item_number }}.md
EXACT_REVIEW_TARGET_BRANCH: ${{ steps.live-item.outputs.target_branch }}
EXACT_REVIEW_TARGET_REPO: ${{ steps.target.outputs.target_repo }}
run: |
set -euo pipefail
mkdir -p .artifacts
pnpm run --silent repair:exact-review-bundle create > .artifacts/exact-review-bundle-create.json
artifact_name="exact-review-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
{
echo "artifact_name=$artifact_name"
echo "generation_attempt=$GITHUB_RUN_ATTEMPT"
} >> "$GITHUB_OUTPUT"
- uses: ./.github/actions/setup-state
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.create-exact-review-bundle.outcome == 'success' && steps.live-item.outputs.proceed == 'true' && (vars.EXACT_REVIEW_DIRECT_PUBLICATION_ENABLED || '1') == '1' }}
id: direct-setup-state
with:
coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
records-repo-slugs: ${{ steps.target.outputs.target_slug }}
records-item-number: ${{ steps.target.outputs.item_number }}
hydrate-git-state: "false"
hydrate-state-blobs: "false"
- uses: ./.github/actions/setup-github-egress-observer
id: direct-github-egress-observer
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.direct-setup-state.outcome == 'success' }}
continue-on-error: true
with:
metrics-path: .artifacts/direct-publication/github-egress-v2.jsonl
rate-limit-path: .artifacts/direct-publication/github-rate-limit-details-v2.jsonl
pool-class: target_app
stage: publication_apply
source-action: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).sourceAction }}
claim-generation: ${{ steps.claim-exact-review-queue.outputs.claim_generation }}
repeat-revision: ${{ steps.claim-exact-review-queue.outputs.repeat_revision }}
- name: Record direct-publication member
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.direct-github-egress-observer.outcome == 'success' }}
continue-on-error: true
env:
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
run: node dist/github-egress-observer-cli.js record-member
- name: Deliver GitHub effects and prepare direct state mutation
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.direct-setup-state.outcome == 'success' }}
id: prepare-direct-exact-review-publication
continue-on-error: true
env:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
EXACT_REVIEW_QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
GH_TOKEN: ${{ steps.target-write-token.outputs.token }}
CLAWSWEEPER_PUBLIC_GH_TOKEN: ${{ steps.target.outputs.target_repo == 'openclaw/openclaw' && github.token || '' }}
REPO_TOKEN: ${{ github.token }}
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.target.outputs.item_number }}
MIN_AGE_MINUTES: "0"
REVIEW_ONLY: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).sourceAction == 'failed_review_shard_recovery' && 'true' || 'false' }}
EXACT_EVENT_PUBLICATION: "true"
EXACT_REVIEW_CLOSE_COVERAGE_DEFERRED: "true"
EXACT_REVIEW_BATCH_ITEM_KEY: ${{ steps.claim-exact-review-queue.outputs.item_key }}
EXACT_REVIEW_BATCH_REVISION: ${{ steps.claim-exact-review-queue.outputs.lease_revision }}
EXACT_REVIEW_BATCH_CLAIM_GENERATION: ${{ steps.claim-exact-review-queue.outputs.claim_generation }}
EXACT_REVIEW_BATCH_MUTATION_OUTPUT: .artifacts/direct-publication-outcome.json
run: pnpm run --silent repair:publish-event-result
- name: Dispatch recommended live proofs
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.prepare-direct-exact-review-publication.outcome == 'success' }}
uses: ./.github/actions/dispatch-live-proofs
with:
target-repo: ${{ steps.target.outputs.target_repo }}
item-numbers: ${{ steps.target.outputs.item_number }}
records-root: records
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
- name: Post direct exact review publication result
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.prepare-direct-exact-review-publication.outcome == 'success' }}
id: direct-exact-review-publication
env:
EXACT_REVIEW_DIRECT_PUBLICATION_ENABLED: ${{ vars.EXACT_REVIEW_DIRECT_PUBLICATION_ENABLED || '1' }}
EXACT_REVIEW_DIRECT_MUTATION_OUTPUT: .artifacts/direct-publication-outcome.json
EXACT_REVIEW_DIRECT_REVISION: ${{ steps.claim-exact-review-queue.outputs.lease_revision }}
EXACT_REVIEW_DIRECT_SOURCE_ACTION: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).sourceAction }}
EXACT_REVIEW_QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
run: pnpm run --silent repair:exact-review-direct-publication
- name: Finalize direct exact review lifecycle
id: finalize-direct-exact-review-lifecycle
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.direct-exact-review-publication.outputs.accepted == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
CLAWSWEEPER_GITHUB_POOL_CLASS: repository_actions
CLAWSWEEPER_GITHUB_STAGE: publication_router
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
TARGET_BRANCH: ${{ steps.target.outputs.target_branch }}
ITEM_NUMBER: ${{ steps.target.outputs.item_number }}
FENCE_KEY: ${{ steps.claim-exact-review-queue.outputs.item_key }}
REVISION: ${{ steps.claim-exact-review-queue.outputs.lease_revision }}
CLAIM_DECISION: ${{ steps.claim-exact-review-queue.outputs.decision }}
DIRECT_PUBLICATION_SUPERSEDED: ${{ steps.direct-exact-review-publication.outputs.superseded }}
DIRECT_OUTCOME: .artifacts/direct-publication-outcome.json
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES: ${{ vars.CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES || '180' }}
CLAWSWEEPER_COMMENT_MAX_COMMENTS: ${{ vars.CLAWSWEEPER_COMMENT_MAX_COMMENTS || '1000' }}
run: |
set -euo pipefail
test -s "$DIRECT_OUTCOME"
jq -e '.kind == "eligible" and .disposition != null' "$DIRECT_OUTCOME" >/dev/null
test -n "$CLAWSWEEPER_WEBHOOK_SECRET"
source_action="$(node -e 'const decision=JSON.parse(process.env.CLAIM_DECISION || "{}"); process.stdout.write(decision.sourceAction || "")')"
lifecycle_terminal=""
lifecycle_router_outcome=""
lifecycle_deferred_coverage="false"
direct_lifecycle_requeue=false
queue_url="${QUEUE_URL%/}"
if [ "${DIRECT_PUBLICATION_SUPERSEDED:-false}" != "true" ]; then
if jq -e '.disposition.requeueLatestExpected == true' "$DIRECT_OUTCOME" >/dev/null; then
lifecycle_terminal="requeue"
# The completion owns this direct receipt. Let its fenced queue
# transition create the fresh source-drift revision atomically.
direct_lifecycle_requeue=true
elif jq -e '.disposition.terminalMissingExpected == true' "$DIRECT_OUTCOME" >/dev/null; then
lifecycle_terminal="target_missing"
elif jq -e '.disposition.terminalClosedExpected == true' "$DIRECT_OUTCOME" >/dev/null; then
lifecycle_terminal="target_closed"
elif jq -e '.disposition.guardedOpenAction != null and .disposition.guardedOpenAction != ""' "$DIRECT_OUTCOME" >/dev/null; then
lifecycle_terminal="guarded_open"
elif [ "$source_action" = "failed_review_shard_recovery" ]; then
lifecycle_router_outcome="not_required"
elif jq -e '.disposition.routableSyncExpected == true' "$DIRECT_OUTCOME" >/dev/null; then
gh workflow run repair-comment-router.yml \
--repo "$GITHUB_REPOSITORY" \
--ref main \
-f execute=true \
-f target_repo="$TARGET_REPO" \
-f target_branch="$TARGET_BRANCH" \
-f item_numbers="$ITEM_NUMBER" \
-f lookback_minutes="$CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES" \
-f max_comments="$CLAWSWEEPER_COMMENT_MAX_COMMENTS"
lifecycle_router_outcome="durable"
elif jq -e '.disposition.deferredCloseCoverageExpected == true' "$DIRECT_OUTCOME" >/dev/null; then
# The close proof remains a separate later concern. The durable
# handoff means this review itself is complete.
lifecycle_router_outcome="durable"
lifecycle_deferred_coverage="true"
else
lifecycle_terminal="policy_noop"
fi
fi
if [ -n "$lifecycle_router_outcome" ]; then
export LIFECYCLE_ROUTER_OUTCOME="$lifecycle_router_outcome"
export LIFECYCLE_DEFERRED_COVERAGE="$lifecycle_deferred_coverage"
lifecycle_payload="$(node -e '
const revision = Number(process.env.REVISION);
if (!Number.isInteger(revision) || revision < 1 || !process.env.FENCE_KEY) process.exit(1);
const prefix = process.env.LIFECYCLE_DEFERRED_COVERAGE === "true"
? "router-direct-proof"
: process.env.LIFECYCLE_ROUTER_OUTCOME === "not_required"
? "router-direct-not-required"
: "router-direct";
process.stdout.write(JSON.stringify({
canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`,
fence_key: process.env.FENCE_KEY,
revision,
outcome: process.env.LIFECYCLE_ROUTER_OUTCOME,
receipt_id: `${prefix}:${process.env.GITHUB_RUN_ID}:${process.env.GITHUB_RUN_ATTEMPT}`,
}));
')"
lifecycle_signature="$(PAYLOAD="$lifecycle_payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')"
lifecycle_response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST \
--header "content-type: application/json" \
--header "x-clawsweeper-exact-review-signature: $lifecycle_signature" \
--data "$lifecycle_payload" \
"$queue_url/internal/exact-review/lifecycle/router-receipt")"
jq -e '.ok == true' <<<"$lifecycle_response" >/dev/null
elif [ -n "$lifecycle_terminal" ]; then
export LIFECYCLE_TERMINAL="$lifecycle_terminal"
lifecycle_payload="$(node -e '
const revision = Number(process.env.REVISION);
if (!Number.isInteger(revision) || revision < 1 || !process.env.FENCE_KEY || !process.env.LIFECYCLE_TERMINAL) process.exit(1);
process.stdout.write(JSON.stringify({
canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`,
fence_key: process.env.FENCE_KEY,
revision,
kind: process.env.LIFECYCLE_TERMINAL,
}));
')"
lifecycle_signature="$(PAYLOAD="$lifecycle_payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')"
lifecycle_response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST \
--header "content-type: application/json" \
--header "x-clawsweeper-exact-review-signature: $lifecycle_signature" \
--data "$lifecycle_payload" \
"$queue_url/internal/exact-review/lifecycle/terminal-disposition")"
jq -e '.ok == true' <<<"$lifecycle_response" >/dev/null
fi
echo "direct_lifecycle_requeue=$direct_lifecycle_requeue" >> "$GITHUB_OUTPUT"
# This is deliberately after the durable lifecycle handoff. Command
# acknowledgement is performed later by the dedicated finalizer and is
# not a precondition for optional implementation dispatch.
- name: Dispatch exact high-confidence bug implementation
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.finalize-direct-exact-review-lifecycle.outcome == 'success' && vars.CLAWSWEEPER_AUTO_IMPLEMENT_ISSUES == '1' && steps.target.outputs.target_repo == 'openclaw/openclaw' && fromJSON(steps.claim-exact-review-queue.outputs.decision).itemKind == 'issue' }}
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.target.outputs.item_number }}
MAX_DISPATCH: ${{ vars.CLAWSWEEPER_AUTO_IMPLEMENT_MAX_DISPATCH_PER_SWEEP || '' }}
run: |
node scripts/dispatch-issue-implementation-candidates.mjs \
--target-repo "$TARGET_REPO" \
--item-number "$ITEM_NUMBER" \
--artifact-dir artifacts/event
- name: Upload exact review artifact bundle
id: upload-exact-review-bundle
# Keep the artifact if direct publication needs publisher recovery after
# its lifecycle handoff fails. A committed handoff owns the final path.
if: ${{ always() && steps.claim-exact-review-queue.outputs.claimed == 'true' && !cancelled() && steps.create-exact-review-bundle.outcome == 'success' && (steps.direct-exact-review-publication.outputs.accepted != 'true' || steps.finalize-direct-exact-review-lifecycle.outcome != 'success') }}
uses: actions/upload-artifact@v7
with:
name: ${{ steps.create-exact-review-bundle.outputs.artifact_name }}
path: .artifacts/exact-review-bundle
include-hidden-files: true
if-no-files-found: error
retention-days: 90
- name: Queue durable exact review publication
id: queue-exact-review-publication
if: ${{ always() && steps.claim-exact-review-queue.outputs.claimed == 'true' && !cancelled() && steps.upload-exact-review-bundle.outcome == 'success' }}
env:
ARTIFACT_NAME: ${{ steps.create-exact-review-bundle.outputs.artifact_name }}
CLAIM_DECISION: ${{ steps.live-item.outputs.decision }}
CLAIM_GENERATION: ${{ steps.claim-exact-review-queue.outputs.claim_generation }}
ITEM_KEY: ${{ steps.claim-exact-review-queue.outputs.item_key }}
LEASE_REVISION: ${{ steps.claim-exact-review-queue.outputs.lease_revision }}
LIVE_GUARDED_OPEN: ${{ steps.live-item.outputs.guarded_open }}
LIVE_PROCEEDED: ${{ steps.review-exact-event-item.outputs.terminal_during_review == 'true' && 'false' || steps.live-item.outputs.proceed }}
LIVE_TERMINAL_MISSING: ${{ steps.live-item.outputs.terminal_missing }}
LIVE_TERMINAL_NOOP: ${{ steps.review-exact-event-item.outputs.terminal_during_review == 'true' && 'true' || steps.live-item.outputs.terminal_noop }}
PROTOCOL_VERSION: ${{ steps.claim-exact-review-queue.outputs.protocol_version }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
run: |
set -euo pipefail
test -n "$CLAWSWEEPER_WEBHOOK_SECRET"
queue_url="${QUEUE_URL%/}"
payload="$(node <<'NODE'
const producerDecision = JSON.parse(process.env.CLAIM_DECISION || "{}");
const producerRunAttempt = Number(process.env.GITHUB_RUN_ATTEMPT);
const protocolVersion = Number(process.env.PROTOCOL_VERSION);
const leaseRevision = process.env.LEASE_REVISION ? Number(process.env.LEASE_REVISION) : null;
const claimGeneration = process.env.CLAIM_GENERATION ? Number(process.env.CLAIM_GENERATION) : null;
const flag = (name) => {
const value = process.env[name];
if (value === "true") return true;
if (value === "false") return false;
process.exit(1);
};
if (!Number.isInteger(producerRunAttempt) || producerRunAttempt < 1) process.exit(1);
process.stdout.write(JSON.stringify({
delivery_id: `publisher:${process.env.GITHUB_RUN_ID}:${producerRunAttempt}`,
decision: {
...producerDecision,
sourceAction: "exact_review_artifact_publish",
supersedesInProgress: false,
publication: {
artifactName: process.env.ARTIFACT_NAME,
producerRunId: process.env.GITHUB_RUN_ID,
producerRunAttempt,
sourceSha: process.env.GITHUB_SHA,
itemKey: process.env.ITEM_KEY,
protocolVersion,
leaseRevision,
claimGeneration,
liveProceeded: flag("LIVE_PROCEEDED"),
liveTerminalNoop: flag("LIVE_TERMINAL_NOOP"),
liveTerminalMissing: flag("LIVE_TERMINAL_MISSING"),
liveGuardedOpen: flag("LIVE_GUARDED_OPEN"),
producerDecision,
},
},
}));
NODE
)"
signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')"
for attempt in 1 2 3; do
response="$(
curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST \
--header "content-type: application/json" \
--header "x-clawsweeper-exact-review-signature: $signature" \
--data "$payload" \
"$queue_url/internal/exact-review/enqueue" || true
)"
if jq -e '.superseded == true' <<< "$response" >/dev/null 2>&1; then
echo "::notice::Exact-review publication revision $(jq -r '.publication_revision // "unknown"' <<< "$response") was superseded by revision $(jq -r '.superseded_by_revision // "unknown"' <<< "$response"); the newer publisher owns final delivery."
exit 0
fi
if jq -e '.ok == true and (.queued == true or .deduped == true)' <<< "$response" >/dev/null; then
exit 0
fi
if [ "$attempt" -lt 3 ]; then
sleep "$((attempt * 5))"
fi
done
exit 1
- name: Release unsuccessful workflow-owned review lease
if: ${{ always() && steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.live-item.outputs.proceed == 'true' && steps.reserve-exact-review-lease.outputs.status != 'held' && steps.prepare-direct-exact-review-publication.outputs.failure_kind != 'github_rate_limit' && steps.prepare-direct-exact-review-publication.outputs.failure_kind != 'github_transient' && steps.direct-exact-review-publication.outputs.accepted != 'true' && steps.queue-exact-review-publication.outcome != 'success' }}
env:
GH_TOKEN: ${{ steps.target-write-token.outputs.token }}
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.target.outputs.item_number }}
LEASE_OWNER: github-run-${{ github.run_id }}-${{ github.run_attempt }}
run: |
set -euo pipefail
test -n "$GH_TOKEN"
lease_ids="$(
gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments?per_page=100" \
--paginate \
--jq ".[] | select(.user.login == \"clawsweeper[bot]\" or .user.login == \"openclaw-clawsweeper[bot]\") | select(.body | contains(\"<!-- clawsweeper-review-lease item=$ITEM_NUMBER -->\")) | select(.body | contains(\"owner=$LEASE_OWNER \")) | .id"
)"
while IFS= read -r lease_id; do
[ -n "$lease_id" ] || continue
gh api "repos/$TARGET_REPO/issues/comments/$lease_id" --method DELETE
done <<< "$lease_ids"
reaction_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')"
while IFS= read -r reaction_id; do
[ -n "$reaction_id" ] || continue
gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions/$reaction_id" --method DELETE
done <<< "$reaction_ids"
- name: Mark unsuccessful re-review
if: ${{ always() && steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.target.outputs.has_command_context == 'true' && steps.setup-pnpm.outcome == 'success' && steps.prepare-direct-exact-review-publication.outputs.failure_kind != 'github_rate_limit' && steps.prepare-direct-exact-review-publication.outputs.failure_kind != 'github_transient' && steps.direct-exact-review-publication.outputs.accepted != 'true' && steps.queue-exact-review-publication.outcome != 'success' && steps.target-write-token.outputs.token != '' }}
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: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).commandStatusMarker || '' }}
STATUS_COMMENT_ID: ${{ fromJSON(steps.claim-exact-review-queue.outputs.decision).statusCommentId || '' }}
RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
REVIEW_OUTCOME: ${{ steps.review-exact-event-item.outcome }}
REVIEW_SUPERSEDED: ${{ steps.review-exact-event-item.outputs.superseded || 'false' }}
RESERVATION_STATUS: ${{ steps.reserve-exact-review-lease.outputs.status }}
RETRY_AT: ${{ steps.reserve-exact-review-lease.outputs.retry_at || steps.review-exact-event-item.outputs.retry_at }}
CLAWSWEEPER_ACTION_LEDGER_DISABLED: "1"
run: |
state="Failed"
detail="The exact review did not produce a publishable artifact. The durable queue will retry it."
if [ "$RESERVATION_STATUS" = "superseded" ] || [ "$REVIEW_SUPERSEDED" = "true" ]; then
state="Waiting"
detail="A newer exact-review queue owner superseded this run. This run completed as a no-op."
elif [ "$RESERVATION_STATUS" = "held" ]; then
state="Waiting"
detail="Another exact-head review is already active. The durable queue will retry after its lease expires."
elif [ "$REVIEW_OUTCOME" = "cancelled" ] || [ -n "$RETRY_AT" ]; then
state="Interrupted"
detail="The exact review was interrupted. The durable queue will retry it."
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: Export exact review generation result
id: exact-review-generation-result
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && always() }}
env:
ADMISSION_RETRY: ${{ steps.live-item.outputs.admission_retry }}
RETRY_KIND: ${{ steps.live-item.outputs.retry_kind || steps.reserve-exact-review-lease.outputs.retry_kind || steps.review-exact-event-item.outputs.retry_kind }}
RETRY_AT: ${{ steps.live-item.outputs.retry_at || steps.reserve-exact-review-lease.outputs.retry_at || steps.review-exact-event-item.outputs.retry_at }}
DIRECT_PUBLICATION_FAILURE_KIND: ${{ steps.prepare-direct-exact-review-publication.outputs.failure_kind }}
DIRECT_PUBLICATION_RETRY_AT: ${{ steps.prepare-direct-exact-review-publication.outputs.retry_at }}
TARGET_ENABLED: ${{ steps.target.outputs.target_enabled }}
LIVE_OUTCOME: ${{ steps.live-item.outcome }}
SCHEDULED_SEMANTIC_NOOP: ${{ steps.live-item.outputs.scheduled_semantic_noop || 'false' }}
REVIEW_OUTCOME: ${{ steps.review-exact-event-item.outcome }}
REVIEW_SUPERSEDED: ${{ steps.review-exact-event-item.outputs.superseded || 'false' }}
RESERVATION_STATUS: ${{ steps.reserve-exact-review-lease.outputs.status }}
PUBLICATION_QUEUE_OUTCOME: ${{ steps.queue-exact-review-publication.outcome }}
DIRECT_PUBLICATION_ACCEPTED: ${{ steps.direct-exact-review-publication.outputs.accepted }}
DIRECT_PUBLICATION_SUPERSEDED: ${{ steps.direct-exact-review-publication.outputs.superseded }}
DIRECT_LIFECYCLE_OUTCOME: ${{ steps.finalize-direct-exact-review-lifecycle.outcome }}
DIRECT_LIFECYCLE_REQUEUE: ${{ steps.finalize-direct-exact-review-lifecycle.outputs.direct_lifecycle_requeue || 'false' }}
run: |
outcome=failure
requeue_latest=false
retry_kind="$RETRY_KIND"
retry_at="$RETRY_AT"
if [ "$DIRECT_PUBLICATION_FAILURE_KIND" = "github_rate_limit" ] && [ "$PUBLICATION_QUEUE_OUTCOME" != "success" ]; then
test -n "$DIRECT_PUBLICATION_RETRY_AT"
retry_kind=throttle
retry_at="$DIRECT_PUBLICATION_RETRY_AT"
fi
if [ "$ADMISSION_RETRY" = "true" ] && [ -z "$retry_kind" ]; then
outcome=success
requeue_latest=true
elif [ "$TARGET_ENABLED" = "false" ]; then
outcome=success
elif [ "$SCHEDULED_SEMANTIC_NOOP" = "true" ] && [ "$LIVE_OUTCOME" = "success" ]; then
outcome=success
elif [ "$RESERVATION_STATUS" = "superseded" ] || [ "$REVIEW_SUPERSEDED" = "true" ]; then
outcome=success
elif [ "$REVIEW_OUTCOME" = "cancelled" ]; then
outcome=cancelled
elif [ "$LIVE_OUTCOME" = "success" ] && { \
{ [ "$DIRECT_PUBLICATION_ACCEPTED" = "true" ] && [ "$DIRECT_LIFECYCLE_OUTCOME" = "success" ]; } || \
{ [ "$DIRECT_PUBLICATION_ACCEPTED" != "true" ] && [ "$PUBLICATION_QUEUE_OUTCOME" = "success" ]; }; \
}; then
outcome=success
fi
echo "outcome=$outcome" >> "$GITHUB_OUTPUT"
echo "requeue_latest=$requeue_latest" >> "$GITHUB_OUTPUT"
echo "direct_lifecycle_requeue=$DIRECT_LIFECYCLE_REQUEUE" >> "$GITHUB_OUTPUT"
echo "retry_kind=$retry_kind" >> "$GITHUB_OUTPUT"
echo "retry_at=$retry_at" >> "$GITHUB_OUTPUT"
- name: Complete exact-review queue lease
id: complete-exact-review-queue
if: ${{ steps.claim-exact-review-queue.outputs.claimed == 'true' && always() && (steps.direct-exact-review-publication.outputs.accepted != 'true' || steps.finalize-direct-exact-review-lifecycle.outcome == 'success') }}
continue-on-error: true
env:
PRIMARY_OUTCOME: ${{ steps.exact-review-generation-result.outputs.outcome || 'failure' }}
CLAIM_GENERATION: ${{ steps.claim-exact-review-queue.outputs.claim_generation }}
ITEM_KEY: ${{ steps.claim-exact-review-queue.outputs.item_key }}
PROTOCOL_VERSION: ${{ steps.claim-exact-review-queue.outputs.protocol_version }}
QUEUE_LEASE_ID: ${{ steps.claim-exact-review-queue.outputs.lease_id }}
QUEUE_LEASE_REVISION: ${{ steps.claim-exact-review-queue.outputs.lease_revision }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
REQUEUE_LATEST: ${{ steps.exact-review-generation-result.outputs.requeue_latest }}
RETRY_KIND: ${{ steps.exact-review-generation-result.outputs.retry_kind }}
RETRY_AT: ${{ steps.exact-review-generation-result.outputs.retry_at }}
DIRECT_PUBLICATION_ACCEPTED: ${{ steps.direct-exact-review-publication.outputs.accepted }}
DIRECT_PUBLICATION_SUPERSEDED: ${{ steps.direct-exact-review-publication.outputs.superseded }}
DIRECT_LIFECYCLE_OUTCOME: ${{ steps.finalize-direct-exact-review-lifecycle.outcome }}
DIRECT_LIFECYCLE_REQUEUE: ${{ steps.exact-review-generation-result.outputs.direct_lifecycle_requeue }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
set -euo pipefail
test -n "$QUEUE_LEASE_ID"
queue_url="${QUEUE_URL%/}"
payload="$(node -e '
const runAttempt = Number(process.env.RUN_ATTEMPT);
const protocolVersion = Number(process.env.PROTOCOL_VERSION);
const leaseRevision = Number(process.env.QUEUE_LEASE_REVISION);
const claimGeneration = Number(process.env.CLAIM_GENERATION);
if (!Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1);
if (protocolVersion !== 1 && protocolVersion !== 2) process.exit(1);
if (
protocolVersion === 2 &&
(!process.env.ITEM_KEY ||
!Number.isInteger(leaseRevision) ||
leaseRevision < 1 ||
!Number.isInteger(claimGeneration) ||
claimGeneration < 1)
) process.exit(1);
const primaryOutcome = String(process.env.PRIMARY_OUTCOME || "");
const outcome = ["success", "cancelled", "failure"].includes(primaryOutcome)
? primaryOutcome
: "failure";
const requeueLatest = process.env.REQUEUE_LATEST === "true";
const retryKind = String(process.env.RETRY_KIND || "").trim();
const retryAt = String(process.env.RETRY_AT || "").trim();
if (retryKind && !["coordination", "throttle"].includes(retryKind)) process.exit(1);
if (retryKind && !retryAt) process.exit(1);
const directPublicationCompleted =
process.env.DIRECT_PUBLICATION_ACCEPTED === "true" &&
process.env.DIRECT_LIFECYCLE_OUTCOME === "success";
const directPublicationSuperseded =
directPublicationCompleted && process.env.DIRECT_PUBLICATION_SUPERSEDED === "true";
const directLifecycleRequeue =
directPublicationCompleted && process.env.DIRECT_LIFECYCLE_REQUEUE === "true";
if (requeueLatest && directLifecycleRequeue) process.exit(1);
process.stdout.write(JSON.stringify({
lease_id: process.env.QUEUE_LEASE_ID,
...(protocolVersion === 2
? {
item_key: process.env.ITEM_KEY,
lease_revision: leaseRevision,
claim_generation: claimGeneration,
}
: {}),
run_id: process.env.GITHUB_RUN_ID,
run_attempt: runAttempt,
outcome,
...(requeueLatest ? { requeue_latest: true } : {}),
...(retryKind ? { retry_kind: retryKind } : {}),
...(directPublicationCompleted
? directPublicationSuperseded
? { completion_kind: "superseded", reason_code: "remote_newer_tuple" }
: { completion_kind: "published", reason_code: "publication_applied" }
: {}),
...(directLifecycleRequeue ? { direct_lifecycle_requeue: true } : {}),
...(retryAt ? { retry_at: retryAt } : {}),
}));
')"
response_file="$(mktemp)"
error_file="$(mktemp)"
trap 'rm -f "$response_file" "$error_file"' EXIT
for attempt in 1 2 3; do
: > "$response_file"
: > "$error_file"
if status="$(curl --silent --show-error --connect-timeout 5 --max-time 20 \
--output "$response_file" \
--write-out '%{http_code}' \
--request POST \
--header "content-type: application/json" \
--data "$payload" \
"$queue_url/internal/exact-review/complete" 2>"$error_file")"; then
response="$(<"$response_file")"
if [[ "$status" == 2* ]]; then
exit 0
fi
# Only this completion-specific conflict is emitted after the
# durable queue verifies that this exact v2 lease tuple was fenced
# by a newer source revision. Every generic ownership conflict
# stays visible so a malformed or mismatched callback cannot turn
# a failed completion into a green workflow.
if [ "$status" = "409" ]; then
conflict_reason="$(RESPONSE="$response" node <<'NODE'
const response = JSON.parse(process.env.RESPONSE || "{}");
const safeConflicts = new Set(["lease_superseded"]);
if (!safeConflicts.has(response.error)) process.exit(1);
process.stdout.write(response.error);
NODE
)" || {
echo "Unexpected exact-review completion conflict: $response" >&2
exit 1
}
echo "::notice::Exact-review completion skipped because its lease was superseded: $conflict_reason"
exit 0
fi
echo "Exact-review completion returned HTTP $status: $response" >&2
if [[ "$status" != 5* ]]; then
exit 1
fi
else
cat "$error_file" >&2
fi
if [ "$attempt" -lt 3 ]; then
sleep "$((attempt * 5))"
fi
done
exit 1
- name: Submit direct GitHub egress telemetry
if: ${{ always() && steps.claim-exact-review-queue.outputs.claimed == 'true' && steps.direct-github-egress-observer.outcome == 'success' }}
continue-on-error: true
env:
EXACT_REVIEW_QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
run: pnpm run --silent repair:github-egress-telemetry submit
- name: Fail unsuccessful exact review generation
# A held review lease is a successful deferral only after the durable queue
# accepts retry ownership; queue completion failures must remain visible.
if: >-
${{
always() &&
steps.claim-exact-review-queue.outputs.claimed == 'true' &&
(
(
steps.direct-exact-review-publication.outputs.accepted != 'true' &&
steps.complete-exact-review-queue.outcome != 'success' &&
steps.reserve-exact-review-lease.outputs.status != 'superseded' &&
steps.review-exact-event-item.outputs.superseded != 'true'
) ||
(
steps.exact-review-generation-result.outputs.outcome != 'success' &&
steps.exact-review-generation-result.outputs.retry_kind == '' &&
steps.reserve-exact-review-lease.outputs.status != 'held' &&
steps.reserve-exact-review-lease.outputs.status != 'superseded'
)
)
}}
env:
RESERVATION_STATUS: ${{ steps.reserve-exact-review-lease.outputs.status || 'unknown' }}
REVIEW_EXIT_CODE: ${{ steps.review-exact-event-item.outputs.exit_code || 'unknown' }}
REVIEW_OUTCOME: ${{ steps.review-exact-event-item.outcome || 'not_started' }}
run: |
echo "::error::Exact review generation failed: classification=codex_or_content_failure reservation=$RESERVATION_STATUS review_outcome=$REVIEW_OUTCOME review_exit=$REVIEW_EXIT_CODE"
exit 1
event-review-publish:
name: Publish exact review artifact
if: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.queue_lease_id != '' && github.event.client_payload.source_action == 'exact_review_artifact_publish' }}
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
actions: write
contents: write
issues: write
pull-requests: write
statuses: read
steps:
- name: Claim durable exact review publication
id: publication-context
env:
ITEM_KEY: ${{ github.event.client_payload.queue_claim.item_key }}
QUEUE_LEASE_ID: ${{ github.event.client_payload.queue_lease_id }}
QUEUE_LEASE_REVISION: ${{ github.event.client_payload.queue_claim.lease_revision }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
set -euo pipefail
printf 'claimed=false\ndecision={}\n' >> "$GITHUB_OUTPUT"
queue_url="${QUEUE_URL%/}"
payload="$(node -e '
const leaseRevision = Number(process.env.QUEUE_LEASE_REVISION);
const runAttempt = Number(process.env.RUN_ATTEMPT);
if (!process.env.QUEUE_LEASE_ID || !process.env.ITEM_KEY) process.exit(1);
if (!Number.isInteger(leaseRevision) || leaseRevision < 1) process.exit(1);
if (!Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1);
process.stdout.write(JSON.stringify({
lease_id: process.env.QUEUE_LEASE_ID,
item_key: process.env.ITEM_KEY,
lease_revision: leaseRevision,
run_id: process.env.GITHUB_RUN_ID,
run_attempt: runAttempt,
}));
')"
response_file="$(mktemp)"
error_file="$(mktemp)"
trap 'rm -f "$response_file" "$error_file"' EXIT
for attempt in 1 2 3; do
: > "$response_file"
: > "$error_file"
if status="$(curl --silent --show-error --connect-timeout 5 --max-time 20 \
--output "$response_file" \
--write-out '%{http_code}' \
--request POST \
--header "content-type: application/json" \
--data "$payload" \
"$queue_url/internal/exact-review/claim" 2>"$error_file")"; then
response="$(<"$response_file")"
if [ "$status" = "409" ]; then
conflict_reason="$(RESPONSE="$response" node <<'NODE'
const response = JSON.parse(process.env.RESPONSE || "{}");
const safeConflicts = new Set([
"lease_not_active",
"lease_already_claimed",
"lease_decision_unavailable",
"stale_run_attempt",
]);
if (!safeConflicts.has(response.error)) process.exit(1);
process.stdout.write(response.error);
NODE
)" || {
echo "Unexpected exact-review publication conflict: $response" >&2
exit 1
}
echo "::notice::Skipping exact review publication because lease claim lost safely: $conflict_reason"
exit 0
fi
if [ "$status" != "200" ]; then
if [[ "$status" != 5* ]]; then
echo "Exact-review publication claim returned HTTP $status: $response" >&2
exit 1
fi
elif RESPONSE="$response" node <<'NODE'
const fs = require("node:fs");
const response = JSON.parse(process.env.RESPONSE || "{}");
const decision = response.decision;
const publication = decision?.publication;
const producerDecision = publication?.producerDecision;
const leaseRevision = Number(response.lease_revision);
const claimGeneration = Number(response.claim_generation);
const repeatRevision = response.repeat_revision;
const publicationLeaseRevision = Number(publication?.leaseRevision);
const directItemKey = `${decision?.targetRepo}#${decision?.itemNumber}`;
const expectedDeferredItemKey = `${directItemKey}@publish:${publication?.producerRunId}:${publication?.producerRunAttempt}`;
// Direct publication converts its already-leased base item into a
// recoverable publication. Its saved revision must still match
// this publisher lease before any external router handoff;
// deferred publication has its own key.
const directLifecycleRecovery =
response.item_key === directItemKey &&
publication?.itemKey === directItemKey &&
Number.isInteger(publicationLeaseRevision) &&
publicationLeaseRevision === leaseRevision;
const directLifecycle = directLifecycleRecovery ? publication?.directLifecycle : null;
const directLifecyclePlan = directLifecycle?.plan;
const directLifecycleReceiptOutcome = directLifecycle?.receiptOutcome;
const directLifecycleKinds = new Set([
"router",
"router_deferred_coverage",
"router_not_required",
"requeue",
"target_missing",
"target_closed",
"guarded_open",
"policy_noop",
]);
// Pre-projection rows have no saved post-effect intent. They
// deliberately retain artifact recovery rather than inferring a
// router or terminal result from a newer publisher run.
const directLifecycleRecoveryReady =
directLifecycleRecovery &&
directLifecyclePlan &&
typeof directLifecyclePlan === "object" &&
!Array.isArray(directLifecyclePlan) &&
Object.keys(directLifecyclePlan).length === 1 &&
directLifecycleKinds.has(directLifecyclePlan.kind) &&
["accepted", "deduped", "superseded"].includes(directLifecycleReceiptOutcome);
const deferredPublication = response.item_key === expectedDeferredItemKey;
if (
response.claimed !== true ||
response.protocol_version !== 2 ||
typeof repeatRevision !== "boolean" ||
response.item_key !== process.env.ITEM_KEY ||
(!deferredPublication && !directLifecycleRecovery) ||
decision?.sourceAction !== "exact_review_artifact_publish" ||
!publication ||
!producerDecision ||
producerDecision.targetRepo !== decision.targetRepo ||
producerDecision.targetBranch !== decision.targetBranch ||
producerDecision.itemNumber !== decision.itemNumber ||
producerDecision.itemKind !== decision.itemKind
) process.exit(1);
if (!Number.isInteger(leaseRevision) || leaseRevision < 1) process.exit(1);
if (!Number.isInteger(claimGeneration) || claimGeneration < 1) process.exit(1);
const targetRepo = String(decision.targetRepo);
const [targetRepoOwner, targetRepoName] = targetRepo.split("/");
const targetSlug = targetRepo.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, "-");
const values = {
artifact_name: publication.artifactName,
generation_attempt: publication.producerRunAttempt,
producer_run_id: publication.producerRunId,
source_sha: publication.sourceSha,
decision: JSON.stringify(producerDecision),
item_key: publication.itemKey,
protocol_version: publication.protocolVersion,
lease_revision: publication.leaseRevision ?? "",
claim_generation: publication.claimGeneration ?? "",
target_repo: targetRepo,
target_repo_owner: targetRepoOwner,
target_repo_name: targetRepoName,
target_slug: targetSlug,
target_branch: decision.targetBranch,
item_number: decision.itemNumber,
item_kind: decision.itemKind,
has_command_context: Boolean(
producerDecision.commandStatusMarker || producerDecision.statusCommentId,
),
live_proceeded: publication.liveProceeded,
live_terminal_noop: publication.liveTerminalNoop,
live_terminal_missing: publication.liveTerminalMissing,
live_guarded_open: publication.liveGuardedOpen,
publisher_lease_id: process.env.QUEUE_LEASE_ID,
publisher_item_key: response.item_key,
publisher_lease_revision: leaseRevision,
publisher_claim_generation: claimGeneration,
repeat_revision: repeatRevision,
direct_lifecycle_recovery: directLifecycleRecoveryReady,
direct_lifecycle_plan: directLifecycleRecoveryReady
? JSON.stringify(directLifecyclePlan)
: "",
direct_lifecycle_receipt_outcome: directLifecycleRecoveryReady
? directLifecycleReceiptOutcome
: "",
};
fs.appendFileSync(process.env.GITHUB_OUTPUT, "claimed=true\n");
for (const [key, value] of Object.entries(values)) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`);
}
NODE
then
exit 0
else
echo "Exact-review publication claim returned an invalid success payload." >&2
exit 1
fi
else
cat "$error_file" >&2
fi
if [ "$attempt" -lt 3 ]; then
sleep "$((attempt * 5))"
fi
done
exit 1
- name: Replay committed direct lifecycle handoff
id: replay-direct-lifecycle
# A direct receipt already owns the GitHub effect. Its persisted plan is
# the only safe source for post-effect recovery; old receipts fall back
# to the artifact path above rather than being inferred here.
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }}
TARGET_BRANCH: ${{ steps.publication-context.outputs.target_branch }}
ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }}
FENCE_KEY: ${{ steps.publication-context.outputs.publisher_item_key }}
REVISION: ${{ steps.publication-context.outputs.publisher_lease_revision }}
CLAIM_DECISION: ${{ steps.publication-context.outputs.decision }}
DIRECT_LIFECYCLE_PLAN: ${{ steps.publication-context.outputs.direct_lifecycle_plan }}
DIRECT_LIFECYCLE_RECEIPT_OUTCOME: ${{ steps.publication-context.outputs.direct_lifecycle_receipt_outcome }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES: ${{ vars.CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES || '180' }}
CLAWSWEEPER_COMMENT_MAX_COMMENTS: ${{ vars.CLAWSWEEPER_COMMENT_MAX_COMMENTS || '1000' }}
run: |
set -euo pipefail
test -n "$CLAWSWEEPER_WEBHOOK_SECRET"
queue_url="${QUEUE_URL%/}"
lifecycle_kind="$(DIRECT_LIFECYCLE_PLAN="$DIRECT_LIFECYCLE_PLAN" node -e '
const plan = JSON.parse(process.env.DIRECT_LIFECYCLE_PLAN || "");
const kinds = new Set([
"router", "router_deferred_coverage", "router_not_required", "requeue",
"target_missing", "target_closed", "guarded_open", "policy_noop",
]);
if (!plan || typeof plan !== "object" || Array.isArray(plan) || Object.keys(plan).length !== 1 || !kinds.has(plan.kind)) process.exit(1);
process.stdout.write(plan.kind);
')"
receipt_outcome="$DIRECT_LIFECYCLE_RECEIPT_OUTCOME"
case "$receipt_outcome" in
accepted|deduped|superseded) ;;
*) exit 1 ;;
esac
post_router_receipt() {
local outcome="$1"
local deferred_coverage="$2"
lifecycle_payload="$(LIFECYCLE_ROUTER_OUTCOME="$outcome" LIFECYCLE_DEFERRED_COVERAGE="$deferred_coverage" node -e '
const revision = Number(process.env.REVISION);
if (!Number.isInteger(revision) || revision < 1 || !process.env.FENCE_KEY) process.exit(1);
const prefix = process.env.LIFECYCLE_DEFERRED_COVERAGE === "true"
? "router-direct-recovery-proof"
: process.env.LIFECYCLE_ROUTER_OUTCOME === "not_required"
? "router-direct-recovery-not-required"
: "router-direct-recovery";
process.stdout.write(JSON.stringify({
canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`,
fence_key: process.env.FENCE_KEY,
revision,
outcome: process.env.LIFECYCLE_ROUTER_OUTCOME,
receipt_id: `${prefix}:${process.env.GITHUB_RUN_ID}:${process.env.GITHUB_RUN_ATTEMPT}`,
}));
')"
lifecycle_signature="$(PAYLOAD="$lifecycle_payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')"
lifecycle_response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST \
--header "content-type: application/json" \
--header "x-clawsweeper-exact-review-signature: $lifecycle_signature" \
--data "$lifecycle_payload" \
"$queue_url/internal/exact-review/lifecycle/router-receipt")"
jq -e '.ok == true' <<<"$lifecycle_response" >/dev/null
}
post_terminal_disposition() {
local terminal_kind="$1"
lifecycle_payload="$(LIFECYCLE_TERMINAL="$terminal_kind" node -e '
const revision = Number(process.env.REVISION);
if (!Number.isInteger(revision) || revision < 1 || !process.env.FENCE_KEY || !process.env.LIFECYCLE_TERMINAL) process.exit(1);
process.stdout.write(JSON.stringify({
canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`,
fence_key: process.env.FENCE_KEY,
revision,
kind: process.env.LIFECYCLE_TERMINAL,
}));
')"
lifecycle_signature="$(PAYLOAD="$lifecycle_payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')"
lifecycle_response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST \
--header "content-type: application/json" \
--header "x-clawsweeper-exact-review-signature: $lifecycle_signature" \
--data "$lifecycle_payload" \
"$queue_url/internal/exact-review/lifecycle/terminal-disposition")"
jq -e '.ok == true' <<<"$lifecycle_response" >/dev/null
}
completion_kind=published
reason_code=publication_applied
requeue_latest=false
direct_requeue=false
if [ "$receipt_outcome" = "superseded" ]; then
# The direct receipt already committed the terminal supersession.
# Do not manufacture a router, requeue, or acknowledgement attempt.
completion_kind=superseded
reason_code=remote_newer_tuple
else
case "$lifecycle_kind" in
router)
gh workflow run repair-comment-router.yml \
--repo "$GITHUB_REPOSITORY" \
--ref main \
-f execute=true \
-f target_repo="$TARGET_REPO" \
-f target_branch="$TARGET_BRANCH" \
-f item_numbers="$ITEM_NUMBER" \
-f lookback_minutes="$CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES" \
-f max_comments="$CLAWSWEEPER_COMMENT_MAX_COMMENTS"
post_router_receipt durable false
;;
router_deferred_coverage)
post_router_receipt durable true
;;
router_not_required)
post_router_receipt not_required false
;;
requeue)
post_terminal_disposition requeue
# The completion endpoint atomically changes this exact direct
# publication into a new fenced source-drift review revision.
# A separate enqueue here could be ignored while this lease is
# still active and would leave the old receipt replayable.
direct_requeue=true
;;
target_missing|target_closed|guarded_open|policy_noop)
post_terminal_disposition "$lifecycle_kind"
;;
*) exit 1 ;;
esac
fi
{
echo "outcome=success"
echo "completion_kind=$completion_kind"
echo "reason_code=$reason_code"
echo "requeue_latest=$requeue_latest"
echo "direct_requeue=$direct_requeue"
} >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v7
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' }}
with:
# The artifact validator binds the producer SHA. The publisher itself
# must run the current repaired implementation, not historical code.
ref: main
filter: blob:none
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup-action-ledger
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' }}
continue-on-error: true
- uses: ./.github/actions/setup-pnpm
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' }}
id: setup-publish-pnpm
with:
build-script: build:all
- uses: ./.github/actions/setup-github-egress-observer
id: artifact-github-egress-observer
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.setup-publish-pnpm.outcome == 'success' }}
continue-on-error: true
with:
metrics-path: .artifacts/artifact-publication/github-egress-v2.jsonl
rate-limit-path: .artifacts/artifact-publication/github-rate-limit-details-v2.jsonl
pool-class: target_app
stage: publication_apply
source-action: ${{ fromJSON(steps.publication-context.outputs.decision).sourceAction }}
claim-generation: ${{ steps.publication-context.outputs.publisher_claim_generation }}
repeat-revision: ${{ steps.publication-context.outputs.repeat_revision }}
- name: Record artifact-publication member
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.artifact-github-egress-observer.outcome == 'success' }}
continue-on-error: true
env:
TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }}
run: node dist/github-egress-observer-cli.js record-member
- name: Download exact review artifact bundle
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' }}
id: download-exact-review-bundle
continue-on-error: true
uses: actions/download-artifact@v8
with:
name: ${{ steps.publication-context.outputs.artifact_name }}
run-id: ${{ steps.publication-context.outputs.producer_run_id }}
github-token: ${{ github.token }}
repository: ${{ github.repository }}
path: .artifacts/exact-review-bundle
- name: Record artifact download transport boundary
if: ${{ always() && steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.artifact-github-egress-observer.outcome == 'success' && steps.download-exact-review-bundle.outcome != 'skipped' }}
continue-on-error: true
env:
CLAWSWEEPER_GITHUB_POOL_CLASS: repository_actions
CLAWSWEEPER_GITHUB_STAGE: publication_prepare
run: node dist/github-egress-observer-cli.js record-unobserved -- run download
- name: Validate exact review artifact bundle
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.download-exact-review-bundle.outcome == 'success' }}
id: validate-exact-review-bundle
continue-on-error: true
env:
EXACT_REVIEW_BUNDLE_DIR: .artifacts/exact-review-bundle
EXACT_REVIEW_CLAIM_GENERATION: ${{ steps.publication-context.outputs.claim_generation }}
EXACT_REVIEW_DECISION: ${{ steps.publication-context.outputs.decision }}
EXACT_REVIEW_GENERATION_ATTEMPT: ${{ steps.publication-context.outputs.generation_attempt }}
EXACT_REVIEW_ITEM_KEY: ${{ steps.publication-context.outputs.item_key }}
EXACT_REVIEW_ITEM_KIND: ${{ steps.publication-context.outputs.item_kind }}
EXACT_REVIEW_ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }}
EXACT_REVIEW_LEASE_REVISION: ${{ steps.publication-context.outputs.lease_revision }}
EXACT_REVIEW_LIVE_GUARDED_OPEN: ${{ steps.publication-context.outputs.live_guarded_open }}
EXACT_REVIEW_LIVE_PROCEEDED: ${{ steps.publication-context.outputs.live_proceeded }}
EXACT_REVIEW_LIVE_TERMINAL_MISSING: ${{ steps.publication-context.outputs.live_terminal_missing }}
EXACT_REVIEW_LIVE_TERMINAL_NOOP: ${{ steps.publication-context.outputs.live_terminal_noop }}
EXACT_REVIEW_PRODUCER_JOB: event-review-apply
EXACT_REVIEW_PRODUCER_RUN_ID: ${{ steps.publication-context.outputs.producer_run_id }}
EXACT_REVIEW_PROTOCOL_VERSION: ${{ steps.publication-context.outputs.protocol_version }}
EXACT_REVIEW_SOURCE_SHA: ${{ steps.publication-context.outputs.source_sha }}
EXACT_REVIEW_TARGET_BRANCH: ${{ steps.publication-context.outputs.target_branch }}
EXACT_REVIEW_TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }}
run: pnpm run --silent repair:exact-review-bundle validate
- name: Identify legacy tuple-less exact artifact
id: legacy-exact-artifact
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.validate-exact-review-bundle.outcome == 'success' }}
env:
REPORT_PATH: .artifacts/exact-review-bundle/review/${{ steps.publication-context.outputs.item_number }}.md
run: |
set -euo pipefail
REPORT_PATH="$REPORT_PATH" node <<'NODE'
const fs = require("node:fs");
const reportPath = process.env.REPORT_PATH;
let legacyTupleless = false;
if (fs.existsSync(reportPath)) {
const markdown = fs.readFileSync(reportPath, "utf8");
const owner = /^review_lease_owner:\s*(.+)\s*$/m.exec(markdown)?.[1]?.trim() || "";
const commentId = Number(
/^review_lease_comment_id:\s*(\d+)\s*$/m.exec(markdown)?.[1] || "0",
);
legacyTupleless =
(!owner || owner === "unknown") &&
(!Number.isInteger(commentId) || commentId <= 0);
}
fs.appendFileSync(
process.env.GITHUB_OUTPUT,
`legacy_tupleless=${legacyTupleless ? "true" : "false"}\n`,
);
NODE
- name: Stage validated exact review artifact
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.validate-exact-review-bundle.outcome == 'success' }}
run: |
set -euo pipefail
mkdir -p artifacts/event
report=".artifacts/exact-review-bundle/review/${{ steps.publication-context.outputs.item_number }}.md"
if [ -f "$report" ]; then
cp "$report" "artifacts/event/${{ steps.publication-context.outputs.item_number }}.md"
fi
- name: Create target write token
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.validate-exact-review-bundle.outcome == 'success' }}
id: target-write-token
continue-on-error: true
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
owner: ${{ steps.publication-context.outputs.target_repo_owner }}
repositories: ${{ steps.publication-context.outputs.target_repo_name }}
permission-checks: read
permission-contents: write
permission-issues: write
permission-pull-requests: write
permission-statuses: read
- uses: ./.github/actions/setup-state
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.validate-exact-review-bundle.outcome == 'success' && steps.legacy-exact-artifact.outputs.legacy_tupleless != 'true' }}
id: setup-state
with:
coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
records-repo-slugs: ${{ steps.publication-context.outputs.target_slug }}
records-item-number: ${{ steps.publication-context.outputs.item_number }}
hydrate-git-state: "false"
hydrate-state-blobs: "false"
- name: Publish event result and apply safe close
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.validate-exact-review-bundle.outcome == 'success' && steps.legacy-exact-artifact.outputs.legacy_tupleless != 'true' }}
id: publish-event-result
env:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
GH_TOKEN: ${{ steps.target-write-token.outputs.token }}
CLAWSWEEPER_PUBLIC_GH_TOKEN: ${{ steps.publication-context.outputs.target_repo == 'openclaw/openclaw' && github.token || '' }}
REPO_TOKEN: ${{ github.token }}
TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }}
MIN_AGE_MINUTES: "0"
REVIEW_ONLY: ${{ fromJSON(steps.publication-context.outputs.decision).sourceAction == 'failed_review_shard_recovery' && 'true' || 'false' }}
EXACT_EVENT_PUBLICATION: "true"
EXACT_REVIEW_CLOSE_COVERAGE_DEFERRED: "true"
LIVE_PROCEEDED: ${{ steps.publication-context.outputs.live_proceeded }}
LIVE_TERMINAL_NOOP: ${{ steps.publication-context.outputs.live_terminal_noop }}
LIVE_TERMINAL_MISSING: ${{ steps.publication-context.outputs.live_terminal_missing }}
LIVE_GUARDED_OPEN: ${{ steps.publication-context.outputs.live_guarded_open }}
EXACT_REVIEW_QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
EXACT_REVIEW_LEASE_ID: ${{ steps.publication-context.outputs.publisher_lease_id }}
EXACT_REVIEW_ITEM_KEY: ${{ steps.publication-context.outputs.publisher_item_key }}
EXACT_REVIEW_LEASE_REVISION: ${{ steps.publication-context.outputs.publisher_lease_revision }}
EXACT_REVIEW_CLAIM_GENERATION: ${{ steps.publication-context.outputs.publisher_claim_generation }}
run: |
set -euo pipefail
record_publication_failure_kind() {
local error_file="$1"
# Only explicit GitHub pressure signals may change global admission;
# artifact, tuple, and policy failures must remain ordinary retries.
if grep -Eqi 'HTTP 429|secondary rate limit|API rate limit|rate limit (exceeded|reached)' "$error_file"; then
echo "failure_kind=github_rate_limit" >> "$GITHUB_OUTPUT"
elif grep -Eqi 'HTTP 5[0-9]{2}|status( code)? 5[0-9]{2}|Bad Gateway|Service Unavailable|Gateway Timeout' "$error_file"; then
echo "failure_kind=github_transient" >> "$GITHUB_OUTPUT"
fi
}
if [ "$LIVE_PROCEEDED" != "true" ]; then
terminal_noop="$LIVE_TERMINAL_NOOP"
terminal_missing="$LIVE_TERMINAL_MISSING"
guarded_open="$LIVE_GUARDED_OPEN"
requeue_latest=false
if [ "$LIVE_TERMINAL_NOOP" = "true" ] || [ "$LIVE_TERMINAL_MISSING" = "true" ] || [ "$LIVE_GUARDED_OPEN" = "true" ]; then
live_item_error="$(mktemp)"
live_locked=false
if ! live_item="$(gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER" 2>"$live_item_error")"; then
if grep -Eqi 'HTTP 404|Not Found' "$live_item_error" && gh api "repos/$TARGET_REPO" >/dev/null; then
live_state=missing
else
record_publication_failure_kind "$live_item_error"
cat "$live_item_error" >&2
rm -f "$live_item_error"
exit 1
fi
else
live_state="$(jq -r '.state' <<< "$live_item")"
live_locked="$(jq -r '.locked == true' <<< "$live_item")"
fi
rm -f "$live_item_error"
case "$live_state" in
open)
terminal_noop=false
terminal_missing=false
if [ "$live_locked" = "true" ]; then
guarded_open=true
else
guarded_open=false
requeue_latest=true
fi
;;
closed)
terminal_noop=true
terminal_missing=false
guarded_open=false
;;
missing)
terminal_noop=false
terminal_missing=true
guarded_open=false
;;
*)
echo "Unexpected live state for $TARGET_REPO#$ITEM_NUMBER: $live_state" >&2
exit 1
;;
esac
fi
{
echo "terminal_noop=$terminal_noop"
echo "terminal_missing=$terminal_missing"
echo "terminal_closed=false"
echo "guarded_open=$guarded_open"
echo "policy_noop=false"
echo "requeue_latest=$requeue_latest"
echo "remote_tuple_verified=false"
echo "routing_deferred=false"
} >> "$GITHUB_OUTPUT"
exit 0
fi
live_item_error="$(mktemp)"
if ! live_state="$(gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER" --jq .state 2>"$live_item_error")"; then
if grep -Eqi 'HTTP 404|Not Found' "$live_item_error" && gh api "repos/$TARGET_REPO" >/dev/null; then
rm -f "$live_item_error"
{
echo "terminal_noop=false"
echo "terminal_missing=true"
echo "terminal_closed=false"
echo "guarded_open=false"
echo "policy_noop=false"
echo "requeue_latest=false"
echo "remote_tuple_verified=false"
echo "routing_deferred=false"
} >> "$GITHUB_OUTPUT"
exit 0
fi
record_publication_failure_kind "$live_item_error"
cat "$live_item_error" >&2
rm -f "$live_item_error"
exit 1
fi
rm -f "$live_item_error"
if [ "$live_state" = "closed" ]; then
{
echo "terminal_noop=true"
echo "terminal_missing=false"
echo "terminal_closed=false"
echo "guarded_open=false"
echo "policy_noop=false"
echo "requeue_latest=false"
echo "remote_tuple_verified=false"
echo "routing_deferred=false"
} >> "$GITHUB_OUTPUT"
exit 0
fi
test -f "artifacts/event/$ITEM_NUMBER.md"
echo "terminal_noop=false" >> "$GITHUB_OUTPUT"
publication_error="$(mktemp)"
set +e
pnpm run repair:publish-event-result 2>&1 | tee "$publication_error"
publication_status="${PIPESTATUS[0]}"
set -e
if [ "$publication_status" -ne 0 ]; then
record_publication_failure_kind "$publication_error"
rm -f "$publication_error"
exit "$publication_status"
fi
rm -f "$publication_error"
- name: Dispatch recommended live proofs
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.validate-exact-review-bundle.outcome == 'success' && steps.legacy-exact-artifact.outputs.legacy_tupleless != 'true' && steps.publish-event-result.outcome == 'success' && (steps.publish-event-result.outputs.remote_tuple_verified == 'true' || (steps.publish-event-result.outputs.completion_kind == 'deferred' && steps.publish-event-result.outputs.reason_code == 'close_coverage_deferred')) }}
uses: ./.github/actions/dispatch-live-proofs
with:
target-repo: ${{ steps.publication-context.outputs.target_repo }}
item-numbers: ${{ steps.publication-context.outputs.item_number }}
records-root: records
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
- name: Record fallback canonical exact review lifecycle receipt
id: record-fallback-canonical-lifecycle-receipt
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.publish-event-result.outcome == 'success' && (steps.publish-event-result.outputs.remote_tuple_verified == 'true' || (steps.publish-event-result.outputs.completion_kind == 'deferred' && steps.publish-event-result.outputs.reason_code == 'close_coverage_deferred')) }}
env:
TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }}
FENCE_KEY: ${{ steps.publication-context.outputs.publisher_item_key }}
REVISION: ${{ steps.publication-context.outputs.publisher_lease_revision }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
run: |
set -euo pipefail
payload="$(node -e '
const revision = Number(process.env.REVISION);
if (!Number.isInteger(revision) || revision < 1 || !process.env.FENCE_KEY) process.exit(1);
process.stdout.write(JSON.stringify({
canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`,
fence_key: process.env.FENCE_KEY,
revision,
outcome: "accepted",
receipt_id: `fallback:${process.env.GITHUB_RUN_ID}:${process.env.GITHUB_RUN_ATTEMPT}`,
}));
')"
signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')"
queue_url="${QUEUE_URL%/}"
curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST \
--header "content-type: application/json" \
--header "x-clawsweeper-exact-review-signature: $signature" \
--data "$payload" \
"$queue_url/internal/exact-review/lifecycle/canonical-receipt" >/dev/null
- name: Queue deferred exact verdict router
id: queue-deferred-verdict-router
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && fromJSON(steps.publication-context.outputs.decision).sourceAction != 'failed_review_shard_recovery' && steps.publish-event-result.outcome == 'success' && steps.publish-event-result.outputs.remote_tuple_verified == 'true' && steps.publish-event-result.outputs.routing_deferred == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
CLAWSWEEPER_GITHUB_POOL_CLASS: repository_actions
CLAWSWEEPER_GITHUB_STAGE: publication_router
TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }}
TARGET_BRANCH: ${{ steps.publication-context.outputs.target_branch }}
ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }}
FENCE_KEY: ${{ steps.publication-context.outputs.publisher_item_key }}
REVISION: ${{ steps.publication-context.outputs.publisher_lease_revision }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES: ${{ vars.CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES || '180' }}
CLAWSWEEPER_COMMENT_MAX_COMMENTS: ${{ vars.CLAWSWEEPER_COMMENT_MAX_COMMENTS || '1000' }}
run: |
gh workflow run repair-comment-router.yml \
--repo "$GITHUB_REPOSITORY" \
--ref main \
-f execute=true \
-f target_repo="$TARGET_REPO" \
-f target_branch="$TARGET_BRANCH" \
-f item_numbers="$ITEM_NUMBER" \
-f lookback_minutes="$CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES" \
-f max_comments="$CLAWSWEEPER_COMMENT_MAX_COMMENTS"
payload="$(node -e '
const revision = Number(process.env.REVISION);
if (!Number.isInteger(revision) || revision < 1 || !process.env.FENCE_KEY) process.exit(1);
process.stdout.write(JSON.stringify({
canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`,
fence_key: process.env.FENCE_KEY,
revision,
receipt_id: `router:${process.env.GITHUB_RUN_ID}:${process.env.GITHUB_RUN_ATTEMPT}`,
}));
')"
signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')"
queue_url="${QUEUE_URL%/}"
curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST \
--header "content-type: application/json" \
--header "x-clawsweeper-exact-review-signature: $signature" \
--data "$payload" \
"$queue_url/internal/exact-review/lifecycle/router-receipt" >/dev/null
- name: Record deferred close-proof exact review lifecycle receipt
id: record-deferred-close-proof-lifecycle-receipt
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.publish-event-result.outcome == 'success' && steps.publish-event-result.outputs.completion_kind == 'deferred' && steps.publish-event-result.outputs.reason_code == 'close_coverage_deferred' }}
env:
TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }}
FENCE_KEY: ${{ steps.publication-context.outputs.publisher_item_key }}
REVISION: ${{ steps.publication-context.outputs.publisher_lease_revision }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
run: |
set -euo pipefail
payload="$(node -e '
const revision = Number(process.env.REVISION);
if (!Number.isInteger(revision) || revision < 1 || !process.env.FENCE_KEY) process.exit(1);
process.stdout.write(JSON.stringify({
canonical_target_key: process.env.TARGET_REPO + "#" + process.env.ITEM_NUMBER,
fence_key: process.env.FENCE_KEY,
revision,
outcome: "durable",
receipt_id: "router-proof:" + process.env.GITHUB_RUN_ID + ":" + process.env.GITHUB_RUN_ATTEMPT,
}));
')"
signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write("sha256="+crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex"))')"
queue_url="${QUEUE_URL%/}"
curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST \
--header "content-type: application/json" \
--header "x-clawsweeper-exact-review-signature: $signature" \
--data "$payload" \
"$queue_url/internal/exact-review/lifecycle/router-receipt" >/dev/null
- name: Record no-router exact review lifecycle receipt
id: record-no-router-lifecycle-receipt
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && fromJSON(steps.publication-context.outputs.decision).sourceAction == 'failed_review_shard_recovery' && steps.publish-event-result.outcome == 'success' && steps.publish-event-result.outputs.remote_tuple_verified == 'true' }}
env:
TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }}
FENCE_KEY: ${{ steps.publication-context.outputs.publisher_item_key }}
REVISION: ${{ steps.publication-context.outputs.publisher_lease_revision }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
run: |
set -euo pipefail
payload="$(node -e '
const revision = Number(process.env.REVISION);
if (!Number.isInteger(revision) || revision < 1 || !process.env.FENCE_KEY) process.exit(1);
process.stdout.write(JSON.stringify({
canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`,
fence_key: process.env.FENCE_KEY,
revision,
outcome: "not_required",
receipt_id: `router-not-required:${process.env.GITHUB_RUN_ID}:${process.env.GITHUB_RUN_ATTEMPT}`,
}));
')"
signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')"
queue_url="${QUEUE_URL%/}"
curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST \
--header "content-type: application/json" \
--header "x-clawsweeper-exact-review-signature: $signature" \
--data "$payload" \
"$queue_url/internal/exact-review/lifecycle/router-receipt" >/dev/null
- name: Dispatch deferred high-confidence bug implementation
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.publish-event-result.outcome == 'success' && steps.publish-event-result.outputs.remote_tuple_verified == 'true' && steps.record-fallback-canonical-lifecycle-receipt.outcome == 'success' && vars.CLAWSWEEPER_AUTO_IMPLEMENT_ISSUES == '1' && steps.publication-context.outputs.target_repo == 'openclaw/openclaw' && fromJSON(steps.publication-context.outputs.decision).itemKind == 'issue' && ((fromJSON(steps.publication-context.outputs.decision).sourceAction == 'failed_review_shard_recovery' && steps.record-no-router-lifecycle-receipt.outcome == 'success') || (fromJSON(steps.publication-context.outputs.decision).sourceAction != 'failed_review_shard_recovery' && (steps.publish-event-result.outputs.routing_deferred != 'true' || steps.queue-deferred-verdict-router.outcome == 'success'))) }}
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }}
MAX_DISPATCH: ${{ vars.CLAWSWEEPER_AUTO_IMPLEMENT_MAX_DISPATCH_PER_SWEEP || '' }}
run: |
node scripts/dispatch-issue-implementation-candidates.mjs \
--target-repo "$TARGET_REPO" \
--item-number "$ITEM_NUMBER" \
--artifact-dir artifacts/event
- name: Queue fresh review after source drift
id: queue-source-drift-review
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && (steps.publish-event-result.outputs.requeue_latest == 'true' || steps.legacy-exact-artifact.outputs.legacy_tupleless == 'true') }}
env:
CLAIM_DECISION: ${{ steps.publication-context.outputs.decision }}
PRODUCER_RUN_ATTEMPT: ${{ steps.publication-context.outputs.generation_attempt }}
PRODUCER_RUN_ID: ${{ steps.publication-context.outputs.producer_run_id }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
run: |
set -euo pipefail
test -n "$CLAWSWEEPER_WEBHOOK_SECRET"
queue_url="${QUEUE_URL%/}"
payload="$(node <<'NODE'
const decision = JSON.parse(process.env.CLAIM_DECISION || "{}");
process.stdout.write(JSON.stringify({
delivery_id: `publisher-source-drift:${process.env.PRODUCER_RUN_ID}:${process.env.PRODUCER_RUN_ATTEMPT}`,
decision: {
...decision,
sourceAction:
decision.sourceAction === "failed_review_shard_recovery"
? decision.sourceAction
: "source_drift_requeue",
supersedesInProgress: true,
},
}));
NODE
)"
signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')"
response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST \
--header "content-type: application/json" \
--header "x-clawsweeper-exact-review-signature: $signature" \
--data "$payload" \
"$queue_url/internal/exact-review/enqueue")"
jq -e '.ok == true and (.queued == true or .deduped == true or .shed == true)' <<< "$response" >/dev/null
if jq -e '.shed == true' <<< "$response" >/dev/null; then
echo "Source-drift recovery shed by exact-review queue backpressure."
fi
- name: Release terminal review leases
id: release-terminal-review-leases
if: ${{ always() && steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && (steps.publish-event-result.outputs.terminal_noop == 'true' || steps.publish-event-result.outputs.terminal_closed == 'true') }}
env:
GH_TOKEN: ${{ steps.target-write-token.outputs.token }}
TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }}
run: |
set -euo pipefail
test -n "$GH_TOKEN"
test "$(gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER" --jq .state)" = "closed"
lease_ids="$(gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments?per_page=100" --paginate --jq ".[] | select(.user.login == \"clawsweeper[bot]\" or .user.login == \"openclaw-clawsweeper[bot]\") | select(.body | contains(\"<!-- clawsweeper-review-lease item=$ITEM_NUMBER -->\")) | .id")"
while IFS= read -r lease_id; do
[ -n "$lease_id" ] || continue
gh api "repos/$TARGET_REPO/issues/comments/$lease_id" --method DELETE
done <<< "$lease_ids"
reaction_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')"
while IFS= read -r reaction_id; do
[ -n "$reaction_id" ] || continue
gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions/$reaction_id" --method DELETE
done <<< "$reaction_ids"
- name: Confirm terminal item remains closed
id: confirm-terminal-item
if: ${{ always() && steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.release-terminal-review-leases.outcome == 'success' }}
env:
GH_TOKEN: ${{ steps.publication-context.outputs.target_repo == 'openclaw/openclaw' && github.token || steps.target-write-token.outputs.token }}
TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }}
run: |
test "$(gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER" --jq .state)" = "closed"
echo "confirmed=true" >> "$GITHUB_OUTPUT"
- name: React to target item completion
if: ${{ steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.publish-event-result.outputs.requeue_latest != 'true' && (steps.publish-event-result.outputs.terminal_closed == 'true' || steps.publish-event-result.outputs.guarded_open == 'true' || steps.publish-event-result.outputs.policy_noop == 'true' || steps.queue-deferred-verdict-router.outcome == 'success' || (fromJSON(steps.publication-context.outputs.decision).sourceAction == 'failed_review_shard_recovery' && steps.publish-event-result.outputs.remote_tuple_verified == 'true')) }}
continue-on-error: true
env:
GH_TOKEN: ${{ steps.target-write-token.outputs.token }}
TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }}
POLICY_NOOP: ${{ steps.publish-event-result.outputs.policy_noop }}
REVIEW_ONLY: ${{ fromJSON(steps.publication-context.outputs.decision).sourceAction == 'failed_review_shard_recovery' && 'true' || 'false' }}
run: |
set -euo pipefail
if [ "$POLICY_NOOP" != "true" ] && [ "$REVIEW_ONLY" != "true" ]; then
gh api -X POST -H "Accept: application/vnd.github+json" \
"repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions" -f content="+1" >/dev/null || true
fi
reaction_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')"
while IFS= read -r reaction_id; do
[ -n "$reaction_id" ] || continue
gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions/$reaction_id" --method DELETE || true
done <<< "$reaction_ids"
- name: Release superseded or unsuccessful publisher-owned review lease
if: ${{ always() && steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.publication-context.outputs.live_proceeded == 'true' && steps.publish-event-result.outputs.failure_kind != 'github_rate_limit' && steps.publish-event-result.outputs.failure_kind != 'github_transient' && (steps.publish-event-result.outputs.completion_kind == 'superseded' || steps.publish-event-result.outcome != 'success' || (steps.publish-event-result.outputs.requeue_latest == 'true' && steps.queue-source-drift-review.outcome != 'success')) }}
env:
GH_TOKEN: ${{ steps.target-write-token.outputs.token }}
TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }}
LEASE_OWNER: github-run-${{ steps.publication-context.outputs.producer_run_id }}-${{ steps.publication-context.outputs.generation_attempt }}
run: |
lease_ids="$(gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments?per_page=100" --paginate --jq ".[] | select(.user.login == \"clawsweeper[bot]\" or .user.login == \"openclaw-clawsweeper[bot]\") | select(.body | contains(\"<!-- clawsweeper-review-lease item=$ITEM_NUMBER -->\")) | select(.body | contains(\"owner=$LEASE_OWNER \")) | .id")"
while IFS= read -r lease_id; do
[ -n "$lease_id" ] || continue
gh api "repos/$TARGET_REPO/issues/comments/$lease_id" --method DELETE
done <<< "$lease_ids"
reaction_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')"
while IFS= read -r reaction_id; do
[ -n "$reaction_id" ] || continue
gh api "repos/$TARGET_REPO/issues/$ITEM_NUMBER/reactions/$reaction_id" --method DELETE
done <<< "$reaction_ids"
- name: Export exact review publication result
id: exact-review-publication-result
if: ${{ steps.publication-context.outputs.claimed == 'true' && always() }}
env:
PRIOR_JOB_STATUS: ${{ job.status }}
PUBLISH_OUTCOME: ${{ steps.publish-event-result.outcome }}
TERMINAL_NOOP: ${{ steps.publish-event-result.outputs.terminal_noop }}
TERMINAL_MISSING: ${{ steps.publish-event-result.outputs.terminal_missing }}
TERMINAL_CLOSED: ${{ steps.publish-event-result.outputs.terminal_closed }}
GUARDED_OPEN: ${{ steps.publish-event-result.outputs.guarded_open }}
POLICY_NOOP: ${{ steps.publish-event-result.outputs.policy_noop }}
REQUEUE_LATEST: ${{ steps.publish-event-result.outputs.requeue_latest }}
LEGACY_TUPLELESS: ${{ steps.legacy-exact-artifact.outputs.legacy_tupleless }}
SOURCE_DRIFT_OUTCOME: ${{ steps.queue-source-drift-review.outcome }}
DEFERRED_ROUTE_OUTCOME: ${{ steps.queue-deferred-verdict-router.outcome }}
FAILURE_KIND: ${{ steps.publish-event-result.outputs.failure_kind }}
DOWNLOAD_OUTCOME: ${{ steps.download-exact-review-bundle.outcome }}
VALIDATE_OUTCOME: ${{ steps.validate-exact-review-bundle.outcome }}
PUBLISH_COMPLETION_KIND: ${{ steps.publish-event-result.outputs.completion_kind }}
PUBLISH_REASON_CODE: ${{ steps.publish-event-result.outputs.reason_code }}
PUBLISH_RETRY_AT: ${{ steps.publish-event-result.outputs.retry_at }}
ERROR_FINGERPRINT: ${{ steps.publish-event-result.outputs.error_fingerprint }}
STATE_WRITER_JSON: ${{ steps.publish-event-result.outputs.state_writer_json }}
DIRECT_RECOVERY_OUTCOME: ${{ steps.replay-direct-lifecycle.outputs.outcome }}
DIRECT_RECOVERY_COMPLETION_KIND: ${{ steps.replay-direct-lifecycle.outputs.completion_kind }}
DIRECT_RECOVERY_REASON_CODE: ${{ steps.replay-direct-lifecycle.outputs.reason_code }}
DIRECT_RECOVERY_REQUEUE_LATEST: ${{ steps.replay-direct-lifecycle.outputs.requeue_latest }}
DIRECT_RECOVERY_DIRECT_REQUEUE: ${{ steps.replay-direct-lifecycle.outputs.direct_requeue }}
REVIEW_ONLY: ${{ fromJSON(steps.publication-context.outputs.decision).sourceAction == 'failed_review_shard_recovery' && 'true' || 'false' }}
run: |
set -euo pipefail
outcome=failure
completion_kind=permanent_failure
reason_code=unknown_failure
retry_at=
requeue_latest="$REQUEUE_LATEST"
direct_requeue=false
if [ "$DIRECT_RECOVERY_OUTCOME" = "success" ]; then
case "$DIRECT_RECOVERY_COMPLETION_KIND:$DIRECT_RECOVERY_REASON_CODE" in
published:publication_applied|superseded:remote_newer_tuple) ;;
*) exit 1 ;;
esac
if [ "$DIRECT_RECOVERY_REQUEUE_LATEST" != "" ] && [ "$DIRECT_RECOVERY_REQUEUE_LATEST" != "false" ]; then
exit 1
fi
if [ "$DIRECT_RECOVERY_DIRECT_REQUEUE" = "true" ]; then
if [ "$DIRECT_RECOVERY_COMPLETION_KIND" != "published" ] || [ "$DIRECT_RECOVERY_REASON_CODE" != "publication_applied" ]; then
exit 1
fi
direct_requeue=true
elif [ "$DIRECT_RECOVERY_DIRECT_REQUEUE" != "" ] && [ "$DIRECT_RECOVERY_DIRECT_REQUEUE" != "false" ]; then
exit 1
fi
outcome=success
completion_kind="$DIRECT_RECOVERY_COMPLETION_KIND"
reason_code="$DIRECT_RECOVERY_REASON_CODE"
elif [ "$PUBLISH_COMPLETION_KIND" = "superseded" ] && { [ "$PUBLISH_REASON_CODE" = "remote_newer_tuple" ] || [ "$PUBLISH_REASON_CODE" = "remote_closed" ]; }; then
outcome=success
completion_kind=superseded
reason_code="$PUBLISH_REASON_CODE"
elif [ "$PUBLISH_COMPLETION_KIND" = "deferred" ] && [ "$PUBLISH_REASON_CODE" = "close_coverage_deferred" ]; then
outcome=success
completion_kind=deferred
reason_code=close_coverage_deferred
elif [ "$PUBLISH_COMPLETION_KIND" = "refresh_required" ] && [ "$PUBLISH_REASON_CODE" = "close_coverage_retry" ]; then
# A pre-deployment publisher checked out the new helper but still runs the old
# workflow without the deferred-lane opt-in. Preserve its established refresh
# completion so the queue can create a fresh proof-capable review.
outcome=success
completion_kind=refresh_required
reason_code=close_coverage_retry
elif [ "$PUBLISH_COMPLETION_KIND" = "retryable_failure" ] && [ "$PUBLISH_REASON_CODE" = "review_lease_active" ] && [ -n "$PUBLISH_RETRY_AT" ]; then
outcome=success
completion_kind=retryable_failure
reason_code=review_lease_active
retry_at="$PUBLISH_RETRY_AT"
elif [ "$LEGACY_TUPLELESS" = "true" ] && [ "$SOURCE_DRIFT_OUTCOME" = "success" ]; then
outcome=success
completion_kind=superseded
reason_code=live_terminal
elif [ "$PRIOR_JOB_STATUS" = "cancelled" ]; then
outcome=cancelled
completion_kind=retryable_failure
reason_code=workflow_cancelled
elif [ "$PRIOR_JOB_STATUS" = "success" ]; then
if [ "$PUBLISH_OUTCOME" = "success" ] && [ "$REQUEUE_LATEST" = "true" ] && [ "$SOURCE_DRIFT_OUTCOME" = "success" ]; then
outcome=success
elif [ "$PUBLISH_OUTCOME" = "success" ] && { [ "$TERMINAL_NOOP" = "true" ] || [ "$TERMINAL_MISSING" = "true" ] || [ "$TERMINAL_CLOSED" = "true" ] || [ "$GUARDED_OPEN" = "true" ] || [ "$POLICY_NOOP" = "true" ] || [ "$REVIEW_ONLY" = "true" ]; }; then
outcome=success
elif [ "$PUBLISH_OUTCOME" = "success" ] && [ "$DEFERRED_ROUTE_OUTCOME" = "success" ]; then
outcome=success
fi
fi
if [ "$outcome" = "success" ] && [ "$completion_kind" != "superseded" ] && [ "$completion_kind" != "deferred" ] && [ "$completion_kind" != "refresh_required" ] && [ "$completion_kind" != "retryable_failure" ]; then
if [ "$PUBLISH_COMPLETION_KIND" = "published" ] && [ "$PUBLISH_REASON_CODE" = "publication_applied" ]; then
completion_kind=published
reason_code=publication_applied
else
completion_kind=superseded
reason_code=live_terminal
fi
elif [ "$outcome" != "success" ] && { [ "$FAILURE_KIND" = "github_rate_limit" ] || [ "$FAILURE_KIND" = "github_transient" ]; }; then
completion_kind=retryable_failure
reason_code="$FAILURE_KIND"
retry_at="$PUBLISH_RETRY_AT"
elif [ "$outcome" != "success" ] && [ "$DOWNLOAD_OUTCOME" = "failure" ]; then
completion_kind=retryable_failure
reason_code=artifact_unavailable
elif [ "$outcome" != "success" ] && [ "$VALIDATE_OUTCOME" = "failure" ]; then
completion_kind=permanent_failure
reason_code=invalid_artifact
elif [ "$outcome" != "success" ] && [ "$PUBLISH_COMPLETION_KIND" = "permanent_failure" ]; then
completion_kind=permanent_failure
reason_code="${PUBLISH_REASON_CODE:-unknown_failure}"
elif [ "$outcome" != "success" ] && [ "$PUBLISH_COMPLETION_KIND" = "retryable_failure" ]; then
completion_kind=retryable_failure
reason_code="${PUBLISH_REASON_CODE:-unknown_failure}"
fi
echo "outcome=$outcome" >> "$GITHUB_OUTPUT"
echo "completion_kind=$completion_kind" >> "$GITHUB_OUTPUT"
echo "reason_code=$reason_code" >> "$GITHUB_OUTPUT"
echo "requeue_latest=$requeue_latest" >> "$GITHUB_OUTPUT"
echo "direct_requeue=$direct_requeue" >> "$GITHUB_OUTPUT"
if [ -n "$ERROR_FINGERPRINT" ]; then
echo "error_fingerprint=$ERROR_FINGERPRINT" >> "$GITHUB_OUTPUT"
fi
if [ -n "$retry_at" ]; then
echo "retry_at=$retry_at" >> "$GITHUB_OUTPUT"
fi
if [ "$outcome" = "failure" ] && { [ "$FAILURE_KIND" = "github_rate_limit" ] || [ "$FAILURE_KIND" = "github_transient" ]; }; then
echo "failure_kind=$FAILURE_KIND" >> "$GITHUB_OUTPUT"
fi
if [ -n "$STATE_WRITER_JSON" ]; then
echo "state_writer_json=$STATE_WRITER_JSON" >> "$GITHUB_OUTPUT"
fi
- name: Complete durable exact review publication
id: complete-exact-review-publication
if: ${{ steps.publication-context.outputs.claimed == 'true' && always() }}
continue-on-error: true
env:
CLAIM_GENERATION: ${{ steps.publication-context.outputs.publisher_claim_generation }}
ITEM_KEY: ${{ steps.publication-context.outputs.publisher_item_key }}
LEASE_REVISION: ${{ steps.publication-context.outputs.publisher_lease_revision }}
OUTCOME: ${{ steps.exact-review-publication-result.outputs.outcome || 'failure' }}
FAILURE_KIND: ${{ steps.exact-review-publication-result.outputs.failure_kind }}
COMPLETION_KIND: ${{ steps.exact-review-publication-result.outputs.completion_kind }}
REASON_CODE: ${{ steps.exact-review-publication-result.outputs.reason_code }}
ERROR_FINGERPRINT: ${{ steps.exact-review-publication-result.outputs.error_fingerprint }}
RETRY_AT: ${{ steps.exact-review-publication-result.outputs.retry_at }}
STATE_WRITER_JSON: ${{ steps.exact-review-publication-result.outputs.state_writer_json }}
PUBLISH_COMPLETION_KIND: ${{ steps.publish-event-result.outputs.completion_kind }}
PUBLISH_REASON_CODE: ${{ steps.publish-event-result.outputs.reason_code }}
TERMINAL_NOOP: ${{ steps.publish-event-result.outputs.terminal_noop }}
TERMINAL_MISSING: ${{ steps.publish-event-result.outputs.terminal_missing }}
TERMINAL_CLOSED: ${{ steps.publish-event-result.outputs.terminal_closed }}
GUARDED_OPEN: ${{ steps.publish-event-result.outputs.guarded_open }}
POLICY_NOOP: ${{ steps.publish-event-result.outputs.policy_noop }}
REQUEUE_LATEST: ${{ steps.publish-event-result.outputs.requeue_latest }}
DIRECT_LIFECYCLE_REQUEUE: ${{ steps.exact-review-publication-result.outputs.direct_requeue }}
LEGACY_TUPLELESS: ${{ steps.legacy-exact-artifact.outputs.legacy_tupleless }}
QUEUE_LEASE_ID: ${{ steps.publication-context.outputs.publisher_lease_id }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
set -euo pipefail
queue_url="${QUEUE_URL%/}"
payload="$(node -e '
const leaseRevision = Number(process.env.LEASE_REVISION);
const claimGeneration = Number(process.env.CLAIM_GENERATION);
const runAttempt = Number(process.env.RUN_ATTEMPT);
if (!process.env.QUEUE_LEASE_ID || !process.env.ITEM_KEY) process.exit(1);
if (!Number.isInteger(leaseRevision) || leaseRevision < 1) process.exit(1);
if (!Number.isInteger(claimGeneration) || claimGeneration < 1) process.exit(1);
if (!Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1);
const outcome = ["success", "failure", "cancelled"].includes(process.env.OUTCOME)
? process.env.OUTCOME
: "failure";
const failureKind = ["github_rate_limit", "github_transient"].includes(process.env.FAILURE_KIND)
? process.env.FAILURE_KIND
: undefined;
const completionKind = ["published", "superseded", "deferred", "retryable_failure", "refresh_required", "permanent_failure"].includes(process.env.COMPLETION_KIND)
? process.env.COMPLETION_KIND
: undefined;
const reasonCode = ["publication_applied", "remote_newer_tuple", "remote_closed", "live_terminal", "github_rate_limit", "github_transient", "state_contention", "review_lease_active", "workflow_cancelled", "artifact_unavailable", "artifact_expired", "close_coverage_retry", "close_coverage_deferred", "invalid_artifact", "missing_record_tuple", "tuple_protocol_invalid", "policy_invariant", "unknown_failure", "retry_exhausted"].includes(process.env.REASON_CODE)
? process.env.REASON_CODE
: undefined;
const errorFingerprint = /^[A-Za-z0-9:._-]{1,200}$/.test(process.env.ERROR_FINGERPRINT || "")
? process.env.ERROR_FINGERPRINT
: undefined;
const retryAt = String(process.env.RETRY_AT || "").trim();
if (retryAt && !Number.isFinite(Date.parse(retryAt))) process.exit(1);
if (!completionKind || !reasonCode) process.exit(1);
const directLifecycleRequeue = process.env.DIRECT_LIFECYCLE_REQUEUE === "true";
const lifecycleTerminal =
directLifecycleRequeue || process.env.REQUEUE_LATEST === "true"
? "requeue"
: process.env.LEGACY_TUPLELESS === "true" || ["retryable_failure", "refresh_required"].includes(completionKind)
? "requeue"
: process.env.TERMINAL_MISSING === "true"
? "target_missing"
: process.env.TERMINAL_CLOSED === "true" || process.env.TERMINAL_NOOP === "true"
? "target_closed"
: process.env.POLICY_NOOP === "true"
? "policy_noop"
: process.env.GUARDED_OPEN === "true"
? "guarded_open"
: completionKind === "superseded"
? reasonCode === "remote_closed"
? "target_closed"
: "superseded"
: completionKind === "permanent_failure"
? "failure"
: undefined;
let stateWriter;
try {
const parsed = JSON.parse(process.env.STATE_WRITER_JSON || "");
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) stateWriter = parsed;
} catch {}
process.stdout.write(JSON.stringify({
lease_id: process.env.QUEUE_LEASE_ID,
item_key: process.env.ITEM_KEY,
lease_revision: leaseRevision,
claim_generation: claimGeneration,
run_id: process.env.GITHUB_RUN_ID,
run_attempt: runAttempt,
outcome,
completion_kind: completionKind,
reason_code: reasonCode,
...(errorFingerprint ? { error_fingerprint: errorFingerprint } : {}),
...(retryAt ? { retry_at: retryAt } : {}),
...(outcome === "failure" && failureKind ? { failure_kind: failureKind } : {}),
...(directLifecycleRequeue ? { direct_lifecycle_requeue: true } : {}),
...(lifecycleTerminal ? { lifecycle_terminal_disposition: lifecycleTerminal } : {}),
...(stateWriter ? { state_writer: stateWriter } : {}),
}));
')"
for attempt in 1 2 3; do
if curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST \
--header "content-type: application/json" \
--data "$payload" \
"$queue_url/internal/exact-review/complete" >/dev/null; then
exit 0
fi
if [ "$attempt" -lt 3 ]; then
sleep "$((attempt * 5))"
fi
done
exit 1
- name: Mark active lease retry waiting
if: ${{ always() && steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.has_command_context == 'true' && steps.exact-review-publication-result.outputs.outcome == 'success' && steps.exact-review-publication-result.outputs.completion_kind == 'retryable_failure' && steps.exact-review-publication-result.outputs.reason_code == 'review_lease_active' && steps.complete-exact-review-publication.outcome == 'success' }}
continue-on-error: true
env:
GH_TOKEN: ${{ steps.target-write-token.outputs.token }}
TARGET_REPO: ${{ steps.publication-context.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.publication-context.outputs.item_number }}
COMMAND_STATUS_MARKER: ${{ fromJSON(steps.publication-context.outputs.decision).commandStatusMarker || '' }}
STATUS_COMMENT_ID: ${{ fromJSON(steps.publication-context.outputs.decision).statusCommentId || '' }}
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 "Waiting" \
--detail "Another same-revision review is active; durable publication will retry after its lease expires." \
--run-url "$RUN_URL"
- name: Submit artifact-publication GitHub egress telemetry
if: ${{ always() && steps.publication-context.outputs.claimed == 'true' && steps.publication-context.outputs.direct_lifecycle_recovery != 'true' && steps.artifact-github-egress-observer.outcome == 'success' }}
continue-on-error: true
env:
EXACT_REVIEW_QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
run: pnpm run --silent repair:github-egress-telemetry submit
- name: Fail unsuccessful exact review publication
if: ${{ always() && steps.publication-context.outputs.claimed == 'true' && (steps.exact-review-publication-result.outputs.outcome != 'success' || steps.complete-exact-review-publication.outcome != 'success') }}
run: exit 1
event-review-terminal-finalization:
name: Finalize exact-review command acknowledgement
if: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.queue_lease_id != '' && github.event.client_payload.source_action == 'exact_review_command_acknowledgement' }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Claim committed terminal finalization
id: finalization-context
env:
ITEM_KEY: ${{ github.event.client_payload.queue_claim.item_key }}
QUEUE_LEASE_ID: ${{ github.event.client_payload.queue_lease_id }}
QUEUE_LEASE_REVISION: ${{ github.event.client_payload.queue_claim.lease_revision }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
set -euo pipefail
printf 'claimed=false\n' >> "$GITHUB_OUTPUT"
queue_url="${QUEUE_URL%/}"
payload="$(node -e '
const leaseRevision = Number(process.env.QUEUE_LEASE_REVISION);
const runAttempt = Number(process.env.RUN_ATTEMPT);
if (!process.env.QUEUE_LEASE_ID || !process.env.ITEM_KEY) process.exit(1);
if (!Number.isInteger(leaseRevision) || leaseRevision < 1) process.exit(1);
if (!Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1);
process.stdout.write(JSON.stringify({
lease_id: process.env.QUEUE_LEASE_ID,
item_key: process.env.ITEM_KEY,
lease_revision: leaseRevision,
run_id: process.env.GITHUB_RUN_ID,
run_attempt: runAttempt,
}));
')"
response_file="$(mktemp)"
error_file="$(mktemp)"
trap 'rm -f "$response_file" "$error_file"' EXIT
for attempt in 1 2 3; do
: > "$response_file"
: > "$error_file"
if status="$(curl --silent --show-error --connect-timeout 5 --max-time 20 \
--output "$response_file" --write-out '%{http_code}' \
--request POST --header "content-type: application/json" --data "$payload" \
"$queue_url/internal/exact-review/claim" 2>"$error_file")"; then
response="$(<"$response_file")"
if [ "$status" = "409" ]; then
conflict_reason="$(RESPONSE="$response" node <<'NODE'
const response = JSON.parse(process.env.RESPONSE || "{}");
const safeConflicts = new Set([
"lease_not_active",
"lease_already_claimed",
"lease_decision_unavailable",
"stale_run_attempt",
]);
if (!safeConflicts.has(response.error)) process.exit(1);
process.stdout.write(response.error);
NODE
)" || {
echo "Unexpected terminal-finalization claim conflict: $response" >&2
exit 1
}
echo "::notice::Skipping terminal finalization because lease claim lost safely: $conflict_reason"
exit 0
fi
if [ "$status" = "200" ]; then
break
fi
if [[ "$status" != 5* ]]; then
echo "Terminal-finalization claim returned HTTP $status: $response" >&2
exit 1
fi
fi
if [ "$attempt" = "3" ]; then
cat "$error_file" >&2
echo "Terminal-finalization claim failed after retries" >&2
exit 1
fi
sleep "$attempt"
done
RESPONSE="$response" node <<'NODE'
const fs = require("node:fs");
const response = JSON.parse(process.env.RESPONSE || "{}");
const decision = response.decision;
const finalization = response.terminal_finalization;
const lifecycleProjection = response.lifecycle_projection;
const leaseRevision = Number(response.lease_revision);
const claimGeneration = Number(response.claim_generation);
if (
response.claimed !== true || response.protocol_version !== 2 ||
response.item_key !== process.env.ITEM_KEY ||
decision?.sourceAction !== "exact_review_artifact_publish" ||
!finalization || !["Complete", "Failed"].includes(finalization.statusState) ||
typeof finalization.statusDetail !== "string" ||
!lifecycleProjection ||
lifecycleProjection.canonicalTargetKey !== `${decision.targetRepo}#${decision.itemNumber}` ||
typeof lifecycleProjection.fenceKey !== "string" || !lifecycleProjection.fenceKey ||
!Number.isInteger(lifecycleProjection.revision) || lifecycleProjection.revision < 1 ||
!Number.isInteger(leaseRevision) || leaseRevision < 1 ||
!Number.isInteger(claimGeneration) || claimGeneration < 1
) process.exit(1);
const [targetRepoOwner, targetRepoName] = String(decision.targetRepo).split("/");
const values = {
target_repo: decision.targetRepo,
target_repo_owner: targetRepoOwner,
target_repo_name: targetRepoName,
item_number: decision.itemNumber,
status_marker: decision.commandStatusMarker || "",
status_comment_id: decision.statusCommentId || "",
item_key: response.item_key,
lease_revision: leaseRevision,
lifecycle_fence_key: lifecycleProjection.fenceKey,
lifecycle_revision: lifecycleProjection.revision,
claim_generation: claimGeneration,
status_state: finalization.statusState,
status_detail: finalization.statusDetail,
};
fs.appendFileSync(process.env.GITHUB_OUTPUT, "claimed=true\n");
for (const [key, value] of Object.entries(values)) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`);
}
NODE
- uses: actions/checkout@v7
if: ${{ steps.finalization-context.outputs.claimed == 'true' }}
with:
# Fetch blobs during the retryable fetch instead of lazily fetching
# them during checkout, which strands claimed finalizations on 5xx.
fetch-depth: 1
- uses: ./.github/actions/setup-pnpm
if: ${{ steps.finalization-context.outputs.claimed == 'true' }}
with:
build-script: build:repair
- name: Create target write token
id: target-write-token
if: ${{ steps.finalization-context.outputs.claimed == 'true' }}
continue-on-error: true
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
owner: ${{ steps.finalization-context.outputs.target_repo_owner }}
repositories: ${{ steps.finalization-context.outputs.target_repo_name }}
permission-issues: write
permission-pull-requests: write
- name: Begin fenced terminal acknowledgement
id: terminal-acknowledgement
if: ${{ steps.finalization-context.outputs.claimed == 'true' && steps.target-write-token.outcome == 'success' }}
env:
ITEM_KEY: ${{ steps.finalization-context.outputs.item_key }}
QUEUE_LEASE_ID: ${{ github.event.client_payload.queue_lease_id }}
LEASE_REVISION: ${{ steps.finalization-context.outputs.lease_revision }}
CLAIM_GENERATION: ${{ steps.finalization-context.outputs.claim_generation }}
STATUS_MARKER: ${{ steps.finalization-context.outputs.status_marker }}
STATUS_COMMENT_ID: ${{ steps.finalization-context.outputs.status_comment_id }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
set -euo pipefail
payload="$(node -e '
const leaseRevision = Number(process.env.LEASE_REVISION);
const claimGeneration = Number(process.env.CLAIM_GENERATION);
const runAttempt = Number(process.env.RUN_ATTEMPT);
const statusMarker = process.env.STATUS_MARKER || null;
const statusCommentId = process.env.STATUS_COMMENT_ID === "" ? null : Number(process.env.STATUS_COMMENT_ID);
if (!process.env.QUEUE_LEASE_ID || !process.env.ITEM_KEY) process.exit(1);
if (!Number.isInteger(leaseRevision) || leaseRevision < 1 || !Number.isInteger(claimGeneration) || claimGeneration < 1 || !Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1);
if (!statusMarker && statusCommentId === null) process.exit(1);
process.stdout.write(JSON.stringify({
lease_id: process.env.QUEUE_LEASE_ID, item_key: process.env.ITEM_KEY,
lease_revision: leaseRevision, claim_generation: claimGeneration,
run_id: process.env.GITHUB_RUN_ID, run_attempt: runAttempt,
...(statusMarker ? { status_marker: statusMarker } : {}),
...(statusCommentId === null ? {} : { status_comment_id: statusCommentId }),
}));
')"
response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST --header "content-type: application/json" --data "$payload" \
"${QUEUE_URL%/}/internal/exact-review/terminal-finalization/attempt")"
jq -e '.ok == true' <<< "$response" >/dev/null
echo "allowed=$(jq -r '.allowed == true' <<< "$response")" >> "$GITHUB_OUTPUT"
echo "acknowledgement_state=$(jq -r '.acknowledgement_state // empty' <<< "$response")" >> "$GITHUB_OUTPUT"
echo "attempt_id=$(jq -r '.attempt_id // empty' <<< "$response")" >> "$GITHUB_OUTPUT"
- name: Update final command status once
id: update-final-command-status
if: ${{ steps.terminal-acknowledgement.outputs.allowed == 'true' }}
continue-on-error: true
env:
GH_TOKEN: ${{ steps.target-write-token.outputs.token }}
TARGET_REPO: ${{ steps.finalization-context.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.finalization-context.outputs.item_number }}
COMMAND_STATUS_MARKER: ${{ steps.finalization-context.outputs.status_marker }}
STATUS_COMMENT_ID: ${{ steps.finalization-context.outputs.status_comment_id }}
STATUS_STATE: ${{ steps.finalization-context.outputs.status_state }}
STATUS_DETAIL: ${{ steps.finalization-context.outputs.status_detail }}
RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
FENCE_KEY: ${{ steps.finalization-context.outputs.lifecycle_fence_key }}
REVISION: ${{ steps.finalization-context.outputs.lifecycle_revision }}
ATTEMPT_ID: ${{ steps.terminal-acknowledgement.outputs.attempt_id }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
run: |
set -euo pipefail
update_log="$(mktemp)"
trap 'rm -f "$update_log"' EXIT
pnpm run repair:update-command-status -- \
--repo "$TARGET_REPO" --item-number "$ITEM_NUMBER" \
--marker "$COMMAND_STATUS_MARKER" --status-comment-id "$STATUS_COMMENT_ID" \
--state "$STATUS_STATE" --detail "$STATUS_DETAIL" --run-url "$RUN_URL" \
--require-mutation --locked-conversation-terminal-skip --verify-terminal-status-receipt 2>&1 | tee "$update_log" || {
update_status="$?"
# End-of-window installation throttling is self-healing: the
# requeue step re-arms the driver for after the rate window, so
# the failure sentinel must not turn this run red.
if grep -Eqi 'rate limit exceeded|secondary rate limit|HTTP 429' "$update_log"; then
echo "throttled=true" >> "$GITHUB_OUTPUT"
echo "::notice::GitHub throttled the terminal status update; the requeue step retries after the rate window."
fi
failure_payload="$(node -e '
const revision = Number(process.env.REVISION);
if (!process.env.FENCE_KEY || !process.env.ATTEMPT_ID || !Number.isInteger(revision) || revision < 1) process.exit(1);
process.stdout.write(JSON.stringify({
canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`,
fence_key: process.env.FENCE_KEY, revision, attempt_id: process.env.ATTEMPT_ID,
...(process.env.COMMAND_STATUS_MARKER ? { status_marker: process.env.COMMAND_STATUS_MARKER } : {}),
...(process.env.STATUS_COMMENT_ID ? { status_comment_id: Number(process.env.STATUS_COMMENT_ID) } : {}),
}));
')"
signature="$(PAYLOAD="$failure_payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')"
curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST --header "content-type: application/json" \
--header "x-clawsweeper-exact-review-signature: $signature" \
--data "$failure_payload" \
"${QUEUE_URL%/}/internal/exact-review/lifecycle/command-ack/failed" >/dev/null
exit "$update_status"
}
- name: Record verified terminal acknowledgement receipt
id: observe-verified-terminal-acknowledgement
if: ${{ always() && steps.finalization-context.outputs.claimed == 'true' && steps.terminal-acknowledgement.outputs.allowed == 'true' && steps.update-final-command-status.outcome == 'success' && steps.update-final-command-status.outputs.terminal_status_verified == 'true' }}
env:
TARGET_REPO: ${{ steps.finalization-context.outputs.target_repo }}
ITEM_NUMBER: ${{ steps.finalization-context.outputs.item_number }}
FENCE_KEY: ${{ steps.finalization-context.outputs.lifecycle_fence_key }}
REVISION: ${{ steps.finalization-context.outputs.lifecycle_revision }}
STATUS_MARKER: ${{ steps.finalization-context.outputs.status_marker }}
STATUS_COMMENT_ID: ${{ steps.finalization-context.outputs.status_comment_id }}
COMMAND_COMMENT_ID: ${{ steps.update-final-command-status.outputs.command_comment_id }}
COMPLETION_COMMENT_ID: ${{ steps.update-final-command-status.outputs.completion_comment_id }}
COMPLETION_COMPLETED_AT: ${{ steps.update-final-command-status.outputs.completion_completed_at }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
run: |
set -euo pipefail
payload="$(node -e '
const fenceKey = process.env.FENCE_KEY || "";
const revision = Number(process.env.REVISION);
const statusMarker = process.env.STATUS_MARKER || null;
const statusCommentId = process.env.STATUS_COMMENT_ID === "" ? null : Number(process.env.STATUS_COMMENT_ID);
const commandCommentId = Number(process.env.COMMAND_COMMENT_ID);
const completionCommentId = Number(process.env.COMPLETION_COMMENT_ID);
const completedAt = process.env.COMPLETION_COMPLETED_AT || "";
if (
!fenceKey || !Number.isSafeInteger(revision) || revision < 1 ||
(!statusMarker && statusCommentId === null) ||
(statusCommentId !== null && (!Number.isInteger(statusCommentId) || statusCommentId < 1)) ||
!Number.isInteger(commandCommentId) || commandCommentId < 1 ||
!Number.isInteger(completionCommentId) || completionCommentId < 1 ||
!completedAt || !Number.isFinite(Date.parse(completedAt))
) process.exit(1);
process.stdout.write(JSON.stringify({
canonical_target_key: `${process.env.TARGET_REPO}#${process.env.ITEM_NUMBER}`,
fence_key: fenceKey,
revision,
...(statusMarker ? { status_marker: statusMarker } : {}),
...(statusCommentId === null ? {} : { status_comment_id: statusCommentId }),
command_comment_id: commandCommentId,
completion_comment_id: completionCommentId,
completed_at: completedAt,
observed_at: Date.now(),
}));
')"
signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')"
response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST --header "content-type: application/json" \
--header "x-clawsweeper-exact-review-signature: $signature" \
--data "$payload" \
"${QUEUE_URL%/}/internal/exact-review/lifecycle/command-ack/observed")"
jq -e '.ok == true and .accepted == true and .acknowledgement_state == "observed"' <<< "$response" >/dev/null
- name: Complete locked terminal acknowledgement skip
id: complete-locked-terminal-acknowledgement
if: ${{ always() && steps.finalization-context.outputs.claimed == 'true' && (steps.update-final-command-status.outputs.locked_conversation == 'true' || steps.update-final-command-status.outputs.missing_status_comment == 'true') }}
env:
ITEM_KEY: ${{ steps.finalization-context.outputs.item_key }}
QUEUE_LEASE_ID: ${{ github.event.client_payload.queue_lease_id }}
LEASE_REVISION: ${{ steps.finalization-context.outputs.lease_revision }}
CLAIM_GENERATION: ${{ steps.finalization-context.outputs.claim_generation }}
STATUS_MARKER: ${{ steps.finalization-context.outputs.status_marker }}
STATUS_COMMENT_ID: ${{ steps.finalization-context.outputs.status_comment_id }}
ATTEMPT_ID: ${{ steps.terminal-acknowledgement.outputs.attempt_id }}
MISSING_STATUS_COMMENT: ${{ steps.update-final-command-status.outputs.missing_status_comment }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
set -euo pipefail
skip_reason="locked_conversation"
expected_state="skipped_locked"
if [ "$MISSING_STATUS_COMMENT" = "true" ]; then
skip_reason="missing_status_comment"
expected_state="skipped_missing_comment"
fi
payload="$(SKIP_REASON="$skip_reason" node -e '
const leaseRevision = Number(process.env.LEASE_REVISION);
const claimGeneration = Number(process.env.CLAIM_GENERATION);
const runAttempt = Number(process.env.RUN_ATTEMPT);
const statusMarker = process.env.STATUS_MARKER || null;
const statusCommentId = process.env.STATUS_COMMENT_ID === "" ? null : Number(process.env.STATUS_COMMENT_ID);
if (!process.env.QUEUE_LEASE_ID || !process.env.ITEM_KEY || !process.env.ATTEMPT_ID) process.exit(1);
if (!["locked_conversation", "missing_status_comment"].includes(process.env.SKIP_REASON)) process.exit(1);
if (!Number.isInteger(leaseRevision) || leaseRevision < 1 || !Number.isInteger(claimGeneration) || claimGeneration < 1 || !Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1);
if (!statusMarker && statusCommentId === null) process.exit(1);
process.stdout.write(JSON.stringify({
lease_id: process.env.QUEUE_LEASE_ID, item_key: process.env.ITEM_KEY,
lease_revision: leaseRevision, claim_generation: claimGeneration,
run_id: process.env.GITHUB_RUN_ID, run_attempt: runAttempt,
attempt_id: process.env.ATTEMPT_ID, reason: process.env.SKIP_REASON,
...(statusMarker ? { status_marker: statusMarker } : {}),
...(statusCommentId === null ? {} : { status_comment_id: statusCommentId }),
}));
')"
response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST --header "content-type: application/json" --data "$payload" \
"${QUEUE_URL%/}/internal/exact-review/terminal-finalization/skip")"
jq -e --arg state "$expected_state" \
'.ok == true and .completed == true and .acknowledgement_state == $state' <<< "$response" >/dev/null
- name: Requeue unobserved terminal acknowledgement
if: ${{ always() && steps.finalization-context.outputs.claimed == 'true' && steps.observe-verified-terminal-acknowledgement.outcome != 'success' && !((steps.update-final-command-status.outputs.locked_conversation == 'true' || steps.update-final-command-status.outputs.missing_status_comment == 'true') && steps.complete-locked-terminal-acknowledgement.outcome == 'success') }}
env:
ITEM_KEY: ${{ steps.finalization-context.outputs.item_key }}
QUEUE_LEASE_ID: ${{ github.event.client_payload.queue_lease_id }}
LEASE_REVISION: ${{ steps.finalization-context.outputs.lease_revision }}
CLAIM_GENERATION: ${{ steps.finalization-context.outputs.claim_generation }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
set -euo pipefail
payload="$(node -e '
const leaseRevision = Number(process.env.LEASE_REVISION);
const claimGeneration = Number(process.env.CLAIM_GENERATION);
const runAttempt = Number(process.env.RUN_ATTEMPT);
if (!process.env.QUEUE_LEASE_ID || !process.env.ITEM_KEY || !Number.isInteger(leaseRevision) || leaseRevision < 1 || !Number.isInteger(claimGeneration) || claimGeneration < 1 || !Number.isInteger(runAttempt) || runAttempt < 1) process.exit(1);
process.stdout.write(JSON.stringify({
lease_id: process.env.QUEUE_LEASE_ID, item_key: process.env.ITEM_KEY,
lease_revision: leaseRevision, claim_generation: claimGeneration,
run_id: process.env.GITHUB_RUN_ID, run_attempt: runAttempt,
}));
')"
response_file="$(mktemp)"
trap 'rm -f "$response_file"' EXIT
response_status="$(curl --silent --show-error --connect-timeout 5 --max-time 20 \
--write-out '%{http_code}' --output "$response_file" \
--request POST --header "content-type: application/json" --data "$payload" \
"${QUEUE_URL%/}/internal/exact-review/terminal-finalization/retry")"
if [ "$response_status" = "409" ]; then
# A GitHub acknowledgement webhook can observe and consume the
# terminal driver between the status mutation and this retry.
# The old fenced lease must not make that successful handoff fail.
jq -e '.error == "lease_not_active"' "$response_file" >/dev/null
exit 0
fi
test "$response_status" = "200"
jq -e '.ok == true and (.requeued == true or .completed == true)' "$response_file" >/dev/null
- name: Fail failed terminal acknowledgement
if: ${{ (steps.update-final-command-status.outcome == 'failure' && steps.update-final-command-status.outputs.throttled != 'true') || steps.complete-locked-terminal-acknowledgement.outcome == 'failure' || steps.terminal-acknowledgement.outcome == 'failure' || steps.target-write-token.outcome == 'failure' }}
run: exit 1
target-fanout:
name: Fan out target repository sweeps
if: ${{ github.event_name == 'schedule' && (github.event.schedule == '4/20 * * * *' || github.event.schedule == '41/10 * * * *' || github.event.schedule == '37 */6 * * *') }}
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
actions: write
contents: write
steps:
- uses: actions/checkout@v7
with:
filter: blob:none
fetch-depth: 0
- uses: ./.github/actions/setup-state
with:
coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
hydrate-git-state: "false"
hydrate-state-blobs: "false"
- uses: ./.github/actions/setup-pnpm
with:
build-script: build:repair
- name: Create OpenClaw inventory token
id: openclaw-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
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@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
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__' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
FANOUT_MODE: ${{ github.event.schedule == '41/10 * * * *' && 'normal-review' || (github.event.schedule == '37 */6 * * *' && 'audit' || 'hot-intake') }}
FANOUT_LIMIT: ${{ github.event.schedule == '41/10 * * * *' && '12' || (github.event.schedule == '37 */6 * * *' && '12' || '20') }}
REVIEW_COVERAGE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
run: |
set -euo pipefail
pnpm run target-fanout -- \
--mode "$FANOUT_MODE" \
--limit "$FANOUT_LIMIT" \
--repo "$GITHUB_REPOSITORY" \
--workflow sweep.yml \
--ref main \
--cursor-store-url "$REVIEW_COVERAGE_URL" \
--coverage-tracked-items-manifest .artifacts/worker-records-manifest.json \
--publish-url "$REVIEW_COVERAGE_URL"
- name: Summarize trailing weekly review coverage
# Also runs for operator dispatches of the audit lane: the signed live
# inventory is what gives /api/review-coverage its true denominator, and
# gating it on one cron string left the metric reporting "missing" for
# up to six hours after a deploy or an on-demand audit.
if: ${{ github.event.schedule == '37 */6 * * *' || (github.event_name == 'workflow_dispatch' && github.event.inputs.audit_dashboard == 'true') }}
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__' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
REVIEW_COVERAGE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
run: pnpm run target-fanout -- coverage --window-days 7 --publish-url "$REVIEW_COVERAGE_URL" >> "$GITHUB_STEP_SUMMARY"
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/20 * * * *' || github.event.schedule == '41/10 * * * *' || github.event.schedule == '37 */6 * * *') || github.event.schedule == '13 * * * *'))) && !(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
concurrency:
group: ${{ format('clawsweeper-planner-{0}', (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.item_number == '' && github.event.inputs.item_numbers == '')) && (github.event.inputs.target_repo || ((github.event.schedule == '2/5 * * * *' || github.event.schedule == '22 * * * *') && 'openclaw/clawhub' || 'openclaw/openclaw')) || github.run_id) }}
cancel-in-progress: false
outputs:
batch_size: ${{ steps.mode.outputs.batch_size }}
codex_timeout_ms: ${{ steps.mode.outputs.codex_timeout_ms }}
hot_intake: ${{ steps.mode.outputs.hot_intake }}
queue_feed: ${{ steps.mode.outputs.queue_feed }}
matrix: ${{ steps.select.outputs.matrix }}
max_pages: ${{ steps.mode.outputs.max_pages }}
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 }}
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 }}
target_slug: ${{ steps.target.outputs.target_slug }}
steps:
- uses: actions/checkout@v7
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_name == 'workflow_dispatch' && github.event.inputs.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_slug="$(printf '%s' "$target_repo" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_.-]+/-/g')"
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_slug=$target_slug"
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@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
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:
coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
records-repo-slugs: ${{ steps.target.outputs.target_slug }}
hydrate-state-blobs: "false"
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
- 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
env:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
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 }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_QUEUE_PRESSURE_SOFT_PENDING: ${{ vars.CLAWSWEEPER_QUEUE_PRESSURE_SOFT_PENDING || '' }}
CLAWSWEEPER_QUEUE_PRESSURE_HARD_PENDING: ${{ vars.CLAWSWEEPER_QUEUE_PRESSURE_HARD_PENDING || '' }}
CLAWSWEEPER_QUEUE_PRESSURE_SOFT_AGE_MS: ${{ vars.CLAWSWEEPER_QUEUE_PRESSURE_SOFT_AGE_MS || '' }}
CLAWSWEEPER_QUEUE_PRESSURE_HARD_AGE_MS: ${{ vars.CLAWSWEEPER_QUEUE_PRESSURE_HARD_AGE_MS || '' }}
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}" \
--paginate \
--jq '.workflow_runs[] | {databaseId:.id, workflowPath:.path, displayTitle:.display_title, event:.event, 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_PATH="$1" STALE_QUEUED_CUTOFF="$STALE_QUEUED_CUTOFF" jq '[.[] | select(.workflowPath == env.WORKFLOW_PATH) | 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_workers() {
local runs total id title status run_event jobs_json active_shards total_shards
local reserved_shards hard_cap requested_shards item_numbers commas item_count
total=0
hard_cap="$(limit review_shards.hard_cap)"
runs="$(printf '%s' "$ACTIVE_RUNS_JSON" \
| STALE_QUEUED_CUTOFF="$STALE_QUEUED_CUTOFF" jq -r '.[] | select(.workflowPath == ".github/workflows/sweep.yml") | 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 | startswith("Review exact item ")) or (.displayTitle | startswith("Review scheduled hot item ")) or (.displayTitle | startswith("Review scheduled normal item ")) or (.event == "workflow_dispatch" and ((.displayTitle | startswith("Review event item ")) or (.displayTitle | startswith("Review event items "))))) | [.databaseId, .displayTitle, .status, .event] | @tsv' 2>/dev/null || true)"
while IFS=$'\t' read -r id title status run_event; do
if [ -z "$id" ]; then
continue
fi
active_shards=0
total_shards=0
if [[ "$title" == Review\ exact\ item\ * ]] || [[ "$title" == Review\ scheduled\ hot\ item\ * ]] || [[ "$title" == Review\ scheduled\ normal\ item\ * ]] || { [ "$run_event" = "workflow_dispatch" ] && [[ "$title" == Review\ event\ item\ * ]]; }; then
active_shards=1
total=$((total + active_shards))
continue
fi
if [ "$status" = "in_progress" ]; then
jobs_json="$(gh run view "$id" --repo "${{ github.repository }}" --json jobs 2>/dev/null || printf '{"jobs":[]}')"
active_shards="$(printf '%s' "$jobs_json" \
| 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')"
total_shards="$(printf '%s' "$jobs_json" \
| jq '[.jobs[]? | select(.name | startswith("Review shard "))] | length' 2>/dev/null \
|| printf '0')"
fi
if ! [[ "$active_shards" =~ ^[0-9]+$ ]]; then
active_shards=0
fi
if ! [[ "$total_shards" =~ ^[0-9]+$ ]]; then
total_shards=0
fi
# Single-item event reviews reserve one worker. Explicit batches carry
# enough run-name metadata to reserve their maximum possible matrix until
# live shard jobs exist; after expansion, only live shard jobs consume slots.
if [ "$active_shards" -lt 1 ] && [ "$total_shards" -lt 1 ]; then
reserved_shards="$(limit review_shards.exact_item_default)"
if [[ "$title" == Review\ event\ items\ * ]]; then
reserved_shards="$hard_cap"
if [[ "$title" =~ \[shards=([0-9]+)\]$ ]]; then
requested_shards="${BASH_REMATCH[1]}"
item_numbers="${title#*#}"
item_numbers="${item_numbers%% *}"
if [[ "$item_numbers" =~ ^[0-9]+(,[0-9]+)*$ ]]; then
if [ "$requested_shards" -lt 1 ]; then
requested_shards=1
fi
commas="${item_numbers//[^,]/}"
item_count=$((${#commas} + 1))
reserved_shards="$requested_shards"
if [ "$reserved_shards" -gt "$item_count" ]; then
reserved_shards="$item_count"
fi
if [ "$reserved_shards" -gt "$hard_cap" ]; then
reserved_shards="$hard_cap"
fi
fi
fi
fi
active_shards="$reserved_shards"
fi
total=$((total + active_shards))
done <<< "$runs"
printf '%s' "$total"
}
active_sweep_background_workers() {
local runs total id title status jobs_json active_shards total_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(.workflowPath == ".github/workflows/sweep.yml") | 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
total_shards=0
if [ "$status" = "in_progress" ]; then
jobs_json="$(gh run view "$id" --repo "${{ github.repository }}" --json jobs 2>/dev/null || printf '{"jobs":[]}')"
active_shards="$(printf '%s' "$jobs_json" \
| 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')"
total_shards="$(printf '%s' "$jobs_json" \
| jq '[.jobs[]? | select(.name | startswith("Review shard "))] | length' 2>/dev/null \
|| printf '0')"
fi
if ! [[ "$active_shards" =~ ^[0-9]+$ ]]; then
active_shards=0
fi
if ! [[ "$total_shards" =~ ^[0-9]+$ ]]; then
total_shards=0
fi
# Planning, queued, and not-yet-expanded matrix runs reserve their quiet
# lane size. Runs with completed shard jobs are publishing and consume no
# Codex slots, so the next serialized planner may refill that capacity.
if [ "$active_shards" -lt 1 ] && [ "$total_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)"
active_critical_workers="$(( $(active_run_count ".github/workflows/repair-cluster-worker.yml") + $(active_sweep_exact_workers) ))"
active_background_workers="$(active_sweep_background_workers)"
exact_item="${{ github.event.client_payload.item_number || github.event.inputs.item_number || github.event.inputs.item_numbers || '' }}"
pressure_level="unknown"
queue_candidate_capacity="50"
hot_intake_unpressured="$(worker_limit hot_intake --active-critical "$active_critical_workers" --active-background "$active_background_workers")"
normal_unpressured="$(worker_limit normal_review --active-critical "$active_critical_workers" --active-background "$active_background_workers")"
if [ -z "$exact_item" ]; then
if ! pressure_json="$(pnpm --dir clawsweeper run --silent workflow -- queue-pressure --queue-url "$QUEUE_URL")"; then
pressure_json='{"ok":false,"reason":"probe_failed","level":"unknown"}'
fi
if ! pressure_level="$(printf '%s' "$pressure_json" | jq -er '.level | select(. == "none" or . == "soft" or . == "hard" or . == "unknown")' 2>/dev/null)"; then
pressure_level="unknown"
fi
if ! queue_candidate_capacity="$(printf '%s' "$pressure_json" | jq -er '.availableCandidateCapacity | select(type == "number" and floor == . and . >= 0)' 2>/dev/null)"; then
queue_candidate_capacity="50"
fi
if [ "$queue_candidate_capacity" -lt 1 ]; then
queue_candidate_capacity="1"
fi
fi
hot_intake_shards="$(worker_limit hot_intake --active-critical "$active_critical_workers" --active-background "$active_background_workers" --pressure-level "$pressure_level")"
normal_shards="$(worker_limit normal_review --active-critical "$active_critical_workers" --active-background "$active_background_workers" --pressure-level "$pressure_level")"
if [ -z "$exact_item" ]; then
pressure_numbers="$(printf '%s' "$pressure_json" | jq -r 'if .ok then "pending=\(.pendingCount), active=\(.activeCount // "unknown")/\(.capacity // "unknown"), candidate-capacity=\(.availableCandidateCapacity // "unknown"), oldest=\((.oldestPendingAgeMs / 3600000 * 10 | round) / 10)h" else "unavailable=\(.reason)" end' 2>/dev/null || printf 'unavailable=probe_failed')"
echo "queue pressure: $pressure_level ($pressure_numbers) — hot_intake $hot_intake_unpressured->$hot_intake_shards, normal_review $normal_unpressured->$normal_shards"
fi
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' }}"
queue_feed="${{ ((github.event_name == 'repository_dispatch' && github.event.action == 'clawsweeper_target_sweep') || github.event_name == 'schedule') && 'true' || 'false' }}"
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 [ "$queue_feed" = "true" ] && [ -z "$exact_item" ]; then
# The Durable Object turns every selected item into an independent exact-review
# workflow. Direct schedules use current free queue capacity; target fanout may
# request a smaller backlog-weighted share. The Worker remains the final owner of
# the fleet-wide 300/hour admission target and queue backpressure.
requested_batch_size="${{ github.event.client_payload.batch_size || '' }}"
if ! [[ "$requested_batch_size" =~ ^[0-9]+$ ]] || [ "$requested_batch_size" -lt 1 ]; then
requested_batch_size="$queue_candidate_capacity"
fi
if [ "$requested_batch_size" -gt "$queue_candidate_capacity" ]; then
requested_batch_size="$queue_candidate_capacity"
fi
if [ "$requested_batch_size" -gt "$hard_shard_cap" ]; then
requested_batch_size="$hard_shard_cap"
fi
batch_size="$requested_batch_size"
shard_count="1"
min_active_shards="0"
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.review_options.codex_timeout_ms || github.event.client_payload.codex_timeout_ms || github.event.inputs.codex_timeout_ms || vars.CLAWSWEEPER_CODEX_TIMEOUT_MS || '1200000' }}"
echo "hot_intake=$hot_intake"
echo "queue_feed=$queue_feed"
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 || '' }}
# A router fallback carries its receipt key in item_numbers because GitHub caps this
# workflow at 25 inputs; item_number remains the exact review target in that case.
ITEM_NUMBERS: ${{ (github.event.inputs.item_number != '' && startsWith(github.event.inputs.item_numbers, 'router-') && github.event.inputs.item_number) || 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 internal \
--codex-reasoning-effort high \
--codex-sandbox read-only \
--min-active-shards "$MIN_ACTIVE_SHARDS" \
--min-backfill-review-age-minutes "$MIN_BACKFILL_REVIEW_AGE_MINUTES" \
--coverage-tracked-items-manifest .artifacts/worker-records-manifest.json \
"${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: Enqueue scheduled review candidates
id: enqueue-scheduled
if: ${{ steps.mode.outputs.queue_feed == 'true' && steps.select.outputs.planned_count != '0' }}
working-directory: clawsweeper
env:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
GH_TOKEN: ${{ steps.target-read-token.outputs.token || github.token }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
run: |
set -euo pipefail
lane="normal_backfill"
if [ "${{ steps.mode.outputs.hot_intake }}" = "true" ]; then
lane="hot_intake"
fi
target_repo="${{ steps.target.outputs.target_repo }}"
target_branch="$(gh api "repos/$target_repo" --jq '.default_branch // empty')"
if [ -z "$target_branch" ] ||
! printf '%s' "$target_branch" | grep -Eq '^[A-Za-z0-9_.\/-]+$' ||
printf '%s' "$target_branch" | grep -Eq '^[0-9]+$' ||
[[ "$target_branch" == *".."* ]]; then
echo "Invalid default branch for scheduled target $target_repo: $target_branch" >&2
exit 1
fi
echo "::notice::Scheduled review payloads will use default branch '$target_branch' for $target_repo."
result="$(pnpm run --silent repair:scheduled-review-enqueue -- \
--plan plan.json \
--lane "$lane" \
--target-repo "$target_repo" \
--target-branch "$target_branch" \
--queue-url "$QUEUE_URL" \
--delivery-prefix "scheduled:${{ github.run_id }}:${{ github.run_attempt }}")"
jq -e '.offered >= .attempted and .attempted >= (.queued + .deduped + .shed + .rejected)' <<<"$result" >/dev/null
{
echo "### Scheduled review funnel"
echo
echo "| Selected | Attempted | Enqueued | Deduped | Shed | Deferred |"
echo "| ---: | ---: | ---: | ---: | ---: | ---: |"
echo "| $(jq -r .offered <<<"$result") | $(jq -r .attempted <<<"$result") | $(jq -r .queued <<<"$result") | $(jq -r .deduped <<<"$result") | $(jq -r .shed <<<"$result") | $(jq -r .deferred <<<"$result") |"
echo
echo "Selected candidate age (hours): p50 $(jq -r '.ageHours.p50 // "n/a"' <<<"$result"), p90 $(jq -r '.ageHours.p90 // "n/a"' <<<"$result"), oldest $(jq -r '.ageHours.max // "n/a"' <<<"$result")."
} >> "$GITHUB_STEP_SUMMARY"
- name: Prepare review runtime artifact
if: ${{ steps.mode.outputs.queue_feed != 'true' }}
working-directory: clawsweeper
env:
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
run: |
target_slug="${TARGET_REPO//\//-}"
node scripts/prepare-review-runtime.mjs \
--output .artifacts/review-runtime \
--plan plan.json \
--state-root . \
--records-path "records/${target_slug}/items"
tar -czf .artifacts/review-runtime.tar.gz -C .artifacts/review-runtime .
- uses: actions/upload-artifact@v7
if: ${{ steps.mode.outputs.queue_feed != 'true' }}
with:
name: clawsweeper-runtime-dist
path: clawsweeper/.artifacts/review-runtime.tar.gz
include-hidden-files: true
if-no-files-found: error
retention-days: 1
- name: Publish planning status
if: ${{ steps.mode.outputs.queue_feed != 'true' && (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:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
GH_TOKEN: ${{ github.token }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
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.outputs.target_repo }}#${{ matrix.item_numbers }}
needs: plan
if: ${{ needs.plan.outputs.queue_feed != 'true' }}
runs-on: ${{ vars.CLAWSWEEPER_REVIEW_RUNNER || 'ubuntu-latest' }}
timeout-minutes: 75
continue-on-error: true
permissions:
actions: read
checks: read
contents: read
issues: read
pull-requests: read
statuses: read
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.plan.outputs.matrix) }}
steps:
- uses: actions/checkout@v7
with:
path: clawsweeper
filter: blob:none
fetch-depth: 1
persist-credentials: false
- uses: ./clawsweeper/.github/actions/setup-action-ledger
continue-on-error: true
with:
worktree-path: clawsweeper
- name: Create target review token
id: target-read-token
continue-on-error: true
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
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-checks: read
permission-contents: read
permission-issues: write
permission-pull-requests: write
permission-statuses: read
- name: Create target Codex inspection token
id: codex-inspection-token
continue-on-error: true
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
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/.artifacts
- name: Extract review runtime artifact
working-directory: clawsweeper
run: |
tar -xzf .artifacts/review-runtime.tar.gz
- name: Install review compiler service
continue-on-error: true
working-directory: clawsweeper
run: node scripts/install-review-native-compiler.mjs
- uses: ./clawsweeper/.github/actions/setup-codex
if: ${{ env.CLAWSWEEPER_RUNNER != 'openclaw' }}
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CLAWSWEEPER_INTERNAL_MODEL: ${{ secrets.CLAWSWEEPER_MODEL }}
with:
login-status: "true"
- uses: ./clawsweeper/.github/actions/setup-openclaw
- uses: actions/cache@v6
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_PUBLIC_GH_TOKEN: ${{ needs.plan.outputs.target_repo == 'openclaw/openclaw' && github.token || '' }}
CLAWSWEEPER_PROOF_INSPECTION_TOKEN: ${{ steps.codex-inspection-token.outputs.token }}
ADDITIONAL_PROMPT: ${{ github.event.inputs.additional_prompt || github.event.client_payload.review_options.additional_prompt || github.event.client_payload.additional_prompt || '' }}
EXACT_ITEM: ${{ github.event.client_payload.item_number || github.event.inputs.item_number || github.event.inputs.item_numbers || '' }}
EXPECTED_SOURCE_REVISION: ${{ github.event.client_payload.expected_source_revision || '' }}
PR_COMMENT_ACTIVITY_REVISIONS: ${{ matrix.pr_comment_activity_revisions }}
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
expected_source_revision_arg=()
if [ -n "$EXPECTED_SOURCE_REVISION" ]; then
expected_source_revision_arg=(--expected-source-revision "$EXPECTED_SOURCE_REVISION")
fi
planned_automatic_review_arg=()
if [ -z "$EXACT_ITEM" ]; then
planned_automatic_review_arg=(--planned-automatic-review)
fi
pr_comment_activity_arg=()
if [ -n "$PR_COMMENT_ACTIVITY_REVISIONS" ]; then
pr_comment_activity_arg=(--pr-comment-activity-revisions "$PR_COMMENT_ACTIVITY_REVISIONS")
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."
set +e
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 internal \
--codex-reasoning-effort high \
--codex-sandbox read-only \
--codex-timeout-ms ${{ needs.plan.outputs.codex_timeout_ms }} \
--item-numbers "${{ matrix.item_numbers }}" \
"${pr_comment_activity_arg[@]}" \
"${planned_automatic_review_arg[@]}" \
"${hot_intake_arg[@]}" \
--readonly-openclaw \
--shard-index ${{ matrix.shard }} \
--shard-count ${{ needs.plan.outputs.planned_shards }} \
"${additional_prompt_arg[@]}" \
"${expected_source_revision_arg[@]}"
review_exit_code=$?
set -e
echo "exit_code=$review_exit_code" >> "$GITHUB_OUTPUT"
exit "$review_exit_code"
- name: Finalize review action ledger
if: always()
continue-on-error: true
working-directory: clawsweeper
env:
REVIEW_EXIT_CODE: ${{ steps.review-shard.outputs.exit_code || '' }}
REVIEW_OUTCOME: ${{ steps.review-shard.outcome || 'not_started' }}
run: |
args=()
if [ "$REVIEW_OUTCOME" = "cancelled" ] || [ "$REVIEW_EXIT_CODE" = "130" ] || [ "$REVIEW_EXIT_CODE" = "143" ]; then
args=(--interrupt-open-attempts --reason cancelled)
elif [ "$REVIEW_EXIT_CODE" = "124" ] || [ "$REVIEW_EXIT_CODE" = "137" ]; then
args=(--interrupt-open-attempts --reason timeout)
elif [ "$REVIEW_OUTCOME" != "success" ]; then
args=(--interrupt-open-attempts --reason workflow_failed)
fi
node dist/clawsweeper.js finalize-action-events "${args[@]}"
- 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
review-artifacts/shard-${{ matrix.shard }}/review-cache-metrics.json
if-no-files-found: ignore
- uses: actions/upload-artifact@v7
if: always()
with:
name: action-ledger-review-${{ matrix.shard }}
path: ${{ runner.temp }}/clawsweeper-action-ledger/${{ github.run_id }}/${{ github.run_attempt }}/${{ github.job }}/**
include-hidden-files: true
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
- uses: actions/upload-artifact@v7
if: ${{ always() && github.event_name == 'repository_dispatch' && github.event.client_payload.expected_source_revision != '' }}
with:
name: review-source-revision-mismatch-${{ matrix.shard }}
path: review-artifacts/shard-${{ matrix.shard }}/source-revision-mismatch.json
if-no-files-found: ignore
requeue-source-revision-drift:
name: Requeue source-revision drift
needs: [plan, review]
if: ${{ always() && needs.plan.result == 'success' && needs.review.result != 'cancelled' && github.event_name == 'repository_dispatch' && github.event.action == 'clawsweeper_target_sweep' && github.event.client_payload.expected_source_revision != '' }}
runs-on: ubuntu-latest
permissions:
actions: read
contents: write
steps:
- uses: actions/download-artifact@v8
continue-on-error: true
with:
pattern: review-source-revision-mismatch-*
path: source-revision-mismatches
merge-multiple: true
- name: Dispatch exact review at current revision
env:
GH_TOKEN: ${{ github.token }}
EXPECTED_SOURCE_REVISION: ${{ github.event.client_payload.expected_source_revision || '' }}
REQUEUE_COUNT: ${{ github.event.client_payload.source_revision_requeue_count || '0' }}
TARGET_REPO: ${{ needs.plan.outputs.target_repo }}
TARGET_BRANCH: ${{ needs.plan.outputs.target_branch }}
CODEX_TIMEOUT_MS: ${{ needs.plan.outputs.codex_timeout_ms }}
run: |
set -euo pipefail
marker="source-revision-mismatches/source-revision-mismatch.json"
if [ ! -f "$marker" ]; then
exit 0
fi
if ! [[ "$REQUEUE_COUNT" =~ ^[0-9]+$ ]] || [ "$REQUEUE_COUNT" -ge 1 ]; then
echo "::warning::Source revision changed again; leaving the normal scheduler to pick up the latest revision."
exit 0
fi
marker_expected="$(jq -r '.expected_source_revision // empty' "$marker")"
actual_revision="$(jq -r '.actual_source_revision // empty' "$marker")"
item_number="$(jq -r '.item_number // empty' "$marker")"
if [ "$marker_expected" != "$EXPECTED_SOURCE_REVISION" ] || \
! [[ "$actual_revision" =~ ^[0-9a-f]{64}$ ]] || \
! [[ "$item_number" =~ ^[0-9]+$ ]]; then
echo "::warning::Ignoring invalid source-revision mismatch marker."
exit 0
fi
jq -n \
--arg target_repo "$TARGET_REPO" \
--arg target_branch "$TARGET_BRANCH" \
--arg item_number "$item_number" \
--arg codex_timeout_ms "$CODEX_TIMEOUT_MS" \
--arg expected_source_revision "$actual_revision" \
'{
event_type: "clawsweeper_target_sweep",
client_payload: {
target_repo: $target_repo,
target_branch: $target_branch,
item_number: $item_number,
batch_size: "1",
shard_count: "1",
hot_intake: "false",
codex_timeout_ms: $codex_timeout_ms,
expected_source_revision: $expected_source_revision,
source_revision_requeue_count: "1"
}
}' | gh api --method POST "repos/$GITHUB_REPOSITORY/dispatches" --input -
echo "::notice::Requeued $TARGET_REPO#$item_number at source revision $actual_revision."
publish:
name: Publish review artifacts
needs: [plan, review]
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.queue_feed != 'true' && 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
concurrency:
group: clawsweeper-target-review-publish-${{ needs.plan.outputs.target_repo }}
cancel-in-progress: false
queue: max
steps:
- uses: actions/checkout@v7
with:
filter: blob:none
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup-action-ledger
continue-on-error: true
- name: Create target write token
id: target-write-token
continue-on-error: true
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
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
id: setup-publish-state
with:
coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
records-repo-slugs: ${{ needs.plan.outputs.target_slug }}
hydrate-state-blobs: "false"
token: ${{ steps.state-token.outputs.token }}
- uses: ./.github/actions/setup-pnpm
id: setup-publish-pnpm
with:
build-script: build:all
- uses: actions/download-artifact@v8
id: download-review-artifacts
with:
pattern: review-shard-*
path: artifacts
merge-multiple: true
- uses: actions/download-artifact@v8
id: download-review-metrics
continue-on-error: true
with:
pattern: review-metrics-*
path: review-metrics
merge-multiple: true
- uses: actions/download-artifact@v8
id: download-review-action-ledger
continue-on-error: true
with:
pattern: action-ledger-review-*
path: .clawsweeper-repair/action-ledger-download
merge-multiple: true
- name: Import immutable review action events
id: import-review-action-ledger
if: ${{ always() && steps.setup-publish-state.outcome == 'success' && steps.setup-publish-pnpm.outcome == 'success' && steps.download-review-action-ledger.outcome == 'success' }}
continue-on-error: true
run: |
set -euo pipefail
mkdir -p .artifacts
: > .artifacts/action-ledger-paths.txt
pnpm run --silent publish-action-events -- \
--source-root .clawsweeper-repair/action-ledger-download \
--state-root . \
--expected-producer-job review \
--expected-producer-max-run-attempt "$GITHUB_RUN_ATTEMPT" |
jq -r '.paths[]?' >> .artifacts/action-ledger-paths.txt
pnpm run --silent publish-action-events -- \
--source-root "$CLAWSWEEPER_ACTION_LEDGER_OUTPUT_ROOT" \
--state-root . \
--expected-producer-job "$GITHUB_JOB" |
jq -r '.paths[]?' >> .artifacts/action-ledger-paths.txt
sort -u -o .artifacts/action-ledger-paths.txt .artifacts/action-ledger-paths.txt
- name: Publish immutable review action ledger
if: ${{ always() && steps.import-review-action-ledger.outcome == 'success' }}
continue-on-error: true
env:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
run: |
if [ ! -s .artifacts/action-ledger-paths.txt ]; then
echo "No immutable review action events to publish."
exit 0
fi
node dist/clawsweeper.js publish-action-event-paths \
--paths-file .artifacts/action-ledger-paths.txt
- name: Sync before applying artifacts
id: sync-review-artifacts
if: ${{ always() && !cancelled() && steps.setup-publish-state.outcome == 'success' && steps.setup-publish-pnpm.outcome == 'success' && steps.download-review-artifacts.outcome == 'success' }}
run: |
if ! pull_output="$(git pull --rebase --autostash 2>&1)"; then
printf '%s\n' "$pull_output" >&2
# A reviewed PR head branch can vanish mid-review (force-push or
# deletion). The newer head triggers its own review event, so this
# publication completes as a superseded no-op instead of failing.
if grep -qiE "no such ref was fetched|couldn.t find remote ref" <<<"$pull_output"; then
echo "::notice::The reviewed branch disappeared upstream; the newer head owns its own review. Skipping artifact publication as a superseded no-op."
echo "superseded=true" >> "$GITHUB_OUTPUT"
exit 0
fi
exit 1
fi
- name: Apply review artifacts
id: apply-review-artifacts
if: ${{ always() && !cancelled() && steps.sync-review-artifacts.outcome == 'success' && steps.sync-review-artifacts.outputs.superseded != 'true' }}
env:
CLAWSWEEPER_CANONICAL_RECORD_BASELINE_DIR: .artifacts/review-canonical-baseline
GH_TOKEN: ${{ github.token }}
EXACT_ITEM: ${{ github.event.client_payload.item_number || github.event.inputs.item_number || github.event.inputs.item_numbers || '' }}
EXPECTED_METRIC_COUNT: ${{ needs.plan.outputs.planned_shards }}
HOT_INTAKE: ${{ needs.plan.outputs.hot_intake }}
MAX_PAGES: ${{ needs.plan.outputs.max_pages }}
TARGET_REPO: ${{ needs.plan.outputs.target_repo }}
run: |
set -euo pipefail
apply_artifacts_args=(--target-repo "$TARGET_REPO" --artifact-dir artifacts --skip-dashboard --skip-reconcile)
publish_state="Review publish complete"
mkdir -p review-metrics
reviewed_count="$(find artifacts -type f -name '*.md' | wc -l | tr -d ' ')"
metric_count="$(find review-metrics -type f -name '*.json' | wc -l | tr -d ' ')"
non_success_metric_count="$(find review-metrics -type f -name '*.json' -print0 | xargs -0 -r jq -r 'select(.review_outcome != "success") | .shard' | wc -l | tr -d ' ')"
review_batch_succeeded=false
if [[ "$EXPECTED_METRIC_COUNT" =~ ^[1-9][0-9]*$ ]] &&
[ "$metric_count" -eq "$EXPECTED_METRIC_COUNT" ] &&
[ "$non_success_metric_count" -eq 0 ]; then
review_batch_succeeded=true
fi
echo "review_batch_succeeded=$review_batch_succeeded" >> "$GITHUB_OUTPUT"
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[@]}"
echo "artifacts_applied=true" >> "$GITHUB_OUTPUT"
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: Publish review artifact action ledger
if: ${{ always() && steps.apply-review-artifacts.outputs.artifacts_applied == 'true' }}
env:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
run: |
set -euo pipefail
source_root="$CLAWSWEEPER_ACTION_LEDGER_OUTPUT_ROOT"
if [ ! -d "$source_root/ledger" ]; then
echo "No review artifact action event shards were finalized."
exit 0
fi
event_paths_file=".artifacts/review-artifact-action-ledger-paths.txt"
mkdir -p .artifacts
pnpm run --silent publish-action-events -- \
--source-root "$source_root" \
--state-root . \
--expected-producer-job "$GITHUB_JOB" |
jq -r '.paths[]?' |
sort -u > "$event_paths_file"
if [ ! -s "$event_paths_file" ]; then
echo "Review artifact action event shards existed but no paths were imported." >&2
exit 1
fi
node dist/clawsweeper.js publish-action-event-paths \
--paths-file "$event_paths_file"
- name: Commit review records
id: commit-review-records
if: ${{ always() && !cancelled() && steps.apply-review-artifacts.outputs.artifacts_applied == 'true' }}
env:
CLAWSWEEPER_CANONICAL_RECORD_BASELINE_DIR: .artifacts/review-canonical-baseline
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
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 }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
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 \
--canonical-record-baseline-dir "$CLAWSWEEPER_CANONICAL_RECORD_BASELINE_DIR"
fi
pnpm run repair:publish-main -- \
--message "chore: update sweep records" \
--path "records/${target_slug}" \
--rebase-strategy normal
echo "records_published=true" >> "$GITHUB_OUTPUT"
if ! pnpm run repair:publish-main -- \
--message "chore: update sweep status" \
--path "results/sweep-status/${target_slug}.json" \
--rebase-strategy theirs; then
echo "::warning title=Sweep status publish failed::Review records are canonical; continuing without the best-effort Git dashboard update."
fi
- name: Dispatch high-confidence bug implementation candidates
if: ${{ always() && !cancelled() && steps.commit-review-records.outputs.records_published == 'true' && vars.CLAWSWEEPER_AUTO_IMPLEMENT_ISSUES == '1' && needs.plan.outputs.target_repo == 'openclaw/openclaw' }}
env:
GH_TOKEN: ${{ github.token }}
TARGET_REPO: ${{ needs.plan.outputs.target_repo }}
ENABLED: ${{ vars.CLAWSWEEPER_AUTO_IMPLEMENT_ISSUES == '1' && 'true' || 'false' }}
MAX_DISPATCH: ${{ vars.CLAWSWEEPER_AUTO_IMPLEMENT_MAX_DISPATCH_PER_SWEEP || '' }}
run: |
target_slug="${TARGET_REPO//\//-}"
node scripts/dispatch-issue-implementation-candidates.mjs \
--target-repo "$TARGET_REPO" \
--artifact-dir artifacts \
--report-dir "records/$target_slug/items"
- name: Collect published review item numbers
id: published-review-items
if: ${{ always() && !cancelled() && steps.commit-review-records.outputs.records_published == 'true' }}
run: |
set -euo pipefail
item_numbers="$(pnpm run --silent workflow -- artifact-item-numbers --artifact-dir artifacts)"
echo "item_numbers=$item_numbers" >> "$GITHUB_OUTPUT"
- name: Dispatch recommended live proofs
if: ${{ always() && !cancelled() && steps.commit-review-records.outputs.records_published == 'true' && steps.published-review-items.outcome == 'success' }}
uses: ./.github/actions/dispatch-live-proofs
with:
target-repo: ${{ needs.plan.outputs.target_repo }}
item-numbers: ${{ steps.published-review-items.outputs.item_numbers }}
records-root: records
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
- name: Dispatch vision-fit implementation candidates
if: ${{ always() && !cancelled() && steps.commit-review-records.outputs.records_published == 'true' && vars.CLAWSWEEPER_AUTO_IMPLEMENT_ISSUES == '1' && 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
target_slug="${TARGET_REPO//\//-}"
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 \
--report-dir "records/$target_slug/items")"
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: Backfill viable open issue implementation candidates
if: ${{ always() && !cancelled() && steps.commit-review-records.outputs.records_published == 'true' && vars.CLAWSWEEPER_AUTO_IMPLEMENT_ISSUES == '1' && steps.target-write-token.outputs.token != '' && needs.plan.outputs.target_repo != 'openclaw/openclaw' && needs.plan.outputs.target_repo != 'openclaw/clawhub' }}
env:
GH_TOKEN: ${{ github.token }}
TARGET_REPO: ${{ needs.plan.outputs.target_repo }}
MAX_DISPATCH: ${{ vars.CLAWSWEEPER_AUTO_IMPLEMENT_MAX_DISPATCH_PER_SWEEP || '' }}
run: |
set -euo pipefail
target_slug="${TARGET_REPO//\//-}"
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 true \
--candidate-kind viable \
--target-repo "$TARGET_REPO" \
--artifact-dir artifacts \
--report-dir "records/$target_slug/items" \
--report-repo openclaw/clawsweeper-state)"
echo "$candidate_output"
CANDIDATE_OUTPUT="$candidate_output" MAX_DISPATCH="$MAX_DISPATCH" node <<'NODE' > /tmp/viable-implementation-candidates.tsv
const output = JSON.parse(process.env.CANDIDATE_OUTPUT || "{}");
const limit = Math.max(0, Number(process.env.MAX_DISPATCH || "0"));
for (const candidate of (output.candidates || []).slice(0, limit)) {
console.log([candidate.item_number, candidate.report_path, candidate.report_url].join("\t"));
}
NODE
if [ ! -s /tmp/viable-implementation-candidates.tsv ]; then
echo "No viable issue implementation candidates."
exit 0
fi
while IFS=$'\t' read -r item_number report_path report_url; do
echo "Dispatching viable issue 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=viable \
-f report_repo=openclaw/clawsweeper-state \
-f report_path="$report_path" \
-f report_url="$report_url"
done < /tmp/viable-implementation-candidates.tsv
- name: Dispatch background review comment sync
if: ${{ always() && !cancelled() && steps.commit-review-records.outputs.records_published == 'true' && 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=__cursor__ \
-f apply_kind=all \
-f apply_limit=40 \
-f apply_min_age_days=0 \
-f apply_comment_sync_min_age_days=0; then
echo "Dispatched coalesced durable 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."
- name: Sync selected review comments
id: sync-selected-review-comments
if: ${{ always() && !cancelled() && steps.commit-review-records.outputs.records_published == 'true' && 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:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
GH_TOKEN: ${{ steps.target-write-token.outputs.token }}
CLAWSWEEPER_PUBLIC_GH_TOKEN: ${{ needs.plan.outputs.target_repo == 'openclaw/openclaw' && github.token || '' }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
TARGET_REPO: ${{ needs.plan.outputs.target_repo }}
run: |
set -euo pipefail
test -n "$GH_TOKEN"
source scripts/apply-workflow-helpers.sh
begin_canonical_record_mutation
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 --autostash
set +e
timeout --kill-after=30s 840s 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
selected_comment_exit_code=$?
set -e
echo "exit_code=$selected_comment_exit_code" >> "$GITHUB_OUTPUT"
if [ "$selected_comment_exit_code" -ne 0 ]; then
exit "$selected_comment_exit_code"
fi
echo "sync_succeeded=true" >> "$GITHUB_OUTPUT"
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}" \
--rebase-strategy normal
if ! pnpm run repair:publish-main -- \
--message "chore: publish selected review comment status" \
--path apply-report.json \
--path "results/sweep-status/${target_slug}.json" \
--rebase-strategy theirs; then
echo "::warning title=Selected-comment bookkeeping failed::Canonical comment records are published; continuing without the best-effort Git report update."
fi
- name: Finalize selected review comment action ledger
id: finalize-selected-review-comment-action-ledger
if: ${{ always() && steps.sync-selected-review-comments.outcome != 'skipped' }}
env:
SELECTED_COMMENT_EXIT_CODE: ${{ steps.sync-selected-review-comments.outputs.exit_code || '' }}
SELECTED_COMMENT_OUTCOME: ${{ steps.sync-selected-review-comments.outcome || 'not_started' }}
run: |
args=()
if [ "$SELECTED_COMMENT_OUTCOME" = "cancelled" ] || [ "$SELECTED_COMMENT_EXIT_CODE" = "130" ] || [ "$SELECTED_COMMENT_EXIT_CODE" = "143" ]; then
args=(--interrupt-open-attempts --reason cancelled)
elif [ "$SELECTED_COMMENT_EXIT_CODE" = "124" ] || [ "$SELECTED_COMMENT_EXIT_CODE" = "137" ]; then
args=(--interrupt-open-attempts --reason timeout)
elif [ "$SELECTED_COMMENT_OUTCOME" != "success" ]; then
args=(--interrupt-open-attempts --reason workflow_failed)
fi
pnpm run --silent finalize-action-events -- "${args[@]}"
- name: Publish selected review comment action ledger
if: ${{ always() && steps.sync-selected-review-comments.outcome != 'skipped' && steps.finalize-selected-review-comment-action-ledger.outcome == 'success' }}
env:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
run: |
set -euo pipefail
if [ ! -d "$CLAWSWEEPER_ACTION_LEDGER_OUTPUT_ROOT/ledger" ]; then
echo "No selected review comment action shards were finalized."
exit 0
fi
event_paths_file=".artifacts/selected-comment-action-ledger-paths.txt"
mkdir -p .artifacts
pnpm run --silent publish-action-events -- \
--source-root "$CLAWSWEEPER_ACTION_LEDGER_OUTPUT_ROOT" \
--state-root . \
--expected-producer-job "$GITHUB_JOB" |
jq -r '.paths[]?' |
sort -u > "$event_paths_file"
if [ ! -s "$event_paths_file" ]; then
echo "Selected review comment action shards existed but no paths were imported." >&2
exit 1
fi
node dist/clawsweeper.js publish-action-event-paths \
--paths-file "$event_paths_file"
- name: Dispatch selected safe close proposals to isolated apply
if: ${{ always() && !cancelled() && steps.sync-selected-review-comments.outputs.sync_succeeded == 'true' && steps.target-write-token.outputs.token != '' && github.event.inputs.apply_after_review == 'true' }}
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 dispatch for isolated 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 || env.CLAWSWEEPER_AUTO_CLOSE_REASONS }}"
min_age_minutes="${{ github.event.inputs.apply_after_review_min_age_minutes || '0' }}"
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_item_numbers="$item_numbers" \
-f apply_limit="$item_count" \
-f apply_min_age_minutes="$min_age_minutes" \
-f apply_kind=all \
-f apply_close_reasons="$close_reasons" \
-f apply_stale_min_age_days=60 \
-f apply_close_delay_ms=1000 \
-f apply_comment_sync_min_age_days=0; then
echo "Dispatched isolated apply for item numbers: $item_numbers"
exit 0
fi
sleep "$((attempt * 10))"
done
echo "Unable to dispatch isolated apply after three attempts." >&2
exit 1
- name: Continue sweep
if: ${{ always() && !cancelled() && steps.apply-review-artifacts.outputs.review_batch_succeeded == 'true' && steps.commit-review-records.outputs.records_published == 'true' && 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 target_branch="${{ needs.plan.outputs.target_branch }}" \
-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.plan.outputs.queue_feed != 'true' && 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: read
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 }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
run: |
set -euo pipefail
test -n "$CLAWSWEEPER_WEBHOOK_SECRET"
queue_url="${QUEUE_URL%/}"
recovery_marker="[clawsweeper-recovery-attempt=1]"
max_additional_prompt_bytes=$((5000 - ${#recovery_marker} - 2))
additional_prompt_bytes="$(printf '%s' "$ADDITIONAL_PROMPT" | LC_ALL=C wc -c | tr -d '[:space:]')"
recovery_prefix="$ADDITIONAL_PROMPT"
if [ "$additional_prompt_bytes" -gt "$max_additional_prompt_bytes" ]; then
# The queue validates JavaScript string length. A UTF-8 byte cap is deliberately
# stricter, so no recovery delivery can exceed that character limit.
recovery_prefix="$(
set +o pipefail
printf '%s' "$ADDITIONAL_PROMPT" | LC_ALL=C head -c "$max_additional_prompt_bytes" | iconv -f UTF-8 -t UTF-8 -c
)"
fi
recovery_prompt="$recovery_marker"
if [ -n "$recovery_prefix" ]; then
recovery_prompt="$(printf '%s\n\n%s' "$recovery_prefix" "$recovery_marker")"
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 ([0-9]+).*$"; "\\1")
' | 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
IFS=',' read -r -a recovery_items <<<"$item_numbers"
failed_recovery_dispatches=()
for item_number in "${recovery_items[@]}"; do
if ! [[ "$item_number" =~ ^[0-9]+$ ]]; then
echo "Invalid failed-review recovery item number: $item_number" >&2
exit 1
fi
dispatch_key="failed-review-recovery-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${item_number}"
payload="$(jq -n \
--arg target_repo "${{ needs.plan.outputs.target_repo }}" \
--arg target_branch "${{ needs.plan.outputs.target_branch }}" \
--arg item_number "$item_number" \
--arg dispatch_key "$dispatch_key" \
--arg codex_timeout_ms "${{ needs.plan.outputs.codex_timeout_ms }}" \
--arg additional_prompt "$recovery_prompt" \
'{
delivery_id: ("router:" + $dispatch_key),
decision: {
targetRepo: $target_repo,
targetBranch: $target_branch,
itemNumber: ($item_number | tonumber),
itemKind: "issue",
sourceEvent: "issues",
sourceAction: "failed_review_shard_recovery",
supersedesInProgress: false,
codexTimeoutMs: ($codex_timeout_ms | tonumber),
additionalPrompt: $additional_prompt
}
}')"
queued=false
for attempt in 1 2 3; do
# shellcheck disable=SC2016 # The Node source must retain its literal template expression.
signature="$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(`sha256=${crypto.createHmac("sha256", process.env.CLAWSWEEPER_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex")}`)')"
response="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 20 \
--request POST \
--header "content-type: application/json" \
--header "x-clawsweeper-exact-review-signature: $signature" \
--data "$payload" \
"$queue_url/internal/exact-review/enqueue")" || response=""
if jq -e '.ok == true and (.queued == true or .deduped == true or .shed == true or .accepted == false)' <<<"$response" >/dev/null; then
if jq -e '.accepted == false' <<<"$response" >/dev/null; then
echo "Recovery skipped because the target is disabled."
elif jq -e '.shed == true' <<<"$response" >/dev/null; then
echo "Recovery shed by exact-review queue backpressure."
fi
queued=true
break
fi
echo "Recovery queue acknowledgement was not accepted on attempt $attempt." >&2
if [ "$attempt" -lt 3 ]; then
sleep "$((attempt * 5))"
fi
done
if [ "$queued" != "true" ]; then
failed_recovery_dispatches+=("$item_number")
fi
done
if [ "${#failed_recovery_dispatches[@]}" -gt 0 ]; then
echo "Unable to queue failed review recovery for item numbers: ${failed_recovery_dispatches[*]}" >&2
exit 1
fi
echo "Queued failed review shards $failed_shards through the exact-review control plane: $item_numbers"
retry-failed-reviews:
name: Retry failed Codex reviews
if: ${{ github.event_name == 'schedule' && github.event.schedule == '13 * * * *' }}
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
actions: write
contents: write
issues: read
pull-requests: read
steps:
- uses: actions/checkout@v7
with:
filter: blob:none
fetch-depth: 0
- uses: ./.github/actions/setup-action-ledger
continue-on-error: true
- 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:
coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
records-repo-slugs: openclaw-openclaw
hydrate-state-blobs: "false"
token: ${{ steps.state-token.outputs.token }}
- uses: ./.github/actions/setup-pnpm
with:
build-script: build:all
- name: Plan or dispatch failed-review retries
id: retry-failed-reviews-run
continue-on-error: true
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' }}
RETRY_MAX_RUNTIME_MS: ${{ vars.CLAWSWEEPER_FAILED_REVIEW_RETRY_MAX_RUNTIME_MS || '600000' }}
CODEX_TIMEOUT_MS: ${{ vars.CLAWSWEEPER_FAILED_REVIEW_RETRY_CODEX_TIMEOUT_MS || vars.CLAWSWEEPER_CODEX_TIMEOUT_MS || '1200000' }}
run: |
set -euo pipefail
git pull --rebase --autostash
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
set +e
pnpm run retry-failed-reviews -- \
--target-repo "$TARGET_REPO" \
--limit "$RETRY_LIMIT" \
--max-attempts "$RETRY_MAX_ATTEMPTS" \
--cooldown-minutes "$RETRY_COOLDOWN_MINUTES" \
--max-runtime-ms "$RETRY_MAX_RUNTIME_MS" \
--codex-timeout-ms "$CODEX_TIMEOUT_MS" \
--workflow-repo "$GITHUB_REPOSITORY" \
--workflow-ref main \
--state-dir results/failed-review-retries/openclaw-openclaw \
--report-path artifacts/failed-review-retry-report.json \
"${dry_run_arg[@]}"
retry_exit_code=$?
set -e
echo "exit_code=$retry_exit_code" >> "$GITHUB_OUTPUT"
exit "$retry_exit_code"
- name: Finalize failed-review retry action ledger
if: ${{ always() }}
env:
RETRY_EXIT_CODE: ${{ steps.retry-failed-reviews-run.outputs.exit_code || '' }}
RETRY_OUTCOME: ${{ steps.retry-failed-reviews-run.outcome || 'not_started' }}
run: |
args=()
if [ "$RETRY_OUTCOME" = "cancelled" ] || [ "$RETRY_EXIT_CODE" = "130" ] || [ "$RETRY_EXIT_CODE" = "143" ]; then
args=(--interrupt-open-attempts --reason cancelled)
elif [ "$RETRY_EXIT_CODE" = "124" ] || [ "$RETRY_EXIT_CODE" = "137" ]; then
args=(--interrupt-open-attempts --reason timeout)
elif [ "$RETRY_OUTCOME" != "success" ]; then
args=(--interrupt-open-attempts --reason workflow_failed)
fi
pnpm run --silent finalize-action-events -- "${args[@]}"
- name: Publish failed-review retry state
if: ${{ always() && vars.CLAWSWEEPER_FAILED_REVIEW_RETRY_ENABLED == '1' && hashFiles('results/failed-review-retries/openclaw-openclaw/*.json') != '' }}
continue-on-error: true
env:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
run: |
pnpm run repair:publish-main -- \
--message "chore: update failed review retry state" \
--path results/failed-review-retries/openclaw-openclaw \
--rebase-strategy theirs
- name: Publish failed-review retry action ledger
if: ${{ always() }}
env:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
run: |
set -euo pipefail
source_root="$CLAWSWEEPER_ACTION_LEDGER_OUTPUT_ROOT"
if [ ! -d "$source_root/ledger" ]; then
echo "No failed-review retry action event shards were finalized."
exit 0
fi
event_paths_file=".artifacts/failed-review-retry-action-ledger-paths.txt"
mkdir -p .artifacts
pnpm run --silent publish-action-events -- \
--source-root "$source_root" \
--state-root . \
--expected-producer-job "$GITHUB_JOB" |
jq -r '.paths[]?' |
sort -u > "$event_paths_file"
if [ ! -s "$event_paths_file" ]; then
echo "Failed-review retry action shards existed but no paths were imported." >&2
exit 1
fi
node dist/clawsweeper.js publish-action-event-paths \
--paths-file "$event_paths_file"
- uses: actions/upload-artifact@v7
if: ${{ always() }}
with:
name: failed-review-retry-report
path: artifacts/failed-review-retry-report.json
if-no-files-found: error
retention-days: 14
- name: Restore failed-review retry outcome
if: ${{ always() && steps.retry-failed-reviews-run.outcome != 'success' && steps.retry-failed-reviews-run.outcome != 'skipped' }}
env:
RETRY_EXIT_CODE: ${{ steps.retry-failed-reviews-run.outputs.exit_code || '1' }}
run: |
retry_exit_code="$RETRY_EXIT_CODE"
if ! [[ "$retry_exit_code" =~ ^[1-9][0-9]*$ ]] || [ "$retry_exit_code" -gt 255 ]; then
retry_exit_code=1
fi
echo "Restoring failed-review retry exit code $retry_exit_code after ledger cleanup."
exit "$retry_exit_code"
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@v7
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@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
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:
PRIMARY_TOKEN: ${{ github.token }}
APP_FALLBACK_TOKEN: ${{ steps.target-read-token.outputs.token }}
run: |
if [ -n "$PRIMARY_TOKEN" ]; then
echo "Using workflow token for public audit reads."
echo "token=$PRIMARY_TOKEN" >> "$GITHUB_OUTPUT"
else
echo "Using ClawSweeper App token fallback for audit reads."
echo "token=$APP_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:
coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
hydrate-state-blobs: "false"
token: ${{ steps.state-token.outputs.token }}
- uses: ./.github/actions/setup-pnpm
with:
build-script: build:all
- name: Refresh Audit Health
env:
CLAWSWEEPER_CANONICAL_RECORD_BASELINE_DIR: .artifacts/audit-canonical-baseline
GH_TOKEN: ${{ steps.target-token.outputs.token }}
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
run: |
set -euo pipefail
git pull --rebase --autostash
reconcile_json="$(pnpm run --silent reconcile -- \
--target-repo "$TARGET_REPO" \
--max-pages 250 \
--skip-closed-at \
--canonical-record-baseline-dir "$CLAWSWEEPER_CANONICAL_RECORD_BASELINE_DIR")"
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")"
mkdir -p .artifacts
pnpm run audit -- --target-repo "$TARGET_REPO" --max-pages 250 --sample-limit 25 --output .artifacts/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: Upload canonical close-verdict audit
uses: actions/upload-artifact@v7
with:
name: close-verdict-audit-${{ github.run_id }}-${{ github.run_attempt }}
path: .artifacts/clawsweeper-audit.json
if-no-files-found: error
retention-days: 14
- name: Commit Audit Health
env:
CLAWSWEEPER_CANONICAL_RECORD_BASELINE_DIR: .artifacts/audit-canonical-baseline
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
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 "records/${target_slug}" \
--rebase-strategy normal
if ! pnpm run repair:publish-main -- \
--message "chore: publish sweep audit status" \
--path README.md \
--path "results/audit/${target_slug}.json" \
--path "results/sweep-status/${target_slug}.json" \
--rebase-strategy theirs; then
echo "::warning title=Audit dashboard publish failed::Canonical reconciliation is complete; continuing without the best-effort Git dashboard update."
fi
- 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-proof:
name: Prove close coverage without write credentials
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: 60
permissions:
actions: read
contents: read
issues: read
pull-requests: read
env:
CLAWSWEEPER_UNCONFIRMED_PRODUCT_DIRECTION_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_UNCONFIRMED_PRODUCT_DIRECTION_CLOSE_ENABLED || 'false' }}
CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED || 'false' }}
CLAWSWEEPER_IDEA_REVIVAL_REACTIONS: ${{ vars.CLAWSWEEPER_IDEA_REVIVAL_REACTIONS || '5' }}
CLAWSWEEPER_AUTHOR_PR_BUDGET_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET_CLOSE_ENABLED || 'false' }}
CLAWSWEEPER_AUTHOR_PR_BUDGET: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET || '15' }}
CLAWSWEEPER_AUTHOR_PR_BUDGET_MAX_CLOSES_PER_RUN: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET_MAX_CLOSES_PER_RUN || '5' }}
outputs:
target_repo: ${{ steps.target.outputs.target_repo }}
artifact_name: ${{ steps.proof-artifact.outputs.name }}
proof_ready: ${{ steps.primary-proof-result.outputs.ready }}
action_ledger_artifact_name: ${{ steps.publishable-action-ledger.outputs.name }}
steps:
- uses: actions/checkout@v7
with:
filter: blob:none
fetch-depth: 1
persist-credentials: false
- uses: ./.github/actions/setup-action-ledger
continue-on-error: true
- name: Resolve proof target repository
id: target
run: |
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
if ! printf '%s' "$target_repo" | grep -Eq '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'; then
echo "Invalid target repository: $target_repo" >&2
exit 1
fi
target_slug="$(printf '%s' "$target_repo" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_.-]+/-/g')"
target_owner="${target_repo%%/*}"
target_name="${target_repo#*/}"
echo "target_repo=$target_repo" >> "$GITHUB_OUTPUT"
echo "target_slug=$target_slug" >> "$GITHUB_OUTPUT"
echo "target_repo_owner=$target_owner" >> "$GITHUB_OUTPUT"
echo "target_repo_name=$target_name" >> "$GITHUB_OUTPUT"
- name: Resolve proof artifact identity
id: proof-artifact
run: |
echo "name=apply-coverage-proofs-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_OUTPUT"
echo "action_ledger_name=action-ledger-apply-proof-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_OUTPUT"
- uses: ./.github/actions/setup-state
with:
coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
records-repo-slugs: ${{ steps.target.outputs.target_slug }}
hydrate-state-blobs: "false"
token: ${{ github.token }}
persist-credentials: "false"
- uses: ./.github/actions/setup-pnpm
with:
build-script: build:all
- name: Reconcile read-only proof inputs
if: ${{ !(github.event_name == 'schedule' && github.event.schedule == '6,21,36,51 * * * *') && !(github.event_name == 'workflow_dispatch' && (github.event.inputs.apply_sync_comments_only == 'true' || github.event.inputs.apply_item_numbers == '__cursor__')) }}
env:
GH_TOKEN: ${{ github.token }}
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
run: |
set -euo pipefail
item_numbers="${{ 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: Select bounded coverage proof work
id: proof-select
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.inputs.apply_close_reasons || env.CLAWSWEEPER_AUTO_CLOSE_REASONS }}"
stale_min_age_days="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_stale_min_age_days || '60' }}"
item_numbers="${{ github.event.inputs.apply_item_numbers || '' }}"
sync_comments_only="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_sync_comments_only || 'false' }}"
if [ "${{ github.event_name }}" = "schedule" ] &&
[ "${{ github.event.schedule || '' }}" = "6,21,36,51 * * * *" ]; then
sync_comments_only="true"
fi
if [ "$item_numbers" = "__cursor__" ]; then
item_numbers=""
sync_comments_only="true"
fi
product_direction_enabled=false
case "${CLAWSWEEPER_UNCONFIRMED_PRODUCT_DIRECTION_CLOSE_ENABLED,,}" in
true|1|yes|on) product_direction_enabled=true ;;
esac
unsponsored_feature_enabled=false
case "${CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED,,}" in
true|1|yes|on) unsponsored_feature_enabled=true ;;
esac
author_pr_budget_enabled=false
case "${CLAWSWEEPER_AUTHOR_PR_BUDGET_CLOSE_ENABLED,,}" in
true|1|yes|on) author_pr_budget_enabled=true ;;
esac
if [ "$sync_comments_only" != "true" ] && [ "$product_direction_enabled" != "true" ] && [ "$apply_close_reasons" != "all" ]; then
apply_close_reasons="$(printf '%s\n' "$apply_close_reasons" | tr ',' '\n' | awk '$0 != "unconfirmed_product_direction" && NF' | paste -sd, -)"
fi
if [ "$sync_comments_only" != "true" ] && [ "$unsponsored_feature_enabled" != "true" ] && [ "$apply_close_reasons" != "all" ]; then
apply_close_reasons="$(printf '%s\n' "$apply_close_reasons" | tr ',' '\n' | awk '$0 != "unsponsored_feature_request" && NF' | paste -sd, -)"
fi
if [ "$sync_comments_only" != "true" ] && [ "$author_pr_budget_enabled" != "true" ] && [ "$apply_close_reasons" != "all" ]; then
apply_close_reasons="$(printf '%s\n' "$apply_close_reasons" | tr ',' '\n' | awk '$0 != "author_pr_budget_exceeded" && NF' | paste -sd, -)"
fi
selected=""
if [ "$sync_comments_only" != "true" ] && [ -n "$apply_close_reasons" ]; then
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"
--batch-size 2
--close-limit 2
--coverage-proof-limit 2
)
if [ -n "$item_numbers" ]; then
proof_args+=(--item-numbers "$item_numbers")
else
target_slug="${TARGET_REPO//\//-}"
proof_args+=(--cursor-path "results/apply-cursors/${target_slug}.json")
fi
selected="$(pnpm run --silent workflow -- proposed-pr-close-coverage-item-numbers "${proof_args[@]}")"
fi
echo "item_numbers=$selected" >> "$GITHUB_OUTPUT"
echo "Selected read-only coverage proof items: ${selected:-none}"
- uses: ./.github/actions/setup-codex
if: ${{ env.CLAWSWEEPER_RUNNER != 'openclaw' && steps.proof-select.outputs.item_numbers != '' }}
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CLAWSWEEPER_INTERNAL_MODEL: ${{ secrets.CLAWSWEEPER_MODEL }}
- uses: ./.github/actions/setup-openclaw
if: ${{ steps.proof-select.outputs.item_numbers != '' }}
- name: Create target proof inspection token
id: proof-inspection-token
if: ${{ steps.proof-select.outputs.item_numbers != '' }}
continue-on-error: true
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
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: Generate bound close coverage proofs
id: generate-apply-proofs
if: ${{ steps.proof-select.outputs.item_numbers != '' }}
timeout-minutes: 50
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GH_TOKEN: ${{ github.token }}
CLAWSWEEPER_PROOF_INSPECTION_TOKEN: ${{ steps.proof-inspection-token.outputs.token }}
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
run: |
set -euo pipefail
item_numbers="${{ steps.proof-select.outputs.item_numbers }}"
item_count="$(pnpm run --silent workflow -- count-csv --items "$item_numbers")"
min_age_minutes="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_min_age_minutes || '' }}"
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-proof/pr-close-coverage-proof
pnpm run apply-decisions -- \
--target-repo "$TARGET_REPO" \
--skip-dashboard \
--dry-run \
--item-numbers "$item_numbers" \
--apply-kind "${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_kind || 'all' }}" \
--apply-close-reasons "${{ github.event.inputs.apply_close_reasons || env.CLAWSWEEPER_AUTO_CLOSE_REASONS }}" \
--stale-min-age-days "${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_stale_min_age_days || '60' }}" \
--min-age-days "${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_min_age_days || '0' }}" \
"${min_age_minutes_arg[@]}" \
--limit "$item_count" \
--processed-limit "$((item_count * 10))" \
--close-delay-ms 0 \
--max-runtime-ms 2700000 \
--codex-model internal \
--codex-reasoning-effort high \
--artifact-dir .artifacts/apply-proof \
--report-path .artifacts/apply-proof/apply-report.json \
--progress-every 1
- name: Create bound close coverage proof manifest
id: proof-manifest
if: ${{ always() && !cancelled() && steps.proof-select.outcome == 'success' && (steps.proof-select.outputs.item_numbers == '' || steps.generate-apply-proofs.outcome == 'success') }}
env:
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
run: |
set -euo pipefail
source scripts/apply-workflow-helpers.sh
write_coverage_proof_manifest \
".artifacts/apply-proof/pr-close-coverage-proof" \
"$TARGET_REPO" \
"${{ steps.proof-select.outputs.item_numbers }}"
- name: Export primary apply proof result
id: primary-proof-result
if: ${{ always() && !cancelled() && steps.proof-select.outcome == 'success' && (steps.proof-select.outputs.item_numbers == '' || steps.generate-apply-proofs.outcome == 'success') && steps.proof-manifest.outcome == 'success' }}
run: echo "ready=true" >> "$GITHUB_OUTPUT"
- name: Finalize apply proof action ledger
if: ${{ always() && steps.proof-select.outputs.item_numbers != '' }}
env:
APPLY_PROOF_OUTCOME: ${{ steps.generate-apply-proofs.outcome || 'not_started' }}
run: |
args=()
if [ "$APPLY_PROOF_OUTCOME" = "cancelled" ]; then
args=(--interrupt-open-attempts --reason cancelled)
elif [ "$APPLY_PROOF_OUTCOME" != "success" ]; then
args=(--interrupt-open-attempts --reason workflow_failed)
fi
pnpm run --silent finalize-action-events -- "${args[@]}"
- name: Upload bound close coverage proofs
if: ${{ always() && steps.proof-manifest.outcome == 'success' }}
uses: actions/upload-artifact@v7
with:
name: ${{ steps.proof-artifact.outputs.name }}
path: .artifacts/apply-proof/pr-close-coverage-proof/*
if-no-files-found: error
retention-days: 1
- name: Upload apply proof action events
id: upload-action-events
if: ${{ always() && steps.proof-select.outputs.item_numbers != '' }}
uses: actions/upload-artifact@v7
with:
name: ${{ steps.proof-artifact.outputs.action_ledger_name }}
path: ${{ runner.temp }}/clawsweeper-action-ledger/${{ github.run_id }}/${{ github.run_attempt }}/${{ github.job }}/**
include-hidden-files: true
if-no-files-found: error
retention-days: 7
- name: Export uploaded apply proof action ledger identity
id: publishable-action-ledger
if: ${{ always() && steps.upload-action-events.outputs.artifact-id != '' }}
run: echo "name=${{ steps.proof-artifact.outputs.action_ledger_name }}" >> "$GITHUB_OUTPUT"
publish-apply-proof-action-ledger:
name: Publish immutable apply proof action ledger
needs: apply-proof
if: ${{ always() && needs.apply-proof.result != 'skipped' }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
filter: blob:none
- name: Download apply proof action events
if: ${{ needs.apply-proof.outputs.action_ledger_artifact_name != '' }}
uses: actions/download-artifact@v8
with:
name: ${{ needs.apply-proof.outputs.action_ledger_artifact_name }}
path: .clawsweeper-repair/action-ledger-proof
- uses: ./.github/actions/setup-pnpm
if: ${{ needs.apply-proof.outputs.action_ledger_artifact_name != '' }}
with:
build-script: build:all
- name: Publish apply proof action events
if: ${{ needs.apply-proof.outputs.action_ledger_artifact_name != '' }}
env:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
run: |
set -euo pipefail
source_root=".clawsweeper-repair/action-ledger-proof"
if [ ! -d "$source_root/ledger" ]; then
echo "The uploaded apply proof artifact did not contain action event shards." >&2
exit 1
fi
event_paths_file=".artifacts/apply-proof-action-ledger-paths.txt"
mkdir -p .artifacts
pnpm run --silent publish-action-events -- \
--source-root "$source_root" \
--state-root . \
--expected-producer-job apply-proof |
jq -r '.paths[]?' |
sort -u > "$event_paths_file"
if [ ! -s "$event_paths_file" ]; then
echo "Apply proof action event shards existed but no paths were imported." >&2
exit 1
fi
node dist/clawsweeper.js publish-action-event-paths \
--paths-file "$event_paths_file"
apply-existing:
name: Apply close proposals
needs: [apply-proof, publish-apply-proof-action-ledger]
if: ${{ always() && !cancelled() && needs.apply-proof.outputs.proof_ready == 'true' && ((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') }}
outputs:
target_repo: ${{ steps.target.outputs.target_repo }}
action_ledger_outcome: ${{ steps.finalize-apply.outcome || 'not_started' }}
state_status_outcome: ${{ steps.retry-final-apply-status-publication.outcome || 'not_started' }}
state_publication_outcome: ${{ steps.publish-apply-action-events.outcome || 'not_started' }}
observability_started_at: ${{ steps.apply-telemetry-start.outputs.started_at || '' }}
runs-on: ubuntu-latest
timeout-minutes: 360
concurrency:
group: clawsweeper-target-apply-${{ needs.apply-proof.outputs.target_repo }}
cancel-in-progress: false
queue: max
env:
CLAWSWEEPER_UNCONFIRMED_PRODUCT_DIRECTION_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_UNCONFIRMED_PRODUCT_DIRECTION_CLOSE_ENABLED || 'false' }}
CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED || 'false' }}
CLAWSWEEPER_IDEA_REVIVAL_REACTIONS: ${{ vars.CLAWSWEEPER_IDEA_REVIVAL_REACTIONS || '5' }}
CLAWSWEEPER_AUTHOR_PR_BUDGET_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET_CLOSE_ENABLED || 'false' }}
CLAWSWEEPER_AUTHOR_PR_BUDGET: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET || '15' }}
CLAWSWEEPER_AUTHOR_PR_BUDGET_MAX_CLOSES_PER_RUN: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET_MAX_CLOSES_PER_RUN || '5' }}
steps:
- uses: actions/checkout@v7
with:
filter: blob:none
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup-action-ledger
continue-on-error: true
- name: Resolve target repository
id: target
env:
PROOF_TARGET_REPO: ${{ needs.apply-proof.outputs.target_repo }}
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
if [ -z "$PROOF_TARGET_REPO" ] || [ "$target_repo" != "$PROOF_TARGET_REPO" ]; then
echo "Resolved mutation target '$target_repo' differs from proof target '${PROOF_TARGET_REPO:-missing}'." >&2
exit 1
fi
target_owner="${target_repo%%/*}"
target_name="${target_repo#*/}"
target_slug="$(printf '%s' "$target_repo" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_.-]+/-/g')"
{
echo "target_repo=$target_repo"
echo "target_repo_owner=$target_owner"
echo "target_repo_name=$target_name"
echo "target_slug=$target_slug"
} >> "$GITHUB_OUTPUT"
- name: Publish apply telemetry start
id: apply-telemetry-start
continue-on-error: true
env:
APPLY_OUTCOME: in_progress
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
run: |
set -euo pipefail
test -n "$CLAWSWEEPER_WEBHOOK_SECRET"
started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
mkdir -p .artifacts
printf '{"started_at":"%s"}\n' "$started_at" > .artifacts/apply-observability-context.json
echo "started_at=$started_at" >> "$GITHUB_OUTPUT"
node scripts/publish-apply-observability.mjs
- name: Create target write token
id: target-write-token
continue-on-error: true
uses: ./.github/actions/create-target-write-token
with:
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
owner: ${{ steps.target.outputs.target_repo_owner }}
repository: ${{ steps.target.outputs.target_repo_name }}
- 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:
coordinator-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-url: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
records-secret: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
records-repo-slugs: ${{ steps.target.outputs.target_slug }}
hydrate-state-blobs: "false"
token: ${{ steps.state-token.outputs.token }}
- uses: ./.github/actions/setup-pnpm
with:
build-script: build:all
- uses: ./.github/actions/setup-openclaw
continue-on-error: true
- uses: actions/download-artifact@v8
with:
name: ${{ needs.apply-proof.outputs.artifact_name }}
path: .artifacts/apply-proof/pr-close-coverage-proof
- name: Validate downloaded close coverage proof tree
run: |
set -euo pipefail
source scripts/apply-workflow-helpers.sh
# Two source items, with at most four canonical candidates per item.
validate_coverage_proof_tree ".artifacts/apply-proof/pr-close-coverage-proof" 8 262144 2097152
- name: Reconcile before apply preselect
if: ${{ !(github.event_name == 'schedule' && github.event.schedule == '6,21,36,51 * * * *') && !(github.event_name == 'workflow_dispatch' && (github.event.inputs.apply_sync_comments_only == 'true' || github.event.inputs.apply_item_numbers == '__cursor__')) }}
env:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
GH_TOKEN: ${{ steps.target.outputs.target_repo == 'openclaw/openclaw' && github.token || steps.target-write-token.outputs.token }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
run: |
set -euo pipefail
source scripts/apply-workflow-helpers.sh
item_numbers="${{ github.event_name == 'repository_dispatch' && github.event.client_payload.item_number || github.event.inputs.apply_item_numbers || '' }}"
prepare_apply_reconciliation_args
persist_reconciliation "${reconcile_args[@]}"
- name: Apply unchanged proposed decisions with checkpoints
id: apply-existing-run
timeout-minutes: 70
env:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
GH_TOKEN: ${{ steps.target-write-token.outputs.token }}
CLAWSWEEPER_PUBLIC_GH_TOKEN: ${{ steps.target.outputs.target_repo == 'openclaw/openclaw' && github.token || '' }}
CLAWSWEEPER_APPLY_TOKEN_MINTED_AT_MS: ${{ steps.target-write-token.outputs.minted-at-ms }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
run: |
set -euo pipefail
test -n "$GH_TOKEN"
source scripts/apply-workflow-helpers.sh
initialize_apply_token_budget
limit="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_limit || '40' }}"
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.inputs.apply_close_reasons || env.CLAWSWEEPER_AUTO_CLOSE_REASONS }}"
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' }}"
checkpoint_size="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_checkpoint_size || '40' }}"
comment_sync_processed_limit=40
base_close_processed_limit=600
close_processed_limit="$base_close_processed_limit"
adaptive_apply_scan_reason="base_window"
sync_batch_size="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_limit || '40' }}"
item_numbers="${{ github.event_name == 'repository_dispatch' && github.event.client_payload.item_number || github.event.inputs.apply_item_numbers || '' }}"
scheduled_comment_sync="${{ github.event_name == 'schedule' && github.event.schedule == '6,21,36,51 * * * *' && 'true' || 'false' }}"
sync_open_pr_batch="$scheduled_comment_sync"
if [ "$item_numbers" = "__cursor__" ]; then
sync_open_pr_batch="true"
item_numbers=""
fi
explicit_item_numbers="$item_numbers"
auto_selected_apply_batch=false
sync_comments_only="${{ github.event_name == 'workflow_dispatch' && github.event.inputs.apply_sync_comments_only || 'false' }}"
product_direction_enabled=false
case "${CLAWSWEEPER_UNCONFIRMED_PRODUCT_DIRECTION_CLOSE_ENABLED,,}" in
true|1|yes|on) product_direction_enabled=true ;;
esac
unsponsored_feature_enabled=false
case "${CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED,,}" in
true|1|yes|on) unsponsored_feature_enabled=true ;;
esac
author_pr_budget_enabled=false
case "${CLAWSWEEPER_AUTHOR_PR_BUDGET_CLOSE_ENABLED,,}" in
true|1|yes|on) author_pr_budget_enabled=true ;;
esac
if [ "$sync_comments_only" != "true" ] && [ "$product_direction_enabled" != "true" ] && [ "$apply_close_reasons" != "all" ]; then
apply_close_reasons="$(printf '%s\n' "$apply_close_reasons" | tr ',' '\n' | awk '$0 != "unconfirmed_product_direction" && NF' | paste -sd, -)"
fi
if [ "$sync_comments_only" != "true" ] && [ "$unsponsored_feature_enabled" != "true" ] && [ "$apply_close_reasons" != "all" ]; then
apply_close_reasons="$(printf '%s\n' "$apply_close_reasons" | tr ',' '\n' | awk '$0 != "unsponsored_feature_request" && NF' | paste -sd, -)"
fi
if [ "$sync_comments_only" != "true" ] && [ "$author_pr_budget_enabled" != "true" ] && [ "$apply_close_reasons" != "all" ]; then
apply_close_reasons="$(printf '%s\n' "$apply_close_reasons" | tr ',' '\n' | awk '$0 != "author_pr_budget_exceeded" && NF' | paste -sd, -)"
fi
if [ "$sync_comments_only" != "true" ] && [ -z "$apply_close_reasons" ]; then
echo "No enabled close reasons remain after policy filtering; skipping apply."
echo "APPLY_NOOP=true" >> "$GITHUB_ENV"
exit 0
fi
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"
apply_cursor_path="results/apply-cursors/${target_slug}.json"
next_cursor=""
apply_ready_count=""
candidate_counts_json=""
cursor_advance_count=""
coverage_proof_item_numbers=""
if [ "$checkpoint_size" -gt 40 ]; then
echo "Capping apply checkpoint size at 40"
checkpoint_size=40
fi
normalize_comment_sync_mode
prepare_comment_sync_batch
sync_comments_arg=()
if [ "$sync_comments_only" = "true" ]; then
sync_comments_arg=(--sync-comments-only)
fi
prepare_comment_sync_cursor_arg
min_age_minutes_arg=()
if [ -n "$min_age_minutes" ]; then
min_age_minutes_arg=(--min-age-minutes "$min_age_minutes")
fi
select_adaptive_apply_batch
mkdir -p .artifacts/apply-reports
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")"
trim_comment_sync_cycle_batch
[ -n "$item_numbers" ] || batch_count=0
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
prepare_apply_reconciliation_args
persist_reconciliation "${reconcile_args[@]}"
summarize_apply_candidate_quality
if [ "$sync_comments_only" != "true" ] && [ -z "$item_numbers" ]; then
auto_selected_apply_batch=true
select_apply_candidate_inventory
if [ -n "$item_numbers" ]; then
select_bounded_coverage_proof_tail
proposed_count="$(pnpm run --silent workflow -- count-csv --items "$item_numbers")"
if [ "$proposed_count" -lt "$limit" ]; then
limit="$proposed_count"
fi
echo "Selected $proposed_count from $close_processed_limit ($adaptive_apply_scan_reason); proof $coverage_proof_count/$coverage_proof_limit: $item_numbers; currently eligible: ${apply_ready_count:-unknown}; inventory: ${candidate_counts_json:-unknown}"
fi
fi
item_numbers_arg=()
if [ -n "$item_numbers" ]; then
item_numbers_arg=(--item-numbers "$item_numbers")
fi
select_automatic_apply_runtime
if [ "$sync_comments_only" != "true" ] && [ -z "$item_numbers" ]; then
publish_automatic_apply_idle
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.$candidate_quality_detail Scan window: $close_processed_limit records ($adaptive_apply_scan_reason). 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 }}"
publish_status "chore: mark sweep apply in progress"
closed_total=0
continue_apply=false
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}"
begin_canonical_record_mutation
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 "$comment_sync_processed_limit" \
--max-runtime-ms 300000 \
--cursor-trace ".artifacts/comment-sync-trace-$checkpoint.json" \
"${comment_sync_cursor_arg[@]}" \
--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)"
comment_sync_checkpoint_items="$item_numbers"
complete_comment_sync_batch ".artifacts/apply-reports/apply-report-$checkpoint.json" ".artifacts/comment-sync-trace-$checkpoint.json"
publish_changes "chore: sync sweep review comments checkpoint $checkpoint" records apply-report.json results/comment-sync-cursors
comment_sync_health_cursor_path=""
comment_sync_health_cursor_required="false"
comment_sync_health_processed_limit="$comment_sync_processed_limit"
if [ "$sync_open_pr_batch" = "true" ]; then
comment_sync_health_cursor_path="$cursor_path"
comment_sync_health_cursor_required="true"
comment_sync_health_processed_limit="$sync_batch_size"
fi
write_comment_sync_health ".artifacts/apply-reports/apply-report-$checkpoint.json" ".artifacts/apply-health-$checkpoint.json"
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: ${comment_sync_checkpoint_items:-all}. Next cursor: ${next_cursor:-none}." \
--run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--apply-health-file ".artifacts/apply-health-$checkpoint.json"
publish_status "chore: update sweep comment sync status"
report_apply_token_budget_stop ".artifacts/apply-reports/apply-report-$checkpoint.json" "$result_count" "unknown"
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))
cursor_trace_path=".artifacts/apply-cursor-trace-$checkpoint.json"
cursor_trace_arg=()
if [ "$auto_selected_apply_batch" = "true" ]; then
cursor_trace_arg=(--cursor-trace "$cursor_trace_path")
fi
echo "::group::Apply checkpoint $checkpoint"
begin_canonical_record_mutation
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 "$close_processed_limit" \
--comment-sync-min-age-days "$comment_sync_min_age_days" \
"${item_numbers_arg[@]}" \
"${max_runtime_arg[@]}" \
"${cursor_trace_arg[@]}" \
"${sync_comments_arg[@]}" \
--artifact-dir .artifacts/apply-proof \
--require-precomputed-pr-close-coverage-proof
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"
apply_publish_paths=(records apply-report.json)
if [ "$auto_selected_apply_batch" = "true" ]; then
pnpm run workflow -- write-apply-cursor \
--cursor-path "$apply_cursor_path" \
--report ".artifacts/apply-reports/apply-report-$checkpoint.json" \
--target-repo "$TARGET_REPO" \
--item-numbers "$item_numbers" \
--coverage-proof-item-numbers "$coverage_proof_item_numbers" \
--cursor-trace "$cursor_trace_path"
cursor_advance_count="$(pnpm run --silent workflow -- apply-cursor-advance-count \
--report ".artifacts/apply-reports/apply-report-$checkpoint.json" \
--item-numbers "$item_numbers" \
--cursor-trace "$cursor_trace_path")"
apply_publish_paths+=(results/apply-cursors)
fi
examined_count="$(apply_checkpoint_examined_count)"
publish_changes "chore: apply sweep decisions checkpoint $checkpoint" "${apply_publish_paths[@]}"
if [ "$auto_selected_apply_batch" = "true" ]; then
select_apply_candidate_inventory false
fi
close_health_cursor_path=""
if [ "$auto_selected_apply_batch" = "true" ]; then
close_health_cursor_path="$apply_cursor_path"
fi
write_apply_health ".artifacts/apply-reports/apply-report-$checkpoint.json" ".artifacts/apply-health-$checkpoint.json" "close" "$close_processed_limit" "$close_health_cursor_path" "$auto_selected_apply_batch" "$apply_ready_count" "15" "$cursor_advance_count" "$candidate_counts_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. Candidates examined: $examined_count. Action records: $result_count, including durable review comment syncs." \
--run-url "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--apply-health-file ".artifacts/apply-health-$checkpoint.json"
publish_status "chore: update sweep apply checkpoint $checkpoint status"
echo "::endgroup::"
drop_bounded_coverage_proof_tail "$cursor_trace_path"
if apply_checkpoint_runtime_reached ".artifacts/apply-reports/apply-report-$checkpoint.json" "$result_count" "$((limit - closed_total))"; then break; fi
if [ "$result_count" -ge "$close_processed_limit" ]; then
if [ "$closed_in_chunk" -gt 0 ]; then
echo "Close scan reached its $close_processed_limit-record budget; queueing a fresh-token continuation."
continue_apply=true
elif [ "$auto_selected_apply_batch" = "true" ]; then
echo "Close scan reached its $close_processed_limit-record budget with no fresh closes; cursor is persisted and the next scheduled apply run will advance the next window."
else
echo "Explicit close scan reached its $close_processed_limit-record budget without fresh closes; stopping to avoid a retry loop."
fi
break
fi
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
continue_apply=true
break
done
final_health_mode="close"
final_health_processed_limit="$close_processed_limit"
final_health_cursor_path=""
final_health_cursor_required="$auto_selected_apply_batch"
final_health_candidate_count=""
final_health_scheduled_interval_minutes=""
final_health_cursor_advance_count=""
final_health_candidate_counts_json=""
if [ "$auto_selected_apply_batch" = "true" ]; then
final_health_cursor_path="$apply_cursor_path"
final_health_candidate_count="$apply_ready_count"
final_health_scheduled_interval_minutes="15"
final_health_cursor_advance_count="$cursor_advance_count"
final_health_candidate_counts_json="$candidate_counts_json"
fi
if [ "$sync_comments_only" = "true" ]; then
final_health_mode="comment_sync"
final_health_processed_limit="$comment_sync_processed_limit"
final_health_cursor_required="false"
final_health_candidate_count=""
final_health_scheduled_interval_minutes=""
final_health_cursor_advance_count=""
final_health_candidate_counts_json=""
if [ "$sync_open_pr_batch" = "true" ]; then
final_health_cursor_path="$cursor_path"
final_health_cursor_required="true"
final_health_processed_limit="$sync_batch_size"
final_health_cursor_advance_count="$comment_sync_cursor_advance_count"
fi
fi
write_apply_health "apply-report.json" ".artifacts/apply-health-final.json" "$final_health_mode" "$final_health_processed_limit" "$final_health_cursor_path" "$final_health_cursor_required" "$final_health_candidate_count" "$final_health_scheduled_interval_minutes" "$final_health_cursor_advance_count" "$final_health_candidate_counts_json"
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 }}" \
--apply-health-file ".artifacts/apply-health-final.json"
publish_status "chore: mark sweep apply finished"
next_apply_item_numbers="$item_numbers"
if [ "$auto_selected_apply_batch" = "true" ] && [ -z "$explicit_item_numbers" ]; then
next_apply_item_numbers=""
fi
{
echo "APPLY_CLOSED_TOTAL=$closed_total"
echo "APPLY_LIMIT=$limit"
echo "APPLY_CONTINUE=$continue_apply"
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=$next_apply_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_AUTO_SELECTED_BATCH=$auto_selected_apply_batch"
echo "APPLY_COMMENT_SYNC_MIN_AGE_DAYS=$comment_sync_min_age_days"
echo "APPLY_CLOSE_PROCESSED_LIMIT=$close_processed_limit"
echo "APPLY_ADAPTIVE_SCAN_REASON=$adaptive_apply_scan_reason"
echo "APPLY_CANDIDATE_QUALITY_SUMMARY=$candidate_quality_summary"
} >> "$GITHUB_ENV"
- name: Export primary apply result
id: primary-apply-result
if: ${{ always() && !cancelled() && steps.apply-existing-run.outcome == 'success' }}
run: echo "succeeded=true" >> "$GITHUB_OUTPUT"
- name: Finalize apply action ledger
id: finalize-apply
if: ${{ always() }}
env:
APPLY_OUTCOME: ${{ steps.apply-existing-run.outcome || 'not_started' }}
run: |
args=()
if [ "$APPLY_OUTCOME" = "cancelled" ]; then
args=(--interrupt-open-attempts --reason cancelled)
elif [ "$APPLY_OUTCOME" != "success" ]; then
args=(--interrupt-open-attempts --reason workflow_failed)
fi
pnpm run --silent finalize-action-events -- "${args[@]}"
- name: Retry final apply status publication
id: retry-final-apply-status-publication
if: ${{ always() && !cancelled() && steps.primary-apply-result.outputs.succeeded == 'true' }}
continue-on-error: true
env:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
run: |
if [ "${APPLY_NOOP:-false}" = "true" ]; then
echo "No proposed closes were ready; no final apply status to retry."
exit 0
fi
target_slug="$APPLY_TARGET_REPO"
target_slug="${target_slug//\//-}"
pnpm run repair:publish-main -- \
--message "chore: mark sweep apply finished" \
--path "results/sweep-status/${target_slug}.json" \
--rebase-strategy theirs
- name: Publish apply action events
id: publish-apply-action-events
if: ${{ always() }}
env:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
run: |
set -euo pipefail
source_root="$CLAWSWEEPER_ACTION_LEDGER_OUTPUT_ROOT"
if [ ! -d "$source_root/ledger" ]; then
echo "No apply action event shards were finalized."
exit 0
fi
event_paths_file=".artifacts/apply-action-ledger-paths.txt"
mkdir -p .artifacts
pnpm run --silent publish-action-events -- \
--source-root "$source_root" \
--state-root . \
--expected-producer-job "$GITHUB_JOB" |
jq -r '.paths[]?' |
sort -u > "$event_paths_file"
if [ ! -s "$event_paths_file" ]; then
echo "Apply action event shards existed but no paths were imported." >&2
exit 1
fi
node dist/clawsweeper.js publish-action-event-paths \
--paths-file "$event_paths_file"
- name: Continue apply sweep
if: ${{ always() && !cancelled() && steps.primary-apply-result.outputs.succeeded == 'true' }}
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_CONTINUE:-false}" != "true" ]; then
echo "Apply checkpoint has no continuation work; not queueing another apply run."
exit 0
fi
can_share_apply_continuation=false
if [ "${APPLY_AUTO_SELECTED_BATCH:-false}" = "true" ] &&
[ -z "${APPLY_ITEM_NUMBERS:-}" ] &&
[ "${APPLY_LIMIT:-40}" = "40" ] &&
[ "${APPLY_MIN_AGE_DAYS:-0}" = "0" ] &&
[ -z "${APPLY_MIN_AGE_MINUTES:-}" ] &&
[ "${APPLY_KIND:-all}" = "all" ] &&
[ "${APPLY_CLOSE_REASONS:-all}" = "all" ] &&
[ "${APPLY_STALE_MIN_AGE_DAYS:-60}" = "60" ] &&
[ "${APPLY_CLOSE_DELAY_MS:-2000}" = "2000" ] &&
[ "${APPLY_CHECKPOINT_SIZE:-40}" = "40" ] &&
[ "${APPLY_COMMENT_SYNC_MIN_AGE_DAYS:-7}" = "7" ]; then
can_share_apply_continuation=true
fi
if [ "$can_share_apply_continuation" != "true" ]; then
echo "Apply continuation uses explicit or non-default inputs; preserving exact continuation dispatch."
else
continuation_env="$(mktemp)"
runs_json="$(mktemp)"
{
for run_status in in_progress pending queued waiting requested; do
gh api --paginate "repos/${{ github.repository }}/actions/runs?per_page=100&status=${run_status}" \
--jq '.workflow_runs[] | {databaseId:.id, workflowPath:.path, displayTitle:.display_title, status:.status, createdAt:.created_at, updatedAt:.updated_at}' 2>/dev/null || true
done
} | jq -s '.' > "$runs_json"
pnpm run --silent workflow -- apply-continuation-blocker \
--runs "$runs_json" \
--current-run-id "${{ github.run_id }}" \
--target-repo "${APPLY_TARGET_REPO:-openclaw/openclaw}" > "$continuation_env"
rm -f "$runs_json"
cat "$continuation_env"
APPLY_CONTINUATION_BLOCKED="$(awk -F= '$1 == "APPLY_CONTINUATION_BLOCKED" { print $2 }' "$continuation_env")"
APPLY_CONTINUATION_BLOCKER_RUN_ID="$(awk -F= '$1 == "APPLY_CONTINUATION_BLOCKER_RUN_ID" { print $2 }' "$continuation_env")"
APPLY_CONTINUATION_BLOCKER_STATUS="$(awk -F= '$1 == "APPLY_CONTINUATION_BLOCKER_STATUS" { print $2 }' "$continuation_env")"
rm -f "$continuation_env"
if [ "${APPLY_CONTINUATION_BLOCKED:-false}" = "true" ]; then
echo "Apply continuation for $APPLY_TARGET_REPO is already covered by ${APPLY_CONTINUATION_BLOCKER_STATUS:-active} run ${APPLY_CONTINUATION_BLOCKER_RUN_ID:-unknown}; not queueing another apply run."
echo "The existing default cursor run will continue the lane."
exit 0
fi
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_checkpoint_size="$APPLY_CHECKPOINT_SIZE"
- name: Save apply telemetry context
if: ${{ always() }}
continue-on-error: true
run: |
mkdir -p .artifacts
noop=false
started_at="${{ steps.apply-telemetry-start.outputs.started_at || '' }}"
if [ "${APPLY_NOOP:-false}" = "true" ]; then
noop=true
fi
if [ -n "$started_at" ]; then
printf '{"noop":%s,"started_at":"%s"}\n' "$noop" "$started_at" > .artifacts/apply-observability-context.json
else
printf '{"noop":%s}\n' "$noop" > .artifacts/apply-observability-context.json
fi
- name: Upload apply telemetry health
if: ${{ always() }}
continue-on-error: true
uses: actions/upload-artifact@v7
with:
name: apply-observability-health-${{ github.run_id }}-${{ github.run_attempt }}
path: |
.artifacts/apply-health-*.json
.artifacts/apply-observability-context.json
include-hidden-files: true
if-no-files-found: ignore
retention-days: 1
- name: Queue review backstops
if: ${{ always() && !cancelled() && steps.primary-apply-result.outputs.succeeded == 'true' && 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 api "repos/${{ github.repository }}/actions/runs?per_page=100" \
--jq '.workflow_runs[] | {databaseId:.id, workflowPath:.path, displayTitle:.display_title, status:.status, createdAt:.created_at, updatedAt:.updated_at}' 2>/dev/null \
|| true
} | jq -s 'unique_by(.databaseId)')"
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.workflowPath !== ".github/workflows/sweep.yml") 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=1200000
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=1200000
fi
publish-apply-observability:
name: Publish apply telemetry
needs: [apply-proof, publish-apply-proof-action-ledger, apply-existing]
if: ${{ always() && (needs.apply-proof.result == 'failure' || needs.publish-apply-proof-action-ledger.result == 'failure' || needs.apply-existing.result == 'failure' || needs.apply-existing.result == 'cancelled' || needs.apply-existing.result == 'success') }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Download apply telemetry health
if: ${{ always() }}
continue-on-error: true
uses: actions/download-artifact@v8
with:
name: apply-observability-health-${{ github.run_id }}-${{ github.run_attempt }}
path: .artifacts
- name: Publish apply telemetry
if: ${{ always() }}
continue-on-error: true
env:
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
APPLY_OUTCOME: ${{ (needs.apply-proof.result == 'failure' || needs.apply-existing.result == 'failure') && 'failure' || needs.apply-existing.result == 'cancelled' && 'cancelled' || needs.apply-existing.result == 'success' && 'success' || 'skipped' }}
ACTION_LEDGER_OUTCOME: ${{ (needs.publish-apply-proof-action-ledger.result == 'failure' || needs.apply-existing.outputs.action_ledger_outcome == 'failure') && 'failure' || needs.apply-existing.outputs.action_ledger_outcome || 'not_started' }}
STATE_STATUS_OUTCOME: ${{ needs.apply-existing.outputs.state_status_outcome || 'not_started' }}
STATE_PUBLICATION_OUTCOME: ${{ needs.apply-existing.outputs.state_publication_outcome || 'not_started' }}
APPLY_STARTED_AT: ${{ needs.apply-existing.outputs.observability_started_at || '' }}
TARGET_REPO: ${{ needs.apply-existing.outputs.target_repo || needs.apply-proof.outputs.target_repo || (github.event_name == 'schedule' && github.event.schedule == '8,23,38,53 * * * *' && 'openclaw/clawhub') || github.event.inputs.target_repo || 'openclaw/openclaw' }}
run: |
set -euo pipefail
test -n "$CLAWSWEEPER_WEBHOOK_SECRET"
node scripts/publish-apply-observability.mjs \
--health-file ".artifacts/apply-health-final.json"