CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/coding-policy

General-purpose coding policy for Baruch's AI agents

91

1.78x
Quality

97%

Does it follow best practices?

Impact

91%

1.78x

Average score across 18 eval scenarios

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

watch-pr-reviews.shskills/release/

#!/usr/bin/env bash
# Block until a PR reaches a merge-gate-relevant terminal state, polling
# poll-pr-reviews.sh at a script-owned interval up to a script-owned budget.
#
# poll-pr-reviews.sh is a single non-blocking snapshot; the loop that wraps
# it — how often to poll, how long to wait, what state ends it — used to be
# hand-rolled per agent, which is how invented wall-clock timeouts (and
# fabricated "N minutes in the policy" citations) crept in. This script owns
# that loop so the cadence and give-up budget are one tuned constant, not an
# agent choice, and it watches EXACTLY the fields the Step 7 merge gate reads:
# each gating bot's LATEST review state (resolved by bot login inside
# poll-pr-reviews.sh), CI status, and merge state. It does NOT wait for inline
# comments to appear (a verdict with zero inline comments is a complete
# review) and does NOT key off any hand-picked run/comment/check id.
#
# Usage: watch-pr-reviews.sh <owner> <repo> <pr-number>
# Out:   the full poll-pr-reviews.sh snapshot with an added top-level object:
#          "watch": {"result": "<result>", "attempts": N, "elapsed_seconds": N}
#        emitted on stdout when a terminal state is reached (rc 0 or 1).
# Exit / result matrix (branch on .watch.result):
#   rc 0, result "ready"             — mergeable, CI success/none, both gating
#                                      bots posted, none requested changes.
#   rc 0, result "changes_requested" — a gating bot's latest verdict is
#                                      CHANGES_REQUESTED. Address and re-push.
#   rc 0, result "ci_failure"        — a check failed. Fix and re-push.
#   rc 0, result "dirty"             — branch conflicts with the base; GitHub
#                                      skipped the pull_request: workflows.
#                                      Rebase, resolve, force-push, re-run.
#   rc 1, result "pending_at_budget" — a signal never arrived within the
#                                      budget (a reviewer that never posted,
#                                      CI stuck pending). Inspect which field
#                                      is still none/pending in the snapshot.
#   rc 2 — argument/env validation error, missing jq, or a poll-pr-reviews.sh
#          failure; stderr-only diagnostic, stdout empty. The watch is
#          idempotent — it re-derives state from scratch — so re-run to resume.
#
# The interval and budget are the constants below (override via the matching
# env vars, used by the test harness to keep runs fast) — never by wrapping
# this call in `timeout`. rules/ci-safety.md "Always Watch CI" names the
# contract and points here rather than restating these.

set -euo pipefail

INTERVAL_SEC="${WATCH_PR_REVIEWS_INTERVAL_SEC:-15}"
BUDGET_SEC="${WATCH_PR_REVIEWS_BUDGET_SEC:-900}"

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
POLL_CMD="${WATCH_PR_REVIEWS_POLL_CMD:-${SCRIPT_DIR}/poll-pr-reviews.sh}"

if ! command -v jq >/dev/null 2>&1; then
  echo "error: jq is not installed; install with 'brew install jq' (macOS) or 'apt install jq' (Debian/Ubuntu) and re-run" >&2
  exit 2
fi

validate_positive_int() {
  local name="$1" value="$2"
  if ! [[ "$value" =~ ^[1-9][0-9]*$ ]]; then
    echo "error: ${name} must be a positive integer, got: '${value}'" >&2
    exit 2
  fi
}

# Overridable in tests so the loop can be driven off queued snapshots
# without a live gh. In production this shells out to poll-pr-reviews.sh
# (invoked via `bash <path>` — tessl packaging strips the exec bit).
fetch_snapshot() {
  local owner="$1" repo="$2" pr="$3"
  bash "$POLL_CMD" "$owner" "$repo" "$pr"
}

emit_and_exit() {
  local result="$1" attempts="$2" elapsed="$3" snapshot="$4" rc="$5"
  printf '%s' "$snapshot" | jq \
    --arg result "$result" \
    --argjson attempts "$attempts" \
    --argjson elapsed "$elapsed" \
    '. + {watch: {result: $result, attempts: $attempts, elapsed_seconds: $elapsed}}'
  exit "$rc"
}

