Skip to content

/sdlc:pr-respond

Generated from solutions/ontological/skills/pr-respond/SKILL.md.

Respond to review feedback on an open task PR with lease coordination. Given a PR number, parses the lease-binding footer from the PR body, cd’s into the task’s worktree, fences the discovered lease against the footer’s lease_id, re-acquires the lease into the responding phase, dispatches a sub-agent to address review comments, pushes the response commits, and transitions the lease back to awaiting-review. Each re-acquisition is a CAS-REPLACE with steal-on-expired semantics: two operators racing the same PR resolve to a single owner.

  • Bash
  • Read
  • Edit
  • Agent

Usage:

  • /sdlc:pr-respond <pr-number> — respond to review feedback on the named PR.

The caller is typically /sdlc:orchestrate’s per-PR dispatch when /sdlc:pr-check returns NEEDS-RESPONSE; an operator may also invoke directly. The skill discovers the task lease via the PR’s footer (planted by /sdlc:task-work at PR open) — no env vars, no argv lease state, no out-of-band handoff.

References:

  • ${CLAUDE_PLUGIN_ROOT}/conventions/lease-aware-skills.md — the lease-aware-skill convention this skill adopts (re-acquire pattern, PR-fencing check, archive-ref convention).
  • ${CLAUDE_PLUGIN_ROOT}/conventions/commit-messages.mdmktemp + quoted-heredoc + git commit -F is the canonical pattern for multiline commit bodies.
  • ${CLAUDE_PLUGIN_ROOT}/skills/task-work/SKILL.md Step 10 — plants the lease-binding footer this skill reads.
  • ${CLAUDE_PLUGIN_ROOT}/skills/task-close-out/SKILL.md — terminal skill in the lease lifecycle; takes over when the PR merges.

Exactly one terminal line on stdout, of the form:

<MARKER> ...

Where MARKER is one of:

  • PR-RESPONSE-COMPLETE: pr=<n> task=<task_id> — the response cycle completed successfully: lease re-acquired into responding, feedback addressed, response commits pushed, lease transitioned back to awaiting-review. The lease ref’s commit history shows both transitions.

Or one of these structured stderr markers (still exits 0 — the marker is the dispatch surface, not the exit code):

  • LEASE-FOOTER-MISSING pr=<n> — the PR’s body does not contain a parseable <!-- sdlc-lease: task=<id> lease=<id> --> footer. task-work never opened this PR, or the footer was edited away. No worktree mutation, no commit, no push.
  • WORKTREE-MISSING task=<task_id> — the footer parsed but .sdlc/worktrees/<task_id> is absent. task-close-out already tore the worktree down (the PR is stale), or task-work was invoked from outside the standard worktree pattern. Caller should re-run task-work or task-close-out as appropriate.
  • LEASE-FENCING-MISMATCH expected=<footer_lease_id> actual=<discovered_lease_id> — the PR’s footer names a lease_id that doesn’t match the authority’s current lease at refs/sdlc/tasks/<task_id>. The lease rotated since this PR was opened (another worker took it, or the PR was retargeted). No worktree mutation, no commit, no push — the caller decides whether the PR is stale or the lease needs manual triage.
  • LEASE-CONFLICT ref=<ref> owner=<other-host-id> — the CAS-REPLACE on the re-acquire raced against another worker. No commits, no push.
  • LEASE-MISSING ref=refs/sdlc/tasks/<task_id> — the footer parsed but the active lease ref doesn’t exist on the authority. The lease was archived (task-close-out ran already), or never claimed in the first place. Point the operator at task-work as the entry.

/sdlc:orchestrate keys off MARKER only; the trailing key=value fields are for the human reading the digest log.

Shell out to the lease CLI’s parse-footer subcommand (see the lease parse-footer op doc-comment + sdlc lease parse-footer --help):

${CLAUDE_PLUGIN_ROOT}cli/sdlc lease parse-footer <pr-number>

