phase-implement

star 14

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`.

coalesce-labs By coalesce-labs schedule Updated 6/11/2026

name: phase-implement description: | 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. user-invocable: true disable-model-invocation: false # invocable by model (Skill tool) AND user (slash command) allowed-tools: - Bash - Read - Grep - Glob - Edit - Write - Task

phase-implement

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.

Prerequisites

  • CATALYST_ORCHESTRATOR_DIR, CATALYST_ORCHESTRATOR_ID, CATALYST_PHASE=implement, CATALYST_TICKET set by [[phase-agent-dispatch]].
  • An approved plan exists at thoughts/shared/plans/<date>-<ticket-lowercase>.md — the dispatcher's prior-artifact gate already validates this; this skill re-reads the file.
  • Current working directory is the ticket's worktree (orchestrator's Phase 2 provisioning).

Prelude (template — copy verbatim into the running session)

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.

/goal condition

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.)"

Phase-specific work

  1. 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.

  2. 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
    
  3. 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.

Inbox check (CTL-749)

Before continuing to the End block, check for mid-flight context updates from the human:

  1. If ${ORCH_DIR}/workers/${TICKET}/inbox.jsonl exists and is non-empty, read it fully.
  2. Parse each JSONL line — entries have kind: "comment" or kind: "description_changed".
  3. For each entry, decide:
    • Absorb and continue: the update is additive context (clarification, extra constraints, "also handle X") — fold it into your working context and continue. Post a brief reply comment acknowledging the update (one sentence).
    • Pause and replan: the update fundamentally changes scope or invalidates the current approach — emit 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.
  4. After reading, archive processed entries:
    [[ -f "${ORCH_DIR}/workers/${TICKET}/inbox.jsonl" ]] && \
      mv "${ORCH_DIR}/workers/${TICKET}/inbox.jsonl" \
         "${ORCH_DIR}/workers/${TICKET}/inbox.processed-$(date +%s).jsonl" || true
    
  5. If no inbox file or it is empty, continue normally.

End block (terminal emit — copy verbatim)

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

Step N — Capture friction (compound loop, CTL-789)

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.

End block — terminal emit (copy verbatim)

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

Failure handling

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.

Comms discipline

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.

Why this is a thin wrapper

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.

Install via CLI
npx skills add https://github.com/coalesce-labs/catalyst --skill phase-implement
Repository Details
star Stars 14
call_split Forks 2
navigation Branch main
article Path SKILL.md
More from Creator
coalesce-labs
coalesce-labs Explore all skills →