| name | epic-story-pr |
| description | Move one story from local review or local DONE into a GitHub PR, recording PR metadata on the story file. Optional stage between IN REVIEW and DONE. |
| disable-model-invocation | true |
| argument-hint | <epic-name> <story-number-or-spec-file> [pr-url|OPEN=true] |
| allowed-tools | Read Edit Write Grep Glob Bash(git status:*) Bash(git log:*) Bash(git branch:*) Bash(gh pr list:*) Bash(gh pr view:*) Bash(gh pr edit:*) Bash(gh pr create:*) Bash(curl:*) |
Epic Story PR
Transition a story from ๐ฃ IN REVIEW to ๐ต IN PR, recording GitHub PR metadata on the step file. This is the optional stage between local review acceptance and merged-to-main (โ
DONE). It can also inject a PR into an explicitly selected, non-archived โ
DONE story when that DONE meant local completion and the remote PR stage is being added late.
Argument: $ARGUMENTS โ <epic_name> <story_number_or_spec_file> [<pr_url_or_OPEN=true>]. Epic and story may be inferred for active ๐ฃ IN REVIEW / ๐ต IN PR work. โ
DONE PR injection requires the story to be explicit. The third arg is either a full GitHub PR URL (attach mode) or the literal OPEN=true to have this flow open the PR via gh (open mode). For an explicitly selected โ
DONE story, omitting the third arg implies open mode after existing PR detection fails.
Intent
This flow applies when a story has passed local review and the changes need to go through the normal GitHub PR review and merge process before the story can be marked โ
DONE. It is not required. Stories that do not need a separate GitHub PR stage skip straight from ๐ฃ IN REVIEW to โ
DONE.
Sometimes a story was already marked โ
DONE because the team treated it as locally complete, then later decides to open a GitHub PR for the same implementation. In that case, run /epic-story-pr <epic> <story>. If the PR is unmerged, this flow records PR metadata and moves the story back to ๐ต IN PR until remote review finishes. If the PR is already merged, it records PR metadata and keeps the story โ
DONE.
Status lifecycle (reference)
โฌ TODO โ not started
๐ IN PROGRESS โ actively being worked
๐ฃ IN REVIEW โ local review pass ready
๐ต IN PR โ this flow โ local review passed, PR opened, awaiting GitHub review + merge
โ
DONE โ PR merged, or local-only review accepted when no PR stage was known
โ BLOCKED โ external blocker
Phase 0 โ Resolution and inference
This flow accepts three positional inputs in $ARGUMENTS โ <epic>, <story>, and <pr_url_or_OPEN=true> โ and runs three independent inference passes for any of them that is missing. Explicit values always win and skip their corresponding inference pass. The goal is that an operator who has just claimed or resumed exactly one story can run /epic-story-pr with no arguments at all.
Parse $ARGUMENTS first. Treat any of the three slots that is empty as a request to infer.
Pass 1 โ Epic inference (when <epic> is empty)
- List
<cwd>/agent_coordination/epics/*/ directories that contain a MASTER.md.
- An epic is active if its
MASTER.md story tracker has at least one row whose status is not โ
DONE and whose Spec link does not point into archive/.
- If exactly one active epic exists, use it. Print:
inferred epic: <name> (single active epic).
- If zero active epics exist, abort with:
no active epic found under agent_coordination/epics/. Pass <epic> explicitly, or create one first.
- If multiple active epics exist, abort with the list and:
multiple active epics; pass <epic> explicitly to disambiguate.
Pass 2 โ Story inference (when <story> is empty)
After the epic is known, read <epic>/MASTER.md and collect every story row whose status is one of:
๐ฃ IN REVIEW โ the canonical entry condition for this flow
๐ต IN PR โ included so re-running this flow for refresh works without args
Do not auto-infer โ
DONE stories. DONE PR injection requires the story argument because DONE rows are completed history, not active work. If <story> was passed explicitly and the row is non-archived โ
DONE, accept it for the late PR injection path.
- If exactly one row matches, use it. Print:
inferred story: <NN> โ <title> (status: <emoji>).
- If zero rows match, do not just abort โ emit a specific recovery hint based on the rest of the tracker:
- if exactly one row is
๐ IN PROGRESS, say: no story is in review yet. Story <NN> โ <title> is still in progress; finish implementation and run /epic-story-review <epic> <NN> first.
- if multiple rows are
๐ IN PROGRESS, list them and recommend /epic-story-review for the one the operator means.
- if no rows are in progress either, say:
no story is in review or in progress. Run /epic-story-claim <epic> to start one.
- If multiple rows match the eligible set, list each candidate as
<step> | <status> | <title> and abort with: multiple stories are eligible; pass <story> explicitly to disambiguate.
Pass 3 โ PR inference (when <pr_url_or_OPEN=true> is empty)
After the story is resolved, decide whether this is an attach (existing PR) or open (new PR) operation. Walk the inference chain in order:
-
PR Tracking section. Read the resolved step file and look for a ## PR Tracking section. If it exists and has a PR URL: <url> line, that is the existing PR. Use attach mode in refresh form. Print: inferred PR (from PR Tracking): <url>. Skip the rest of the chain.
- If the story is
โ
DONE and PR Tracking points to an unmerged PR, report the status drift and ask what to do before changing files. Recommend moving the story back to ๐ต IN PR and refreshing PR metadata. If the user declines, abort without changing MASTER.md, the story file, or the PR.
-
Project repo detection. Parse the story's ## Active Claim section for the Primary write surfaces: field. Take the first path. Walk up the directory tree until you find a .git/ directory; that is the project repo. If no .git/ is found, fall back to the workspace .git/ if one exists. If still none, skip directly to step 4.
-
Branch-based PR lookup.
- Inside the detected project repo, run
git rev-parse --abbrev-ref HEAD to get the current branch.
- If the branch is the repo's default branch, skip to step 4 (operating directly on
main/master is not how PRs are opened).
- Otherwise run
gh pr list --head <branch> --state open --json url,number,headRefName,title from inside the project repo.
- If exactly one open PR is returned, print
inferred PR (from current branch <branch>): <url> and ask the user to confirm before attaching. The branch may legitimately host work unrelated to this story.
- If multiple are returned, list each as
<number> | <title> | <url> and ask which to attach.
- If zero are returned, fall through to step 4.
-
Fall through to OPEN mode. If no existing PR was found by any previous step:
- For an explicitly selected
โ
DONE story, treat OPEN=true as implicit and proceed to open mode without an extra confirmation.
- For all other stories, ask the user:
no existing PR found for branch <branch>. Open a new one via gh? [Y/n]
- If yes, proceed exactly as the existing
OPEN=true path in "PR creation mode".
- If no, abort with:
pass <pr_url> explicitly when one exists, or rerun with OPEN=true to open a fresh PR.
Inference summary printout
Before doing any work that affects MASTER.md, the PR, or the step file, print a single resolved-context block so the user can verify what was inferred:
Resolved context:
- epic: <name> (explicit | inferred from single active epic)
- story: <NN> โ <title> (explicit | inferred from single eligible row)
- status: <emoji>
- PR: <url> (explicit | from PR Tracking | from current branch | new via OPEN)
Print this even when everything was passed explicitly โ the printout is the contract the user approves before any destructive step runs.
Entry-condition check
Once the story is resolved, abort fast unless its current status is one of ๐ฃ IN REVIEW, ๐ต IN PR, or โ
DONE.
๐ฃ IN REVIEW is the canonical entry condition for opening or attaching a PR.
๐ต IN PR is refresh/resync mode per "Refresh existing PR metadata" below.
โ
DONE is allowed only when the story was explicitly selected and the Spec link is not in archive/. Treat it as late PR injection from local DONE. Do not require a second confirmation after explicit epic + story selection, but still ask before attaching a branch-inferred PR and before overwriting a substantial PR body.
- Archived
โ
DONE stories are ineligible. Abort with: Story <NN> is archived. PR injection only works for non-archived DONE stories.
Also abort when the matched row has a Plan column and Plan is not ๐ข PLAN APPROVED. Recovery hint: run /epic-story-plan-converge <epic> <story> before opening, refreshing, or merging PR state.
Abort for IN PROGRESS, BLOCKED, TODO, or any unknown status with a recovery hint naming the correct preceding command.
Known limitations
- Cross-repo PR detection is not supported. The PR inference looks at the project repo derived from the story's
Primary write surfaces. If a story's surfaces span multiple repos, pass the PR URL explicitly.
- Stories without an
Active Claim section cannot have their project repo inferred. This usually means the story has never been claimed via /epic-story-claim / /epic-story-resume. Pass the PR URL explicitly in that case.
PR description โ product-focused, NOT implementation-focused
This is the most important rule of this flow. The PR body is a spec-vs-code verification contract for the GitHub reviewer, not a developer diary. The reviewer must be able to answer one question from the PR body alone: does this code deliver what the story promised?
What belongs in the PR body
Extract only product-facing content from the resolved step file:
- the story Purpose / Goal (what outcome the user gets)
- role-based Actors when they clarify who the PR affects
- normative Scenarios / Behavior Examples only when they are linked to a single acceptance id with
Covers: A<n> and help the reviewer understand expected behavior
- explicit original ticket/card links from
Triggering Need, Purpose,
Scope, or other product-facing prose, when present
- the Acceptance criteria (observable behavior the code must satisfy)
- the Out of Scope section (what this PR deliberately does not deliver)
- Contract / interface changes โ if and only if they affect external behavior (config keys, CLI surface, API shape, file formats, env vars, persisted metadata schema, user-visible defaults, migration requirements). These live in the PR body because they change what the code promises to the outside world.
- User-facing verification โ how a reviewer can manually confirm the outcome without reading the code (CLI commands, config snippets, expected UI/log output)
What does NOT belong in the PR body
Suppress anything that describes how the code was implemented rather than what it delivers:
## Active Claim (session-local metadata)
## Progress Log (implementation diary with timestamps)
## Session Handoff (inter-session handoff notes)
## Review Log (prior review round notes)
- file paths, function names, class names, internal module names โ unless they ARE the contract (e.g. a public API endpoint, a documented config key)
- internal refactoring decisions, helper extractions, private naming choices
- test file paths, fixture paths, parameterization notes
- "why we chose X over Y" architecture rationale, unless the choice is user-visible
- any content that would go stale if the implementation were rewritten without changing the contract
Test the inclusion boundary: if a reviewer could hypothetically accept a completely different implementation that still passes the Acceptance criteria and preserves the Contract changes, then the piece you're considering does not belong in the PR body.
PR body template
Generate the body using this structure. Omit any section that has no content rather than writing "N/A".
## Summary
<one short paragraph in product language โ the user-visible outcome this PR delivers>
## Original tickets
- <optional label>: <url>
## Requirements
<bulleted list extracted from the step file's Purpose / Goal>
## Acceptance criteria
<bulleted list extracted from the step file's Acceptance section, rephrased in reviewer-verifiable terms>
## Contract changes
<only if the story changes external contracts: config keys added/removed/renamed, CLI flag changes, API/file-format changes, migration requirements, user-visible defaults>
## Out of scope
<bulleted list from the step file's Out of Scope section>
## How to verify
<user-facing verification steps a reviewer can run or inspect without reading the code>
## Epic reference
- Epic: <epic name>
- Story: <story number> โ <story title>
- Step file: <relative path to step file>
Sourcing the content
Read these sections of the step file in order and map them to the body:
## Purpose / ## Goal โ Summary + Requirements
## Triggering Need, ## Purpose, ## Scope, and any visible
product-facing prose โ explicit original ticket/card links only. Include
links near the top when found; omit ## Original tickets silently when no
link is found.
## Actors โ Requirements only when role context is product-facing
## Scenarios / Behavior Examples โ Acceptance criteria only for normative scenarios linked with exactly one Covers: A<n>; omit orientation-only examples
## Acceptance / ## Acceptance criteria โ Acceptance criteria
## Scope โ filter for contract-affecting parts only โ Contract changes
## Out of Scope โ Out of scope
## Verification โ filter for user-facing checks only โ How to verify
For original ticket/card links:
- Include links only; never summarize or quote original ticket text.
- Detect explicit URLs and stable identifiers only. Do not infer from vague
prose.
- When multiple links are found, keep a unique compact list.
- Try to fetch or infer short labels when reasonably available from the link
target or local markdown link text. If a label is unavailable, include only
the link.
- Missing ticket links are not a prompt and not a blocker for this story flow.
Do not paste sections verbatim if they contain internal terminology. Rephrase into reviewer-facing language. A reviewer who has never seen the step file should understand the PR body.
PR creation mode
Attach mode (default) โ a PR URL is provided:
- The user has already opened the PR
- Verify the URL is well-formed (
https://github.com/<org>/<repo>/pull/<n>)
- Call
gh pr view <PR_URL> --json number,title,headRefName,state,url,body to enrich metadata and read the current body
- Update the PR body to the generated product description via
gh pr edit <PR_URL> --body-file <tmpfile>
- If the existing body already contains substantial content authored by the user, show a diff and ask confirmation before overwriting. Offer to prepend/append instead of replacing.
- If the existing body is empty or auto-generated (e.g. commit messages), replace silently.
- If
gh is unavailable, skip enrichment and the body edit, record only what the user provided, and tell the user the PR body was not updated
Open mode โ user passes OPEN=true with no URL, or explicitly selects a โ
DONE story with no URL and no existing PR was found:
- Verify
git status is clean or only contains intended changes
- Verify the current branch is not the default branch
- Generate the PR body (see template above) and write it to a tempfile
- Call
gh pr create --title "<story title>" --body-file <tmpfile>
- Capture the returned URL
- If the current branch is the default branch, abort. For a
โ
DONE story, leave MASTER.md, the story header, and ## PR Tracking untouched.
- If
gh fails or is unavailable, abort fast and ask the user to open the PR manually and rerun in attach mode. For a โ
DONE story, leave MASTER.md, the story header, and ## PR Tracking untouched.
In both modes, never force-push, never bypass hooks, never rewrite history without explicit user confirmation.
Write-back to the step file
Add or refresh a ## PR Tracking section in the resolved step file:
## PR Tracking
- PR URL: <url>
- Number: <n>
- Title: <pr title>
- Branch: <head ref>
- Opened at: <UTC ISO timestamp>
- PR status: open | changes_requested | approved | merged | closed
- Merge commit: <sha or "โ">
- Last synced: <UTC ISO timestamp>
Append a timestamped bullet under ## Progress Log. Use the entry that matches the transition:
- <UTC ISO timestamp> Moved step to `๐ต IN PR` โ <PR URL>
- <UTC ISO timestamp> Reopened remote review from local `โ
DONE`; moved step to `๐ต IN PR` โ <PR URL>
- <UTC ISO timestamp> Attached merged PR to local `โ
DONE` story โ <PR URL>
Do not create a duplicate PR Tracking section. If one already exists, update its fields in place.
When the story file has a recognizable header status, update it with the same status written to MASTER.md. If the header is missing or ambiguous, leave it unchanged and report that MASTER.md remains authoritative.
Refresh existing PR metadata
If the step is already ๐ต IN PR and the user reinvokes this flow, treat it as a refresh:
- Re-query
gh pr view if available and update PR status, Merge commit, and Last synced
- If
PR status is now merged, transition the step to โ
DONE
- If
PR status is changes_requested or the reviewer requested code changes, transition the step back to ๐ IN PROGRESS and record the reason under ## Progress Log. Tell the user to rerun /epic-story-resume to address the feedback.
- Otherwise leave the step at
๐ต IN PR and update Last synced
If the step is โ
DONE and already has ## PR Tracking for an unmerged PR, report the DONE/PR drift and ask what to do. Recommend moving the story back to ๐ต IN PR and refreshing metadata. If the user declines, abort without write-back.
MASTER.md update
Before transitioning to โ
DONE, check ## Active Claim -> - Worktrees: for uncommitted changes. If any worktree is dirty (e.g., local metadata or post-PR adjustments), offer to commit before marking done. If the step has no ## Active Claim section, skip.
Update the selected row in <epic>/MASTER.md:
| From | Action |
|---|
๐ฃ IN REVIEW | set to ๐ต IN PR |
๐ต IN PR (refresh, PR still open) | leave at ๐ต IN PR |
๐ต IN PR (PR merged) | set to โ
DONE |
๐ต IN PR (PR requests code changes) | set to ๐ IN PROGRESS |
โ
DONE (explicit, non-archived, PR still open) | set to ๐ต IN PR |
โ
DONE (explicit, non-archived, PR merged) | leave at โ
DONE |
If the epic's MASTER.md Legend section does not list ๐ต IN PR, add it immediately after the ๐ฃ IN REVIEW line:
- `๐ต IN PR` โ local review passed, PR opened, awaiting GitHub review + merge
Rules
- Use the PR description inclusion boundary above. The PR body is a product contract for reviewers, not an implementation diary.
- Never mark a story
โ
DONE from this flow unless the PR is actually merged. Merged means gh pr view --json state returns MERGED, or the user explicitly states so with a merge commit.
- Never touch product code in this flow. It is a coordination-only transition (except for the optional
gh pr create call in open mode).
- Never archive a
๐ต IN PR story. /epic-squash skips them by design.
- Never skip the step file write-back. The PR URL is the only durable link between the step and the GitHub review.
- Never leave
Last synced stale across transitions.
- Never paste
Progress Log, Active Claim, Session Handoff, or Review Log content into the PR body. Those sections are implementation diary, not product contract.
- Never silently infer a DONE story. Late PR injection from
โ
DONE requires explicit story selection.
- Never leave an unmerged PR represented as
โ
DONE after the operator chooses to proceed. Move both MASTER.md and a parseable story header to ๐ต IN PR.
- Never summarize or quote original tickets. Include detected links only,
and omit the section when no link is found.
Final response
State:
- which epic and step were transitioned
- the PR URL, number, and branch
- the new status in
MASTER.md
- whether the story file header was updated or left unchanged
- whether
gh enrichment was used
- exactly what the user should do next:
- wait on PR review
- rerun
/epic-story-resume to address PR feedback
- rerun
/epic-story-pr with the same PR URL to resync PR state
- rerun
/epic-squash once the story is โ
DONE and stable