Skip to content

/sdlc:pr-respond

Generated from solutions/ontological/plugin/plugins/sdlc/skills/pr-respond/pr-respond.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.

This skill is the fresh-route (cold-dispatch) leg of the Router’s ([[M-27ZR]]) live/resume/fresh chain, and can also be invoked directly by an operator. See ${CLAUDE_PLUGIN_ROOT}/conventions/sdlc-yaml.md’s orchestrator.router section for the full chain and the manual sdlc pr route/ sdlc task dispatch escape hatches. 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.md — mktemp + 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.
  • Whatever orchestrator.review.response_policy points at (this repo’s own copy sets it to [[S-0018-review-response-policy]], one example shape) is the triage policy Step 7’s sub-agent applies when sorting review comments — fix / decline-with-a-reply / verify-before-acting / minimize-churn / loop-guard-at-max_rounds. See Step 6 for how the skill resolves it (and its built-in fallback) before Step 7 hands it to the sub-agent.
  • pr-tools’ check-pr skill (a different plugin — see its own SKILL.md when installed) is a general, lease-unaware “tend any PR” tool with no notion of sdlc’s task lease — it must never be invoked directly against a PR carrying an sdlc-lease footer (it would rebase/push/comment outside the lease protocol, racing a concurrent task-work or pr-respond run). The two skills are not duplicate implementations of one loop: check-pr is the portable, standalone comment-triage + rebase tool for repos without sdlc’s lease infrastructure; pr-respond is the lease-coordinated write path sdlc’s own orchestrate loop dispatches on NEEDS-RESPONSE.

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 5 — LEASE-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 1 — gh 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 5 — LEASE-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 4 — LEASE-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 5 — REF-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 8 targets the PID file to stop the loop.

Before drafting the sub-agent’s brief (Step 7), resolve which triage policy governs this round:

  1. Run ${CLAUDE_PLUGIN_ROOT}/cli/sdlc config get orchestrator.review.response_policy.
  2. If it prints a path, read that file (a standard document, e.g. docs/planning/standards/S-0018-review-response-policy.md) and carry its text forward as the resolved policy — it is project-tunable independently of this skill’s own prose.
  3. If it prints nothing (the key is unset), use this built-in policy as the resolved text instead:
    • Fix blocking comments, real bugs, and cheap/clear nits — a typo, a genuinely clearer name, a stale comment — replying Fixed — <what changed>.
    • Decline, with a reply (Declined — <one-clause reason>, thread resolved) bikeshedding: a preference with no clear winner, or a change whose churn outweighs its value.
    • Verify each comment against the code before acting. If it doesn’t hold up, reply No change — <reason> instead of editing; if a later commit already covers it, reply Already addressed — <where>.
    • Make surgical edits only, no reflowing or tidying of neighboring text.
    • After orchestrator.review.max_rounds rounds on this PR, stop fixing should-fix items, leave those threads open, and flag them for the owner instead — a capped item is a decision for a human, not a closed triage.

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). Be honest — a reviewer’s suggestion is not a fix just because a reviewer made it.
  • Tell it to triage every comment using the policy resolved in Step 6, verbatim — quote or attach that text in the brief rather than re-deriving it.
  • 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 7 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 10: the lease must transition back to awaiting-review regardless so the next pr-respond invocation can re-acquire cleanly.

10. Transition the lease back to awaiting-review

Section titled “10. 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 11.
  • Exit 1 — HANDOFF-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 2 — CAS-FAILED: another worker moved the ref between Step 4’s re-acquire and this transition. Surface to the user; do not retry blindly.
  • Exit 3 — LEASE-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 10 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 ${CLAUDE_PLUGIN_ROOT}/skills/CLAUDE.md on marker namespacing).

Emit this marker only on the success path through Steps 4–10. 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 10 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 9’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).