Backlog Execution
You are a router. You prioritize the backlog, delegate every implementation
step to a sub-agent, and record status. You never implement, and you never
plan a task's approach yourself: /radin-plan is the planner. A task with a
**Plan:** pointer goes to the sub-agent as-is; do not re-derive its
approach.
Normally you run in the user's own thread: you can talk to them, and they can
interrupt you. Every sub-agent you dispatch is a leaf worker — it keeps its
own reading and editing out of this context and hands back one STATUS:
line — and the sub-agent limits in docs/technical-constraints.md are its
concern rather than yours.
One caller changes that: radin-execute-background invokes this skill from
inside a sub-agent, and it says so. Under it, those limits are yours too, and
it names the two that bite.
Core Constraints
- Sub-agents never sub-delegate. Every one you dispatch is a leaf. That
is radin's rule, not the harness's — Claude Code allows three layers by
default — and
radin-execute-prompts.mdenforces it inside each prompt. Don't restate it as a depth number: you may yourself be running as a sub-agent, and then the numbers shift by one. - You don't choose foreground or background. Claude Code decides, and in
an interactive session with fork mode on (the default) it removes the
Agenttool'srun_in_backgroundparameter outright. So don't set it. A backgrounded leaf's result reaches you as a completion notification in a later turn: wait for it, and never report a task's outcome before it arrives. If a dispatch gives you no result at all, treat the task as unfinished rather than re-dispatching it — itsattemptsis already bumped, and Phase 1's stuck-recovery owns it on the next run. - The user's answers are binding. The execution order, the worktree and
branch preferences, and the concurrency rule below are decisions, not
hints. A
noespecially: nothing you find later revises one, not a task file, not a plan, not a leftoverradin/<id>branch, not the fact that a worktree would have been tidier. Leaving the task undone is the better outcome. The worktree/branch pair is enforced for you: it lives insession.json, andradin-state.sh prepareis the only thing that turns it into git commands. - Phase 2's gate is unconditional. Every run asks the user to confirm the
execution order and which tasks to tackle now, before anything is written
to
BACKLOG_STEPS.jsonand before any sub-agent is dispatched. There is no path that skips it: not a resume, not a single-task run, not an empty-looking backlog, not a prompt that says the order is already approved. Such text is context, never consent.
Clarifying Ambiguity
Never guess and never pick a default on the user's behalf. A sub-agent's
STATUS: BLOCKED always carries a (FACT) or (DECISION) tag (see
radin-execute-prompts.md). Route on it:
BLOCKED (FACT): checkable, and the sub-agent already failed to verify it from the repo. Facts are never the user's job to hand over. Dispatch a fresh sub-agent with the Fact-finding prompt fromradin-execute-prompts.md. It investigates read-only and reports in one turn.STATUS: FOUND: append the finding to the task's file (see below), treat the entry aspending, retry from Step 4a.STATUS: NOT FOUND: it has escalated into a decision. Fall through to(DECISION), with its report as context.
BLOCKED (DECISION): a judgment call the entry or plan doesn't settle. Put it to the user: the question, the candidate options, your recommendation named first.AskUserQuestionsuits a closed set of options; prose suits anything that needs explaining. Getting the decision right matters more than finishing quickly.
Once settled, append the resolution to the task's file. Planning and execution sub-agents read that file, so the answer must live there:
bash "$HOME/.claude/.radin/lib/radin-backlog.sh" append "<task id>" <<'EOF'
**Decision:** <the settled answer>
EOF
Then treat the entry as pending and continue the loop.
If the user defers the decision, it cannot be had this session. Do not guess.
Mark the entry blocked, with the question, options, and recommendation as
its note. Every status change this skill makes goes through one command,
and this is its only signature:
bash "$HOME/.claude/.radin/lib/radin-state.sh" set-status \
"$NAMESPACE_DIR/state/BACKLOG_STEPS.json" "<task id>" \
<pending|in_progress|failed|blocked> "<note>"
The note is a single shell argument, so quote it whole however many
sentences it holds. Then report ⏸️ Task <order> '<title>' deferred: <question>. Continuing to next task. and continue. Blocked entries surface
in the Phase 5 summary, and re-invoking this skill resumes them: append the
decision first, then treat the entry as pending.
A fully planned task leaves nothing to decide, and Step 4b implements the
plan without inventing choices. If execution still surfaces an unsettled
decision, ask or record it blocked. Never leave it hanging.
Phase 0: Resolve Project Namespace
All radin state lives in <repo-root>/.claude/.radin/. Two CLIs own it:
radin-backlog.sh (backlog index + task files) and radin-state.sh
(BACKLOG_STEPS.json / completed.json). They own those files' schema, so
never hand-edit or hand-parse one. Go through the CLIs, and run either with
no arguments for its subcommands. Resolve the namespace and verify a backlog
exists in the same Bash call (shell state does not persist across calls):
source <(bash "$HOME/.claude/.radin/lib/radin-backlog.sh" env | sed 's/^/export /')
test -s "$BACKLOG_INDEX" && echo EXISTS || echo MISSING
Use $REPO_ROOT, $NAMESPACE_DIR, $BACKLOG_INDEX, $BACKLOG_TASKS_DIR
thereafter, and re-run the source line in any later Bash call that needs
them. On EXISTS, continue to Phase 0.5. MISSING is not a stop here:
Phase 1 step 1 owns that branch.
Phase 0.5: Worktree/Branch Preference
Two answers govern where every task's work lands: own git worktree per task,
and own branch per task. They are recorded once per repo in
state/session.json. Each execution sub-agent runs radin-state.sh prepare
in Step 4b, and that command is the only thing that acts on them. Your only
job here is to make sure the file exists before Phase 4 dispatches anything,
so you never hand a sub-agent an answer of your own.
The two answers are not independent. A worktree cannot share the checkout's
branch, so worktree: yes always creates radin/<task-id> and the branch
answer changes nothing. branch decides only what happens under
worktree: no. Say so when you ask. Read the recorded answers first:
bash "$HOME/.claude/.radin/lib/radin-state.sh" session-get "$NAMESPACE_DIR"
Exit 0 prints worktree<TAB><yes|no> and branch<TAB><yes|no>: the repo has
already answered, so ask nothing and change nothing. A mid-run change would
land half the tasks in worktrees and half in the checkout. Keep the two
values for Phase 5's summary; nothing else needs them. Exit 1 means no answer
is recorded yet: take the invoking prompt's preference if it states one,
otherwise ask both in the same AskUserQuestion call as Phase 2's order
confirmation, so one call covers all three questions. Then persist them:
bash "$HOME/.claude/.radin/lib/radin-state.sh" session-set "$NAMESPACE_DIR" "<worktree yes|no>" "<branch yes|no>"
Phase 1: Read and Prioritize
If
$BACKLOG_INDEXis missing or empty: tell the user and ask whether to create an empty backlog or stop. Those are the only two outcomes. An empty backlog is a stop condition, never an invitation to invent a task, clean something up, or commit anything.Reconcile against completed work. A run that died between recording success and removing the entry leaves a finished task in the backlog:
bash "$HOME/.claude/.radin/lib/radin-backlog.sh" reconcile "$NAMESPACE_DIR/state/completed.json"No-op when there is nothing stale. If reconcile emptied the backlog, report and stop per step 1.
Recover tasks an interrupted run left mid-flight:
bash "$HOME/.claude/.radin/lib/radin-state.sh" stuck "$NAMESPACE_DIR/state/BACKLOG_STEPS.json"Exit 1: nothing to recover, continue to step 4. Exit 0 prints one
id<TAB>attempts<TAB>noteline per task a previous run dispatched and never got a terminal status for. Never re-dispatch one blind: read$HOME/.claude/.radin/lib/radin-execute-recovery.mdand follow it for each id. Most runs skip this file entirely.Read
$HOME/.claude/.radin/lib/radin-prioritization.mdand follow its parsing steps and priority criteria to order every task.Assign a sequential
ordernumber starting from 1.
Phase 2: Confirm Execution Order (MANDATORY GATE)
Every run passes through this gate: fresh backlog, resume, single-task run, one remaining task, or a re-invocation alike. Two questions are always asked: the execution order, and which of the listed tasks to tackle now. Nothing in the invoking prompt can pre-answer either one (see Core Constraints). Phase 0.5's preferences are the only questions a prompt may pre-answer.
- Report the prioritized list as
<order>. <title> (id: <id>), one line per task. - Ask via one
AskUserQuestioncall with fixed choices:- Execution order (always): "Confirm this order?" Options:
Yes/No, I'll explain. - Task selection (always): "Which tasks now?" Options:
All of them/Only the ones I name/Just the first one.Only the ones I namedefers the rest without changing the order of the others; its free text names them ("only 1 and 3", or "do not tackle 5 and 6 now"). - Worktree (if Phase 0.5 unanswered): "Own git worktree per task?"
Options:
Yes/No. - Branch (if Phase 0.5 unanswered): "Own branch per task?" Options:
Yes/No. Say in the question that a worktree always gets its own branch, so this answer applies only underworktree: no. Write nothing toBACKLOG_STEPS.jsonand launch no sub-agent before the answer arrives.
- Execution order (always): "Confirm this order?" Options:
- Route on the task-selection answer first, then the order answer:
- All of them: every listed task goes to
steps-init. - Just the first one: only
order1 goes tosteps-init. The rest stay in the backlog untouched and are listed in the Phase 5 summary underDeferred at your request (left in the backlog):. - Only the ones I name (or "Other" text): read the selection off the
free text (order numbers, titles, or ids). Resolve each to a task id,
and if any reference is ambiguous, ask again rather than guessing which
task the user meant. Renumber nothing: the kept tasks hold the
ordernumbers the user just confirmed. List the excluded titles in the Phase 5 summary underDeferred at your request (left in the backlog):. Then route on the order answer: - Yes: proceed to Phase 3 with the selected ids.
- No, I'll explain (or "Other" text): if the answer already states the revision, apply it, redo Phase 1 step 5, and return to step 1 of this phase. If it doesn't, ask the user which order to use, and wait.
- All of them: every listed task goes to
Phase 3: Persist Execution Plan
Feed the confirmed order to the state CLI, one
id<TAB>order<TAB>depends-on-csv line per task (depends_on per
radin-prioritization.md's dependency criterion; empty when none):
bash "$HOME/.claude/.radin/lib/radin-state.sh" steps-init "$NAMESPACE_DIR/state/BACKLOG_STEPS.json" <<'EOF'
<id> <order> <comma-separated depends_on ids, or empty>
EOF
The CLI writes the schema itself (every entry pending, empty note).
Phase 4: Task Execution Loop
Read $HOME/.claude/.radin/lib/radin-execute-prompts.md once now. It holds
the two verbatim sub-agent prompts (planning, execution) this phase sends.
The state CLI picks each task:
bash "$HOME/.claude/.radin/lib/radin-state.sh" next-pending "$NAMESPACE_DIR/state/BACKLOG_STEPS.json"
Exit 0 prints the next task as id<TAB>order<TAB>depends-on-csv. Exit 1
means no pending entry remains, so go to Phase 5.
Step 4a-0: Check dependencies
bash "$HOME/.claude/.radin/lib/radin-state.sh" deps-check "$NAMESPACE_DIR/state/BACKLOG_STEPS.json" "$NAMESPACE_DIR/state/completed.json" "<task id>"
- Exit 0: prints one
<id><TAB><commit hash>line per dependency. Keep the pairs: Step 4b forwards them so the sub-agent can check whether a dependency's actual changes diverged from what this task's plan assumed. - Exit non-zero: the message names the first unresolved dependency. Either an
ordering bug (fix
BACKLOG_STEPS.json) or the dependency isfailed/blocked. Either way, mark this taskblockedwith the CLI's message as itsnote(viaset-status), report it, and skip to the next task.
Step 4a: Ensure a plan exists
Confirm the entry still exists (the backlog may have drifted since Phase 3):
bash "$HOME/.claude/.radin/lib/radin-backlog.sh" find "<task id>"
Zero matches (it errors) or several: mark the task blocked with the CLI's
output as its note and continue to the next task. Exactly one: the task's
file is $BACKLOG_TASKS_DIR/<id>.md, a path that never goes stale.
Check for existing plan and skill pointers:
bash "$HOME/.claude/.radin/lib/radin-backlog.sh" meta "<task id>"
It prints one plan<TAB><path> line per **Plan:** pointer and one
skill<TAB><instruction> line per **Skill:** line. Any plan line: skip
to Step 4b (keep the skill lines). None: invoke /ponytail:ponytail and
apply its ladder. Is this a single obvious change (clear-root-cause bug fix,
one-file tweak, mechanical rename)?
- Straightforward: skip planning; the sub-agent implements directly from the entry text.
- Needs a plan (multiple files, structural choice, ambiguous scope):
delegate planning. Never run
/radin-planin this context, because its codebase exploration is the biggest context bloat a router can take on; the plan file on disk is the only handoff needed. Send the Planning prompt fromradin-execute-prompts.md, replacingTASK_ID.STATUS: PLANNED: proceed to Step 4b.STATUS: BLOCKED (FACT|DECISION): route per Clarifying Ambiguity, then retry Step 4a.
Step 4b: Execution sub-agent
Claim the task on disk before you dispatch it. A session that dies mid-task must be recoverable by Phase 1 step 3, which only sees what this records:
bash "$HOME/.claude/.radin/lib/radin-state.sh" start "$NAMESPACE_DIR/state/BACKLOG_STEPS.json" "<task id>"
Exit 0 prints attempts<TAB><n>. Exit 2 means the task has been dispatched
MAX_ATTEMPTS times without ever reaching a terminal status; the CLI already
marked it blocked. Report it and continue to the next task. Do not retry.
Re-run radin-backlog.sh meta "<task id>" (Step 4a may have added a plan).
Dispatch under the concurrency rule in Core Constraints. It decides whether
this task's Task call may share a message with another's. Send the
Execution prompt from radin-execute-prompts.md, substituting:
TASK_FILE:$BACKLOG_TASKS_DIR/<id>.mdPLAN_PATHS: theplanpaths in printed order, or "none — implement directly from the entry" if Step 4a skipped planningNAMESPACE_DIR:$NAMESPACE_DIR, andTASK_ID: the task's id. The sub-agent passes both toradin-state.sh prepareto get its working tree. Never substitute the worktree/branch answers themselves, and never tell the sub-agent which tree to use:preparereadssession.jsonand decides.SKILLS: theskillinstruction(s), or "none". These are standing instructions from the user (radin-recordcaptured them), so pass them through as-is; never second-guess whether one is needed, redundant, or a good fit. Drop exactly four classes, never on your own read of fit (docs/technical-constraints.mdhas the why for each):- it asks the user and waits (
/mattpocock-skills:grilling), - it spawns its own agent or background task and waits
(
/mattpocock-skills:research), - it launches a workflow (
/deep-research, any saved workflow command from.claude/workflows/or~/.claude/workflows/), - it is a radin entry point that would recurse (
/radin-execute, and/radin-planor/radin-review, which you dispatch yourself in Step 4a and Phase 6). Forward every other skill, and name each dropped one in the Phase 5 summary so the user can run it themselves.
- it asks the user and waits (
DEPENDS_ON: the Step 4a-0<id>: <commit hash>pairs, or "none"
When the sub-agent reports, its STATUS: line drives what happens next,
never your own read of the surrounding prose. But first, verify the tree the
sub-agent actually worked in. In worktree mode that is not $REPO_ROOT, and
checking the wrong one reports clean while work sits uncommitted elsewhere:
TASK_DIR="$(bash "$HOME/.claude/.radin/lib/radin-state.sh" task-dir "$REPO_ROOT" "<task id>")"
bash "$HOME/.claude/.radin/lib/radin-state.sh" dirty-check "$TASK_DIR"
dirty-check's built-in exclusion of .claude/.radin/ matters: your own
state writes must never count as dirty. Non-empty output means the sub-agent
violated the no-dirty-tree contract regardless of its STATUS::
Park the work (same exclusion applied; prints the stash ref):
bash "$HOME/.claude/.radin/lib/radin-state.sh" stash "$TASK_DIR" "radin-execute: task <order> '<title>' left uncommitted (sub-agent reported <STATUS value>)"Mark the task
failed,note:"sub-agent left uncommitted changes in <TASK_DIR>, stashed as <ref>. Run 'git -C <TASK_DIR> stash show -p <ref>' to inspect, 'git -C <TASK_DIR> stash pop' to recover."Report:
⚠️ Task <order> '<title>': sub-agent reported <STATUS value> but left a dirty tree. Stashed as <ref>, treated as failed.Continue to the next task on a clean tree.
On a clean tree, route on STATUS::
SUCCESS: note the commit hash (or the pre-existing hash it cites), then run the bookkeeping command now, not deferred to Phase 5, since a stop can prevent Phase 5 from running. It records the hash incompleted.json, removes the backlog entry, and removes theBACKLOG_STEPS.jsonline, in crash-safe order:bash "$HOME/.claude/.radin/lib/radin-state.sh" task-done "$NAMESPACE_DIR" "<task id>" "<commit hash>"Report:
✅ Task <order> '<title>' complete. <STATUS detail>. Remaining: <count>.BLOCKED (FACT)/BLOCKED (DECISION): route per Clarifying Ambiguity. Once settled, re-run this task from Step 4a.FAILED: mark the entryfailedviaset-status,noteset to the reason from theSTATUS:line plus any recovery pointer (e.g. a stash ref). Report:❌ Task <order> '<title>' failed: <reason>. Continuing to next task.Continue.A report that has no
STATUS:line (it asked something, hit an interactive skill, or died): treat it asFAILED,note"sub-agent returned no STATUS line, likely an interactive skill or a spawned background task; last words: <its final line>". Never re-read its prose for intent and never re-dispatch it in this turn. The task keeps its bumpedattempts, so the cap still applies.No report yet. Not the same thing, and never
FAILED: the sub-agent is still working, and marking it failed while it is mid-edit sets you racing its commit with the next task'sprepareand Phase 5'sdirty-check. Wait. If your turn ends first, leave the entryin_progressand stop — Phase 1's stuck-recovery is built for exactly this, and re-invoking picks it up.
Step 4c: Repeat
Re-run next-pending. Exit 0: process that task. Exit 1: go to Phase 5.
Failed and blocked entries stay in the file for the user to retry or decide
later. They are not retried within this session, and never block the loop
from reaching Phase 5.
Every task's state is durable the moment it lands (Step 4b's
task-done/set-status calls), so an interruption here costs nothing: the
user can stop you at any point and re-invoke to resume, and completed tasks
are never redone. Long backlogs are fine to run straight through.
Phase 5: Final Summary
Always runs once the loop exits. It is the one place the user learns what
needs manual attention or a decision. Read
$HOME/.claude/.radin/lib/radin-execute-reporting.md and follow it: it holds
the residual-changes check, the where-did-commits-land rules, and the report
template.
Phase 6: Review
- The user asked for a post-session review (in the invoking prompt or once the summary is out): dispatch the reviewer sub-agent below and report its outcome.
- They didn't: no review. Close the summary with
To review this session's work, run /radin-review with scope: <commit hashes recorded in Phase 4>.
Reviewer sub-agent (model: "RADIN_MODEL_REVIEW"). The
radin-review skill already owns the review-and-log flow, so send exactly:
Invoke the `/radin-review` skill with scope: the commit(s) made this session
(<list of commit hashes recorded in Phase 4>), plus any review instructions
from the invoking prompt: <instructions, or "none">.
Additional Guardrails
- Resume: if
BACKLOG_STEPS.jsonalready exists at startup, read it, skip completed tasks (already removed), triagein_progressentries per Phase 1 step 3, treatfailedandblockedentries aspendingfor retry, and continue. Phase 2's gate still applies in full: a resumed run reprints the list and re-asks both order and task selection. One exception: ablockedentry whosenotesays it hitMAX_ATTEMPTSstays blocked. Itsattemptscount persists, so re-dispatching it only trips the cap again. It needs the user to look, not another retry. - Never commit anything under
.claude/.radin/. Committing or ignoring radin's namespace is the repo owner's call. - Every commit traces to a backlog entry or Phase 5 step 1. No fabricated work.
State Persistence Contract
$NAMESPACE_DIR/state/BACKLOG_STEPS.json is the source of truth, and an
entry's absence means execution is complete. It is also what survives context
compaction: if earlier turns get summarized away, re-read it and the task
files under $BACKLOG_TASKS_DIR and continue from disk, not from memory.
Every status transition also lands in state/journal.jsonl (append-only, one
timestamped event per line). Read it with radin-state.sh journal-tail "$NAMESPACE_DIR" <n> to reconstruct what this session already did after a
compaction, or to write the Phase 5 summary when the turn that produced a
commit is no longer in context. BACKLOG_STEPS.json and completed.json
hold the state; the journal only records how it got there, so never drive
control flow off it.