| branch |
spp branch <epic> [--from <base>] creates the Epic's integration branch feat/<slug> from origin/<base> (default main), pushes it, records it on the plan comment through spp plan --branch, and prints the branch name. When the branch exists it switches to it, and writes the plan comment only when the comment does not already name the branch, so a second run finishes whatever the first left undone. It has no --json. |
| brief |
spp brief <issue> [--out FILE] writes the worker brief for one Sub-issue and prints the file's path. The brief holds the ticket body verbatim, the Laws sources from the Routing block, the branch mechanics (cut from the Epic's integration branch, or from the default branch for a One-off), the worker's name t-<N> and the report format (one fenced JSON object). Without --out the file goes in a new temporary directory. It has no --json. |
| checks |
spp checks <pr \| sha> [--json] reads CI for a pull request (its checks), or for a commit given as 7 to 40 hex characters (its check runs). It prints the result, followed by the failing or pending run names. The reading comes from each run's bucket, never from gh's exit code: pending is never a pass, a cancelled run counts as a failure, and runs that all skip read as none. With --json it prints Checks. |
| claim |
spp claim <issue> sets the Claim mark: it adds the claimed label, creating the label first when the repo lacks it, and prints nothing. The Claim mark is a label and never an assignee (ADR 0006). spp unclaim removes it. |
| dispatch |
spp dispatch <issue> readies one worker on a Sub-issue that nobody holds: it writes the brief and prints its path, for the skill to start the spp:worker subagent on. It refuses a Sub-issue held by the claimed label or an assignee. It takes no flags and has no --json. |
| feedback |
spp feedback works on Skill feedback: it checks a draft before anyone files it, and lists the Signals waiting to become one. |
| findings |
spp findings merge <file...> [--json] [--verdict <round> ...] [--fix-diff <file>] merges the review panel's Persona findings. Each file is one PersonaFindings. Findings under 80 confidence are dropped. The rest merge in two passes per file: findings whose titles match once case and punctuation are ignored, less than 5 lines from the first of them, form a title cluster; then title clusters whose first lines are less than 5 lines from the first line of the group they join fold into one finding whatever their titles, so a defect that several Personas word differently counts once. A title cluster is never split. The merged finding keeps the highest severity and every Persona, and its body ends with one Also reported as: <title> (<Persona>, line <line>) line for each other title. Its Law tag is the speaking finding's when that cites one. A tagged title (ticket-mandated:, Candidate ADR:, Check after deploy: or Terminology) merges only with its own tag. --verdict adds the panel's verdict over the merged list, as a last Verdict: line or, with --json, a verdict key; from round 3 a FIX FIRST is written HUMAN DECISION. With --json it prints PanelFindings. --fix-diff names the patch of the fixes the round reviewed: each merged finding with any of its findings on a line that patch added or changed, and not merged with a recheck- file's finding, records a fix-regression Signal (the Signal gates apply), and a line on stderr says whether it was recorded. Each merge with --fix-diff also records one run of implement-issue step fix round, which is what the fix-regression rate is counted over. The run is named by the patch's content, fix diff <hash> (the first 12 hex characters of its SHA-256), after pull request <N> when the patch sits in the panel's spp-panel-<N> folder, so merging the same round again records no second run. stdout, the --json shape and the exit code do not change. |
| gate |
spp gate <pr number \| pr URL> [--json] reads a pull request's slice gate the way spp merge does: the verdict comment from a trusted author (ADR 0009), judged against the pull request's current head, or its merged head once it merged. It prints gate-pass <pr> head <sha7>: spec pass, quality pass, gate-fail <pr> head <sha7>: spec fail, quality pass (spec or quality failed), or pr-open <pr> head <sha7>: no passing verdict on this head when there is no trusted verdict or it names an older head. With --json it prints { pr, head, state, verdict, by }: verdict is the Verdict or null, and by is the maker the verdict comment's trailer records, or null when it records none. Every state exits 0, so a caller branches on the state. |
| guard |
spp guard worktree is the PreToolUse hook on Bash. In a session working in a linked worktree, it reads the command from the hook payload on stdin and denies one that would write into the main checkout outside that worktree; it allows the rest. It reads the command without running it: $(...), variables and globs are not expanded, so they never deny. It has no --json. |
| hand-back |
spp hand-back <issue> needs-info\|ready-for-human\|blocked --reason TEXT [--blocked-by <issue>] returns a Sub-issue to a person or a queue in one call. It writes in this order: the progress comment (status handed-back with the route and reason), then the route label, then removes the Claim mark, so a failure part-way leaves the ticket held, never released without a route, and running the same call again finishes it. --blocked-by goes only with blocked, where it adds the blocked-by edge and keeps ready-for-agent; every other Hand-back removes ready-for-agent. --by "<skill> <step>" names the progress write's maker in its trailer's by (implement-issue when absent). It prints the progress comment's URL and has no --json. |
| kind |
spp kind <issue> [--json] says what an issue is: an Epic (it has Sub-issues), a hardened Sub-issue (it passes spp lint ticket, its Verify pin included, and has a parent Epic), a hardened One-off (it passes the lint and has no Epic) or a raw issue (anything else, including one whose Verify block was never pinned or changed since), and why. /build routes on it, sends a raw issue to /triage to be hardened again, and builds a Sub-issue through its Epic, never as a One-off. Without --json the answer is one line, such as one-off: carries the ticket contract and one size; with --json it prints Kind. |
| lint |
spp lint ticket <issue> is the mechanical half of hardening, and spp lint pr <pr> is the worker's check of its pull request body before it reports. Each prints ok, or one <rule>: <detail> line per failure, and exits 1 on a failure. |
| merge |
spp merge <pr> [--json] merges a slice pull request, or a Fix pull request, into its Epic's integration branch, only when its gate passed on the current head, its blockers' pull requests have merged, its base is the integration branch and its checks pass. The gate's verdict counts only from a trusted author (the gh user, or an OWNER, MEMBER or COLLABORATOR), and spp merge re-reads the verdict comment to check that before it merges (ADR 0009). It merges with the Routing block's merge method (squash by default) pinned to the head the gate passed, then records the pull request on the wave comment. A Fix pull request (it closes no Sub-issue of its Epic; its body Refs a Sub-issue) merges the same way when a trusted author's verdict passes on its head, its base is the Epic's integration branch and its checks pass, and writes nothing to the wave comment. A closed issue that is missing, Epic-less, or not this pull request's own Sub-issue sends the pull request down the fix path, which demands the same verdict, base and checks, so that cannot merge more than the slice path does. A conflict is never resolved here: the worker rebases. It prints merged <pr> into <branch> or not merged: <reason>; with --json it prints MergeResult. |
| next |
spp next <epic> [--json] [--resume] [--running <tickets>] prints the build's next actions, one per line. The build loop is: run spp next, do what it says, run it again. It reads the Epic's preconditions, its state, CI on open pull requests, the gate's verdict history and the pull request to the base branch from GitHub, never from a session list; the one input that comes from the caller is --running. With --json it prints NextActions. |
| note |
spp note keeps a local log of one-line notes, keyed by repo and Sub-issue, in spp's own state directory outside every repository: it appends a note, lists the latest notes of an Epic, or notes a hook event. |
| plan |
spp plan <epic> [--goal TEXT] [--constraints TEXT] [--branch NAME] [--repos a/b,c/d] [--narrowed TEXT]... \| [--narrowed none] [--add-deviation TEXT] [--table-file FILE] [--by "<skill> <step>"] writes the Epic's <!-- sdd-plan --> comment, rewriting it in place, and prints its URL. A field you do not pass keeps its stored value. --narrowed replaces the stored list with every --narrowed of the call, --narrowed none clears it, and a call without it keeps it; --add-deviation appends to the Deviated list. The table file is copied in verbatim. The trailer's by records the write's maker: to-tickets, or the skill and step --by names. It has no --json. A trailer that still holds the driver an older spp recorded is read without error, and no line renders it. |
| preflight |
spp preflight <epic> [--json] checks the Epic's preconditions before a build, in order: gh 2.94 or later, the Epic open, at least one Sub-issue, a plan comment, and a Routing block in CLAUDE.md or AGENTS.md. It prints ok: <check> per passing check; at the first failure it prints fail: and fix: lines on stderr. With --json a pass prints the parsed Routing block instead of the ok: lines. |
| progress |
spp progress <issue> [--status S] [--worker TEXT] [--branch NAME] [--pr N\|URL] [--verify V] [--notes TEXT] [--by "<skill> <step>"] writes the Sub-issue's one <!-- sdd-progress --> comment, rewriting it in place, and prints its URL. A field you do not pass keeps its stored value. The comment's data trailer holds the fields (ADR 0007); a trailer that does not decode stops the write rather than erase the state. The trailer's by records the write's maker: implement-issue, or the skill and step --by names. It has no --json. |
| retro |
spp retro <epic> [--json] collects the Epic's retro numbers from GitHub alone: findings by Law tag and by Sub-issue from the gate's verdicts, fix rounds, Hand-backs, the panel's rounds, and ponytail's hits. A pull request is a Sub-issue's when GitHub links it or the wave comment's trailer records it. An input it cannot read is named on stderr as not found and never counted as 0. With --json it prints Retro; the field docs there say where each number comes from. |
| signal |
spp signal <category> --by "<skill> [step]" --evidence "<text>" [--maker-version <v>] records one Signal, a correction only judgment can see, in the repo's Signal log beside the notes log. The Signal is charged to the skill step named by --by and to the plugin version in --maker-version, or to the current plugin version without it. It records nothing under CI, outside an Attended session, or when the Routing block's Self-improvement mode is Off, and says why, as it does for a Signal the log already holds (the same fingerprint and evidence). The repo is the current checkout's origin. --by is parsed as on every other command. It has no --json. |
| status |
spp status <epic> [--json] prints every Sub-issue of an Epic with its state, grouped by Layer, from one GraphQL query. Each line is the ticket's state, its number and title, and its pull request when it has one; a blocking cycle gets a group of its own. The Epic's Fix pull requests (trusted pull requests into its integration branch that close no Sub-issue, open or merged) follow under Fixes, one line each with its state, its number and the Sub-issue its Refs line names or unattributed, and the header line ends with the open count, as in (3 fixes open); with no fixes it prints neither. With --json it prints the whole EpicState instead, the fixes as fixes. |
| unclaim |
spp unclaim <issue> clears the Claim mark: it removes the claimed label and prints nothing. A worker that stops without a pull request hands the ticket back with spp hand-back instead, which also records why. |
| upsert |
spp upsert <issue> <marker> <body-file> writes a one-writer Marker comment, such as the Epic's summary or retro, verbatim and in place, and prints its URL. The body file's first line must be the marker; the comment gets no data trailer. It has no --json. |
| verdict |
spp verdict <pr> --ticket N --head SHA --spec pass\|fail --quality pass\|fail records the slice gate's two verdicts as the pull request's <!-- spp-verdict --> Marker comment, rewritten in place, and prints its URL. It writes only while the pull request is still at SHA, because a verdict counts only on the head it was given on; when the head has moved it writes nothing and the reviewer reviews the new head. The trailer's by records the write's maker: review, or the skill and step --by "<skill> <step>" names. It has no --json. |
| verify |
spp verify runs a Sub-issue's Verify block and records the result on HEAD, and as a stop hook it holds a worker on an sdd/* branch until that record exists. It has no --json. |
| wait |
spp wait <epic> [--interval SECONDS] [--timeout SECONDS] polls the Epic's state every interval (default 120 seconds) and exits when something changes, with one line per change: a ticket's state (#12 claimed -> pr-open), or a Fix pull request that appeared, merged or went (#77 OPEN -> MERGED, absent on either side), got a new head or a gate verdict (#77 OPEN -> OPEN (new head), (gate-pass)). It reads no session list: the build learns of its own workers finishing from them, and spp next --resume asks about the ones a dead session lost. When nothing changes for --timeout seconds (default 1800) it exits with one heartbeat: line, so the orchestrator never sleeps on a change it cannot see. Run it in the background so its exit wakes the orchestrator. It has no --json. |
| wave |
spp wave plans the next Wave, and records waves, their pull requests and the review panel's rounds on the Epic's one <!-- sdd-wave --> comment. |