Branch on exit code:

  • Exit 0 — stdout carries one line in the shape FOOTER task=<task_id> lease=<lease_id>. Parse the task= and lease= tokens off that line and keep both in scope; downstream steps need them. The literal footer that /sdlc:task-work plants is <!-- sdlc-lease: task=<task_id> lease=<lease_id> -->, the last line of the PR body — parse-footer returns the same pair.
  • Exit 5LEASE-FOOTER-MISSING pr=<n> on stderr. The PR’s body has no parseable footer. Exit cleanly with the same marker on stderr. No worktree mutation, no commit, no push. task-work never opened this PR (or the footer was edited away post-open); the caller decides whether the PR is in scope for the lease protocol.
  • Exit 1gh is unavailable, unauthenticated, or the PR doesn’t exist. The CLI’s stderr names the cause. Surface to the operator and stop.

The worktree at .sdlc/worktrees/<task_id> MUST exist — task-work created it at PR open and is responsible for its lifetime until task-close-out tears it down. If absent → exit WORKTREE-MISSING task=<task_id> cleanly. No commit, no push.

if [ ! -d .sdlc/worktrees/<task_id> ]; then
echo "WORKTREE-MISSING task=<task_id>" >&2
exit 0
fi
cd .sdlc/worktrees/<task_id>

After the cd, the working directory is the worktree’s root and the current branch is task/<task_id> (the same branch the PR’s HEAD points at). Run every downstream step from inside the worktree so the lease library’s branch-derivation contract resolves to the right task_id (see ${CLAUDE_PLUGIN_ROOT}conventions/lease-aware-skills.md section 1).

3. Fencing check — verify the PR matches the live lease

Section titled “3. Fencing check — verify the PR matches the live lease”

Confirm the active lease at refs/sdlc/tasks/<task_id> carries the same lease_id as the footer — the PR ↔ lease binding check. A mismatch means the lease rotated since this PR was opened (another worker took it, or a PR was re-opened against a different lease cycle). Shell out to sdlc lease task fence (see the lease task fence op doc-comment + sdlc lease task fence --help):

${CLAUDE_PLUGIN_ROOT}cli/sdlc lease task fence <task_id> --expect-lease <footer_lease_id>

Branch on exit code:

  • Exit 0, LEASE-FENCING-OK task=<task_id> lease_id=<id> on stdout — the discovered lease_id matches the footer’s. Proceed to Step 4.
  • Exit 0, LEASE-FENCING-MISMATCH expected=<footer_lease_id> actual=<discovered_lease_id> on stderr — the ids differ. The op exits cleanly (the marker, not the exit code, is the dispatch surface). No worktree mutation, no commit, no push — the caller decides whether the PR is stale or the lease needs manual triage.
  • Exit 5LEASE-MISSING ref=refs/sdlc/tasks/<task_id> on stderr. The active lease ref is gone (task-close-out ran already, or the lease was never claimed); the op owns the remap from the authority’s ref-not-found. Exit cleanly with that same marker and point the operator at task-work as the entry.
  • Exit 1 / other — surface the CLI’s stderr to the operator and stop.

4. Re-acquire the lease into the responding phase

Section titled “4. Re-acquire the lease into the responding phase”

Use task reacquire — not task acquire (the FIRST claim) and not task transition (which does not steal on expired). The reacquire runs a steal-on-expired check: a lease past expires_at is taken over with an informational STOLEN ref=<ref> from=<previous-owner> stderr marker (no operator decision required); a fresh lease held by another host that loses the CAS race conflicts. See the lease task reacquire op doc-comment + sdlc lease task reacquire --help.

Shell out:

${CLAUDE_PLUGIN_ROOT}cli/sdlc lease task reacquire <task_id> --phase responding

