AGENT SKILL

x-watch-pr-ci

Polls a PR's CI checks and Copilot review status, blocking until checks complete or timeout. Returns one of 8 stable exit codes (SUCCESS=0, CI_PENDING_PROCEED=10, CI_FAILED=20, TIMEOUT=30, PR_ALREADY_MERGED=40, NO_CI_CONFIGURED=50, PR_CLOSED=60, PR_NOT_FOUND=70). Writes a versioned state-file for session resume.

License 许可
MIT
最近更新
2026-10-27
Stars
★ 0
Forks
1

01 前言 Front Matter

name
x-watch-pr-ci
description
Polls a PR's CI checks and Copilot review status, blocking until checks complete or timeout. Returns one of 8 stable exit codes (SUCCESS=0, CI_PENDING_PROCEED=10, CI_FAILED=20, TIMEOUT=30, PR_ALREADY_MERGED=40, NO_CI_CONFIGURED=50, PR_CLOSED=60, PR_NOT_FOUND=70). Writes a versioned state-file for session resume.
allowed-tools
Bash
argument-hint
--pr-number <N> [--timeout-seconds 1800] [--poll-interval-seconds 60] [--require-copilot-review true] [--require-checks-passing true] [--copilot-review-timeout 900] [--state-file <path>] [--no-state-file]
context-budget
medium
user-invocable
true
requires-capabilities
[]

02 资源文件 1

03 SKILL.md 正文

Global Output Policy

  • Language: English ONLY.
  • Tone: Technical, Direct, and Concise.
  • Efficiency: Remove all conversational fillers and greetings to save tokens.

Skill: x-watch-pr-ci — CI Watch for PR Approval Gates

Purpose

Polls a pull request's CI checks and Copilot review until checks complete (or timeout), producing a stable exit code that orchestrators use to build the interactive gate menu (EPIC-0043).

Solves the gap identified in spec-ci-watch.md §2: when x-fix-pr is invoked from the FIX-PR slot it finds zero comments because Copilot hasn't posted yet (review typically takes 30–180s). x-watch-pr-ci encapsulates the wait so every caller receives real feedback before presenting a decision gate.

Triggers

  • /x-watch-pr-ci --pr-number 42 — watch PR #42 with defaults
  • /x-watch-pr-ci --pr-number 42 --timeout-seconds 600 — custom timeout
  • /x-watch-pr-ci --pr-number 42 --require-copilot-review false — skip Copilot wait
  • /x-watch-pr-ci --pr-number 42 --no-state-file — fire-and-forget (no state persistence)

Parameters

Parameter Type Default Bounds Description
--pr-number <N> int — (required) >0 PR number to monitor.
--timeout-seconds <N> int 1800 60–7200 Global timeout.
--poll-interval-seconds <N> int 60 15–300 Sleep between polls.
--require-copilot-review boolean true — Wait for copilot-pull-request-reviewer[bot].
--require-checks-passing boolean true — Require all checks success (not just neutral/skipped).
--copilot-review-timeout <N> int 900 60–timeout Copilot-specific sub-timeout.
--state-file <path> path .claude/state/pr-watch-<N>.json 512 chars State file for resume.
--no-state-file flag — — Disable state persistence.

Exit Codes (Stable Public Contract — RULE-045-05)

Code Name Condition
0 SUCCESS All checks green + Copilot review present (or --require-copilot-review=false).
10 CI_PENDING_PROCEED Checks green + Copilot timeout elapsed without review. Proceed with caution.
20 CI_FAILED A check concluded with failure, timed_out, cancelled, or action_required.
30 TIMEOUT Global timeout elapsed with checks still pending.
40 PR_ALREADY_MERGED PR was already merged — idempotent exit.
50 NO_CI_CONFIGURED statusCheckRollup is empty — no CI configured.
60 PR_CLOSED PR closed without merge.
70 PR_NOT_FOUND PR does not exist or caller lacks permission.

