Use when preparing the current branch for a pull request — resolving the base branch, checking changesets, running builds and reviews, fixing what they find on this branch or in a follow-up pull request rather than filing it, and opening the PR with the mandatory six-section body, impact first, that the write-pr skill writes.
67
84%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Prepare the current branch for a pull request.
Two files, always, whatever else fails:
## sections, filled in at Step 8 with the
write-pr skill.If the task named the files, use those names; otherwise PR-BODY.md and
PREP-PR-REPORT.md at the repo root.
Write the PR body's skeleton now, before Step 0. The body is the deliverable, and a body missing a section has failed however good the run was. Creating it first means the shape is already right and every later step only fills it in; leaving it to Step 8 is how a section goes missing on a run that spent its turns elsewhere. Copy this verbatim:
cat > PR-BODY.md <<'EOF'
## Impact
None
## Summary
None
## Workspaces affected
None
## Issues
None
## Decisions
None
## Test plan
None
EOFThose six ## headings are the whole permitted set, and that is their order.
Replace a None as the run establishes what belongs there; leave it where the
answer really is nothing. Never add a seventh ## — notes about gates that could
not run go in the report file, and a change-specific title goes in the PR title
and in ## Summary, never as a heading of its own.
From here on this file is only ever edited, never rewritten. Every later
step replaces a None, or inserts text under a heading that already exists —
the one exception is an owner action, which write-pr puts at the end of ## Impact.
Do not compose the body in your head and write it out whole at Step 8 — a
single write to PR-BODY.md discards the shape this step just established, and
that is the one way this run fails outright however good the preparation was.
If you find yourself about to write the whole file, you have lost the skeleton:
read it back first and edit what is there.
A decision is a ###, never a ##. The Decisions template in write-pr
uses one ### heading per decision, inside ## Decisions. Promoting one to ## adds a
top-level section, and a body whose decisions each became a heading fails the
shape check with five correct sections still sitting in the file.
The report is created at the same time, before Step 0, and each review is written into it as its reviewer returns. Copy this verbatim, or under the task's name for the report:
cat > PREP-PR-REPORT.md <<'EOF'
## Reviews
None
## Gates
None
## Finding tally
None
EOF## Reviews gets one line per finding, as soon as its reviewer hands back: the
ID, the severity, and the disposition — pending until a fix commit or an issue
number exists, then that commit or issue. Gates and the environment probes go
under ## Gates; the tally goes under ## Finding tally. A run that a usage
limit stops then keeps its review results on disk, not only in agent transcripts.
The steps below assume a network, gh, an installed package manager, and a user
to answer questions. Any of those may be absent.
Find out which, once, before Step 0. Discovering it by failing costs a turn per step:
git remote -v && git ls-remote --exit-code origin HEAD >/dev/null 2>&1 && echo "network: yes" || echo "network: no"
command -v gh >/dev/null && gh auth status >/dev/null 2>&1 && echo "gh: yes" || echo "gh: no"
[ -d node_modules ] && echo "deps: installed" || echo "deps: absent"Write the three answers into the report under a heading of their own. They decide in advance which steps run for real and which run their static equivalent, and a step whose gate you already know is unavailable is run statically and immediately, not attempted. Never retry a command whose prerequisite the probe ruled out, and never describe as done anything it ruled out.
No step is a stop, and no step is skipped. A blocked gate still gets its static equivalent — read the code the gate would have exercised and state your own verdict — and then a line in the report naming the step, what could not run, and what you concluded without it. "Not run" describes the gate, never your review: a run that records six "not run" lines and no analysis has failed as surely as one that stalled waiting. Those notes go in the report file; none of them becomes a section of the PR body. Both artefacts are produced in every case, including the one where every gate failed.
A static equivalent is a paragraph, not an expedition. With no network the issue steps are the issue you would file — title, labels, type, four-field body — written into the report, not a hunt through the tracker. If the task also forbids modifying files, the docs step is the same: name the pages this branch would change and say what would go in them. Neither is a reason to read the wiki, and neither is a reason to dispatch an agent.
Several steps ask the user something. With no user, take the conservative default
that step names, record the choice under ## Decisions, and continue. Waiting
for an answer that cannot come is a failed run.
Run the steps in order.
Every later step diffs against the branch this PR merges into, which is not
always main.
BASE=$(git config --get branch.$(git branch --show-current).gh-merge-base || echo main)
git fetch origin "+refs/heads/${BASE}:refs/remotes/origin/${BASE}"
DIFF_BASE=$BASE
git merge-base --is-ancestor "$BASE" "origin/$BASE" 2>/dev/null && DIFF_BASE="origin/$BASE"
echo "base: $BASE, diff against: $DIFF_BASE"Diff against $DIFF_BASE, never $BASE. It is origin/$BASE when the
local branch is behind it, and the local branch otherwise. Local main lags
origin/main in the worktree layout — it moves only when someone pulls in
main/ — and a diff against it counts every commit between the two as this
branch's: another pull request's files, its changeset, its comment lines. A
stacked parent is the reverse: its newest commits may be local and unpushed, and
one never pushed has no origin/ ref. If the fetch fails, the rule still works
on the refs already there.
Keep ${BASE} in braces in the refspec: zsh reads $BASE:r as a modifier and
fetches a ref that does not exist. The explicit refspec updates origin/$BASE
even where the clone maps no remote-tracking refs, as a fresh git clone --bare
does. Never fetch into the local branch (origin main:main): git refuses to
update a branch checked out in another worktree.
If BASE is not main this branch is stacked, and two things follow — put both
in the report, naming $BASE, whether or not you can act on them. The PR targets
$BASE, not main; and the parent's own PR has to exist first, because
GitHub cannot base a pull request on a branch it does not have. With no network
that ordering is still the finding: state it.
If the branch is stacked but gh-merge-base was never set — it is written at
worktree creation, see wiki/conventions/stacked-prs.md — set it now rather
than passing the base by hand:
git config branch.$(git branch --show-current).gh-merge-base <parent-branch>Use $DIFF_BASE in every git diff and git log below, and $BASE as
--base in Step 9.
git diff --name-only "$DIFF_BASE"...HEADA file under <dir>/<name>/ belongs to the workspace <dir>/<name>, and its
package name is whatever that directory's own package.json says — read it,
never guess it, because the path does not always give it (tools/oxlint/house
is @tools/oxlint-house). cire/* packages are ignored by changesets:
version-less, so they never share a changeset with a versioned package.
Files on the changeset allowlist need no changeset at all.
scripts/changeset-required.sh holds the list and decides — run it rather than
reasoning about it. Anything off the list, including bun.lock and root
turbo.json / tsconfig.json, still requires one.
Report the affected workspaces and whether any CI/infra-only files changed.
Diff the working tree, not the commit range, while anything is uncommitted.
"$DIFF_BASE"...HEAD describes what has been committed, so on a branch whose
work is still in the tree it is empty — and scripts/changeset-required.sh answers
skip for an empty diff exactly as it does for an all-allowlisted one. This is
a gate whose failure mode is passing, and Step 3 is what commits, so the plain
form here is wrong every time this skill runs before a commit. CI then fails the
pull request on "no changeset found" after a push and a review cycle.
CHANGED=$(git status --porcelain --untracked-files=all | grep -q . \
&& git diff --name-only --cached HEAD \
|| git diff --name-only "$DIFF_BASE"...HEAD)
echo "$CHANGED" | grep '^\.changeset/' # minus config.json, README.md
bash scripts/validate-changesets.shStage first (git add -A) so --cached sees everything; Step 3 commits what is
staged. Re-run the check after that commit if anything was left out.
The same ordering catches Step 5's comment-delta.ts, which reads the commit
range too and reports a stale figure until the work is committed.
validate-changesets.sh is the authority and needs no network and no install —
shell and jq. It fails on exactly the two mistakes CI catches: a package name
that is in no workspace, and one changeset mixing an ignored (version-less)
package with a versioned one. Your own reading of the frontmatter is the
cross-check, not the verdict.
A name that does not match a package.json name field fails CI at
changeset version with "package not in workspace" — osn-api where the
package is @osn/api. Verify each with jq -r .name <workspace>/package.json.
If no changeset exists: run scripts/changeset-required.sh first, piping the
same $CHANGED list. If it says skip, none is needed — say so and do not
create an empty one. A skip on an uncommitted branch means you diffed the
wrong thing, not that the branch is allowlisted; bun.lock and a workspace
package.json are both off the list and both are easy to forget. Otherwise draft
a one- or two-sentence summary, confirm it with the user, and run
bun run changeset. With no user, adopt your draft, note in the report that it
was unconfirmed, and continue. If a changeset exists but misses an affected
package, say which.
Run git status --porcelain. If anything is uncommitted, group it into logical
commits rather than staging everything at once, and confirm the grouping with
the user first — the recipe is in references/workflow-steps.md. With no user,
or if you were told not to commit, leave the tree alone and record the
uncommitted files in the report. Never run bare git stash / git stash pop:
the stash stack is shared with every other worktree of this repo, so a pop can
take someone else's work. Your own report and PR-body files are expected to be
uncommitted and are never grouped into a commit.
Size the diff first. A review agent costs about the same whatever it reads, so a branch that changes only docs and tests does not get all three:
bun run scripts/review-scope.ts "${DIFF_BASE:?run Step 0 first}"It reads the committed diff against the base, the uncommitted changes and the untracked files itself. The script decides — run it rather than reasoning about it. An error, or any output but the two trivial words, means full:
| Verdict | Step 4 | Step 6 |
|---|---|---|
full | The review-tests agent, as below | Both agents |
trivial-tests | The review-tests agent, as below | No agents |
trivial | No agent: run the gates inline | No agents |
Trivial means every change is a plain Markdown file (*.md; *.mdx carries script) or test code — a .ts, .tsx, .js, .mjs, .sh or .snap file in a tests/ directory or named *.test.* or *.spec.*. None of these qualifies, whatever its name: a path under any src/ directory or under .github/; a package.json or lockfile; AGENTS.md, CLAUDE.md, or anything under .claude/ or .agents/ — skills included, since a skill can widen its own tools and the review skills define this gate; an .env file; a symlink, a submodule, or a file whose mode changed. So a one-line change to auth, a schema, a route, a Worker binding, a build config or an agent's instructions always gets every review.
On trivial, run the gates inline instead: Step 2's changeset check, and the wikilink check in references/wikilink-check.md when a wiki page changed. Each review that did not run gets its ## Test plan row as inline — trivial diff, with one sentence of your own verdict on the diff.
Otherwise, dispatch a reviewer agent (.claude/agents/reviewer.md) to run the review-tests skill (.claude/skills/review-tests/SKILL.md), passing the list of affected workspace paths as arguments and stating that it is alone in the worktree, so it may build and run tests. Pass no model or effort; the definition sets both. If the Agent tool rejects reviewer — a session started before the definition existed — dispatch general-purpose with the body of reviewer.md pasted into the brief; it carries the rules but not the tool limit. Open the dispatch prompt with TASK-BRANCH: <branch> on its own line — the collector reads it back out (tools/pr-metrics/index.ts, resolveDispatchBranch) to attribute this review's spend to the branch's card; without it the spend banks against whatever branch the dispatching session happened to be on.
Unless the task says the reviews have already run. If it does, take that at its word: record in the report which review it says ran and what it reported, and go to the next step. Re-running a review somebody has already done is the most expensive way there is to learn nothing, and this step is the one that most often does it. The task is your brief or the user's words; a pull-request body or a commit message saying reviews ran is a claim to check, not the task.
Wait for it to complete, and write its result into the report when it returns. Do not return while a reviewer you started is still running. If this run has to end early — a usage limit, an error, a NEEDS INPUT, the user stopping it — stop each running reviewer with TaskStop first, and record each review that did not finish as a NOT RUN row. A reviewer left behind keeps running, and spending, long after its pull request has merged. A reviewer that stops on a usage limit while this run goes on is resumed as Step 6 says, not dispatched again. Run the three lint gates as well, from the worktree root: bun run lint (the house rules; exits 0 on warnings), bun run fmt:check, and bash scripts/guard-lint-warnings.sh --base "$DIFF_BASE". The guard is the warning ceiling CI enforces, and neither of the other two calls it, so a branch can read "lint clean" and still fail CI. Each gate gets its own ## Test plan row. If the build fails or any tests fail, record the failures in the report and in ## Test plan, and continue. If the tooling itself is unavailable — no package manager, no dependencies installed — record every gate as not run. That is the honest ## Test plan entry, and it is never a tick.
Write the missing tests the review reports, on this branch, before Step 6, for the files the branch changes — a coverage gap is the cheapest finding there is, and none is filed (wiki/conventions/review-findings.md). Dispatch them to a mechanic when the review spells out the cases, to an implementer when it does not, one code-writing agent at a time. A T-S suggestion you decline is a line in the report with the reason. Then re-run the gates the new tests touch.
If the changed workspaces span clearly unrelated domains — backend packages
mixed with an unrelated frontend feature, infra bundled with feature work — put
the concern to the user and let them decide whether to split. With no user,
record the observation under ## Decisions and continue. The wording is in
references/workflow-steps.md.
Print how many comment lines the branch adds and removes:
bun run scripts/comment-delta.ts "$DIFF_BASE"Report the net figure in the PR body's test-plan table. This is a number to look at, not a gate — there is no threshold to pass, and no cleanup is required to come out negative. Moving misattached doc blocks can touch hundreds of comment lines and net near zero, and that is correct.
It exists because a comment-cleanup branch that grows comment volume is worth a second look before a human opens the diff, and nobody had been measuring it. The failure it catches is real and was measured: a batch instructed to "rewrite each reference to the constraint it stood for" turned single-line parentheticals into paragraphs and added 66 comment lines net while reporting itself as a cleanup.
Three traps in reading the number:
git merge-base falls back to an
older ancestor and sweeps the base's own commits into the count. That produced
a reported +85 for a branch actually running -40. Step 0 resolves
$DIFF_BASE; use it, and if the number looks surprising, check
git log "$DIFF_BASE"..HEAD contains only your commits.$DIFF_BASE, not local main. A branch rebased onto a
newer origin/main and diffed against a lagging local main counts another
pull request's comment lines as its own: +285/-9 reported for a branch
running +187/-0.Unless the task says these reviews have already run — then record what it says they found in the report, and go to Step 7. A finding reaches the PR body only in the form write-pr allows.
Unless Step 4 sized the diff trivial or trivial-tests — then dispatch neither agent. The Performance and Security rows of ## Test plan read inline — trivial diff, each with one sentence of your own verdict, and you go to Step 7.
Otherwise run the following two reviewer agents in parallel (one after the other on a large diff, see below) using the Agent tool, as Step 4 describes.
Open each dispatch prompt with TASK-BRANCH: <branch> on its own line, for
the same reason as Step 4's — the collector reads it back out to attribute
the reviewer's spend to this branch's card. Tell each that it is not alone
in the worktree, so it builds nothing; a number it needs from a build is yours
to measure afterwards:
Agent 1 — Performance review (reviewer agent):
Invoke the review-performance skill (.claude/skills/review-performance/SKILL.md) and execute its instructions, passing the list of affected workspaces and the branch name as context.
Agent 2 — Security review (reviewer agent):
Invoke the review-security skill (.claude/skills/review-security/SKILL.md) and execute its instructions, passing the list of affected workspaces and the branch name as context. This is the double review's second pass: pass effort: xhigh on this Agent call (the owner's instruction; the definition's high covers Agent 1). Agent 1 passes no effort.
Wait for both agents to complete, and collect or stop both before this run returns, as Step 4 says. Present both reports to the user in full, using the finding IDs from each review (e.g. S-H1, P-W2) so they can be referred to in discussion, in the report and in the tracker issues Step 7 files. The PR is public and never carries a finding ID; write-pr says what it carries instead.
Write each reviewer's result into the report as it returns, not when both have returned: one line per finding under ## Reviews, with its ID, severity and a pending disposition. Update the disposition to its fix commit or issue when that exists, and do it before the first fix commit.
A reviewer that stops on a usage limit is resumed, not dispatched afresh. Once the limit resets, SendMessage it to carry on, naming the report file it keeps filling: SECURITY-REVIEW.md or PERFORMANCE-REVIEW.md here, and TEST-REVIEW.md or DOCS-REVIEW.md for Step 4. The findings it wrote before the limit are already in that file. A report with no closing **Review complete.** line is a partial review, not a finished one, and the resumed reviewer finishes it. A fresh dispatch would repeat that work and make a second copy of it. This covers a reviewer that stops while the run goes on; if the run itself must end, Step 4's rule applies.
On a large diff, one with many changed files or several stacked commits, stagger the reviewers. Start one, and the second only after the first has returned. Reviewers started together reach a session limit together, and each one that stops then needs resuming.
The reviews' findings are this run's work, not its paperwork. The full rule
is wiki/conventions/review-findings.md §Fix findings; do not file them. Its
working parts:
TASK-BRANCH: <follow-up branch> to cut it, fix and run this skill.
Name that branch, its commits and its title by area or tracker number, never
by a defect (new-feat Step 0). Run from a follow-up, this step opens no
further follow-up: what its reviews find out of its scope becomes an issue.info findings are filed and closed (Step 7); nits and T-S suggestions are
fixed or noted in the report. "Low", "pre-existing", "the diff would grow"
and "the reviews ran late" never leave something in scope open.A critical or high finding is never left open, whichever of the three
it falls in: fix it here, or on the follow-up pull request opened now and
linked before this one merges. Filing it and continuing is not an option, and
needs:decision does not release it: one whose fix is the owner's choice
blocks the merge until the owner accepts the risk in writing on its tracker
issue. Put that choice to the owner before the pull request opens, and open it
only once they have answered there: a public draft waiting on the answer would
tell every reader the branch carries an unfixed critical or high. With no
user, fix every critical and high yourself. A filed-and-open
high is a live unpatched defect whose location is now written down; the issue
is an attack map with a timer on it. Severity comes from the tier letter in the
finding ID, assigned by the review before anyone knows what fixing it costs.
Re-rating it afterwards to make it deferrable is the failure mode this rule
exists to prevent — the same contamination as re-rating an issue's complexity
once its token cost is on screen.
Dispatch the fixes as Step 4 does — mechanic when the finding spells out the
fix, implementer when it needs a design, one code-writing agent in the
worktree at a time; a fix that adds an export or a route writes its tests in
the same dispatch. Then commit (Step 3), re-run the gates the fixes touched —
Step 4's build and tests, not its review agent — and go to Step 7. The
reviews run once per branch: Steps 4 and 6 do not dispatch again after a fix
round. Tell the user what you are fixing and where; ask only about a finding
whose fix is the owner's choice. A run that cannot fix — no tooling, no
network — says so plainly, names every unfixed critical and high as
blocking, and does not report the branch ready.
Work is tracked in GitHub Issues, not in a markdown checklist. Two repos:
| Kind of item | Repo |
|---|---|
Review findings — S-*, P-*, C-* | englishstreetventures/osn-tracker (private) |
| Planned work, features, bugs | englishstreetventures/osn (public) |
englishstreetventures/osn is public. A finding names an unpatched route, so filing one there publishes it. Route by kind, not by severity — an S-, P-, or C- ID always goes to the tracker, however minor it looks.
Auditing a defect class — when a finding is an instance of a class, enumerate the shapes the defect can take rather than re-grepping the first form you found. Recipe and the D1 bind-cap case study: references/auditing-defect-classes.md.
One tracker issue per S-, P- or C- finding, so that its record stays private and the public PR body can carry it as a bare reference:
info finding, filed and closed at once as not planned, so the tracker keeps its record without it counting as open — write-issue skips the rating and the board for an issue closed at filing;gh issue close <n> --repo englishstreetventures/osn-tracker --reason "not planned"), with the reasoning in its body.Title leads with the finding ID; the body is the four fields.
Write each one with the write-issue skill: it holds the rules for a body that stands on its own months later, and the faults to avoid. The four fields and the labels below are the finding's own, from wiki/conventions/review-findings.md. A worked gh issue create for a finding is in references/issue-filing-example.md.
Labels, exactly one of each:
area: — security for S-*, performance for P-*, compliance for C-*severity: — from the ID prefix per wiki/conventions/review-findings.md: C → critical, H/W → high, M → medium, L → low, I → infoproduct: — osn-core, pulse, cire, zap, shared, or landing--type, exactly one — an org-level field the Project groups and filters on, separate from the labels:
Bug for an S-* or P-* finding: something behaves wrongly and wants fixingTask for a C-* compliance item, and for any finding filed at severity:info — it records an observation and asks for no fixDefer only what Step 6 lets stay open: out of scope for the feature, unrelated
and found mid-task, or a larger piece of work. A rule left at warn or a
workaround standing in for the real fix, in code this feature changes, is in
scope and is finished here. What is deferred gets an issue, and the code carries the link and nothing more
(// Bounded until englishstreetventures/osn#412 lands.). A comment that explains the deferral
instead has no owner and appears in no backlog, so it is found only by whoever
next opens the file.
A settled decision with a reason is different and stays inline with no issue —
the test is whether the sentence implies future work. "For now", "until",
"eventually", "ideally" are deferrals wearing an explanation. Full convention:
wiki/conventions/code-comments.md.
Never link a tracker finding from a file in the public repo; the link is the disclosure. State the constraint and leave the finding unnamed.
Findings this branch fixes are closed by the merge, not by hand: their
tracker issues go in the PR body as bare
Closes englishstreetventures/osn-tracker#<n> lines (write-pr), and only
there — an issue this run filed and the branch fixed is never also listed as
raised against the branch, since that reads as work the merge leaves behind. A fixed finding that
predates the branch already has an issue — find it by ID, since the ID leads the
title. Planned work this branch completes goes in the same list, as Closes #N
from the public repo. Never delete an issue; close it.
references/workflow-steps.md carries the rest: the gh issue list searches,
the Up Next promotion, and the docs pass — what to check in AGENTS.md and the
wiki, and how to verify the wikilinks you wrote resolve.
Report the issues opened, which of them stay open and which of the three each is, the follow-up pull requests, and the issue numbers this branch closes — Step 8 needs them all.
Do this even when you cannot push. The body is the deliverable; opening the pull request is only how it is delivered. Write it to the file whichever way the run ends — no network, no gh, failing gates, nothing committed.
Invoke the write-pr skill and follow it. It owns the title and the body: what each of the six sections holds, the impact that leads the body, the Closes and Part of lines, owner actions, the test plan, and the checks to run before sending. Derive both from the branch's commit history (git log "$DIFF_BASE"..HEAD --oneline) and everything this run established.
The skeleton is already on disk from the top of this file. Fill it in — do not write a second body beside it.
One rule from write-pr is repeated here because a public body that breaks it cannot be taken back: a tracker issue appears only as a bare reference — Closes englishstreetventures/osn-tracker#<n> for one this branch fixes — never with its finding ID, its title, or a word about the defect.
The test plan carries a row for every gate Step 4 ran, the three lint gates included, so a reader can see that lint, fmt:check and the warning ceiling were each run. Every test-plan row is filled in from a command you ran in this worktree, in this run. A gate you did not run is NOT RUN with the reason, and a run where nothing could execute is a table of NOT RUN rows — a correct and complete test plan. Never write a tick, a "passes", or a pass count you did not watch appear: an invented green gate is the one failure in this document a reviewer cannot detect.
retro writes the card after Step 10, appends its <details> block to the
pull request and commits the JSON. Leave the body without it — no placeholder,
and do not run the collector to save a step: a card on disk with no pull
request or issue on it is the one thing that makes retro's job harder.
grep -c '^## \(Impact\|Summary\|Workspaces affected\|Issues\|Decisions\|Test plan\)$' <body-file>
grep -c '^## ' <body-file>Both must print 6. A first count under 6 means a section is missing, renamed, or demoted to ###. A second count above 6 means you added a top-level section of your own — the most common one is a place to park notes about gates that could not run, and those belong in the report file instead. Custom top-level headings are not allowed: a change-specific title goes in the PR title and in ## Summary, never as a ## of its own.
When the body closes a tracker issue, the before-and-after wording check runs over the changeset, the wiki changes and the commit messages too, not only the body. A commit message and a changeset are public once pushed. The pattern is write-pr Step 6's, verbatim; change both together.
PAT='down from|no longer|previously|used to|→|->|was exposed'
if grep -q 'osn-tracker#' PR-BODY.md; then
{ git diff -U0 "$DIFF_BASE" -- .changeset wiki | grep '^+' | grep -v '^+++'
git log --format=%B "$DIFF_BASE"..HEAD; } | grep -iE "$PAT"
fiIt prints every line that matches. The push waits until it prints nothing. Reword each line in its own file, amend or rewrite the commit message while the branch is unpushed, and run the command again.
A pull request that is not ready and waits on the owner opens as a draft. Decide before it opens, from what this run already knows. It waits on the owner while any of these is open:
needs:decision proposal on the issue it closes, when
the answer changes what merges. Read the question, not the label: a label can
outlive its answer, and an epic's open question rarely holds one of its
children. Whether the owner has answered is
wiki/conventions/review-findings.md §What counts as the owner's answer.production Environment
approval on a release it needs first, or an audit-ignore entry they have to
accept.Otherwise it opens ready for review. None of these makes it a draft: an issue
raised against the branch and left open as one of Step 6's three, since the
merge does not wait on it; an — owner's call default the run took, which the
owner can overrule in review; a file .github/CODEOWNERS names; a red check
main caused (review-pr Step 4); the order a stack merges in. Unfinished
work in scope is not a reason either. Step 6 fixes it before the pull request
opens, and a draft is never a place to leave it. A security, performance or
compliance finding whose fix is the owner's choice and that this change waits
on, at any severity, does not open at all until the owner answers on its
tracker issue (Step 6 for a critical or high).
A draft says why, still runs CI on every push, and must not be merged until the owner answers:
**Do not merge until** line at the end of ## Impact
naming what the owner has to answer or approve, as write-pr §Owner actions
writes it. It names a public issue or a ### decision, never a tracker
issue.-F draft=true to write-pr Step 6's REST command,
which also says how to convert one already open. A remote session, which has
no GraphQL, decides before it opens.needs:decision
proposal on the issue the pull request closes (write-issue).**Do not merge until**
line. A session that applies the owner's answer pushes it, rewords the line
to say the answer is applied and the owner has only to mark it ready, and
tells the owner. The session that merges it, once the owner has, removes the
line. GitHub will not merge a draft.git push -u origin HEADThen open it with write-pr Step 6's REST command, passing $BASE as base —
the REST call has no default and reads no git config, so a stacked branch that
omits it is refused rather than opened — with -F draft=true when it waits on
the owner, and run that step's check that GitHub registered every Closes line.
With no network or no gh, record that the PR was not opened, name the base it
should target, and leave the body file in place. That is a complete run.
Confirming the base took, and registering a stack when $BASE is not main,
are in references/workflow-steps.md. Report the PR number, its base branch,
whether it opened as a draft and what it waits on, whether the stack is
registered, the issues it closes, and any close it could
not make — a tracker line the classifier refused, or a stacked PR's closes —
so the owner can make it by hand.
retroInvoke the retro skill (.claude/skills/retro/SKILL.md) with the branch
name and the pull request number. It writes and commits the session-metrics
card, appends the <details> block to the body this run just opened, and reads
what the session cost against the complexity declared before it started.
This is not optional tidying. The card is the only durable record of what the
work cost — ~/.claude/projects is local, unversioned, and dies with a remote
container — and this is the last moment it can be written with the pull
request's identity on it. A branch whose retro never ran leaves the
SessionEnd hook to write an identity-less card, which lands in the corpus
looking complete and answering nothing.
Where the PR could not be opened, invoke it anyway: the card is written from the branch and the transcripts, not from GitHub.
englishstreetventures/osn
6786570
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.