Receber por email
← Voltar para o catálogo

phase-monitor-merge

Phase-agent that watches the open PR through to merge (CTL-449 Initiative 1 Phase 3). Lifts the active listen loop from the legacy `oneshot` Phase 5 body: event-driven wait on `catalyst-events wait-for`, inline resolution of CI fix-ups, bot review threads, and BEHIND rebases, then `gh pr merge --squash --delete-branch` when the PR reaches CLEAN. Linear Done transition and worktree teardown are own

18estrelas
Atualizado há 3 meses

Ver no GitHub ↗

Como adicionar

/plugin marketplace add coalesce-labs/catalyst

O comando exato pode variar conforme o repositório. Confira o README no GitHub.

Para o autor da skill

Cole no README do seu repo

Mostra que sua skill está catalogada na Skillteca, gera backlink e tráfego rastreável.

[![Skillteca](https://www.skillteca.com.br/api/badge/phase-monitor-merge/svg)](https://www.skillteca.com.br/skills/phase-monitor-merge)
#ai

Skills relacionadas

Ver todas de Pesquisa e Web →

Alerta por categoria

Receba novas skills de Pesquisa e Web toda segunda

1 email curto, só com as skills novas de Pesquisa e Web. 4 minutos de leitura, sem spam, cancela com 1 clique.

Você confirma o email no primeiro envio. Sem spam. Cancela com 1 clique.

Ler descrição completa

phase-monitor-merge

The reactive half of the worker lifecycle. The PR exists (opened by [[phase-pr]]); this phase agent drives it to MERGED. Linear Done transition and worktree teardown are owned by [[phase-teardown]] (CTL-703). Implementation lifts the loop from plugins/dev/skills/oneshot/SKILL.md §"Step 2: Active PR Listen Loop" — same event names, same mergeable_state state machine, same inline fix-up cap — wrapped in the phase-agent envelope (signal file, comms channel, terminal event emission).

Prerequisites

  • CATALYST_ORCHESTRATOR_DIR, CATALYST_ORCHESTRATOR_ID, CATALYST_PHASE=monitor-merge,

CATALYST_TICKET set by [[phase-agent-dispatch]].

  • The prior phase's signal file ${ORCH_DIR}/workers/<TICKET>/phase-pr.json exists with

status=done AND .pr.number populated by [[phase-pr]].

  • gh CLI authenticated; broker daemon optionally running (the loop falls back to direct

catalyst-events wait-for filtering when it is not — see [[wait-for-github]]).

Prelude

set -euo pipefail

: "${CATALYST_ORCHESTRATOR_DIR:?required}"
: "${CATALYST_ORCHESTRATOR_ID:?required}"
: "${CATALYST_PHASE:?required}"
: "${CATALYST_TICKET:?required}"

ORCH_DIR="$CATALYST_ORCHESTRATOR_DIR"
ORCH_ID="$CATALYST_ORCHESTRATOR_ID"
PHASE="$CATALYST_PHASE"
TICKET="$CATALYST_TICKET"
CHANNEL="${ORCH_ID}"

SIGNAL_FILE="${ORCH_DIR}/workers/${TICKET}/phase-${PHASE}.json"
[[ -f "$SIGNAL_FILE" ]] || { echo "phase-${PHASE}: signal file missing" >&2; exit 1; }

PR_SIGNAL="${ORCH_DIR}/workers/${TICKET}/phase-pr.json"
PR_NUMBER=$(jq -r '.pr.number // empty' "$PR_SIGNAL" 2>/dev/null || echo "")
[[ -n "$PR_NUMBER" ]] || { echo "phase-monitor-merge: no PR number in $PR_SIGNAL" >&2; exit 1; }

PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-}"
[[ -n "$PLUGIN_ROOT" ]] || PLUGIN_ROOT="$(dirname "$(dirname "$(dirname "$(realpath "${BASH_SOURCE[0]:-$0}" 2>/dev/null || echo .)")")")"

COMMS="${PLUGIN_ROOT}/scripts/catalyst-comms"
[[ -x "$COMMS" ]] || COMMS="$(command -v catalyst-comms 2>/dev/null || true)"
if [[ -n "$COMMS" && -x "$COMMS" ]]; then
  "$COMMS" join "$CHANNEL" --as "$TICKET" \
    --capabilities "phase-monitor-merge: ${TICKET} pr#${PR_NUMBER}" \
    --orch "$ORCH_ID" --parent orchestrator --ttl 86400 >/dev/null 2>&1 || true
  "$COMMS" send "$CHANNEL" "phase-monitor-merge watching pr#${PR_NUMBER}" \
    --as "$TICKET" --type info --orch "$ORCH_ID" >/dev/null 2>&1 || true
fi

SESSION_SCRIPT="${PLUGIN_ROOT}/scripts/catalyst-session.sh"
if [[ -x "$SESSION_SCRIPT" ]]; then
  CATALYST_SESSION_ID=$("$SESSION_SCRIPT" start \
    --skill "phase-monitor-merge" --ticket "$TICKET" \
    --workflow "${CATALYST_SESSION_ID:-}")
  export CATALYST_SESSION_ID
fi

REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "")
[[ -n "$REPO" ]] || { echo "phase-monitor-merge: cannot resolve repo" >&2; exit 1; }

TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
TMP="${SIGNAL_FILE}.tmp.$$"
# CTL-496: persist catalystSessionId so orchestrate-roll-usage --phase can
# attribute cost to the right session_metrics row.
jq --arg ts "$TS" --argjson pr "$PR_NUMBER" --arg sid "${CATALYST_SESSION_ID:-}" '
  .status = "running"
  | .updatedAt = $ts
  | .pr = {number: $pr}
  | if $sid != "" then .catalystSessionId = $sid else . end
' "$SIGNAL_FILE" > "$TMP" && mv "$TMP" "$SIGNAL_FILE"

/goal condition

Plan §"Per-phase /goal conditions":

/goal "`gh pr view --json merged` returns `true` for the PR linked to
       ${TICKET} (PR #${PR_NUMBER}) AND I have posted the merge mirror
       comment to Linear and emitted phase-monitor-merge.complete (I have
       printed both confirmations to my transcript);
       OR 24 wall-clock hours have elapsed without merge completion
       and I have recorded status:timeout."

Wall-clock cap is 24h (per plan §Failure handling).

Phase-specific work — active listen loop

Reuse the reactive listen loop from [[oneshot]] § Phase 5 Step 2. The full control flow lives there; this skill copies the body verbatim, substituting phase-monitor-merge framing in place of oneshot's session-id machinery. Key elements that MUST be preserved:

  1. Event-driven, not polling. catalyst-events wait-for blocks until a PR-lifecycle event

fires. Filter clause matches the canonical event names github.pr.merged, github.check_suite.completed, github.pr_review*, and github.push keyed by attributes."vcs.pr.number" (PR/review events) or body.payload.prNumbers (check_suite/workflow_run — see [[event-schema]]). When the broker daemon is up, register a pr_lifecycle interest via agent.checkin.claimed_pr and wait on filter.wake.${CATALYST_SESSION_ID} instead (the single-wake path — see [[monitor-events]] Pattern 3).

  1. REST is authoritative. Every loop iteration calls gh api repos/${REPO}/pulls/${PR_NUMBER}

and reads .merged + .mergeable_state. Never use gh pr view --json mergeable (GraphQL is eventually consistent for the merge-state fields and frequently lies).

  1. State machine. Branch on mergeable_state:

| state | action | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | clean | proceed to merge step | | blocked | resolve via /catalyst-dev:review-comments (bot threads) or run an inline CI fix-up commit (up to 3 attempts); 4th attempt → stalled | | behind | git fetch && git rebase origin/<base> && git -c core.hooksPath=/dev/null push --force-with-lease | | dirty | merge conflicts — emit failed with reason "merge conflicts (DIRTY)" | | unknown/unstable | continue waiting for the next event |

  1. Human reviewer changes-requested. After every wake, query gh pr view --json reviews for the

most recent CHANGES_REQUESTED from a human reviewer (filter on .author.login not matching known bots). If present, emit failed with reason "human reviewer ${LOGIN} requested changes — operator action required". Do NOT attempt to address human review comments programmatically.

  1. Wake narration. Every iteration produces one short line of assistant text before re-entering

the wait (defeats the assistant end_turn rendering bleed described in [[monitor-events]] § Narration). Shape: wake: <event.name> #<PR_NUMBER> — <action being taken>.

Merge

Once mergeable_state == "clean" (and the PR isn't already merged):

# CTL-864: cross-host fence — bow out if a takeover superseded us. No-op single-host.
"${PLUGIN_ROOT}/scripts/lib/cluster-fence-guard.sh" --phase "$PHASE" --ticket "$TICKET" || exit 10
# CTL-1051: never merge a stale ref. Compare the PR head to the worktree HEAD;
# on mismatch, re-push with lease and re-verify before merging.
if [[ -r "${PLUGIN_ROOT}/scripts/lib/draft-pr.sh" ]]; then
  source "${PLUGIN_ROOT}/scripts/lib/draft-pr.sh"
  PR_HEAD_OID="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha' 2>/dev/null || true)"
  LOCAL_HEAD="$(git rev-parse HEAD 2>/dev/null || true)"
  if [[ -n "$PR_HEAD_OID" && -n "$LOCAL_HEAD" && "$PR_HEAD_OID" != "$LOCAL_HEAD" ]]; then
    echo "phase-monitor-merge: PR head ${PR_HEAD_OID} != worktree HEAD ${LOCAL_HEAD}; re-pushing" >&2
    if ! draft_pr_push_verify >/dev/null; then
      echo "phase-monitor-merge: could not reconcile stale ref before merge" >&2
      exit 1
    fi
  fi
fi
gh pr merge "$PR_NUMBER" --squash --delete-branch
# REST is authoritative — confirm via REST, never GraphQL
ME
Compartilhar X LinkedIn