These codes are a public contract. Adding a new code = MINOR bump; changing semantics = MAJOR bump (Rule 08 — SemVer).

Invocation by Orchestrators (Rule 13 INLINE-SKILL)

Skill(skill: "x-watch-pr-ci", args: "--pr-number 42")

Orchestrators MUST use this Pattern 1 INLINE-SKILL form. Bare-slash (/x-watch-pr-ci) is forbidden in delegation contexts (Rule 13 §Forbidden).

Workflow

1. VALIDATE   -> Parse and validate arguments; reject out-of-bounds values
2. RESUME     -> Load state-file if present (skip elapsed time already consumed)
3. POLL LOOP  -> while elapsed < timeout:
   a. gh pr view <N> --json state,mergedAt,statusCheckRollup
   b. gh api repos/{owner}/{repo}/pulls/{N}/reviews
   c. Classify (classify checks + copilot + prState + elapsed)
   d. Write state-file (atomic: .tmp + rename)
   e. If terminal condition → emit JSON + exit with code
   f. Sleep poll-interval-seconds
4. TIMEOUT    -> emit JSON + exit 30

Step 1 — Argument Validation

# Validate --pr-number
if [[ -z "$PR_NUMBER" || "$PR_NUMBER" -le 0 ]]; then
  echo "ERROR: --pr-number is required and must be > 0" >&2
  exit 70
fi

# Validate --timeout-seconds (60..7200)
if [[ "$TIMEOUT_SECONDS" -lt 60 || "$TIMEOUT_SECONDS" -gt 7200 ]]; then
  echo "ERROR: timeout-seconds must be in range 60..7200" >&2
  exit 1
fi

# Validate --poll-interval-seconds (15..300)
if [[ "$POLL_INTERVAL" -lt 15 || "$POLL_INTERVAL" -gt 300 ]]; then
  echo "ERROR: poll-interval-seconds must be in range 15..300" >&2
  exit 1
fi

# Validate --copilot-review-timeout (60..timeout)
if [[ "$COPILOT_TIMEOUT" -lt 60 || "$COPILOT_TIMEOUT" -gt "$TIMEOUT_SECONDS" ]]; then
  echo "ERROR: copilot-review-timeout must be in range 60..${TIMEOUT_SECONDS}" >&2
  exit 1
fi

Step 2 — Resume from State File

STATE_FILE="${STATE_FILE:-.claude/state/pr-watch-${PR_NUMBER}.json}"
ELAPSED_OFFSET=0

