plugins/dev/skills/phase-implement/SKILL.md
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`.
npx skillsauth add coalesce-labs/catalyst phase-implementInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Phase-agent that owns the implementation half of the legacy oneshot cycle — this is the biggest
single cost line of a worker run, which is why it leaves -p for --bg first (plan §Initiative 1
Phase 3 rationale). The skill body is intentionally thin: the canonical
/catalyst-dev:implement-plan skill already handles TDD rhythm, quality gates, agent-team mode, and
findings collection — phase-implement adds only the phase-agent envelope (signal file, comms
channel, /goal cap, terminal emit) around it.
CATALYST_ORCHESTRATOR_DIR, CATALYST_ORCHESTRATOR_ID, CATALYST_PHASE=implement,
CATALYST_TICKET set by [[phase-agent-dispatch]].thoughts/shared/plans/<date>-<ticket-lowercase>.md — the dispatcher's
prior-artifact gate already validates this; this skill re-reads the file.set -euo pipefail
: "${CATALYST_ORCHESTRATOR_DIR:?required (set by phase-agent-dispatch)}"
: "${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}"
# CTL-484: continuation-worker orientation. Set by orchestrate-revive's
# continuation branch when this skill is resumed via `claude --bg --resume`
# after a previous session hit its /goal turn cap. Read the handoff doc and
# trust its summary instead of re-walking the plan from scratch.
if [[ "${CATALYST_IS_CONTINUATION:-}" == "true" ]]; then
CONT_HANDOFF="${CATALYST_HANDOFF_PATH:-}"
CONT_N="${CATALYST_CONTINUATION_COUNT:-?}"
if [[ -n "$CONT_HANDOFF" && -f "$CONT_HANDOFF" ]]; then
echo "phase-implement: continuation #${CONT_N} — resuming from ${CONT_HANDOFF}"
echo "phase-implement: reading handoff (do NOT re-read full plan from scratch)"
cat "$CONT_HANDOFF"
else
echo "warn: CATALYST_IS_CONTINUATION=true but handoff path missing or unreadable" >&2
fi
fi
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 .)")")")"
# 0. Codified bg_job_id yield (CTL-615). If the signal file's bg_job_id
# names a DIFFERENT live bg job, we are a redispatch duplicate of a
# still-running canonical worker. Bow out without touching the signal,
# without emitting any phase event. Encodes operator memories
# #43/#44/#49/#50 — the playbook is now code. phase-implement carries
# the highest blast radius (commits land here), so it gets the gate
# even though the template inheritance also provides it.
YIELD_CHECK="${PLUGIN_ROOT}/scripts/phase-agent-yield-check.sh"
if [[ -x "$YIELD_CHECK" ]] && bash "$YIELD_CHECK" \
--signal "$SIGNAL_FILE" \
--phase "$PHASE" \
--worker-dir "$(dirname "$SIGNAL_FILE")"; then
echo "phase-${PHASE}: yielding to canonical worker (CTL-615)" >&2
exit 0
fi
# 1. Join the shared comms channel (best-effort).
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-implement: ${TICKET}" \
--orch "$ORCH_ID" --parent orchestrator --ttl 3600 >/dev/null 2>&1 || true
"$COMMS" send "$CHANNEL" "phase-implement started" --as "$TICKET" --type info \
--orch "$ORCH_ID" >/dev/null 2>&1 || true
fi
# 2. Start a catalyst-session for cost/token instrumentation.
SESSION_SCRIPT="${PLUGIN_ROOT}/scripts/catalyst-session.sh"
if [[ -x "$SESSION_SCRIPT" ]]; then
CATALYST_SESSION_ID=$("$SESSION_SCRIPT" start \
--skill "phase-implement" \
--ticket "$TICKET" \
--workflow "${CATALYST_SESSION_ID:-}")
export CATALYST_SESSION_ID
fi
# 3. Mark the signal file as running + persist catalystSessionId (CTL-496:
# orchestrate-roll-usage --phase reads this to attribute cost).
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
TMP="${SIGNAL_FILE}.tmp.$$"
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"
# CTL-587: test-kill after-prelude. Exits AFTER the signal is flipped to
# running (so classifyWorker sees a non-terminal worker) but BEFORE any
# commit work, so reclaimDeadWorkIfPossible's implement-probe returns false
# on the next staleness tick and the revive path engages. Mode suffix
# `${PHASE}:after-prelude` keeps the env var phase-agnostic — only the
# matching phase aborts.
if [[ "${CATALYST_TEST_KILL_PHASE:-}" == "${PHASE}:after-prelude" ]]; then
echo "[CTL-587 test-kill] aborting after prelude" >&2
exit 137
fi
# 4. Locate the approved plan. The dispatcher already validated this glob;
# we re-resolve to capture the actual filename for the delegated skill.
TICKET_LC="$(printf '%s' "$TICKET" | tr '[:upper:]' '[:lower:]')"
shopt -s nullglob
PLAN_MATCHES=( thoughts/shared/plans/*-"${TICKET_LC}".md )
shopt -u nullglob
[[ ${#PLAN_MATCHES[@]} -gt 0 ]] || { echo "no plan found for ${TICKET} under thoughts/shared/plans/" >&2; exit 1; }
PLAN_PATH="${PLAN_MATCHES[0]}"
echo "phase-implement: plan = ${PLAN_PATH}"
# 5. Linear status is written by the coordinator (CTL-558): the execution-core
# scheduler / orchestrate-phase-advance applies the `Implement` state when
# it commits the implement-phase transition. The phase agent no longer
# transitions Linear itself.
Transcript-evaluable so a /goal evaluator (which only sees Claude's text output, not the
filesystem) can decide pass/fail from what the agent prints. Plan §"Per-phase /goal conditions":
/goal "I have run /catalyst-dev:implement-plan on ${PLAN_PATH} to completion
AND `git diff <base>..HEAD` on this branch is non-empty AND the targeted
tests pass (I have printed the test command + `exit 0` to my transcript);
(Linear status is written by the coordinator — CTL-558 — not this agent.)"
Invoke the canonical implementation skill via the Task tool. It owns TDD, quality gates,
agent-team mode (--team), findings collection, and the per-phase commit cadence:
Use the Task tool to launch /catalyst-dev:implement-plan on PLAN_PATH.
Pass through any --team flag if the caller set CATALYST_IMPLEMENT_TEAM=1
in the env. Wait for completion and surface its stdout summary.
The canonical skill is responsible for committing each plan phase as a discrete commit AND for
running the post-implementation quality gates (/validate-type-safety, /security-review,
code-reviewer agent, pr-test-analyzer agent). phase-implement does NOT add commits or gates of
its own. If implement-plan exits with errors, the failure-handling block below runs.
After the delegated skill returns, print a one-line summary to stdout so the /goal evaluator
has signal that the work landed:
git diff --stat "$(git merge-base HEAD main)..HEAD" # base depends on the
# worktree's tracking
When the broader plan's Phase 4 (CTL-450) introduces dedicated phase-verify and phase-review
agents, this skill will pass --skip-quality-gates to implement-plan so those concerns move into
their own phase agents (plan §"Phase agents wrap canonical skills"). For the MVP this skill runs
the gates inline via implement-plan because no phase-verify exists yet — the cutover is a
one-line change to the Task invocation when that phase lands.
Before continuing to the End block, check for mid-flight context updates from the human:
${ORCH_DIR}/workers/${TICKET}/inbox.jsonl exists and is non-empty, read it fully.kind: "comment" or kind: "description_changed".failed with reason: "mid_flight_replan_needed" via
${PLUGIN_ROOT}/scripts/phase-agent-emit-complete and post the reason to Linear as a comment
before exiting.[[ -f "${ORCH_DIR}/workers/${TICKET}/inbox.jsonl" ]] && \
mv "${ORCH_DIR}/workers/${TICKET}/inbox.jsonl" \
"${ORCH_DIR}/workers/${TICKET}/inbox.processed-$(date +%s).jsonl" || true
Mirror the phase output to Linear as a single comment (CTL-632). Re-derives the commit list at
end-block time (no captured variable upstream), falling back to _base branch unknown_ if neither
origin/main nor main exists. Fail-open and idempotent via the per-phase marker file.
Uniquely-named fence so the e2e test can extract just this block.
# 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
LINEAR_MIRROR_MARKER="${ORCH_DIR}/workers/${TICKET}/.linear-mirror-${PHASE}"
if [[ ! -e "${LINEAR_MIRROR_MARKER}" ]]; then
BASE_REF=""
if git rev-parse --verify --quiet origin/main >/dev/null 2>&1; then
BASE_REF="origin/main"
elif git rev-parse --verify --quiet main >/dev/null 2>&1; then
BASE_REF="main"
fi
BASE_SHA=""
if [[ -n "${BASE_REF}" ]]; then
BASE_SHA="$(git merge-base HEAD "${BASE_REF}" 2>/dev/null || true)"
fi
if [[ -n "${BASE_SHA}" ]]; then
COMMIT_LIST="$(git log --no-merges --oneline "${BASE_SHA}..HEAD" 2>/dev/null | sed 's/^/- /')"
COMMIT_COUNT="$(printf '%s\n' "${COMMIT_LIST}" | grep -c '^- ' || true)"
: "${COMMIT_COUNT:=0}"
DIFF_STAT="$(git diff --stat "${BASE_SHA}..HEAD" 2>/dev/null | tail -1)"
NAME_STATUS="$(git diff --name-status "${BASE_SHA}..HEAD" 2>/dev/null)"
FILES_ADDED="$(printf '%s\n' "${NAME_STATUS}" | grep -c '^A' || true)"
FILES_MODIFIED="$(printf '%s\n' "${NAME_STATUS}" | grep -c '^M' || true)"
FILES_DELETED="$(printf '%s\n' "${NAME_STATUS}" | grep -c '^D' || true)"
LINES_ADDED="$(git diff --numstat "${BASE_SHA}..HEAD" 2>/dev/null | awk '$1 ~ /^[0-9]+$/ {a+=$1} END {print a+0}')"
LINES_DELETED="$(git diff --numstat "${BASE_SHA}..HEAD" 2>/dev/null | awk '$2 ~ /^[0-9]+$/ {d+=$2} END {print d+0}')"
else
COMMIT_LIST="_base branch unknown_"
COMMIT_COUNT="?"
DIFF_STAT="_unavailable_"
FILES_ADDED="?"; FILES_MODIFIED="?"; FILES_DELETED="?"
LINES_ADDED="?"; LINES_DELETED="?"
fi
BRANCH_NAME="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "${TICKET}")"
MIRROR_BODY="$(cat <<EOF
**Phase Implement**
- **Branch**: \`${BRANCH_NAME}\`
- **Commits**: ${COMMIT_COUNT}
- **Files**: ${FILES_ADDED} added, ${FILES_MODIFIED} modified, ${FILES_DELETED} deleted
- **Lines**: +${LINES_ADDED} / -${LINES_DELETED}
- **Diff**: ${DIFF_STAT}
<details>
<summary>Commit list</summary>
${COMMIT_LIST}
</details>
_Posted automatically by phase-implement (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; then
: > "${LINEAR_MIRROR_MARKER}"
else
echo "phase-implement: linear-comment-post failed (continuing)" >&2
fi
fi
Then the empty-branch self-emit gate (CTL-608). Runs before the terminal --status complete so
a worker cannot self-report implement success on an empty ticket branch (0 commits ahead of its
integration base). This is the ADV-1128 failure mode: sub-agent commits stranded in nested
.claude/worktrees/agent-* worktrees never reach refs/heads/<ticket>, leaving HEAD at base and
opening an empty PR. The gate now also refuses when HEAD is on a worktree-* branch or the
worktree resolves under .claude/worktrees/ (CTL-1105) — the stranding case the commits-ahead
check alone cannot see. Uniquely-named fence so the e2e harness can extract+exercise it; uses only
POSIX/zsh-safe git rev-list --count (no ${VAR,,} / shopt). Fail-open (warn + allow) only when
the base is unresolvable, mirroring the mirror block's _base branch unknown_ tolerance.
# CTL-1105: stranded-in-transient-worktree guard. Claude Code bgIsolation can
# migrate a phase worker into .claude/worktrees/<name> on a worktree-* branch;
# commits there never reach refs/heads/<ticket>. The CTL-608 commits-ahead arm
# below does NOT catch this (the transient worktree has real commits), so check
# it explicitly first. Deterministic + local; POSIX/zsh-safe.
GATE_TOPLEVEL="$(git rev-parse --show-toplevel 2>/dev/null || true)"
GATE_BRANCH="$(git branch --show-current 2>/dev/null || true)"
case "${GATE_TOPLEVEL}" in
*/.claude/worktrees/*) GATE_STRANDED="claude_worktree:${GATE_TOPLEVEL}";;
*) GATE_STRANDED="";;
esac
case "${GATE_BRANCH}" in
worktree-*) GATE_STRANDED="${GATE_STRANDED:-transient_branch:${GATE_BRANCH}}";;
esac
if [[ -n "${GATE_STRANDED}" ]]; then
echo "phase-implement: stranded in a transient isolation worktree (${GATE_STRANDED}); refusing to emit complete (CTL-1105)" >&2
"${PLUGIN_ROOT}/scripts/phase-agent-emit-complete" \
--phase "$PHASE" --ticket "$TICKET" --status failed \
--reason "stranded_transient_worktree:${GATE_STRANDED}"
[[ -n "$COMMS" && -x "$COMMS" ]] && "$COMMS" send "$CHANNEL" \
"phase-implement failed: stranded in transient worktree (${GATE_STRANDED})" \
--as "$TICKET" --type attention --orch "$ORCH_ID" >/dev/null 2>&1 || true
exit 1
fi
EMPTY_BRANCH_GATE_BASE=""
if git rev-parse --verify --quiet origin/main >/dev/null 2>&1; then
EMPTY_BRANCH_GATE_BASE="origin/main"
elif git rev-parse --verify --quiet main >/dev/null 2>&1; then
EMPTY_BRANCH_GATE_BASE="main"
fi
if [[ -n "${EMPTY_BRANCH_GATE_BASE}" ]]; then
AHEAD="$(git rev-list --count "${EMPTY_BRANCH_GATE_BASE}..HEAD" 2>/dev/null || echo 0)"
if [[ "${AHEAD:-0}" -le 0 ]]; then
echo "phase-implement: 0 commits ahead of ${EMPTY_BRANCH_GATE_BASE}; refusing to emit complete on an empty branch (CTL-608)" >&2
"${PLUGIN_ROOT}/scripts/phase-agent-emit-complete" \
--phase "$PHASE" --ticket "$TICKET" --status failed \
--reason "empty_branch:0_commits_ahead_of_${EMPTY_BRANCH_GATE_BASE}"
[[ -n "$COMMS" && -x "$COMMS" ]] && "$COMMS" send "$CHANNEL" \
"phase-implement failed: empty branch (0 commits ahead of ${EMPTY_BRANCH_GATE_BASE})" \
--as "$TICKET" --type attention --orch "$ORCH_ID" >/dev/null 2>&1 || true
exit 1
fi
else
echo "phase-implement: could not resolve integration base (no origin/main or main); skipping empty-branch gate (CTL-608)" >&2
fi
CTL-783: The canonical implement-plan skill opens the draft PR at the first plan-phase commit
via the implement-plan-draft-pr-early fence (idempotent — later commits just push). This End-block
fence is the idempotent backstop: it fires after all phases complete and is the sole writer of
.draftPr into the signal file. Gated on draftPr.enabled (default true) so it can be disabled
with one config key. Phase-pr detects and promotes the draft instead of creating a new PR (avoiding
the create-pr interactive "PR already exists" hang).
# CTL-709: open a draft PR + push as soon as we have commits, so CI runs during
# verify/review and CTL-708 can see the branch. Fail-open — never blocks completion.
if [[ -r "${PLUGIN_ROOT}/scripts/lib/draft-pr.sh" ]]; then
# shellcheck source=/dev/null
source "${PLUGIN_ROOT}/scripts/lib/draft-pr.sh"
if [[ "$(draft_pr_enabled)" == "true" ]]; then
draft_pr_push || true
DPR_OUT="$(draft_pr_ensure "main" "$TICKET" 2>/dev/null || true)"
if [[ -n "${DPR_OUT}" ]]; then
DPR_NUM="$(printf '%s' "$DPR_OUT" | cut -f1)"
DPR_URL="$(printf '%s' "$DPR_OUT" | cut -f2)"
DPR_DRAFT="$(printf '%s' "$DPR_OUT" | cut -f3)"
TS_DPR=$(date -u +%Y-%m-%dT%H:%M:%SZ)
TMP="${SIGNAL_FILE}.tmp.$$"
jq --arg ts "$TS_DPR" --argjson num "${DPR_NUM:-null}" \
--arg url "${DPR_URL:-}" --argjson draft "${DPR_DRAFT:-false}" \
'.updatedAt=$ts | if $num!=null and $num!="" then .draftPr={number:($num|tonumber),url:$url,isDraft:$draft} else . end' \
"$SIGNAL_FILE" > "$TMP" && mv "$TMP" "$SIGNAL_FILE" || true
echo "phase-implement: draft PR #${DPR_NUM:-?} ${DPR_URL:-}" >&2
fi
fi
fi
Just before the terminal emit, append this phase's friction to the shared per-ticket friction
log. This is the producer half of the engineering compound loop: ticket-compound later harvests
thoughts/shared/friction/<TICKET>.md to turn what hurt this run into durable learnings/ADRs.
REPLACE each <…> placeholder below with your real experience from this phase (terse, 3–6 lines
total; "None." is a valid answer when the phase was frictionless). ${TICKET} is already resolved
upstream — do not re-derive it. This append is best-effort and off the critical path: it must
NEVER fail the phase or block the emit-complete below. (Note this phase emits a signal JSON, not a
thoughts/ markdown doc — which is exactly why friction goes to the shared friction LOG; do not
touch any *.json signal file.)
# --- Compound-engineering friction capture (CTL-789, Slice 1). Off critical path; NEVER block emit. ---
FRICTION_LOG="thoughts/shared/friction/${TICKET}.md"
mkdir -p "$(dirname "$FRICTION_LOG")"
[ -f "$FRICTION_LOG" ] || printf '# Friction log — %s\n' "${TICKET}" > "$FRICTION_LOG"
cat >> "$FRICTION_LOG" <<EOF
## implement · ${TICKET} · $(date +%Y-%m-%dT%H:%M:%S%z)
- **Backtracks / redone work:** <where you backtracked or redid work this phase — or "None.">
- **Missing / wrong / hard-to-find context:** <context that was absent, stale, or hard to locate — or "None.">
- **If I'd known:** <the ADR / guidance / past learning that would have saved this — the compounding signal — or "None.">
EOF
The record header ## implement · ${TICKET} · $(date +%Y-%m-%dT%H:%M:%S%z) is a CROSS-PHASE
contract: ## <phase> · <TICKET> · <ISO-8601 timestamp> carrying DATE+TIME+offset (e.g.
2026-06-06T14:23:01+0900). Keep this format byte-identical across all five phases — the per-record
stamp is what lets the morning briefing / daily review scan and sort "friction since last review";
never drop to a date-only stamp.
EMIT="${PLUGIN_ROOT}/scripts/phase-agent-emit-complete"
if [[ -x "$EMIT" ]]; then
"$EMIT" --phase "$PHASE" --ticket "$TICKET" --status complete
fi
# Self-halt after complete to prevent zombie workers (CTL-778 step 2).
# Read our own bg_job_id from the signal file and ask Claude to stop us.
# Best-effort: a failed stop is covered by the daemon reaper backstop.
if [[ -n "${ORCH_DIR:-}" && -f "${ORCH_DIR}/workers/${TICKET}/phase-${PHASE}.json" ]]; then
_SELF_BG=$(jq -r '.bg_job_id // empty' \
"${ORCH_DIR}/workers/${TICKET}/phase-${PHASE}.json" 2>/dev/null || true)
[[ -n "$_SELF_BG" ]] && claude stop "${_SELF_BG:0:8}" >/dev/null 2>&1 || true
fi
[[ -n "$COMMS" && -x "$COMMS" ]] && "$COMMS" done "$CHANNEL" --as "$TICKET" >/dev/null 2>&1 || true
One failure mode — hard error (caller-supplied reason).
REASON="${1:-implement-plan exited non-zero}" # caller-supplied short string
"$EMIT" --phase "$PHASE" --ticket "$TICKET" --status failed --reason "$REASON"
[[ -n "$COMMS" && -x "$COMMS" ]] && "$COMMS" send "$CHANNEL" \
"phase-implement failed: ${REASON}" \
--as "$TICKET" --type attention --orch "$ORCH_ID" >/dev/null 2>&1 || true
exit 1
The orchestrator's Phase 4 monitor receives phase.implement.failed.${TICKET} via the broker
phase_lifecycle route (CTL-447) and dispatches one fix-up phase agent. A second failure escalates
to the user via the attention post.
Inherits the contract from [[_phase-agent-template]]:
| Type | When |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| info | At start; once after implement-plan returns. ~1-2 per session. |
| attention | Missing plan, unresolved 3+ test failures, hard error. (Turn caps are enforced daemon-side — CTL-748 — not self-detected by this skill.) |
| question | Plan ambiguity the agent cannot resolve unilaterally. |
| done | Emitted by phase-agent-emit-complete on success. |
Read inbound directive / pause / abort after every Task-tool round-trip back from
implement-plan — the orchestrator may abort the worker while implementation is in flight.
Architectural commitment #3 in the plan: "phase agents are thin wrappers around the canonical
skills." Improvements to /catalyst-dev:implement-plan (TDD agent-team mode, findings filing,
quality-gate iteration limits) propagate to every phase-agent run without code duplication. The
phase-agent boundary owns only the envelope: signal file, comms, /goal cap, terminal event
emission. See plan §"Phase agents wrap canonical skills" for the full delegation table.
development
Migrate a single-harness repo to the dual-harness layout so both Claude Code and Codex load the same instructions and skills — AGENTS.md as the portable canonical doc, a thin CLAUDE.md `@AGENTS.md` bridge, and a `.agents/skills` dir with a `.claude/skills` symlink onto it. Use when asked to migrate to dual-harness, make this repo work in both Claude and Codex, or for agent metadata cleanup.
tools
Goal-driven senior-engineer pipeline-unstick sweep (CTL-1176 rung 3). Given the stuck/failed/needs-human set (or ONE ticket handed by the recovery router), its GOAL is to get the pipeline MOVING again — not to fix one ticket's review findings (that is phase-remediate). It runs AFTER the eyes (diagnostician evidence) and the hands (deterministic unstuck-sweep seams) have already tried, and it CONSUMES their output from a recovery-pass.json brief rather than re-diagnosing or redoing their narrow work. It acts like a senior engineer with full tool access — it resolves merge conflicts, rebases, force-pushes, merges green PRs, and re-dispatches stalled phases AUTONOMOUSLY — and escalates to the operator ONLY for a genuine value judgment / something that degrades other functionality / a real cost-benefit trade-off / a serious architecture change / an ADR conflict. On escalation it AUTHORS the operator inbox row + the push notification (executive-voiced). Dispatched as a `claude --bg` job by phase-agent-dispatch via slash command, AND invocable bare by the operator as a sweep — hence `user-invocable: true`. Ships behind CATALYST_RECOVERY_PASS (off by default — no live behavior change until shadow/enforce).
tools
Diagnose and fix Catalyst setup issues. Validates tools, database, config, OTel, direnv, and thoughts. Automatically fixes what it can — creates directories, initializes the database, sets WAL mode, runs migrations. Use for new installs, upgrades, or when something isn't working.
tools
--- name: phase-triage description: Phase agent that triages a Linear ticket — expands acronyms, classifies (feature/bug/docs/refactor/chore), identifies genuine blockers (a semantic second-pass over the backlog — NOT a prose scrape; CTL-838), 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.fai