main() {
  if [[ $# -ne 3 ]]; then
    echo "usage: $0 <owner> <repo> <pr-number>" >&2
    exit 2
  fi
  validate_positive_int "WATCH_PR_REVIEWS_INTERVAL_SEC" "$INTERVAL_SEC"
  validate_positive_int "WATCH_PR_REVIEWS_BUDGET_SEC" "$BUDGET_SEC"
  if (( INTERVAL_SEC > BUDGET_SEC )); then
    echo "error: WATCH_PR_REVIEWS_INTERVAL_SEC (${INTERVAL_SEC}) cannot exceed WATCH_PR_REVIEWS_BUDGET_SEC (${BUDGET_SEC})" >&2
    exit 2
  fi

  local owner="$1" repo="$2" pr="$3"
  local elapsed=0 attempts=0 snapshot=""

  # Loop bound is `<= BUDGET_SEC` (not strictly less) so a signal that lands
  # exactly at the budget boundary is still caught by the final poll.
  while (( elapsed <= BUDGET_SEC )); do
    attempts=$(( attempts + 1 ))

    snapshot=$(fetch_snapshot "$owner" "$repo" "$pr") \
      || { echo "error: poll-pr-reviews.sh failed for ${owner}/${repo}#${pr} (attempt ${attempts}) — run 'bash ${POLL_CMD} ${owner} ${repo} ${pr}' directly to see the underlying gh/jq error; the watch is idempotent, re-run once resolved" >&2; exit 2; }

    if ! printf '%s' "$snapshot" | jq -e . >/dev/null 2>&1; then
      echo "error: poll-pr-reviews.sh returned non-JSON for ${owner}/${repo}#${pr} (attempt ${attempts}): ${snapshot}" >&2
      exit 2
    fi

    local mergeable mstatus ci gh_aw copilot
    mergeable=$(printf '%s' "$snapshot" | jq -r '.merge_state.mergeable')
    mstatus=$(printf '%s'   "$snapshot" | jq -r '.merge_state.status')
    ci=$(printf '%s'        "$snapshot" | jq -r '.ci.status')
    gh_aw=$(printf '%s'     "$snapshot" | jq -r '.reviews.gh_aw.state')
    copilot=$(printf '%s'   "$snapshot" | jq -r '.reviews.copilot.state')

    # Order matters: a conflicting branch or a failed check or a
    # CHANGES_REQUESTED verdict is terminal-for-this-round — the agent must
    # go act (rebase / fix / address) before any further waiting helps.
    if [[ "$mergeable" == "CONFLICTING" || "$mstatus" == "DIRTY" ]]; then
      emit_and_exit "dirty" "$attempts" "$elapsed" "$snapshot" 0
    fi
    if [[ "$ci" == "failure" ]]; then
      emit_and_exit "ci_failure" "$attempts" "$elapsed" "$snapshot" 0
    fi
    if [[ "$gh_aw" == "CHANGES_REQUESTED" || "$copilot" == "CHANGES_REQUESTED" ]]; then
      emit_and_exit "changes_requested" "$attempts" "$elapsed" "$snapshot" 0
    fi

    # Ready = the exact Step 7 merge-gate field conjunction: mergeable, CI
    # green-or-absent, and BOTH gating bots have posted a verdict (state left
    # "none"). Neither is CHANGES_REQUESTED here — the guard above returned.
    if [[ "$mergeable" == "MERGEABLE" \
       && ( "$ci" == "success" || "$ci" == "none" ) \
       && "$gh_aw" != "none" \
       && "$copilot" != "none" ]]; then
      emit_and_exit "ready" "$attempts" "$elapsed" "$snapshot" 0
    fi

    # Still pending (UNKNOWN mergeability, CI pending, or a bot yet to post).
    # Skip the trailing sleep on the boundary iteration; otherwise cap the
    # sleep at remaining budget so total sleep tracks BUDGET_SEC.
    (( elapsed == BUDGET_SEC )) && break
    local remaining=$(( BUDGET_SEC - elapsed ))
    local sleep_for=$INTERVAL_SEC
    (( sleep_for > remaining )) && sleep_for=$remaining
    sleep "$sleep_for"
    elapsed=$(( elapsed + sleep_for ))
  done

  emit_and_exit "pending_at_budget" "$attempts" "$elapsed" "$snapshot" 1
}

[[ "${BASH_SOURCE[0]}" == "${0}" ]] && main "$@"

README.md

tile.json