if [[ -z "$NO_STATE_FILE" && -f "$STATE_FILE" ]]; then
  # Load previous elapsed from state-file (resume after session restart)
  PREV_STARTED=$(jq -r '.startedAt' "$STATE_FILE" 2>/dev/null)
  if [[ "$PREV_STARTED" != "null" && -n "$PREV_STARTED" ]]; then
    ELAPSED_OFFSET=$(( $(date +%s) - $(date -d "$PREV_STARTED" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%SZ" "$PREV_STARTED" +%s) ))
    echo "[resume] Loaded state from $STATE_FILE; offset=${ELAPSED_OFFSET}s" >&2
  fi
fi

mkdir -p "$(dirname "$STATE_FILE")"
START_EPOCH=$(date +%s)
STARTED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

Step 3 — Polling Loop

COPILOT_LOGIN="copilot-pull-request-reviewer[bot]"
RATE_LIMIT_RETRIES=0
MAX_RATE_LIMIT_RETRIES=3

while true; do
  ELAPSED=$(( $(date +%s) - START_EPOCH + ELAPSED_OFFSET ))

  # ── Fetch PR state ──────────────────────────────────────────────────────
  PR_JSON=$(gh pr view "$PR_NUMBER" \
    --json state,mergedAt,statusCheckRollup 2>&1)
  GH_EXIT=$?

  if [[ $GH_EXIT -ne 0 ]]; then
    if echo "$PR_JSON" | grep -qi "Could not resolve"; then
      emit_json "PR_NOT_FOUND" "$ELAPSED" "[]" '{"present":false}' && exit 70
    fi
    if echo "$PR_JSON" | grep -qi "rate limit\|secondary rate"; then
      RATE_LIMIT_RETRIES=$((RATE_LIMIT_RETRIES + 1))
      if [[ $RATE_LIMIT_RETRIES -gt $MAX_RATE_LIMIT_RETRIES ]]; then
        emit_json "TIMEOUT" "$ELAPSED" "[]" '{"present":false}' && exit 30
      fi
      BACKOFF=$(( 30 * (2 ** (RATE_LIMIT_RETRIES - 1)) ))  # 30s, 60s, 120s
      echo "[rate-limit] retry ${RATE_LIMIT_RETRIES}/${MAX_RATE_LIMIT_RETRIES}, sleeping ${BACKOFF}s" >&2
      sleep "$BACKOFF"
      continue
    fi
    echo "[warn] gh pr view failed (exit $GH_EXIT): $PR_JSON" >&2
  fi

  RATE_LIMIT_RETRIES=0  # reset on success

  PR_STATE=$(echo "$PR_JSON" | jq -r '.state // "UNKNOWN"' 2>/dev/null)
  MERGED_AT=$(echo "$PR_JSON" | jq -r '.mergedAt // "null"' 2>/dev/null)
  CHECKS_JSON=$(echo "$PR_JSON" | jq -c '[.statusCheckRollup[]? | {name: .name, conclusion: (.conclusion // .status // "pending")}]' 2>/dev/null || echo "[]")

  # ── Classify early-exit conditions ─────────────────────────────────────
  if [[ "$PR_STATE" == "MERGED" || ("$MERGED_AT" != "null" && -n "$MERGED_AT") ]]; then
    emit_json "PR_ALREADY_MERGED" "$ELAPSED" "$CHECKS_JSON" '{"present":false}' && exit 40
  fi

  if [[ "$PR_STATE" == "CLOSED" ]]; then
    emit_json "PR_CLOSED" "$ELAPSED" "$CHECKS_JSON" '{"present":false}' && exit 60
  fi

  # ── Fetch Copilot review ────────────────────────────────────────────────
  OWNER_REPO=$(gh repo view --json nameWithOwner -q '.nameWithOwner' 2>/dev/null)
  REVIEWS_JSON=$(gh api "repos/${OWNER_REPO}/pulls/${PR_NUMBER}/reviews" 2>/dev/null || echo "[]")
  COPILOT_REVIEW=$(echo "$REVIEWS_JSON" | jq --arg login "$COPILOT_LOGIN" \
    'map(select(.user.login == $login)) | if length > 0 then {present: true, reviewId: .[0].id} else {present: false} end' 2>/dev/null \
    || echo '{"present":false}')
  COPILOT_PRESENT=$(echo "$COPILOT_REVIEW" | jq -r '.present' 2>/dev/null || echo "false")

  # ── Check empty CI (NO_CI_CONFIGURED) ──────────────────────────────────
  CHECK_COUNT=$(echo "$CHECKS_JSON" | jq 'length' 2>/dev/null || echo "0")
  if [[ "$CHECK_COUNT" -eq 0 ]]; then
    emit_json "NO_CI_CONFIGURED" "$ELAPSED" "[]" "$COPILOT_REVIEW" && exit 50
  fi

  # ── Detect failing checks ───────────────────────────────────────────────
  FAILING=$(echo "$CHECKS_JSON" | jq '[.[] | select(.conclusion | IN("failure","timed_out","cancelled","action_required"))] | length' 2>/dev/null || echo "0")
  if [[ "$FAILING" -gt 0 ]]; then
    emit_json "CI_FAILED" "$ELAPSED" "$CHECKS_JSON" "$COPILOT_REVIEW" && exit 20
  fi

  # ── Check all-green ─────────────────────────────────────────────────────
  NON_GREEN=$(echo "$CHECKS_JSON" | jq '[.[] | select(.conclusion | IN("success","neutral","skipped") | not)] | length' 2>/dev/null || echo "1")
  ALL_GREEN=$(( NON_GREEN == 0 ))

  if [[ "$ALL_GREEN" -eq 1 ]]; then
    if [[ "$REQUIRE_COPILOT_REVIEW" == "false" || "$COPILOT_PRESENT" == "true" ]]; then
      emit_json "SUCCESS" "$ELAPSED" "$CHECKS_JSON" "$COPILOT_REVIEW" && exit 0
    fi
    COPILOT_ELAPSED=$(( $(date +%s) - START_EPOCH ))
    if [[ "$COPILOT_ELAPSED" -ge "$COPILOT_REVIEW_TIMEOUT" ]]; then
      emit_json "CI_PENDING_PROCEED" "$ELAPSED" "$CHECKS_JSON" "$COPILOT_REVIEW" && exit 10
    fi
  fi

  # ── Global timeout check ────────────────────────────────────────────────
  if [[ "$ELAPSED" -ge "$TIMEOUT_SECONDS" ]]; then
    emit_json "TIMEOUT" "$ELAPSED" "$CHECKS_JSON" "$COPILOT_REVIEW" && exit 30
  fi

  # ── Write state-file (atomic: .tmp + rename) ────────────────────────────
  if [[ -z "$NO_STATE_FILE" ]]; then
    POLL_COUNT=$(( ${POLL_COUNT:-0} + 1 ))
    LAST_POLL_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    jq -n \
      --argjson prNumber "$PR_NUMBER" \
      --arg startedAt "$STARTED_AT" \
      --arg lastPollAt "$LAST_POLL_AT" \
      --argjson pollCount "$POLL_COUNT" \
      --argjson checksSnapshot "$CHECKS_JSON" \
      --argjson copilotReview "$COPILOT_REVIEW" \
      --arg schemaVersion "1.0" \
      '{
        prNumber: $prNumber,
        startedAt: $startedAt,
        lastPollAt: $lastPollAt,
        pollCount: $pollCount,
        checksSnapshot: $checksSnapshot,
        copilotReview: $copilotReview,
        schemaVersion: $schemaVersion
      }' > "${STATE_FILE}.tmp" 2>/dev/null \
    && mv "${STATE_FILE}.tmp" "$STATE_FILE"
  fi

  echo "[poll ${POLL_COUNT}] elapsed=${ELAPSED}s allGreen=${ALL_GREEN} copilot=${COPILOT_PRESENT}" >&2
  sleep "$POLL_INTERVAL"
done

Helper: emit_json

emit_json() {
  local status="$1" elapsed="$2" checks="$3" copilot="$4"
  jq -n \
    --arg status "$status" \
    --argjson prNumber "$PR_NUMBER" \
    --argjson checks "$checks" \
    --argjson copilotReview "$copilot" \
    --argjson elapsedSeconds "$elapsed" \
    '{
      status: $status,
      prNumber: $prNumber,
      checks: $checks,
      copilotReview: $copilotReview,
      elapsedSeconds: $elapsedSeconds
    }'
}

State File Schema (RULE-045-03)

Location: .claude/state/pr-watch-<N>.json

{
  "prNumber": 42,
  "startedAt": "2026-04-20T10:00:00Z",
  "lastPollAt": "2026-04-20T10:01:05Z",
  "pollCount": 2,
  "checksSnapshot": [
    {"name": "build", "conclusion": "success"},
    {"name": "test", "conclusion": "success"}
  ],
  "copilotReview": {"present": true, "reviewId": 12345678},
  "schemaVersion": "1.0"
}

Write is atomic: write to <path>.tmp, then rename. On corrupted state: log warning and restart from zero.

Stdout Contract

Progress logs go to stderr. The final JSON summary is the last line on stdout:

{
  "status": "SUCCESS",
  "prNumber": 42,
  "checks": [
    {"name": "build", "conclusion": "success"},
    {"name": "test",  "conclusion": "success"}
  ],
  "copilotReview": {"present": true, "reviewId": 12345678},
  "elapsedSeconds": 87
}

Edge Cases

Scenario Behaviour
Rate limit Exponential backoff 3× (30s, 60s, 120s); after 3rd failure exit 30 (TIMEOUT)
Copilot not configured in repo Detected after 1st poll (absent from requested_reviewers); downgrade to CI_PENDING_PROCEED
State file corrupted Log warning, reset poll state, continue from elapsed=0
PR closed during polling Exit 60 immediately
PR merged during polling Exit 40 immediately

Rule Compliance

  • Rule 13: orchestrators invoke via Skill(skill: "x-watch-pr-ci", args: "...") — no bare-slash in delegation
  • Rule 14: skill does NOT create worktrees (sequential, runs in caller's working tree)
  • RULE-045-03: state-file atomic write + resume
  • RULE-045-04: Copilot identified by exact login copilot-pull-request-reviewer[bot]
  • RULE-045-05: exit codes 0/10/20/30/40/50/60/70 are stable public contract
  • RULE-045-06: orchestrators use Rule 13 Pattern 1 INLINE-SKILL

04 同仓库更多 Skills

review-bench
review-bench/edercnj_ia-dev-environment

x-parallel-eval

Detects and classifies file-collision risks between work units (tasks, stories, or epic phases) before parallel execution. Reads ## File Footprint blocks produced by x-task-plan / x-story-plan, applies the parallelism-heuristics knowledge pack (hard / regen / soft categories + hotspot overrides), and emits a collision matrix + serialization recommendation in Markdown (default) or JSON.

★ 0
review-bench
review-bench/edercnj_ia-dev-environment

parallelism-heuristics

Canonical catalog of file-collision heuristics for parallel task, story, and epic execution. Defines the File Footprint block format, the three Conflict Categories (hard / regen / soft), a hotspot list of high-contention paths, and the degrade-with-warning policy. Consumed by x-task-plan, x-story-plan, x-epic-map, x-parallel-eval, and x-dev-*-implement.

★ 0
review-bench
review-bench/edercnj_ia-dev-environment

x-watch-pr-ci

Polls a PR's CI checks and Copilot review status, blocking until checks complete or timeout. Returns one of 8 stable exit codes (SUCCESS=0, CI_PENDING_PROCEED=10, CI_FAILED=20, TIMEOUT=30, PR_ALREADY_MERGED=40, NO_CI_CONFIGURED=50, PR_CLOSED=60, PR_NOT_FOUND=70). Writes a versioned state-file for session resume.

★ 0
review-bench
review-bench/edercnj_ia-dev-environment

x-validate-docs

Documentation freshness gate: validates 6 dimensions (readme, api-specs, grpc-proto, adr, skill-docs, system-architecture) against code changes in a PR. Stack-aware — only validates targets declared/auto-detected from ProjectConfig.documentation.targets. Returns exit non-zero on staleness; produces structured report.

★ 0
review-bench
review-bench/edercnj_ia-dev-environment

x-update-system-architecture

Incrementally updates docs/architecture/system.md after an epic completes. Appends new entries to the Decision Log (§11) using x-internal-write-report --append (dedup by ## ID: marker) and surgically inserts component/integration changes into sections 1-10 via Edit. Idempotent: re-running with the same epic produces a byte-identical system.md.

★ 0
review-bench
review-bench/edercnj_ia-dev-environment

x-update-architecture

Incrementally updates the service architecture document with changes from architecture plans. Adds new components, integrations, flows, and ADR references without rewriting existing content. Use after implementation to keep architecture documentation current.

★ 0