Skip to content

repair comment router #316824

repair comment router

repair comment router #316824

name: repair comment router
on:
repository_dispatch:
types: [clawsweeper_comment]
workflow_dispatch:
inputs:
execute:
description: "Post replies and dispatch repair workers"
required: true
default: false
type: boolean
force_reprocess:
description: "Replay matching comments even if their current version is already in the ledger"
required: true
default: false
type: boolean
attempt_id:
description: "Optional durable identity for one forced replay across workflow retries"
required: false
default: ""
type: string
target_repo:
description: "Repository to scan for ClawSweeper commands"
required: true
default: openclaw/openclaw
type: string
target_branch:
description: "Optional target repository branch for exact review dispatches"
required: false
default: ""
type: string
lookback_minutes:
description: "How far back to scan comments"
required: true
default: "180"
type: string
since:
description: "Optional ISO timestamp lower bound for scanned comments"
required: false
default: ""
type: string
max_comments:
description: "Maximum recent comments to inspect"
required: true
default: "100"
type: string
item_numbers:
description: "Optional comma-separated issue or PR numbers to route directly"
required: false
default: ""
type: string
comment_ids:
description: "Optional comma-separated issue comment ids to route directly"
required: false
default: ""
type: string
runner:
description: "Runner label for repair planning/review work"
required: true
default: blacksmith-4vcpu-ubuntu-2404
type: string
execution_runner:
description: "Runner label for fix/apply execution work"
required: true
default: blacksmith-16vcpu-ubuntu-2404
type: string
schedule:
- cron: "*/5 * * * *"
permissions:
contents: write
actions: write
issues: write
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
CLAWSWEEPER_APP_CLIENT_ID: Iv23liOECG0slfuhz093
CLAWSWEEPER_PROOF_WORKFLOW_PATH: ${{ vars.CLAWSWEEPER_PROOF_WORKFLOW_PATH }}
CLAWSWEEPER_PROOF_WORKFLOW_REF: ${{ vars.CLAWSWEEPER_PROOF_WORKFLOW_REF }}
CLAWSWEEPER_PROOF_WORKFLOW_SHA: ${{ vars.CLAWSWEEPER_PROOF_WORKFLOW_SHA }}
CLAWSWEEPER_PROOF_HARNESS_SHA: ${{ vars.CLAWSWEEPER_PROOF_HARNESS_SHA }}
CLAWSWEEPER_TELEGRAM_PROOF_WORKFLOW_PATH: ${{ vars.CLAWSWEEPER_TELEGRAM_PROOF_WORKFLOW_PATH }}
CLAWSWEEPER_TELEGRAM_PROOF_WORKFLOW_REF: ${{ vars.CLAWSWEEPER_TELEGRAM_PROOF_WORKFLOW_REF }}
CLAWSWEEPER_TELEGRAM_PROOF_WORKFLOW_SHA: ${{ vars.CLAWSWEEPER_TELEGRAM_PROOF_WORKFLOW_SHA }}
CLAWSWEEPER_TELEGRAM_PROOF_HARNESS_SHA: ${{ vars.CLAWSWEEPER_TELEGRAM_PROOF_HARNESS_SHA }}
CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_UNSPONSORED_FEATURE_CLOSE_ENABLED || 'false' }}
CLAWSWEEPER_AUTHOR_PR_BUDGET_CLOSE_ENABLED: ${{ vars.CLAWSWEEPER_AUTHOR_PR_BUDGET_CLOSE_ENABLED || 'false' }}
concurrency:
# GitHub retains only one pending run per concurrency group. Exact-item verdict
# handoffs need their own group so unrelated router traffic cannot replace them.
group: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.comment_id && format('repair-comment-router-{0}-comment-{1}', github.event.client_payload.target_repo || 'openclaw/openclaw', github.event.client_payload.comment_id) || github.event_name == 'workflow_dispatch' && github.event.inputs.item_numbers != '' && format('repair-comment-router-{0}-items-{1}', github.event.inputs.target_repo || 'openclaw/openclaw', github.event.inputs.item_numbers) || format('repair-comment-router-{0}', github.event.inputs.target_repo || github.event.client_payload.target_repo || 'openclaw/openclaw') }}
cancel-in-progress: false
jobs:
hosted-target-admission:
uses: ./.github/workflows/hosted-target-admission.yml
with:
target_repo: ${{ github.event_name == 'workflow_dispatch' && inputs.target_repo || github.event_name == 'repository_dispatch' && github.event.client_payload.target_repo || vars.CLAWSWEEPER_TARGET_REPO || 'openclaw/openclaw' }}
secrets:
CLAWSWEEPER_APP_PRIVATE_KEY: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
route-comments:
needs: hosted-target-admission
if: ${{ needs.hosted-target-admission.outputs.outcome == 'public' }}
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
filter: blob:none
fetch-depth: 0
sparse-checkout: |
.github
config
jobs
prompts/pr-close-coverage-proof.md
results
schema/clawsweeper-pr-close-coverage-proof.schema.json
scripts/hydrate-state.ts
scripts/comment-router-runner.mjs
scripts/operator-skip-reasons.mjs
scripts/prepare-worker-record-cache.ts
scripts/worker-blobs.ts
scripts/worker-records.ts
src
package.json
pnpm-lock.yaml
pnpm-workspace.yaml
tsconfig.json
tsconfig.repair.json
sparse-checkout-cone-mode: false
- name: Resolve target repository
id: target
env:
TARGET_REPO: ${{ github.event_name == 'workflow_dispatch' && inputs.target_repo || github.event_name == 'repository_dispatch' && github.event.client_payload.target_repo || vars.CLAWSWEEPER_TARGET_REPO || 'openclaw/openclaw' }}
ITEM_NUMBERS: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.item_number || github.event_name == 'workflow_dispatch' && inputs.item_numbers || '' }}
run: |
set -euo pipefail
target_repo="${TARGET_REPO:-openclaw/openclaw}"
if ! printf '%s' "$target_repo" | grep -Eq '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'; then
echo "Invalid target_repo: $target_repo" >&2
exit 1
fi
{
echo "target_repo=$target_repo"
echo "target_repo_owner=${target_repo%%/*}"
echo "target_repo_name=${target_repo#*/}"
echo "target_slug=$(printf '%s' "$target_repo" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9_.-]+/-/g')"
item_number="${ITEM_NUMBERS:-}"
if [[ "$item_number" =~ ^[1-9][0-9]*$ ]] &&
(( ${#item_number} < 16 ||
( ${#item_number} == 16 && 10#$item_number < 9007199254740992 ) )); then
echo "records_item_number=$item_number"
fi
} >> "$GITHUB_OUTPUT"
- name: Create target GitHub App token
id: app_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: ${{ steps.target.outputs.target_repo_owner }}
repositories: ${{ steps.target.outputs.target_repo_name }}
permission-actions: write
permission-checks: read
permission-contents: write
permission-issues: write
permission-pull-requests: write
permission-statuses: read
- name: Create central dispatch token
id: dispatch-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
repositories: clawsweeper
permission-actions: write
permission-contents: write
- name: Create state token
id: state-token
uses: ./.github/actions/create-state-token
with:
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
- uses: ./.github/actions/setup-state
with:
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.records_item_number || '' }}
hydrate-state-blobs: "false"
token: ${{ steps.state-token.outputs.token }}
fetch-depth: 1
- uses: ./.github/actions/setup-action-ledger
- uses: ./.github/actions/setup-pnpm
id: setup-pnpm
with:
# This job publishes the action ledger with dist/clawsweeper.js, which
# only the main build emits, so build both Node bundles and skip the
# dashboard leg the job never runs.
build-script: build:node
- name: Schedule Endor test repository automerge
if: ${{ github.event_name == 'schedule' && vars.CLAWSWEEPER_COMMENT_ROUTER_EXECUTE == '1' && steps.target.outputs.target_repo != 'openclaw/endor-clawsweeper-e2e' }}
continue-on-error: true
env:
GH_TOKEN: ${{ steps.dispatch-token.outputs.token }}
run: |
gh workflow run repair-comment-router.yml \
--repo "$GITHUB_REPOSITORY" --ref "$GITHUB_REF_NAME" \
-f execute=true -f target_repo=openclaw/endor-clawsweeper-e2e
- name: Enrol Endor remediation PRs
if: ${{ steps.target.outputs.target_repo == 'openclaw/endor-clawsweeper-e2e' && ((github.event_name == 'schedule' && vars.CLAWSWEEPER_COMMENT_ROUTER_EXECUTE == '1') || (github.event_name == 'workflow_dispatch' && inputs.execute)) }}
continue-on-error: true
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
TARGET_REPO: ${{ steps.target.outputs.target_repo }}
run: node dist/repair/endor-automerge-intake.js --repo "$TARGET_REPO" --execute
- name: Prepare bounded proof planner
if: ${{ steps.target.outputs.target_repo == 'openclaw/openclaw' && (vars.CLAWSWEEPER_PROOF_WORKFLOW_SHA != '' || vars.CLAWSWEEPER_TELEGRAM_PROOF_WORKFLOW_SHA != '') && (github.event_name == 'repository_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.execute) || (github.event_name == 'schedule' && vars.CLAWSWEEPER_COMMENT_ROUTER_EXECUTE == '1')) }}
continue-on-error: true
uses: ./.github/actions/setup-codex
env:
OPENAI_API_KEY: ${{ vars.CLAWSWEEPER_CODEX_AUTH_MODE != 'clawrouter' && secrets.OPENAI_API_KEY || '' }}
CLAWSWEEPER_INTERNAL_MODEL: ${{ vars.CLAWSWEEPER_CODEX_AUTH_MODE != 'clawrouter' && secrets.CLAWSWEEPER_MODEL || '' }}
CLAWSWEEPER_CLAWROUTER_CONFIG: ${{ secrets.CLAWSWEEPER_CLAWROUTER_CONFIG }}
with:
auth-mode: ${{ vars.CLAWSWEEPER_CODEX_AUTH_MODE || 'proxy' }}
- name: Route ClawSweeper comments
id: route-comments
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
CLAWSWEEPER_PUBLIC_GH_TOKEN: ${{ github.token }}
CLAWSWEEPER_DISPATCH_TOKEN: ${{ steps.dispatch-token.outputs.token }}
CLAWSWEEPER_MUTATION_TOKEN_SOURCE: clawsweeper-app
CLAWSWEEPER_ALLOW_MERGE: ${{ vars.CLAWSWEEPER_ALLOW_MERGE || '0' }}
CLAWSWEEPER_STATUS_INGEST_TOKEN: ${{ secrets.CLAWSWEEPER_STATUS_INGEST_TOKEN }}
CLAWSWEEPER_ACTION_LEDGER_INVOCATION: initial
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
CLAWSWEEPER_TARGET_INSTALLATION_ID: ${{ steps.app_token.outputs.installation-id }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
run: |
set -euo pipefail
target_repo="${{ github.event_name == 'workflow_dispatch' && inputs.target_repo || vars.CLAWSWEEPER_TARGET_REPO || 'openclaw/openclaw' }}"
target_branch="${{ github.event_name == 'workflow_dispatch' && inputs.target_branch || '' }}"
lookback_minutes="${{ github.event_name == 'workflow_dispatch' && inputs.lookback_minutes || vars.CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES || '180' }}"
since="${{ github.event_name == 'workflow_dispatch' && inputs.since || '' }}"
max_comments="${{ github.event_name == 'workflow_dispatch' && inputs.max_comments || vars.CLAWSWEEPER_COMMENT_MAX_COMMENTS || '100' }}"
item_numbers="${{ github.event_name == 'workflow_dispatch' && inputs.item_numbers || '' }}"
comment_ids="${{ github.event_name == 'workflow_dispatch' && inputs.comment_ids || '' }}"
runner="${{ github.event_name == 'workflow_dispatch' && inputs.runner || vars.CLAWSWEEPER_WORKER_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}"
execution_runner="${{ github.event_name == 'workflow_dispatch' && inputs.execution_runner || vars.CLAWSWEEPER_EXECUTION_RUNNER || 'blacksmith-16vcpu-ubuntu-2404' }}"
force_reprocess="${{ github.event_name == 'workflow_dispatch' && inputs.force_reprocess || 'false' }}"
attempt_id="${{ github.event_name == 'workflow_dispatch' && inputs.attempt_id || '' }}"
if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
target_repo="${{ github.event.client_payload.target_repo || 'openclaw/openclaw' }}"
target_branch="${{ github.event.client_payload.target_branch || '' }}"
lookback_minutes="${{ github.event.client_payload.lookback_minutes || vars.CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES || '180' }}"
since=""
max_comments="${{ github.event.client_payload.max_comments || '1' }}"
item_numbers="${{ github.event.client_payload.item_number || '' }}"
comment_ids="${{ github.event.client_payload.comment_id || '' }}"
status_comment_id="${{ github.event.client_payload.status_comment_id || '' }}"
source_delivery_id="${{ github.event.client_payload.source_delivery_id || '' }}"
source_event="${{ github.event.client_payload.source_event || 'issue_comment' }}"
source_action="${{ github.event.client_payload.source_action || '' }}"
dispatch_actor="${{ github.actor }}"
comment_event_auth="${{ github.event.client_payload.comment_event_auth || '' }}"
comment_updated_at="${{ github.event.client_payload.comment_updated_at || '' }}"
comment_body_sha256="${{ github.event.client_payload.comment_body_sha256 || '' }}"
force_reprocess="${{ github.event.client_payload.force_reprocess || 'false' }}"
attempt_id="${{ github.event.client_payload.attempt_id || '' }}"
else
status_comment_id=""
source_delivery_id=""
source_event=""
source_action=""
dispatch_actor=""
comment_event_auth=""
comment_updated_at=""
comment_body_sha256=""
fi
if [ "$force_reprocess" = "true" ] && [ -z "$attempt_id" ]; then
attempt_id="forced-replay-${GITHUB_RUN_ID}"
fi
if [ "$force_reprocess" != "true" ] && [ -n "$attempt_id" ]; then
echo "attempt_id requires force_reprocess=true" >&2
exit 1
fi
args=(
--write-report
--repo "$target_repo"
--lookback-minutes "$lookback_minutes"
--max-comments "$max_comments"
--runner "$runner"
--execution-runner "$execution_runner"
--wait-for-capacity
)
if [ -n "$target_branch" ]; then
args+=(--target-branch "$target_branch")
fi
if [ -n "$since" ]; then
args+=(--since "$since")
fi
if [ -n "$item_numbers" ]; then
args+=(--item-numbers "$item_numbers")
fi
if [ -n "$comment_ids" ]; then
args+=(--comment-ids "$comment_ids")
fi
if [ -n "$status_comment_id" ]; then
args+=(--status-comment-id "$status_comment_id")
fi
if [ -n "$source_delivery_id" ]; then
args+=(--source-delivery-id "$source_delivery_id")
fi
if [ -n "$source_event" ]; then
args+=(--source-event "$source_event")
fi
if [ -n "$source_action" ]; then
args+=(--source-action "$source_action")
fi
if [ -n "$dispatch_actor" ]; then
args+=(--dispatch-actor "$dispatch_actor")
fi
if [ -n "$comment_event_auth" ]; then
args+=(--comment-event-auth "$comment_event_auth")
fi
if [ -n "$comment_updated_at" ]; then
args+=(--comment-updated-at "$comment_updated_at")
fi
if [ -n "$comment_body_sha256" ]; then
args+=(--comment-body-sha256 "$comment_body_sha256")
fi
if [ "$force_reprocess" = "true" ]; then
args+=(--force-reprocess)
args+=(--attempt-id "$attempt_id")
fi
if { [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.execute }}" = "true" ]; } ||
{ [ "${{ github.event_name }}" = "repository_dispatch" ]; } ||
{ [ "${{ github.event_name }}" = "schedule" ] && [ "${{ vars.CLAWSWEEPER_COMMENT_ROUTER_EXECUTE || '0' }}" = "1" ]; }; then
args+=(--execute)
fi
pnpm run repair:comment-router -- "${args[@]}"
- name: Reconcile explicitly requested behavioral proof
if: ${{ success() && steps.target.outputs.target_repo == 'openclaw/openclaw' && (github.event_name == 'repository_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.execute) || (github.event_name == 'schedule' && vars.CLAWSWEEPER_COMMENT_ROUTER_EXECUTE == '1')) }}
continue-on-error: true
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
run: node dist/repair/command-proof-cli.js reconcile
- name: Commit comment router ledger
if: always()
env:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
run: |
if jq -e '.short_circuited == true' results/comment-router-latest.json >/dev/null 2>&1; then
echo "Exact terminal comment version already recorded; skipping state publication."
exit 0
fi
jobs_changed=0
cursor_changed="$(jq -r '.routing_cursor_changed == true' results/comment-router-latest.json 2>/dev/null || true)"
if ! git diff --no-index --quiet -- "$CLAWSWEEPER_STATE_DIR/jobs" jobs; then
jobs_changed=1
fi
if [ "$jobs_changed" = "0" ] &&
[ "$cursor_changed" != "true" ] &&
jq -e '((.ledger_claimed // 0) + (.ledger_changed // 0)) == 0' results/comment-router-latest.json >/dev/null 2>&1; then
echo "No durable router state changed; skipping state publication."
exit 0
fi
publish_args=(
--message "chore: record ClawSweeper comment routing" \
--path results/comment-router.json \
--rebase-strategy theirs
)
# The latest scan report is run-local. Only commands that actually
# changed repair jobs need the serialized Git state writer.
if [ "$jobs_changed" = "1" ]; then
publish_args+=(--path jobs)
fi
if [ "$cursor_changed" = "true" ]; then
publish_args+=(--path "results/comment-router-cursors/${{ steps.target.outputs.target_slug }}.json")
fi
pnpm run repair:publish-main -- "${publish_args[@]}"
- name: Detect waiting repair dispatches
id: waiting-repair-dispatches
if: ${{ success() }}
run: |
set -euo pipefail
count="0"
if [ -f results/comment-router-latest.json ]; then
count="$(pnpm run --silent workflow -- count-command-actions --report results/comment-router-latest.json --action dispatch_repair --status waiting,active)"
fi
echo "count=$count" >> "$GITHUB_OUTPUT"
- name: Retry waiting repair dispatches
if: ${{ steps.waiting-repair-dispatches.outputs.count != '' && steps.waiting-repair-dispatches.outputs.count != '0' }}
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
CLAWSWEEPER_PUBLIC_GH_TOKEN: ${{ github.token }}
CLAWSWEEPER_DISPATCH_TOKEN: ${{ steps.dispatch-token.outputs.token }}
CLAWSWEEPER_MUTATION_TOKEN_SOURCE: clawsweeper-app
CLAWSWEEPER_ALLOW_MERGE: ${{ vars.CLAWSWEEPER_ALLOW_MERGE || '0' }}
CLAWSWEEPER_STATUS_INGEST_TOKEN: ${{ secrets.CLAWSWEEPER_STATUS_INGEST_TOKEN }}
CLAWSWEEPER_ACTION_LEDGER_INVOCATION: retry
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
CLAWSWEEPER_TARGET_INSTALLATION_ID: ${{ steps.app_token.outputs.installation-id }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
run: |
set -euo pipefail
target_repo="${{ github.event_name == 'workflow_dispatch' && inputs.target_repo || vars.CLAWSWEEPER_TARGET_REPO || 'openclaw/openclaw' }}"
target_branch="${{ github.event_name == 'workflow_dispatch' && inputs.target_branch || '' }}"
lookback_minutes="${{ github.event_name == 'workflow_dispatch' && inputs.lookback_minutes || vars.CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES || '180' }}"
since="${{ github.event_name == 'workflow_dispatch' && inputs.since || '' }}"
max_comments="${{ github.event_name == 'workflow_dispatch' && inputs.max_comments || vars.CLAWSWEEPER_COMMENT_MAX_COMMENTS || '100' }}"
item_numbers="${{ github.event_name == 'workflow_dispatch' && inputs.item_numbers || '' }}"
comment_ids="${{ github.event_name == 'workflow_dispatch' && inputs.comment_ids || '' }}"
runner="${{ github.event_name == 'workflow_dispatch' && inputs.runner || vars.CLAWSWEEPER_WORKER_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}"
execution_runner="${{ github.event_name == 'workflow_dispatch' && inputs.execution_runner || vars.CLAWSWEEPER_EXECUTION_RUNNER || 'blacksmith-16vcpu-ubuntu-2404' }}"
force_reprocess="${{ github.event_name == 'workflow_dispatch' && inputs.force_reprocess || 'false' }}"
attempt_id="${{ github.event_name == 'workflow_dispatch' && inputs.attempt_id || '' }}"
if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
target_repo="${{ github.event.client_payload.target_repo || 'openclaw/openclaw' }}"
target_branch="${{ github.event.client_payload.target_branch || '' }}"
lookback_minutes="${{ github.event.client_payload.lookback_minutes || vars.CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES || '180' }}"
since=""
max_comments="${{ github.event.client_payload.max_comments || '1' }}"
item_numbers="${{ github.event.client_payload.item_number || '' }}"
comment_ids="${{ github.event.client_payload.comment_id || '' }}"
status_comment_id="${{ github.event.client_payload.status_comment_id || '' }}"
source_delivery_id="${{ github.event.client_payload.source_delivery_id || '' }}"
source_event="${{ github.event.client_payload.source_event || 'issue_comment' }}"
source_action="${{ github.event.client_payload.source_action || '' }}"
dispatch_actor="${{ github.actor }}"
comment_event_auth="${{ github.event.client_payload.comment_event_auth || '' }}"
comment_updated_at="${{ github.event.client_payload.comment_updated_at || '' }}"
comment_body_sha256="${{ github.event.client_payload.comment_body_sha256 || '' }}"
force_reprocess="${{ github.event.client_payload.force_reprocess || 'false' }}"
attempt_id="${{ github.event.client_payload.attempt_id || '' }}"
else
status_comment_id=""
source_delivery_id=""
source_event=""
source_action=""
dispatch_actor=""
comment_event_auth=""
comment_updated_at=""
comment_body_sha256=""
fi
if [ "$force_reprocess" = "true" ] && [ -z "$attempt_id" ]; then
attempt_id="forced-replay-${GITHUB_RUN_ID}"
fi
if [ "$force_reprocess" != "true" ] && [ -n "$attempt_id" ]; then
echo "attempt_id requires force_reprocess=true" >&2
exit 1
fi
args=(
--write-report
--repo "$target_repo"
--lookback-minutes "$lookback_minutes"
--max-comments "$max_comments"
--runner "$runner"
--execution-runner "$execution_runner"
--wait-for-capacity
)
if [ -n "$target_branch" ]; then
args+=(--target-branch "$target_branch")
fi
if [ -n "$since" ]; then
args+=(--since "$since")
fi
if [ -n "$item_numbers" ]; then
args+=(--item-numbers "$item_numbers")
fi
if [ -n "$comment_ids" ]; then
args+=(--comment-ids "$comment_ids")
fi
if [ -n "$status_comment_id" ]; then
args+=(--status-comment-id "$status_comment_id")
fi
if [ -n "$source_delivery_id" ]; then
args+=(--source-delivery-id "$source_delivery_id")
fi
if [ -n "$source_event" ]; then
args+=(--source-event "$source_event")
fi
if [ -n "$source_action" ]; then
args+=(--source-action "$source_action")
fi
if [ -n "$dispatch_actor" ]; then
args+=(--dispatch-actor "$dispatch_actor")
fi
if [ -n "$comment_event_auth" ]; then
args+=(--comment-event-auth "$comment_event_auth")
fi
if [ -n "$comment_updated_at" ]; then
args+=(--comment-updated-at "$comment_updated_at")
fi
if [ -n "$comment_body_sha256" ]; then
args+=(--comment-body-sha256 "$comment_body_sha256")
fi
if [ "$force_reprocess" = "true" ]; then
args+=(--force-reprocess)
args+=(--attempt-id "$attempt_id")
fi
args+=(--execute)
echo "Retrying ${{ steps.waiting-repair-dispatches.outputs.count }} waiting repair dispatch(es) after publishing router state."
pnpm run repair:comment-router -- "${args[@]}"
- name: Commit comment router retry ledger
if: ${{ always() && steps.waiting-repair-dispatches.outputs.count != '' && steps.waiting-repair-dispatches.outputs.count != '0' }}
env:
CLAWSWEEPER_WEBHOOK_SECRET: ${{ secrets.CLAWSWEEPER_WEBHOOK_SECRET }}
QUEUE_URL: ${{ vars.CLAWSWEEPER_EXACT_REVIEW_QUEUE_URL || 'https://clawsweeper.openclaw.ai' }}
run: |
# The initial publish copies every published path back into the state
# checkout, making it the baseline for retry-only job mutations.
jobs_changed=0
cursor_changed="$(jq -r '.routing_cursor_changed == true' results/comment-router-latest.json 2>/dev/null || true)"
if ! git diff --no-index --quiet -- "$CLAWSWEEPER_STATE_DIR/jobs" jobs; then
jobs_changed=1
fi
if [ "$jobs_changed" = "0" ] &&
[ "$cursor_changed" != "true" ] &&
jq -e '((.ledger_claimed // 0) + (.ledger_changed // 0)) == 0' results/comment-router-latest.json >/dev/null 2>&1; then
echo "No durable router retry state changed; skipping state publication."
exit 0
fi
publish_args=(
--message "chore: record ClawSweeper repair dispatch retry" \
--path results/comment-router.json \
--rebase-strategy theirs
)
# Retry receipts are append-only unless the retry changed a repair job.
if [ "$jobs_changed" = "1" ]; then
publish_args+=(--path jobs)
fi
if [ "$cursor_changed" = "true" ]; then
publish_args+=(--path "results/comment-router-cursors/${{ steps.target.outputs.target_slug }}.json")
fi
pnpm run repair:publish-main -- "${publish_args[@]}"
- name: Finalize command action ledger
id: finalize-command-action-ledger
if: ${{ always() && steps.setup-pnpm.outcome == 'success' }}
run: |
set -euo pipefail
: "${CLAWSWEEPER_ACTION_LEDGER_OUTPUT_ROOT:?setup-action-ledger output root is required}"
mkdir -p .artifacts
allow_empty_args=()
# Empty is valid only when this run's report proves no command lifecycle started.
if [ "${{ steps.route-comments.outcome }}" = "success" ] &&
jq -e '.commands_seen == 0' results/comment-router-latest.json >/dev/null 2>&1; then
allow_empty_args+=(--allow-empty)
fi
manifest_file=".artifacts/comment-router-command-action-ledger-manifest.json"
pnpm run --silent repair:action-ledger -- finalize \
--lane comment-router \
"${allow_empty_args[@]}" \
> "$manifest_file"
if [ -s "$manifest_file" ]; then
echo "publish=true" >> "$GITHUB_OUTPUT"
else
echo "publish=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish immutable command action ledger
if: ${{ always() && steps.setup-pnpm.outcome == 'success' && steps.finalize-command-action-ledger.outputs.publish == '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:?setup-action-ledger output root is required}"
manifest_file=".artifacts/comment-router-command-action-ledger-manifest.json"
test -s "$manifest_file"
event_paths_file=".artifacts/command-action-ledger-paths.txt"
import_result_file=".artifacts/command-action-ledger-import.json"
pnpm run --silent repair:action-ledger -- publish \
--lane comment-router \
--manifest "$manifest_file" \
--source-root "$source_root" \
--state-root . > "$import_result_file"
if ! jq -e --slurpfile manifest "$manifest_file" \
'.eventPaths == $manifest[0].event_paths' \
"$import_result_file" >/dev/null; then
echo "Imported command action event paths do not match the finalized manifest." >&2
exit 1
fi
jq -r '.paths[]?' "$import_result_file" |
sort -u > "$event_paths_file"
if [ ! -s "$event_paths_file" ]; then
echo "Command 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"