Skip to content
Development
Command

/recover-orphans

Scan for pipeline-orphaned issues stuck in intermediate workflow states and recover them — diagnose each orphan's actual GitHub state, apply recovery actions, clean up worktrees

BOOST
From plugin
forgedock
11980 skills80 commands
Install
> /plugin marketplace add RapierCraftStudios/ForgeDock
> /plugin install forgedock@forgedock

How it fires

How this command gets triggered: by you, by Claude, or both.

  • Fires itselfClaude auto-loads it when your prompt matches the work.
  • You can call itInvoke it directly when you want it.
  • Slash command/recover-orphans

Context preview

What this command does when you run it.

Scan for pipeline-orphaned issues stuck in intermediate workflow states and recover them — diagnose each orphan's actual GitHub state, apply recovery actions, clean up worktrees

Command definition

recover-orphans.md
description: Scan for pipeline-orphaned issues stuck in intermediate workflow states and recover them — diagnose each orphan's actual GitHub state, apply recovery actions, clean up worktrees
argument-hint: "[--dry-run | --since <hours> | --issue <number>]"

<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios --> <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->

/recover-orphans — Pipeline Orphan Recovery

> **Skill names**: `{FORGE_SKILL_PREFIX}` is `forgedock:` (plugin install) or empty (`install.sh`), resolved once per run by `commands/work-on.md` § Skill Name Resolution. If the skill is not found under either name, STOP and report "skill not found" — never run the phase inline.

**Input**: $ARGUMENTS

Scan ALL open issues with intermediate workflow labels for orphaned state — issues where the agent died mid-pipeline (context expired, rate-limited, crashed) and no active agent is continuing. Diagnose each orphan's actual GitHub state and apply the appropriate recovery action.

**Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet". Fallback: `model: "opus"` if rate-limited. **NEVER use plan mode (EnterPlanMode).** **NEVER use the Agent tool** — recover-orphans re-enters the pipeline via `Skill(skill="{FORGE_SKILL_PREFIX}work-on", ...)` and `Skill(skill="{FORGE_SKILL_PREFIX}review-pr", ...)` only.

<!-- FORGE:SPEC_LOADED — recover-orphans.md loaded and active. Agent is bound by this spec. -->

---

Config Resolution

Read `forge.yaml` at the project root before running any commands:

CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
GH_FLAG="-R $GH_REPO"
REPO_PATH=$(yq '.paths.root' "$CONFIG_FILE")
WORKTREE_BASE=$(yq '.paths.worktree_base' "$CONFIG_FILE")
STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE")

If `forge.yaml` is missing: stop and tell the user to run `npx forgedock init` to generate it.

---

Argument Parsing

DRY_RUN=false
SINCE_HOURS=""
TARGET_ISSUE=""

# Parse flags
for arg in $ARGUMENTS; do
  case "$arg" in
    --dry-run)    DRY_RUN=true ;;
    --since)      : ;;  # value follows
    --issue)      : ;;  # value follows
  esac
done

# Parse --since <hours> and --issue <number> (value follows flag)
PREV=""
for arg in $ARGUMENTS; do
  if [ "$PREV" = "--since" ]; then
    SINCE_HOURS="$arg"
  elif [ "$PREV" = "--issue" ]; then
    TARGET_ISSUE="$arg"
  fi
  PREV="$arg"
done

echo "=== /recover-orphans: $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="
echo "DRY_RUN=$DRY_RUN | SINCE_HOURS=${SINCE_HOURS:-all} | TARGET_ISSUE=${TARGET_ISSUE:-all}"

| Flag | Effect | |------|--------| | (none) | Scan all intermediate-state open issues | | `--dry-run` | Report-only — print what would be done, no mutations | | `--since <hours>` | Only scan issues not updated in the last N hours (default: all) | | `--issue <number>` | Recover a single specific issue |

---

Phase 1: Find Orphans

**Note**: `gh issue list --label` uses AND semantics when multiple labels are given. To find issues with ANY intermediate workflow label, query each label separately and merge results.

