agentic-toolkit/skills/engineering/ralph-loop/SKILL.md
Khalim Conn-Kowlessar db76a9f361 fix(ralph-loop): single-pr autopromote across DAG; env vs ticket failures
Two issues found running /ralph-loop end-to-end on a real project:

1. single-pr mode mirrored the toolkit's autopromote rule (blockers must
   be `Done`), which halts after each phase since tickets stay
   `In progress` until the final PR merges. That contradicts the
   README's "one PR for the whole DAG" semantics. Now `single-pr`
   counts a blocker resolved at status `In progress | In review | Done`
   so the loop completes the whole DAG in one invocation. `per-ticket`
   keeps the strict `Done` rule.

2. v1 failure handling treated all failures as ticket-failures and
   parked the issue as `Needs human` with a failure comment — wrong
   for environmental failures (Claude usage limit, network, interrupt)
   where the issue itself is fine. Now distinguished: env failures
   reset issue to `Ready` + un-assign; ticket failures park as before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 13:51:37 +00:00

10 KiB

name description
ralph-loop Run a GitHub Project (v2) of issues against the current repo using Claude Code subagents — one fresh subagent per ticket. Subscription-only alternative to `agentic-toolkit run` (which uses sandcastle + Docker + Anthropic API). Use after `/to-project` once the project is ready.

Ralph Loop

Implement a GitHub Project's issues by dispatching each Ready issue to a fresh Claude Code subagent (subagent_type=general-purpose). Each subagent gets a clean context window — the "ralph loop" property that keeps long-running multi-ticket work coherent.

This skill is the subscription-based counterpart to agentic-toolkit run. Pick the path that fits your setup:

Path Auth Sandbox Cost Parallelism
agentic-toolkit run (sandcastle) ANTHROPIC_API_KEY Docker container API metered Per-container
/ralph-loop (this skill) Claude Code subscription None — host repo Subscription flat Serial (one per Claude session)

Trade-offs you accept by using this skill:

  • No sandbox isolation. The subagent has full tool access on your host repo. Run on a clean checkout with no uncommitted personal work.
  • Serial only — one ticket at a time per Claude Code session. Cannot parallelise across machines.
  • Visible: every tick happens in the foreground Claude Code UI. You can interrupt anytime.

Inputs

Confirm the following before starting. If anything is missing, ask the user.

  • Project number (e.g. 2).
  • Modeper-ticket or single-pr. Same semantics as the runner CLI.
  • Owner — defaults to current gh repo owner (gh repo view --json owner --jq .owner.login).
  • Repo — defaults to current gh repo (gh repo view --json name --jq .name).
  • Target repo path — the local checkout the subagent will work in. Defaults to the current working directory.
  • Base branch — defaults to current HEAD of the target repo (git -C <path> rev-parse --abbrev-ref HEAD).

Pre-flight:

  • gh auth status shows scopes project, read:project, repo. If project missing, run gh auth refresh -s project,read:project --hostname github.com.
  • The target repo working tree is clean (git -C <path> status --porcelain is empty). If not, halt and tell the user.

Algorithm

The loop runs in the current Claude Code conversation. The orchestrator (you) coordinates state via gh and dispatches one subagent per ticket via the Agent tool.

1. Read project state

Run a GraphQL query equivalent to the runner's PROJECT_QUERY:

query($owner: String!, $repo: String!, $number: Int!) {
  repository(owner: $owner, name: $repo) {
    projectV2(number: $number) {
      id
      field(name: "Status") { ... on ProjectV2SingleSelectField { id options { id name } } }
      items(first: 100) {
        nodes {
          id
          fieldValues(first: 20) {
            nodes { ... on ProjectV2ItemFieldSingleSelectValue { name field { ... on ProjectV2SingleSelectField { name } } } }
          }
          content {
            ... on Issue { id number title body labels(first: 20) { nodes { name } } assignees(first: 5) { nodes { login } } }
          }
        }
      }
    }
  }
}

Parse each item to: { number, nodeId, itemId, title, body, labels, status, blockedBy, kind } where:

  • kind = "HITL" if labels include hitl or ready-for-human, else "AFK".
  • blockedBy = numbers extracted from the ## Blocked by section of the body.

If the Status field is missing any of the six required options (Backlog, Ready, In progress, In review, Needs human, Done), halt and tell the user to run /to-project.

2. Autopromote

Mode-specific blocker-resolution rule:

  • per-ticket — a blocker counts as resolved only if its Status is Done (work has merged). Mirrors the toolkit CLI autopromoteAfk exactly.
  • single-pr — a blocker counts as resolved if its Status is one of { In progress, In review, Done }. The shared branch already carries the blocker's commits, so downstream tickets can stack on top within the same invocation.

For each issue with kind === "AFK" AND status === "Backlog" AND every blockedBy is either resolved (per the rule above) OR not present in this project: set its Status to Ready.

This rule lets single-pr mode complete the whole DAG in one invocation, matching the README's "one PR for the whole DAG" semantics. per-ticket mode keeps the strict gate so reviewers see one merged PR before downstream work begins.

3. Compute current phase + pick next

Topologically partition issues by Blocked by (issues with all blockers in earlier phases form phase N). The current phase = lowest-indexed phase with any non-Done issue.

Within the current phase, pickNextReady = the lowest-numbered issue with status === "Ready" AND kind === "AFK".

