/sdlc:pr-respond
Generated from solutions/ontological/skills/pr-respond/SKILL.md.
Description
Section titled “Description”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.
Allowed tools
Section titled “Allowed tools”BashReadEditAgent
Source
Section titled “Source”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.md—mktemp+ quoted-heredoc +git commit -Fis the canonical pattern for multiline commit bodies.${CLAUDE_PLUGIN_ROOT}/skills/task-work/SKILL.mdStep 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.
Output contract — deterministic markers
Section titled “Output contract — deterministic markers”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 intoresponding, feedback addressed, response commits pushed, lease transitioned back toawaiting-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 atrefs/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.
1. Parse the PR footer
Section titled “1. Parse the PR footer”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 thetask=andlease=tokens off that line and keep both in scope; downstream steps need them. The literal footer that/sdlc:task-workplants is<!-- sdlc-lease: task=<task_id> lease=<lease_id> -->, the last line of the PR body —parse-footerreturns 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 —
ghis unavailable, unauthenticated, or the PR doesn’t exist. The CLI’s stderr names the cause. Surface to the operator and stop.
2. cd into the worktree
Section titled “2. cd into the worktree”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 0ficd .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 respondingBranch 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 therespondingphase. The returnedlease_idis the SAME stable identifier as before, so carry it forward. Stderr may carry an informationalSTOLEN 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-FOUNDon stderr. The lease vanished between Step 3’s inspect and this re-acquire (extremely rare — would require a concurrent close-out). Exit withLEASE-MISSING ref=refs/sdlc/tasks/<task_id>on stderr. - Exit 1 / other — surface the CLI’s stderr and stop.
5. Start the heartbeat in the background
Section titled “5. Start the heartbeat in the background”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>.pidThe 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.
6. Dispatch the response sub-agent
Section titled “6. Dispatch the response sub-agent”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(andgh pr view <pr-number> --json reviews,reviewThreadsfor 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-purposefor 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.
7. Stop the heartbeat
Section titled “7. Stop the heartbeat”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/nullrm -f .sdlc/runtime/lease-heartbeat-<task_id>.pidSend 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).
8. Push the response commits
Section titled “8. Push the response commits”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 pushIf 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 fromgh pr view <pr-number> --json url -q .url).<quality-status>— the result of the post-response quality checks (e.g.OK 12/12or1 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 singlenonebullet) — 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.mdis in the lease commit tree. Proceed to Step 10. - 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’sexpires_atpassed before this call. Step 5’s heartbeat should have prevented this; if it fires, something interrupted the heartbeat loop. Surface and stop.
10. Emit the terminal marker
Section titled “10. Emit the terminal marker”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.
Failure modes
Section titled “Failure modes”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).