Skip to content

/sdlc:spawn-task-pr

Generated from solutions/ontological/skills/spawn-task-pr/SKILL.md.

Open a PR carrying a single scaffolded follow-up task file on a target repo. /sdlc:spawn-from-post-mortem (Step 4) dispatches one invocation per classified friction bullet, so each follow-up lands as its own PR. For Upstream-plugin / Cross-project-request bullets the PR goes against the resolved foreign repo; for Local bullets it goes against this repo. Idempotent and non-interactive: takes the target-repo path, slug, headline, classification, and tag; reuses or creates a worktree on that repo; branches from origin/main (—target-base default main, —target-remote default origin); scaffolds the task via sdlc task create at the fallback status (default planning/draft); optionally drives it to open/ready via auto-define + ensure-ready when —drive-to-ready is set; commits on —branch (default meta-task/); pushes; and per —pr either opens a fresh PR or appends to that branch’s open rolling PR — the seam the PR-grouping policy (per-task / per-execution / per-project) rides on. Emits a deterministic stdout marker. —no-push rehearses without pushing or opening a PR.

  • Bash
  • Read
  • Edit
  • Write
  • Agent

Usage:

/sdlc:spawn-task-pr \
--target-repo <absolute-path-to-target-repo-root> \
--slug <kebab-slug> \
--headline "<one-line headline>" \
--classification <Local|Upstream-plugin|Cross-project-request> \
[--tag <classification-tag>] # e.g. sdlc-meta or cross-project-request; omit for Local
--originating-task <originating-basename> \
[--brief "<short body brief>"] # passed into the spawned task's Goal section
[--drive-to-ready true|false] # default false; true attempts open/ready before the PR
[--fallback-status <planning/...>] # default planning/draft; landing status when not driven
[--branch <name>] # default meta-task/<slug>; the branch to scaffold onto
[--pr open|rolling] # default open (one fresh PR); rolling appends to the branch's open PR
[--pr-title "<title>"] # PR title when creating; default is the headline
[--target-remote <name>] # default: origin
[--target-base <branch>] # default: main
[--no-push] # rehearse: skip push and PR creation

--drive-to-ready and --fallback-status come from the caller’s resolved task.execution.spawn_from_post_mortem policy (see ${CLAUDE_PLUGIN_ROOT}conventions/sdlc-yaml.md); /sdlc:spawn-from-post-mortem passes them through. Omitted, they default to the pre-drive behavior: land the task at planning/draft, no readiness drive.

--branch / --pr / --pr-title implement the PR-grouping policy (target.pr_grouping + target.pr_title_pattern). The caller resolves the branch, PR mode, and final title per grouping mode and passes them here; this skill just honors them. Omitted, they default to today’s per-task behavior: branch meta-task/<slug>, one fresh PR, title = the headline.

Acting on the result — recording the PR URL in the originating post-mortem, reporting back to a parent sub-agent — is the caller’s job.

Exactly one line on stdout, of the form:

<MARKER> ...

Where MARKER is one of:

  • SPAWN-TASK-PR-DONE pr=<pr-url> target=<owner/name> branch=<branch> status=<final-status> action=<created|appended> — the spawned task landed on <branch> and is carried by an open PR. The PR URL is the full https://github.com/... link. status= is the spawned task’s final status: open/ready when the readiness drive succeeded, otherwise the fallback status. action=created when this call opened the PR, action=appended when it added the task to a branch whose PR was already open (--pr rolling). Keep the SPAWN-TASK-PR- prefix: a bare DONE pr= collides with task-work’s TASK-WORK-DONE pr=#<N> and short-circuits an outer task-work LLM (2026-05-28 incident).
  • SPAWN-TASK-PR-REHEARSED branch=<meta-task/...>--no-push was supplied; the target worktree exists, the task file was scaffolded and committed on a local meta-task branch, but nothing was pushed and no PR was opened. The branch is left in place for human inspection.
  • SPAWN-TASK-PR-EXISTING pr=<pr-url> — a PR with the same head branch already exists on the target. The skill is a no-op: branch, commits, and PR are left untouched. The caller should treat this as success and reuse the URL.
  • ERROR reason="<one-line reason>" — could not proceed (target repo missing, push rejected, gh pr create failed, etc.). Side effects may be partial; the message names the failing step. ERROR is the one un-prefixed marker (shared by convention across all skills).