If no such issue:

  • single-pr: jump to step 9 (finalise).
  • per-ticket: exit with "Phase N complete. Merge open PRs, then re-invoke."

4. Claim

  • Set the issue's Status to In progress.
  • Assign to the current viewer: gh api user --jq .login -> gh issue edit <N> --add-assignee <login> (best-effort; ignore failure).

5. Prepare branch

Project slug = <repo>-p<projectNumber> (matches CLI's projectSlugFrom).

Branch name:

  • per-ticket: claude/<projectSlug>/<issueNumber>-<title-slug> (slug = lowercased, non-alphanumeric -> -, trimmed, max 50 chars).
  • single-pr: claude/<projectSlug> (reused across tickets).

In the target repo:

git fetch origin
git checkout <base-branch>
git pull
if branch exists: git checkout <branch>
else: git checkout -b <branch>

6. Dispatch subagent

Use the Agent tool. subagent_type=general-purpose. The prompt MUST be fully self-contained — the subagent has no memory of this conversation.

Prompt template:

You are implementing GitHub issue #<NUMBER> in <OWNER>/<REPO>.

# Issue title
<TITLE>

# Issue body
<BODY>

# Working environment
- Target repo path: <TARGET-REPO-PATH>
- You are on branch: <BRANCH-NAME>
- Branch base: <BASE-BRANCH>
- Do NOT change branches. Do NOT push. Do NOT open PRs. The orchestrator handles those.

# Your task
1. Implement the issue's acceptance criteria fully.
2. If the repo has a test suite, run it and resolve failures before reporting complete.
3. Make commits as you go using the repo's commit style. Don't squash; the orchestrator may amend.
4. If you encounter ambiguity that blocks progress (e.g. unclear acceptance criteria, missing context, conflicting requirements), commit any progress made and report blocked rather than guessing.

# Reporting back
Your FINAL message must be exactly one of:

- `TICKET_COMPLETE: <one-line summary of what was implemented>`
- `TICKET_BLOCKED: <one-line reason and what's needed to unblock>`

Do not include any text after that line. The orchestrator parses it.

7. Validate result

After the subagent returns:

  • Read the last line of the subagent's final message.
  • Count commits: git -C <target> rev-list --count <base-branch>..HEAD.
  • Success = subagent reported TICKET_COMPLETE AND commit count > 0.
  • Otherwise: failure (treat TICKET_BLOCKED, missing tag, or zero commits all as failure).

8. After-success actions

per-ticket:

  • git -C <target> push -u origin <branch>.
  • gh pr create --base <base> --head <branch> --title "<title> (#<N>)" --body "Closes #<N>\n\nImplemented by /ralph-loop.".
  • Set Status = In review.
  • Loop back to step 1 (state may have changed; re-read).

single-pr:

  • Leave branch local — no push, no PR yet.
  • Loop back to step 1.

9. Finalise (single-pr only)

When pickNextReady returns nothing:

  • commits = git -C <target> rev-list --count <base>..<branch>.
  • If commits === 0: halt with "No commits to PR. Nothing was implemented."
  • Else: git push -u origin <branch>; open ONE PR Implement project #<projectNumber> with body listing Closes #<number> for every non-Done issue. Set those issues' Status to In review.
  • Print PR URL.

Failure handling (v1)

Halt on first failure. No retry. Distinguish two kinds of failure:

Environmental failure — subagent didn't get a fair chance (Claude usage limit hit, network drop, runtime crash, user interrupt). The issue itself isn't blocked.

  • Reset the issue's Status from In progress back to Ready.
  • Un-assign the viewer (gh issue edit <N> --remove-assignee @me).
  • Print: Halted on issue #<N> (environmental: <reason>). Re-invoke when conditions are clear.
  • Do NOT post a Needs human comment — nothing for a human to action.

Ticket failure — subagent reported TICKET_BLOCKED, returned without TICKET_COMPLETE, or produced zero commits. The issue needs human investigation.

  1. Set the failing issue's Status to Needs human.
  2. Post a comment on the issue:
    ### Automated run failed (/ralph-loop)
    
    <subagent's TICKET_BLOCKED reason or "Subagent returned without completing">
    
    <details><summary>Last subagent message</summary>
    
    <last 6000 chars of subagent's final message>
    
    </details>
    
  3. Stop the loop. Print: Halted on issue #<N>: <reason>. Tell the user to fix the underlying issue and re-invoke.

Future versions can add retry logic mirroring failure-handler.ts in the toolkit's src/modules/.

Idempotency

Re-invoking the skill is safe and resumes from current state:

  • Issues already Done are excluded from phase computation.
  • Issues In review are skipped (their PR is awaiting human merge).
  • The loop picks up at the next Ready issue.

In single-pr mode, the shared branch is reused across invocations — do not delete it between runs unless you're abandoning the project.

Notes

  • The skill needs gh CLI authed with project + read:project + repo scopes. The runner's CLI shares the same requirement; if you've already used agentic-toolkit run on the same machine, you're set.
  • This skill must run with Bash and Agent tools enabled. The subagent uses standard general-purpose permissions (file edit, bash, etc.).
  • For privacy/security, the subagent prompt includes the full issue body — do not put secrets in issue descriptions.
  • The orchestrator (you, the calling Claude session) keeps full conversation context across the entire loop. Only the subagents reset. If the orchestrator's context fills up, the user can /clear and re-invoke; idempotency handles resumption.