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.
Recursos
1Instal·la
npx skillscat add osdlc/osdlc/pr-respond Instal·la des del registre de SkillsCat.
$sdlc:pr-respond
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 -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.- Whatever
orchestrator.review.response_policypoints 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. craft'scheck-prskill (a different plugin — see its ownSKILL.mdwhen 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 ansdlc-leasefooter (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 onNEEDS-RESPONSE.
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> author=<tool>/<model>@<effort> -->footer
(author=is optional).
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
Shell out to the lease CLI's parse-footer subcommand (see thelease parse-footer op doc-comment + sdlc lease parse-footer --help):
scripts/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> author=<tool>/<model>@<effort> -->, the
last line of the PR body (author=is optional and not used here) —parse-footerreturns the
task and lease 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
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 → exitWORKTREE-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
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):
scripts/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
Use task reacquire — not task acquire (the FIRST claim) and nottask 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:
scripts/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
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
scripts/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 8 targets the PID file to stop the loop.
6. Resolve the response policy
Before drafting the sub-agent's brief (Step 7), resolve which triage
policy governs this round:
- Run
scripts/sdlc config get orchestrator.review.response_policy. - 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. - 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 — replyingFixed — <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, replyNo change — <reason>instead of editing;
if a later commit already covers it, replyAlready addressed — <where>. - Make surgical edits only, no reflowing or tidying of
neighboring text. - After
orchestrator.review.max_roundsrounds 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.
- Fix blocking comments, real bugs, and cheap/clear nits — a
7. 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). 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, after substantive changes, to run the project's
generate
workflow (scripts/sdlc quality run --config <project-root>/sdlc.yaml --name generate --log --allow-empty;
a no-op whenworkflows.generate:is not configured) and commit what
it changed, so the committed generated files match the edited sources.
Never hand-edit a generated file. - Tell it to run the project's quality checks after that (via
scripts/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.
8. Stop the heartbeat
When the Agent invocation returns (success, failure, or exception),
always stop the heartbeat — pair it with Step 7 like atry/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>.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).
9. 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 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
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/<base>" instead of--handoff-files-changed <path>, where <base> is the PR's own base
branch (gh pr view <pr-number> --json baseRefName -q .baseRefName),
not necessarily main. See the lease task transition op
doc-comment + sdlc lease task transition --help.
Then transition with the handoff embedded:
scripts/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/<base>" \
--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 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'sexpires_atpassed
before this call. Step 5's heartbeat should have prevented this;
if it fires, something interrupted the heartbeat loop. Surface
and stop.
11. Emit the terminal marker
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 thePR-RESPONSE-COMPLETE: line.
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 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.
Notes
- 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).
Finish the session
If the SDLC_SESSION environment variable is set, this run is an sdlc-tracked
session, and the orchestrator learns the task is finished only from you. Checkecho "$SDLC_SESSION"; when it is empty, skip this section.
Run it only when this skill was the session's own top-level task (your first
message asked for it). If you loaded this skill from inside another skill or a
sub-agent, do not run it: the invoker finishes the session.
When all of this skill's work is complete, as the last thing you do:
- Success:
sdlc session done --outcome ok - Failure, or the work cannot continue:
sdlc session done --outcome failed --note "<one line why>"
Never run it before the work is complete: the orchestrator treats done as
"go on to the next step". Do not run it while a question to the person is
open. Headless runs run it too, which is harmless.