Required: --target-repo, --slug, --headline, --classification, --originating-task. --tag is required when --classification is Upstream-plugin or Cross-project-request, and omitted (or empty) for Local. If any required flag is missing, emit ERROR reason="missing required flag <name>" and exit 2.

--classification MUST be one of Local, Upstream-plugin, Cross-project-request. Reject any other value with ERROR reason="unknown classification: <value>" and exit 2.

--drive-to-ready, if present, MUST be true or false (default false). --fallback-status, if present, MUST be a planning/* status (planning/draft, planning/needs-definition, planning/proposed, planning/backlog; default planning/draft). Reject other values with ERROR reason="bad fallback-status: <value>" and exit 2 — a spawned follow-up never lands at a ready/in-progress/closed status.

--pr, if present, MUST be open or rolling (default open). Reject other values with ERROR reason="bad pr mode: <value>" and exit 2.

Resolve the branch and worktree leaf (used by Steps 2, 3, 7):

  • <branch> = --branch if given, else meta-task/<slug>.
  • <wt-leaf> = <branch> with a leading meta-task/ stripped and any remaining / replaced by - (so the default meta-task/<slug><slug>, unchanged; a shared meta-task/followups-<orig>followups-<orig>).
  • <pr-title> = --pr-title if given, else <headline>. Used only when Step 7 CREATES a PR.

For a shared branch (--pr rolling), multiple tasks reuse the SAME <branch> and <wt-leaf> across dispatches — reusing the worktree/branch is expected, not an error.

Resolve the target repo:

  • --target-repo MUST be an absolute path to a directory.
  • Confirm it is a git working tree (git -C <target-repo> rev-parse --show-toplevel succeeds). If not, emit ERROR reason="not a git working tree: <path>" and exit 1.
  • Read the target’s origin URL (git remote get-url <target-remote>, default origin); if absent, emit ERROR reason="target has no <remote> remote" and exit 1.
  • Derive the <owner>/<name> for gh pr create --repo from the origin URL (strip .git; handle both SSH git@github.com:o/n.git and HTTPS https://github.com/o/n.git shapes).

Always operate inside a dedicated worktree on the target repo, never in its main checkout — the main checkout may hold in-flight work. This applies to the foreign-repo case (Upstream-plugin / Cross-project-request) and the same-repo case (Local), where the target IS this project. The worktree path convention is:

<target-repo>/.sdlc/worktrees/<wt-leaf>

Procedure:

  1. Inside the target repo, git fetch <target-remote> <target-base> so the branch off-point is up to date.

  2. If <target-repo>/.sdlc/worktrees/<wt-leaf> already exists AND its HEAD branch matches <branch>, reuse it (expected for a shared --pr rolling branch). Otherwise, if it exists with a mismatched branch, emit ERROR reason="target worktree path occupied: <path>" and exit 1.

  3. Otherwise create the worktree. For --pr rolling on a shared branch, base it on the branch’s remote tip if that branch already exists on the remote (so the rolling branch keeps its accumulated tasks), else on <target-remote>/<target-base>:

    # fresh branch (per-task, or first task of a rolling branch):
    git -C <target-repo> worktree add \
    <target-repo>/.sdlc/worktrees/<wt-leaf> \
    -b <branch> <target-remote>/<target-base>
    # rolling branch that already exists on the remote:
    git -C <target-repo> worktree add \
    <target-repo>/.sdlc/worktrees/<wt-leaf> \
    -b <branch> <target-remote>/<branch>
  4. If a <branch> branch already exists locally but is NOT checked out anywhere, prefer worktree add -B <branch> so it is reset to the correct base (the remote branch tip for a rolling branch that exists remotely, else <target-remote>/<target-base>).

3. Check for an existing PR before doing anything destructive

Section titled “3. Check for an existing PR before doing anything destructive”

Before scaffolding or committing, ask GitHub whether a PR with this head branch is already open, and record <pr-url> if so:

gh pr list --repo <owner/name> --head <branch> --json url,state --limit 1

Branch on --pr:

  • --pr open (default, per-task): if a result returns with state == "OPEN", emit SPAWN-TASK-PR-EXISTING pr=<url> and exit 0 — the caller reuses it (today’s idempotency). The worktree may or may not exist locally; do not tear it down.
  • --pr rolling (shared branch): do NOT exit — an open PR is the expected append target. Instead:
    • Record pr_exists=true, pr_url=<url> (or pr_exists=false) for Step 7.
    • Per-slug idempotency: check whether this task already exists on the branch — git -C <target-worktree> ls-files docs/planning/tasks/ | command grep -F "<slug>" (also check the working tree). If a file for <slug> is already present, this task was added on a prior run: skip Steps 4–6a entirely and go to Step 7 to report the existing rolling PR (action=appended). Otherwise continue to Step 4 to add it.

Invoke sdlc task create against the target’s project-root. The plugin root for this skill is ${CLAUDE_PLUGIN_ROOT} (the calling skill’s plugin install, which is where this SKILL.md lives).

${CLAUDE_PLUGIN_ROOT}cli/sdlc task create \
<slug> \
--headline "<headline>" \
--status <fallback-status> \
--impact medium \
--complexity small \
--related <originating-basename> \
[--tags <classification-tag>] \
--project-root <target-repo>

--status is the resolved --fallback-status (default planning/draft). The task is always scaffolded at this non-ready status; the optional readiness drive (Step 6a) is what promotes it to open/ready.

Pass --tags <classification-tag> only when --classification is Upstream-plugin or Cross-project-request. Omit --tags for Local — local follow-ups don’t need a classification tag.

The op prints the new file’s absolute path on stdout (inside the target’s docs/planning/tasks/ directory). Capture it for the next step.

Edit the new task file to fill at minimum:

  • ## Goal — one paragraph: what gap on this repo, originating from [[<originating-basename>]] on <originating-repo-url>, needs closing. If --brief was supplied, splice its text in here verbatim as a quote block, attributed to the originating task.
  • ## Discovery context — one line: Spawned by /sdlc:spawn-task-pr on <YYYY-MM-DD UTC> from [[<originating-basename>]] in <originating-repo-url>.

Other body sections (Today / Proposed / Approach / Acceptance criteria / Out of scope / Dependencies) get placeholder stubs. The placeholder convention is one line per section: _TBD — receiver to fill before promoting from planning/draft._

These stubs are the landing state when the task is NOT driven to ready. When --drive-to-ready true, Step 6a runs /sdlc:task-auto-define, which best-effort replaces these stubs with synthesized content and promotes the task to open/ready; if it cannot, the stubs remain and the receiver fills them before promoting from the fallback status.

Run the frontmatter validator:

${CLAUDE_PLUGIN_ROOT}cli/sdlc entities validate <new-task-path>

If it fails, emit ERROR reason="frontmatter validation failed: <stderr line>" and exit 1.

Stage only the new task file:

git -C <target-worktree> add docs/planning/tasks/<file>.md

Commit through sdlc commit create --message - (per ${CLAUDE_PLUGIN_ROOT}conventions/commit-messages.md) so the message reaches git through a tempfile and survives shell expansion on the parens-heavy docs(tasks): subject. --project-root <target-worktree> targets the worktree; the op commits the already-staged index, so the git add above narrows the scope.

${CLAUDE_PLUGIN_ROOT}cli/sdlc commit create \
--project-root <target-worktree> --message - <<'EOF'
docs(tasks): spawn <slug> from <originating-basename> post-mortem
Follow-up spawned from [[<originating-basename>]] in
<originating-repo-url>. Classification: <classification>.
EOF

6a. Drive to ready (only when —drive-to-ready true)

Section titled “6a. Drive to ready (only when —drive-to-ready true)”

Skip this step entirely when --drive-to-ready is absent or false — the task’s final status is the fallback status from Step 4, and you proceed to Step 7.

When true, best-effort drive the scaffolded task to open/ready before the PR opens, so the follow-up lands pickable instead of as a draft. The commit from Step 6 gives the drive a clean tree to work from (auto-define reads a committed body; ensure-ready refuses to stamp over uncommitted edits).

Dispatch ONE sub-agent (Agent tool, subagent_type: general-purpose) that runs the two readiness skills in the target worktree and reports the outcome. Brief it:

Drive a freshly-scaffolded follow-up task to open/ready, best-effort.
Working directory / repo: <target-worktree>
Plugin root: ${CLAUDE_PLUGIN_ROOT}
Task file: <absolute path to the new task file in the worktree>
Do, in order:
1. Follow /sdlc:task-auto-define for this one task with --set-ready true:
read ${CLAUDE_PLUGIN_ROOT}skills/task-auto-define/SKILL.md and execute it
against the task file. It synthesizes the missing implementation-ready
sections from the task's prose + this repo's code, commits on the current
branch, and flips status to open/ready — OR bails with
TASK-AUTO-DEFINE-INSUFFICIENT if the spec can't be synthesized.
2. If (and only if) auto-define reported TASK-AUTO-DEFINE-DEFINED, follow
/sdlc:task-ensure-ready for the same task to verify and stamp
readiness_verified_at (read ${CLAUDE_PLUGIN_ROOT}skills/task-ensure-ready/SKILL.md).
Do not push and do not open a PR — the parent skill owns that.
Report exactly one line: DRIVE-RESULT status=<final-status> where final-status
is open/ready on success, or the task's current fallback status if auto-define
was INSUFFICIENT or ensure-ready found a gap.

Read the sub-agent’s DRIVE-RESULT status=<...> line and carry <final-status> into Steps 7–8. If the sub-agent errored or returned no parseable line, treat the drive as unsuccessful: the final status is the fallback status (the Step 6 commit already landed the valid draft), and you continue to Step 7 — a failed drive never fails the spawn. If Agent is unavailable, inline the two skills’ procedures yourself against the task file, holding the same order and the never-fabricate discipline.

The drive’s commits (synthesized body, status flip, readiness stamp) are on <branch> and ride the push in Step 7.

7. Push and open or update the PR (unless —no-push)

Section titled “7. Push and open or update the PR (unless —no-push)”

If --no-push was supplied, emit SPAWN-TASK-PR-REHEARSED branch=<branch> and exit 0. The branch is left in place for human inspection; no remote state changes.

Otherwise push <branch> (this both creates the remote branch and, when a PR already tracks it, updates that PR with the new commits):

git -C <target-worktree> push -u <target-remote> <branch>

If the push fails (auth, protected branch, etc.), emit ERROR reason="push failed: <stderr first line>" and exit 1.

Then decide create-vs-append:

  • A PR is already open for <branch> — the --pr rolling append case (pr_exists=true from Step 3). The push above already updated it. Do NOT create a second PR. Set action=appended and <pr-url> to the recorded URL, and skip to Step 8.

  • No PR open for <branch> — create one (--pr open, or the first task of a rolling branch). Set action=created. The body shape varies by classification so the reviewer sees the right framing:

    gh pr create --repo <owner/name> \
    --base <target-base> \
    --head <branch> \
    --title "<pr-title>" \
    --body "$(cat <<'EOF'
    ## Summary
    - Follow-up(s) spawned from a /sdlc:task-work post-mortem.
    - Originating task: [[<originating-basename>]] in <originating-repo-url>.
    - Classification: <classification>.
    ## What this PR delivers
    - A task file at `docs/planning/tasks/<file>.md` (status `<final-status>`).
    - <If final-status is open/ready:> The spec was best-effort machine-authored
    by `/sdlc:task-auto-define` and carries an `AUTO-DEFINED:` note — review the
    Goal, Approach, Today, Files-to-touch, and Acceptance-criteria before
    trusting it.
    - <If final-status is a planning/* status:> The body sections are stubs — the
    receiver should fill them before promoting to `open/ready`.
    <!-- include exactly one of the two bullets above, matching final-status -->
    - <If this is a rolling branch (--pr rolling):> Further follow-ups append to
    this PR as later runs spawn them.
    <!-- include only for --pr rolling -->
    ## Notes
    - <For --pr open:> Each spawned follow-up gets its own PR so the reviewer can
    accept or reject this gap on its own merits, independent of the originating
    task's PR.
    - <Tagged `<classification-tag>` so this repo's task review can filter
    for inbound cross-repo asks.> <!-- include this bullet only when --tag was supplied -->
    EOF
    )"

    Capture the URL gh pr create prints on stdout.

Emit on stdout:

SPAWN-TASK-PR-DONE pr=<pr-url> target=<owner/name> branch=<branch> status=<final-status> action=<created|appended>

<final-status> is open/ready when Step 6a’s drive succeeded, otherwise the fallback status. Exit 0.

  • --pr open — re-invoking with identical arguments after a successful run returns SPAWN-TASK-PR-EXISTING pr=<url> (caught in Step 3), never re-push or re-create.
  • --pr rolling — re-invoking for a <slug> already present on <branch> is a no-op that reports the open rolling PR with action=appended (Step 3’s per-slug idempotency check); it never double-adds the task.

The local worktree and branch are left in place — the caller (typically /sdlc:spawn-from-post-mortem) decides whether to keep or tear them down.

Capture context, do not silently abandon:

  • Target is dirty. If git -C <target-worktree> status --porcelain is non-empty before step 4, emit ERROR reason="target worktree dirty: <first line>" and exit 1. Never auto-stash on the target repo — the dirty state may be the user’s in-progress work (true for both foreign-repo and Local dispatch).
  • sdlc task create errors. Surface the op’s stderr verbatim in the ERROR reason; do not invent fallback content.
  • Validator failure. Surface the first failing field; do not attempt to auto-fix frontmatter on the target.
  • gh pr create rate-limited or auth-denied. Surface as ERROR; the branch IS pushed at that point. Idempotency in step 3 means a manual re-invocation will find no open PR (push without PR is fine) and proceed to a second gh pr create call.
  • Readiness drive fails (Step 6a). Never an ERROR — the Step 6 commit already landed a valid draft. The drive is best-effort: on INSUFFICIENT, a gate gap, or a sub-agent error, the task simply keeps the fallback status and the spawn proceeds to push + PR.
  • Drive to ready is best-effort and off by default. --drive-to-ready true (Step 6a) runs /sdlc:task-auto-define --set-ready true + /sdlc:task-ensure-ready so the follow-up can land open/ready instead of a draft; when the spec can’t be synthesized or fails the gate, it lands at --fallback-status (default planning/draft). Both flags come from the caller’s resolved task.execution.spawn_from_post_mortem policy — see ${CLAUDE_PLUGIN_ROOT}conventions/sdlc-yaml.md.
  • Commit messages. See ${CLAUDE_PLUGIN_ROOT}conventions/commit-messages.md — route the commit through sdlc commit create --message - (Step 6) so the parens-bearing docs(tasks): subject reaches git through a tempfile; single-line -m mis-quotes the parens-bearing classification tokens in the subject.
  • Branch naming. See ${CLAUDE_PLUGIN_ROOT}/conventions/branch-naming.md — this skill uses the meta-task/<...> namespace on the target repo, distinct from the task/<slug> branch a future task-work run creates for the same slug. The meta-task/ prefix also keeps these PRs out of the orchestrator’s in-flight task count. Per-task uses meta-task/<slug>; shared modes use a caller-chosen shared branch (e.g. meta-task/followups-<orig> per-execution, meta-task/spawn-followups per-project).
  • PR grouping. --branch + --pr open|rolling are the mechanism; the CALLER (/sdlc:spawn-from-post-mortem) maps target.pr_grouping / target.pr_title_pattern (see ${CLAUDE_PLUGIN_ROOT}conventions/sdlc-yaml.md) to a branch, PR mode, and resolved title. This skill stays a pure mechanism — it never reads config or resolves title placeholders itself.