phase-pr
Phase-agent wrapper that opens the pull request after implementation completes (CTL-449 Initiative 1 Phase 3). Delegates to `/catalyst-dev:create-pr` (which already auto-runs `describe-pr` and transitions Linear to `inReview`), then writes the PR number + URL into the phase signal file so the downstream `phase-monitor-merge` agent can read it without re-querying GitHub. Dispatched as a `claude --bg` job by `phase-agent-dispatch`, which invokes it via slash command — hence `user-invocable: true`.
Best use case
phase-pr is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Phase-agent wrapper that opens the pull request after implementation completes (CTL-449 Initiative 1 Phase 3). Delegates to `/catalyst-dev:create-pr` (which already auto-runs `describe-pr` and transitions Linear to `inReview`), then writes the PR number + URL into the phase signal file so the downstream `phase-monitor-merge` agent can read it without re-querying GitHub. Dispatched as a `claude --bg` job by `phase-agent-dispatch`, which invokes it via slash command — hence `user-invocable: true`.
Teams using phase-pr should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/phase-pr/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How phase-pr Compares
| Feature / Agent | phase-pr | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Phase-agent wrapper that opens the pull request after implementation completes (CTL-449 Initiative 1 Phase 3). Delegates to `/catalyst-dev:create-pr` (which already auto-runs `describe-pr` and transitions Linear to `inReview`), then writes the PR number + URL into the phase signal file so the downstream `phase-monitor-merge` agent can read it without re-querying GitHub. Dispatched as a `claude --bg` job by `phase-agent-dispatch`, which invokes it via slash command — hence `user-invocable: true`.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# phase-pr
Thin wrapper around `/catalyst-dev:create-pr`. The canonical skill already
handles: commit, push, base-branch detection, PR creation, `describe-pr`
auto-invocation, workflow-context tracking, Linear `inReview` transition,
and the post-PR resolution loop. Phase-pr adds only the phase-agent envelope
plus persisting `pr.number` + `pr.url` to the signal file for
`phase-monitor-merge`.
## Prerequisites
- `CATALYST_ORCHESTRATOR_DIR`, `CATALYST_ORCHESTRATOR_ID`, `CATALYST_PHASE=pr`, `CATALYST_TICKET` set by [[phase-agent-dispatch]].
- The prior phase's signal file `${ORCH_DIR}/workers/<TICKET>/phase-review.json` exists with `status=done` — the dispatcher validates this; this skill assumes it.
- Current working directory is the ticket's worktree on the implementation branch (not main).
## Prelude
```bash
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; }
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-pr: ${TICKET}" \
--orch "$ORCH_ID" --parent orchestrator --ttl 3600 >/dev/null 2>&1 || true
"$COMMS" send "$CHANNEL" "phase-pr started" --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-pr" --ticket "$TICKET" \
--workflow "${CATALYST_SESSION_ID:-}")
export CATALYST_SESSION_ID
fi
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" --arg sid "${CATALYST_SESSION_ID:-}" '
.status = "running"
| .updatedAt = $ts
| if $sid != "" then .catalystSessionId = $sid else . end
' "$SIGNAL_FILE" > "$TMP" \
&& mv "$TMP" "$SIGNAL_FILE"
```
## Already-merged detection (CTL-714)
Before delegating to `create-pr`, detect whether this branch's `HEAD` is already contained in
`origin/main` (manual rescue, or a sibling PR landed the same commits). If so, skip PR creation
to avoid a duplicate / empty-diff PR. Two complementary checks: `git merge-base --is-ancestor`
(works even if the branch was deleted from the remote) and `gh pr list --state merged` (recovers
the merged PR number for the downstream probe).
The detection fence is **side-effect-free** so the e2e test can source it in isolation.
```bash phase-pr-already-merged-detect
git fetch origin main --quiet 2>/dev/null || true
ALREADY_MERGED=0
MERGED_PR_NUMBER=""
MERGED_PR_URL=""
# Check 1: is HEAD already contained in origin/main? (must be in `if` — set -e)
if git merge-base --is-ancestor HEAD origin/main 2>/dev/null; then
ALREADY_MERGED=1
fi
# Check 2: does a MERGED PR exist for this branch? (--state merged is required —
# `gh pr list --head` with no --state returns only OPEN PRs; orchestrate-verify.sh:563)
BRANCH_NAME="$(git branch --show-current 2>/dev/null || true)"
if [[ -n "$BRANCH_NAME" ]]; then
MERGED_PR_JSON="$(gh pr list --head "$BRANCH_NAME" --state merged \
--json number,url --limit 1 2>/dev/null || echo '[]')"
MERGED_PR_NUMBER="$(echo "$MERGED_PR_JSON" | jq -r '.[0].number // empty' 2>/dev/null || true)"
MERGED_PR_URL="$(echo "$MERGED_PR_JSON" | jq -r '.[0].url // empty' 2>/dev/null || true)"
if [[ -n "$MERGED_PR_NUMBER" ]]; then
ALREADY_MERGED=1
fi
fi
```
When `ALREADY_MERGED=1`, write the disposition into the signal file and complete without
creating a PR:
```bash
if [[ "$ALREADY_MERGED" -eq 1 ]]; then
echo "phase-pr: HEAD already in origin/main — skipping PR creation (CTL-714)" >&2
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
TMP="${SIGNAL_FILE}.tmp.$$"
jq --arg ts "$TS" \
--arg reason "already-merged-to-main" \
--argjson prNum "${MERGED_PR_NUMBER:-null}" \
--arg prUrl "${MERGED_PR_URL:-}" '
.updatedAt = $ts
| .attentionReason = $reason
| if $prNum != null then .pr = {number: $prNum, url: $prUrl} else . end
' "$SIGNAL_FILE" > "$TMP" && mv "$TMP" "$SIGNAL_FILE"
"${PLUGIN_ROOT}/scripts/phase-agent-emit-complete" \
--phase "$PHASE" --ticket "$TICKET" --status complete
[[ -n "$COMMS" && -x "$COMMS" ]] && "$COMMS" done "$CHANNEL" --as "$TICKET" >/dev/null 2>&1 || true
exit 0
fi
```
## Existing open PR detection (CTL-709)
CTL-709's `phase-implement` may have already opened a draft PR for this branch. Detect it here so
we can **promote** it (`gh pr ready`) rather than re-entering `create-pr`'s interactive
"PR already exists" prompt (`create-pr/SKILL.md:96–104`) — that prompt would hang a `--bg`
worker forever. Detection order: merged → existing-open → create-new. The detection fence is
**side-effect-free** so the e2e test can source it in isolation.
```bash phase-pr-existing-pr-detect
# CTL-709: phase-implement may have already opened a (draft) PR for this branch.
# Detect it here so we can promote it rather than re-entering create-pr's
# interactive "PR already exists" prompt (create-pr/SKILL.md:96 — would hang --bg).
EXISTING_PR_NUMBER=""
EXISTING_PR_URL=""
EXISTING_PR_IS_DRAFT=""
EXISTING_PR_JSON="$(gh pr view --json number,url,state,isDraft 2>/dev/null || true)"
if [[ -n "$EXISTING_PR_JSON" ]]; then
if [[ "$(printf '%s' "$EXISTING_PR_JSON" | jq -r '.state // empty' 2>/dev/null)" == "OPEN" ]]; then
EXISTING_PR_NUMBER="$(printf '%s' "$EXISTING_PR_JSON" | jq -r '.number // empty' 2>/dev/null || true)"
EXISTING_PR_URL="$(printf '%s' "$EXISTING_PR_JSON" | jq -r '.url // empty' 2>/dev/null || true)"
EXISTING_PR_IS_DRAFT="$(printf '%s' "$EXISTING_PR_JSON" | jq -r '.isDraft // false' 2>/dev/null || true)"
fi
fi
```
When an existing open PR is found, promote it (if draft) and finish — **without** delegating to
`create-pr`. The promote-and-finish block is NOT side-effect-free.
```bash
if [[ -n "$EXISTING_PR_NUMBER" ]]; then
echo "phase-pr: promoting existing PR #${EXISTING_PR_NUMBER} (draft=${EXISTING_PR_IS_DRAFT})" >&2
if [[ "$EXISTING_PR_IS_DRAFT" == "true" ]]; then
if [[ -r "${PLUGIN_ROOT}/scripts/lib/draft-pr.sh" ]]; then
# shellcheck source=/dev/null
source "${PLUGIN_ROOT}/scripts/lib/draft-pr.sh"
draft_pr_promote || gh pr ready "$EXISTING_PR_NUMBER" 2>/dev/null || true
else
gh pr ready "$EXISTING_PR_NUMBER" 2>/dev/null || true
fi
fi
# Enrich the PR body now that the draft is ready (deferred from phase-implement to keep
# its End block free of Task-tool calls — research Q4).
#
# Use the Task tool to invoke /catalyst-dev:describe-pr on $EXISTING_PR_NUMBER.
# describe-pr is non-interactive when CATALYST_PHASE is set (it skips the Linear
# inReview transition — the coordinator owns that — create-pr/SKILL.md:226–232).
TS2=$(date -u +%Y-%m-%dT%H:%M:%SZ)
TMP="${SIGNAL_FILE}.tmp.$$"
jq --argjson pr "$EXISTING_PR_NUMBER" --arg url "$EXISTING_PR_URL" --arg ts "$TS2" \
'.pr={number:$pr,url:$url} | .updatedAt=$ts' \
"$SIGNAL_FILE" > "$TMP" && mv "$TMP" "$SIGNAL_FILE"
echo "phase-pr: existing PR #${EXISTING_PR_NUMBER} promoted — skipping create-pr delegation" >&2
fi
```
## /goal condition
Plan §"Per-phase /goal conditions":
```
/goal "`gh pr view --json number,state,headRefName` shows an open PR linked
to ${TICKET} AND Linear state is `In Review` AND describe-pr has run
successfully (I have printed the PR URL and `describe-pr ran` to my
transcript)."
```
Turn cap defaults to 12 (from `phase-agent-dispatch:phase_default_turn_cap`).
This is intentionally tight because the work is mostly tool calls — most of
the reasoning happens upstream in `create-pr` itself.
## Phase-specific work
1. When `EXISTING_PR_NUMBER` is set (the draft opened by `phase-implement` was detected and
promoted above): invoke `/catalyst-dev:describe-pr` via the Task tool on `$EXISTING_PR_NUMBER`.
Then proceed directly to the End block — **do not** invoke `/catalyst-dev:create-pr`.
2. When `EXISTING_PR_NUMBER` is empty (Phase 3 draft creation failed or was disabled):
invoke `/catalyst-dev:create-pr` via the Task tool. The canonical skill handles: branch push,
base-branch resolution, idempotent PR creation if one already exists, `describe-pr`
invocation, and Linear `inReview` transition.
3. After either path, capture the PR metadata via `gh` and write it into the phase signal file
so `phase-monitor-merge` can read it directly without re-querying GitHub:
```bash
PR_INFO=$(gh pr view --json number,url,headRefName,baseRefName 2>/dev/null || echo "{}")
PR_NUMBER=$(echo "$PR_INFO" | jq -r '.number // empty')
PR_URL=$(echo "$PR_INFO" | jq -r '.url // empty')
if [[ -n "$PR_NUMBER" ]]; then
TS2=$(date -u +%Y-%m-%dT%H:%M:%SZ)
TMP="${SIGNAL_FILE}.tmp.$$"
jq --argjson pr "$PR_NUMBER" --arg url "$PR_URL" --arg ts "$TS2" \
'.pr = {number: $pr, url: $url} | .updatedAt = $ts' \
"$SIGNAL_FILE" > "$TMP" && mv "$TMP" "$SIGNAL_FILE"
echo "phase-pr: opened PR #${PR_NUMBER} at ${PR_URL}"
else
echo "phase-pr: gh pr view returned no PR — create-pr may have failed" >&2
fi
```
4. The post-PR active resolution loop (CI fix-up, bot review threads, BEHIND
rebase) is **not** run here — that is `phase-monitor-merge`'s
responsibility. `create-pr`'s own brief monitoring window stays inside
`create-pr`; phase-pr exits as soon as the PR exists in `OPEN` state.
## End block
Mirror the phase output to Linear as a single comment (CTL-632). Describes the
PR that was opened (number, URL, title, files changed, additions/deletions,
commit count) plus the pre-merge verification surfaced from the verify phase's
`verify.json` (test/typecheck/lint gate status + regression risk) so the trail
records what was checked before the PR went up. PR metadata is re-read from the
phase signal file (`.pr.number`/`.pr.url`, written in the phase-specific work
above) and enriched via `gh pr view`; the verify summary is fail-soft if no
`verify.json` exists. Body hard-truncated to 30,000 bytes. Fail-open and
idempotent via the per-phase marker file. Uniquely-named fence so the e2e test
can extract just this block.
```bash phase-pr-mirror
LINEAR_MIRROR_MARKER="${ORCH_DIR}/workers/${TICKET}/.linear-mirror-${PHASE}"
if [[ ! -e "${LINEAR_MIRROR_MARKER}" ]]; then
PR_SIGNAL="${ORCH_DIR}/workers/${TICKET}/phase-${PHASE}.json"
PR_NUMBER="$(jq -r '.pr.number // empty' "${PR_SIGNAL}" 2>/dev/null || true)"
PR_URL="$(jq -r '.pr.url // empty' "${PR_SIGNAL}" 2>/dev/null || true)"
PR_VIEW="{}"
if [[ -n "${PR_NUMBER}" ]]; then
PR_VIEW="$(gh pr view "${PR_NUMBER}" --json title,files,additions,deletions,commits 2>/dev/null || echo '{}')"
fi
PR_TITLE="$(printf '%s' "${PR_VIEW}" | jq -r '.title // "_untitled_"' 2>/dev/null || echo '_untitled_')"
FILES_CHANGED="$(printf '%s' "${PR_VIEW}" | jq -r '(.files // []) | length' 2>/dev/null || echo '?')"
ADDITIONS="$(printf '%s' "${PR_VIEW}" | jq -r '.additions // "?"' 2>/dev/null || echo '?')"
DELETIONS="$(printf '%s' "${PR_VIEW}" | jq -r '.deletions // "?"' 2>/dev/null || echo '?')"
COMMIT_COUNT="$(printf '%s' "${PR_VIEW}" | jq -r '(.commits // []) | length' 2>/dev/null || echo '?')"
VERIFY_JSON_FILE="${ORCH_DIR}/workers/${TICKET}/verify.json"
VERIFY_RENDERED="_no verify.json found — verification ran in a prior phase or was skipped_"
if [[ -f "${VERIFY_JSON_FILE}" ]]; then
VERIFY_RENDERED="$(jq -r '
("- **Regression risk**: " + ((.regression_risk // "?")|tostring) + " / 10")
+ "\n"
+ ((.gates // {})
| to_entries
| map(select(.key | test("test|typecheck|lint|coverage")))
| map("- **" + .key + "**: " + (.value.status // "unknown")
+ (if .value.summary then " — " + .value.summary else "" end))
| join("\n"))
' "${VERIFY_JSON_FILE}" 2>/dev/null || echo '_verify.json unreadable_')"
fi
MIRROR_BODY="$(cat <<EOF
**Phase PR** — opened PR #${PR_NUMBER:-?}
- **PR**: ${PR_URL:-_url unavailable_}
- **Title**: ${PR_TITLE}
- **Files changed**: ${FILES_CHANGED} (+${ADDITIONS} / -${DELETIONS})
- **Commits**: ${COMMIT_COUNT}
**Pre-merge verification** (from the verify phase):
${VERIFY_RENDERED}
_Posted automatically by phase-pr (CTL-632)._
EOF
)"
MIRROR_FOOTER=""
if [[ -n "${PLUGIN_ROOT:-}" && -x "${PLUGIN_ROOT}/scripts/lib/phase-mirror-footer.sh" ]]; then
MIRROR_FOOTER="$("${PLUGIN_ROOT}/scripts/lib/phase-mirror-footer.sh" --orch-dir "${ORCH_DIR}" --ticket "${TICKET}" --phase "${PHASE}" 2>/dev/null || true)"
fi
[[ -n "${MIRROR_FOOTER}" ]] && MIRROR_BODY="${MIRROR_BODY}
${MIRROR_FOOTER}"
if [[ ${#MIRROR_BODY} -gt 30000 ]]; then
MIRROR_BODY="${MIRROR_BODY:0:30000}
_... (truncated)_"
fi
COMMENT_POST="${CATALYST_COMMENT_POST_HELPER:-${PLUGIN_ROOT}/scripts/lib/linear-comment-post.sh}"
if [[ ! -x "$COMMENT_POST" ]]; then COMMENT_POST="$(command -v linear-comment-post.sh 2>/dev/null || true)"; fi
if [[ -n "$COMMENT_POST" && -x "$COMMENT_POST" ]] && "$COMMENT_POST" "${TICKET}" "${MIRROR_BODY}" >/dev/null 2>&1; then
: > "${LINEAR_MIRROR_MARKER}"
else
echo "phase-pr: linear-comment-post failed (continuing)" >&2
fi
fi
```
```bash
EMIT="${PLUGIN_ROOT}/scripts/phase-agent-emit-complete"
if [[ -x "$EMIT" ]]; then
"$EMIT" --phase "$PHASE" --ticket "$TICKET" --status complete
fi
[[ -n "$COMMS" && -x "$COMMS" ]] && "$COMMS" done "$CHANNEL" --as "$TICKET" >/dev/null 2>&1 || true
```
## Failure handling
```bash
REASON="${1:-create-pr exited non-zero}"
"$EMIT" --phase "$PHASE" --ticket "$TICKET" --status failed --reason "$REASON"
[[ -n "$COMMS" && -x "$COMMS" ]] && "$COMMS" send "$CHANNEL" \
"phase-pr failed: ${REASON}" \
--as "$TICKET" --type attention --orch "$ORCH_ID" >/dev/null 2>&1 || true
exit 1
```
Common failure modes:
- **Branch not pushable** (e.g., diverged, network failure): `create-pr` errors;
phase-pr emits `failed` with the underlying reason.
- **PR already exists with no new commits**: not a failure — `create-pr` is
idempotent and returns the existing PR. The signal file gets the existing
PR number written, downstream phases proceed normally.
- **`gh` not authenticated**: emit `failed` with the gh stderr; orchestrator's
retry path will not unstick this — escalate via `attention`.
## Why this is a thin wrapper
Plan architectural commitment #3. `/catalyst-dev:create-pr` is mature (504
lines as of CTL-373) and owns workflow-context, Linear linking, describe-pr,
and idempotency. phase-pr adds the phase-agent envelope (~80 lines) and
nothing else — improvements to create-pr propagate for free.Related Skills
phase-verify
Phase agent for the verify step of the 9-phase orchestrator pipeline (CTL-450). NEW skill — has no canonical wrapper. Runs read-only adversarial verification against the implement-phase diff: tsc, tests, lint, security scan, reward-hacking scan, code review, test coverage, silent-failure hunt. Writes ${ORCH_DIR}/workers/<TICKET>/verify.json then emits phase.verify.complete.<ticket>. Reads phase-implement.json as its prior-phase artifact. NEVER writes application code — only test files allowed. Spawned via phase-agent-dispatch via slash command — hence `user-invocable: true`.
phase-triage
Phase agent that triages a Linear ticket — expands acronyms, classifies (feature/bug/docs/refactor/chore), identifies dependencies, estimates scope, writes triage.json, and posts a triage analysis comment to Linear. Triage completion is signaled by that comment plus the local triage.json — there is no `triaged` label. Emits phase.triage.complete.<TICKET> on success and phase.triage.failed.<TICKET> on error. Dispatched by the phase-agent orchestrator (CTL-452) via slash command — `user-invocable: true` so the dispatcher's `claude --bg "/catalyst-dev:phase-triage ..."` resolves.
phase-review
Phase agent for the review step of the 9-phase orchestrator pipeline (CTL-450). Wraps the /review skill (gstack) — explicitly skips /ultrareview per user decision. Reads verify.json from the prior phase, runs /review against the diff, writes ${ORCH_DIR}/workers/<TICKET>/review.json, and creates a remediation commit for any HIGH-severity finding that has a deterministic fix. Emits phase.review.complete.<ticket>. Spawned via phase-agent-dispatch via slash command — hence `user-invocable: true`.
phase-research
Phase agent for the research step of the 9-phase orchestrator pipeline (CTL-450). Wraps /catalyst-dev:research-codebase and produces thoughts/shared/research/<date>-<ticket>.md, then emits phase.research.complete.<ticket>. Reads triage.json from the worker dir as its prior-phase artifact. Spawned via plugins/dev/scripts/phase-agent-dispatch, which invokes it via slash command — hence `user-invocable: true`.
phase-remediate
Phase-agent that fixes a failing verify verdict so the pipeline self-heals instead of stalling to needs-human (CTL-653). Reads `${ORCH_DIR}/workers/<ticket>/verify.json`, fixes the `findings[]` (every severity:"high" plus the regression_risk drivers) directly via Edit/Write, commits the remediation, and emits `phase.remediate.complete.<ticket>`. The scheduler's router then re-dispatches `verify` to re-check (the verify⇄remediate cycle, cap 3). Dispatched as a `claude --bg` job by `phase-agent-dispatch`, which invokes it via slash command — hence `user-invocable: true`.
phase-plan
Phase agent for the plan step of the 9-phase orchestrator pipeline (CTL-450). Wraps /catalyst-dev:create-plan and produces thoughts/shared/plans/<date>-<ticket>.md, then emits phase.plan.complete.<ticket>. Reads the prior research document from thoughts/shared/research/ as its prior-phase artifact. Spawned via plugins/dev/scripts/phase-agent-dispatch, which invokes it via slash command — hence `user-invocable: true`.
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, then transitions Linear to `done`. Dispatched as a `claude --bg` job by `phase-agent-dispatch`, which invokes it via slash command — hence `user-invocable: true`.
phase-monitor-deploy
Phase agent that watches the post-merge deployment for a ticket. Reads the merge SHA from phase-monitor-merge.json (the signal file phase-monitor-merge writes after `gh pr merge` confirms via REST), subscribes via `catalyst-events wait-for` to deploy events on that SHA, then delegates a live verification check to the /canary skill (gstack). Emits phase.monitor-deploy.complete.<TICKET> on canary success, phase.monitor-deploy.failed.<TICKET> on deploy or canary failure, and phase.monitor-deploy.skipped.<TICKET> when no deploy event arrives before the timeout. Dispatched by the phase-agent orchestrator (CTL-452) via slash command — `user-invocable: true` so the dispatcher's `claude --bg "/catalyst-dev:phase-monitor-deploy ..."` resolves.
phase-implement
Phase-agent wrapper that drives TDD implementation from an approved plan (CTL-449 Initiative 1 Phase 3). Reads `thoughts/shared/plans/*-<ticket>.md`, delegates the red→green→refactor cycle to `/catalyst-dev:implement-plan`, commits each plan phase as it lands, and transitions the Linear ticket to `inProgress`. Dispatched as a `claude --bg` job by `phase-agent-dispatch`, which invokes it via slash command — hence `user-invocable: true`.
_phase-agent-template
Reference template every phase-agent skill copies (CTL-448). The leading underscore on the directory name prevents the skill loader from picking this up — it is NOT a runnable skill, only a structural template the nine real phase agents (phase-triage, phase-research, phase-plan, phase-implement, phase-verify, phase-review, phase-pr, phase-monitor-merge, phase-monitor-deploy) clone and specialize. The real phase skills MUST set `user-invocable: true` so `phase-agent-dispatch`'s `claude --bg "/catalyst-dev:phase-X ..."` slash command resolves (CTL-490).
write-prod-strategy
Product strategy docs using 7-component framework
strategy-sprint
Create product strategy in 1 day, 1 week, or 1 month timeframes. Progressive strategy development framework.