# Compute stale cutoff if --since was provided
CUTOFF=""
if [ -n "$SINCE_HOURS" ]; then
  CUTOFF=$(python3 -c "
from datetime import datetime, timedelta, timezone
h = int('$SINCE_HOURS')
print((datetime.now(timezone.utc) - timedelta(hours=h)).strftime('%Y-%m-%dT%H:%M:%SZ'))
" 2>/dev/null || echo "")
fi

if [ -n "$TARGET_ISSUE" ]; then
  # Single-issue mode
  ORPHAN_LIST="$TARGET_ISSUE"
  echo "Single-issue mode: targeting #$TARGET_ISSUE"
else
  # Fleet scan: query each workflow label separately, merge, deduplicate
  ORPHAN_JSON=$(
    for LABEL in "workflow:investigating" "workflow:ready-to-build" "workflow:building" "workflow:in-review" "workflow:remediating"; do
      gh issue list ${GH_FLAG} \
        --state open \
        --label "$LABEL" \
        --limit 100 \
        --json number,title,labels,updatedAt
    done | jq -s '
      flatten |
      unique_by(.number) |
      sort_by(.updatedAt) | reverse |
      .[]
    ')

  # Apply --since filter if specified
  if [ -n "$CUTOFF" ]; then
    ORPHAN_JSON=$(echo "$ORPHAN_JSON" | jq -s --arg cutoff "$CUTOFF" '
      [.[] | select(.updatedAt < $cutoff)] | .[]
    ')
  fi

  ORPHAN_LIST=$(echo "$ORPHAN_JSON" | jq -r '.number' | sort -un)
fi

ORPHAN_COUNT=$(echo "$ORPHAN_LIST" | grep -c '[0-9]' 2>/dev/null || echo 0)
echo "Orphan candidates found: $ORPHAN_COUNT"

If `ORPHAN_COUNT` is 0: print `No orphaned issues found — pipeline is clean.` and STOP.

---

Phase 2: Diagnose Each Orphan

For each issue number in `ORPHAN_LIST`, run the full diagnostic and determine the recovery action.

# Diagnosis result arrays (accumulated for Phase 3 and Phase 5 report)
declare -A DIAG_ACTION      # issue_num -> action name
declare -A DIAG_REASON      # issue_num -> human-readable reason
declare -A DIAG_PR_NUM      # issue_num -> associated PR number (if any)
declare -A DIAG_BRANCH      # issue_num -> associated branch (if any)

for NUM in $ORPHAN_LIST; do
  echo ""
  echo "--- Diagnosing #$NUM ---"

  # Read current issue state
  ISSUE=$(gh issue view "$NUM" ${GH_FLAG} \
    --json number,title,labels,state,updatedAt,body 2>/dev/null)
  if [ -z "$ISSUE" ]; then
    echo "#$NUM: Could not fetch — skipping"
    continue
  fi

  ISSUE_STATE=$(echo "$ISSUE" | jq -r '.state')
  ISSUE_TITLE=$(echo "$ISSUE" | jq -r '.title')
  ISSUE_LABELS=$(echo "$ISSUE" | jq -r '[.labels[].name] | join(", ")')
  WORKFLOW_LABEL=$(echo "$ISSUE" | jq -r '[.labels[].name | select(startswith("workflow:"))] | first // "none"')

  echo "#$NUM: $ISSUE_TITLE"
  echo "  State: $ISSUE_STATE | Labels: $ISSUE_LABELS"

  # Skip if already in terminal state (race condition: label changed since query)
  if [ "$ISSUE_STATE" = "CLOSED" ]; then
    DIAG_ACTION[$NUM]="skip"
    DIAG_REASON[$NUM]="Issue already closed"
    continue
  fi
  if echo
Read more
Ships withforgedock

ForgeDock — autonomous AI dev pipeline for Claude Code. GitHub as agent memory: issue in, PR out.

Get the whole plugin

Other commands on forgedock.