Branch on exit code:

  • Exit 0 — stdout carries one line of the shape REACQUIRED task=<task_id> phase=responding lease_id=<uuid>. The lease is now held by this host at the responding phase. The returned lease_id is the SAME stable identifier as before, so carry it forward. Stderr may carry an informational STOLEN ref=<ref> from=<previous-owner> line if the previous lease was expired; that is normal and the run proceeds.
  • Exit 4LEASE-CONFLICT ref=<ref> owner=<other-host-id> on stderr. Another worker raced the CAS-REPLACE. Exit cleanly with the same marker on stderr. No commits, no push — the conflicting owner UUID gives the operator something to grep against the dispatcher’s logs.
  • Exit 5REF-NOT-FOUND on stderr. The lease vanished between Step 3’s inspect and this re-acquire (extremely rare — would require a concurrent close-out). Exit with LEASE-MISSING ref=refs/sdlc/tasks/<task_id> on stderr.
  • Exit 1 / other — surface the CLI’s stderr and stop.

Long-running response work can exceed the lease’s expires_at window. Background a heartbeat loop that issues a CAS-REPLACE every TTL/2 seconds for the duration of the response sub-agent’s run.

Reuse the same helper task-work uses (single script across both skills — see ${CLAUDE_PLUGIN_ROOT}conventions/lease-aware-skills.md section 5 for the heartbeat thread shape). Use absolute paths so the PID file lands inside the worktree’s .sdlc/runtime/ directory (create it first if it doesn’t exist):

mkdir -p .sdlc/runtime
${CLAUDE_PLUGIN_ROOT}cli/sdlc lease heartbeat-loop start <task_id> 2>>.sdlc/runtime/lease-heartbeat-<task_id>.log &
echo $! > .sdlc/runtime/lease-heartbeat-<task_id>.pid

The script’s stderr (one HEARTBEAT ref=<ref> expires_at=<rfc3339> line per tick) lands in the log file so the parent shell stays quiet. Step 7 targets the PID file to stop the loop.

Launch a sub-agent with the Agent tool. Brief it like a colleague who just walked in to the PR:

  • Tell it the PR number and ask it to read the review comments via gh pr view <pr-number> --comments (and gh pr view <pr-number> --json reviews,reviewThreads for richer threading context if needed).
  • Tell it the absolute path to the worktree it must operate in (already its cwd, since we cd’d in Step 2 — but be explicit).
  • Tell it the branch name (task/<task_id>) and that it must commit on that branch.
  • Tell it to address each open review thread, either by making code changes or by replying to the thread via ${CLAUDE_PLUGIN_ROOT}skills/pr-check/post_self_comment.sh <pr-number> <body> (the wrapper that records the comment in the pr-check cursor so the next tick doesn’t re-fire NEEDS-RESPONSE on our own reply).
  • Tell it to run the project’s quality checks after substantive changes (via ${CLAUDE_PLUGIN_ROOT}cli/sdlc quality run --config <project-root>/sdlc.yaml --line) and to fix issues before reporting done.
  • Pick subagent_type appropriately: general-purpose for mixed work.

If review threads are large, break the work into waves and brief sequential sub-agents — but each sub-agent must leave the worktree in a clean, committed state before the next runs.

When the Agent invocation returns (success, failure, or exception), always stop the heartbeat — pair it with Step 6 like a try/finally so the sibling process never leaks:

kill $(cat .sdlc/runtime/lease-heartbeat-<task_id>.pid) 2>/dev/null
rm -f .sdlc/runtime/lease-heartbeat-<task_id>.pid

Send SIGTERM, not -9, so the loop’s signal handler exits the next tick cleanly. The 2>/dev/null swallows the race where the loop already exited on its own (e.g. CAS-FAILED because another worker advanced the ref).

Push the commits the sub-agent landed on task/<task_id> to the remote. The PR will pick them up automatically (GitHub re-renders the diff against the PR’s head branch on push).

git push

If the push fails (network, force-with-lease, race lost), surface the failure to the operator. Still run Step 9: the lease must transition back to awaiting-review regardless so the next pr-respond invocation can re-acquire cleanly.

9. Transition the lease back to awaiting-review

Section titled “9. Transition the lease back to awaiting-review”

Compose the updated handoff inputs and shell out to the CLI’s transition subcommand. The handoff body reflects the response work the sub-agent did, not task-work’s original implementation summary — write a fresh handoff.md for the next downstream consumer (another pr-respond invocation, or task-close-out when the PR merges).

First, capture the inputs the handoff body needs (the same four inputs task-work’s Step 10 supplies; see that skill’s prose for the canonical shape):

  • <summary> — a single-line description of what the response addressed (from the sub-agent’s return value).
  • <pr-url> — the PR URL (carry forward from gh pr view <pr-number> --json url -q .url).
  • <quality-status> — the result of the post-response quality checks (e.g. OK 12/12 or 1 FAIL: foo.py).
  • <followups-path> — a tempfile listing spawned follow-up task slugs, one per line. If the response sub-agent reported no follow-ups, write an empty file (the CLI renders the section with a single none bullet) — do not omit the flag.

The files-changed table needs no tempfile: pass --handoff-files-changed-from-diff origin/main instead of --handoff-files-changed <path>. See the lease task transition op doc-comment + sdlc lease task transition --help.

Then transition with the handoff embedded:

${CLAUDE_PLUGIN_ROOT}cli/sdlc lease task transition <task_id> \
--phase awaiting-review \
--handoff-summary "<summary>" \
--handoff-pr-url "<pr-url>" \
--handoff-quality-status "<quality-status>" \
--handoff-files-changed-from-diff origin/main \
--handoff-followups <followups-path>

awaiting-review is a non-heartbeating placeholder state (review can take days), so the op clears expires_at. See the lease task transition op doc-comment + sdlc lease task transition --help.

Exit codes the caller must dispatch on:

  • Exit 0 — lease transitioned; the updated handoff.md is in the lease commit tree. Proceed to Step 10.
  • Exit 1HANDOFF-REQUIRED message="..." on stderr means one of the three required textual flags is missing or empty. Fill it in and re-invoke; do NOT skip the gate by dropping back to a flag-less call.
  • Exit 2CAS-FAILED: another worker moved the ref between Step 4’s re-acquire and this transition. Surface to the user; do not retry blindly.
  • Exit 3LEASE-EXPIRED: the lease’s expires_at passed before this call. Step 5’s heartbeat should have prevented this; if it fires, something interrupted the heartbeat loop. Surface and stop.

The successful end of Step 9 MUST emit one final stdout line in this exact shape as the last line of the run:

PR-RESPONSE-COMPLETE: pr=<pr-number> task=<task_id>

<pr-number> is the integer the skill was invoked with; <task_id> is the basename parsed from the footer in Step 1. This is the only marker that signals the entire /sdlc:pr-respond flow finished successfully; its absence means the sub-agent stopped early — guard against mistaking an intermediate marker for end-of-flow (see solutions/ontological/skills/CLAUDE.md on marker namespacing).

Emit this marker only on the success path through Steps 4–9. The structured-failure paths (footer-missing, worktree-missing, fencing-mismatch, lease-conflict, lease-missing) end with their own stderr markers and exit cleanly without the PR-RESPONSE-COMPLETE: line.

If gh is unavailable or the user is unauthenticated, Step 1 will fail; surface the CLI’s stderr to the operator and exit without any state changes.

If the heartbeat loop in Step 5 exits with exit code 2 mid-run (the log file will show a CAS-FAILED ref=<ref> ... line), the lease was taken from under us by another worker — likely a concurrent pr-respond invocation. The response sub-agent’s commits are still local; surface the conflict to the operator and stop. Do NOT push (another worker’s response is already pushing, and our push would collide).

If Step 8’s push race-conditions against the concurrent worker’s push, git push will fail with a non-fast-forward. Do NOT force-push; surface the failure to the operator and stop.

  • This skill’s artifacts are the response commits, the PR review comments, and the PR description. The task file is owned by /sdlc:task-work (start) and /sdlc:task-close-out (end) — leave it untouched here.
  • The PR-RESPONSE-COMPLETE: marker and the structured-failure stderr markers are /sdlc:orchestrate’s dispatch surface.
  • Committing model-generated messages. Use the canonical multiline-commit pattern in ${CLAUDE_PLUGIN_ROOT}conventions/commit-messages.md (don’t drop back to single-line -m).