Skip to content

Protocol

This is the contract between Shipper and the agents it runs. Commands prepare context, agents do the stage work, and the result file tells Shipper how to interpret the stage.

Stage prompt bodies are authored once under packages/core/src/prompts/templates/. Intentional per-agent wording differences, such as setup instruction-file wording and sandbox wording, come from the capability table in packages/core/src/lib/agent-capabilities.ts. Prompt invocation settings and issue/PR append flags are structured data in packages/core/src/lib/prompts.ts.

runPrompt() in packages/core/src/lib/prompt-runner.ts consumes the rendered prompt text plus the structured invocation metadata for the selected agent and step. It then applies runtime mode/model/MCP/worktree normalization, appends issue or PR context when requested, injects environment settings, and spawns the configured agent. Repository files under .shipper/prompts/ are ignored legacy leftovers and do not change prompt text.

When effective transport is origin, Shipper refreshes ordinary conversation comments from the managed GitHub issue and its linked pull request at entry to groom, design, plan, implement, PR review, and review-rework implementation. Revision-pinned PR review pre-flight refreshes after its configured review window and CI settling. With transport none, Shipper performs no refresh, writes no observation, emits no remote warning, and injects no external-advisory block. Each origin-backed stage receives exactly one frozen snapshot: question rounds, correction and transport retries, adversarial rounds, and fresh worktree attempts reuse it without polling GitHub again.

The prompt renders these comments in a separate github-external-advisory/authority="advisory-only" block with stable GitHub comment ID, target, author, timestamps, and URL. An edit keeps the same identity and supplies its current body at the next refresh. A deletion is represented by the comment’s absence from a successful current snapshot, including a successful empty snapshot. If any target cannot be fetched or parsed, the refresh is unavailable: Shipper appends no partial observation and renders an unavailable notice without replaying stale text. Edits or deletions made after stage entry are intentionally not visible until the next stage.

If the complete advisory block exceeds the inline prompt threshold, Shipper writes it intact to .shipper/input/external-github-comments.md and inserts only a pointer. It does not truncate, summarize, poll, or flatten inline review threads into that file.

Choose among the three comment channels according to durability and meaning:

  1. shipper_comment notes are the durable, retractable steering and decision record. Use them when later stages must retain a decision as Shipper-owned history.
  2. External GitHub issue/PR conversation comments are transient convenience input refreshed only at stage entry. They may steer an agent but cannot change issue state and are not the preferred durable record.
  3. Inline PR review threads are acquired with full conversations for initial PR review and refreshed for implementation review-rework.

The external channel excludes tracked Shipper workflow-comment and note-projection identities, so store-to-GitHub projections are never read back as external input. It does not change note append or retraction semantics, local-store authority, or the inline review-thread protocol.

Result files use one of three valid verdicts:

VerdictMeaning
acceptThe stage completed and Shipper should move the issue forward.
rejectThe stage found a problem and Shipper should roll back as configured.
failThe stage could not complete and the issue should be marked as failed.

An accepted design result must include the exact object routing: { route: 'simple' | 'complex', reason: string }, and no other stage or verdict may carry it. A missing route or a blank reason is invalid stage output: result validation rejects it, the design stage’s bounded correction pass retries with feedback naming what is missing, and there is no silent default. Route declarations are monotonic: a simple route may be promoted to complex, but a complex route cannot return to simple. The stored route affects stage dispatch — a simple route sends an accepted design straight to the combined implementation-and-delivery step instead of planning.

A crash, timeout, or missing result file is treated as a terminal failure. reject is not a command failure for next or ship; it is an interior workflow event as long as Shipper can apply the rollback transition from the agent’s result.

Rollback targets come from STAGE_TRANSITIONS in packages/core/src/lib/stage-transitions.ts. For example, an implementation reject rolls the issue back to shipper:designed. A derived PR review rejection returns every route to shipper:rework without changing the route or packaged-change identity. For recovery from crashes, missing results, timeouts, and reject loops, see failed issues and rollback loops.

Implementation and review-rework implementation consult a valid terminal result when branch finalization reports that the branch contributes no content relative to its base. Verdict presence, not branch emptiness alone, selects the stage outcome:

  • reject applies the ordinary successful rollback and records the agent’s reason plus an explicit statement that nothing was published;
  • fail marks the issue failed, reports the agent’s reason instead of the publication condition, and retains the normal terminal-stage-failure repository-safety classification; and
  • accept, or missing, unreadable, or invalid output, retains the publication refusal and durable failure path.

The recovery read is strict and non-correcting. Invalid output does not receive a correction retry, and a secondary validation error does not replace the publication diagnostic. Contentful rejects retain their stage-specific behavior: implementation publishes before rollback, while review-rework implementation rolls back without publishing rejecting work.

A no-content reject remains an interior event and does not increment the repository-wide auto-ship safety stop. A no-content fail counts normally. These result, transition, diagnostic, and safety rules are identical for remote-backed and fully local repository policies.

The implement result points to a pull-request spec and body whose content rules depend on the effective conventions posture. Under conventions: "shipper", the first eligible automatic host template frames the body for a real pull request. Shipper preserves and completes the template’s structure; when no automatic template exists, it retains the fixed Summary, Changes, Acceptance Criteria, Test Plan, Pre-merge actions, Post-merge actions, and Notes structure.

A template-framed body always includes a genuine change summary and the mode-correct issue reference in suitable template fields, or immediately after the summary when there is no issue field. The existing mirror-mode rules remain in force: mirror-on bodies carry the required Closes #<issue> reference, while store-only titles and bodies carry S#<localId> and commit subjects use the corresponding s<localId> scope. Optional ## Agent Feedback remains the final section. Existing in-stage content correction continues to enforce required and prohibited issue references; template framing adds no new hard-failure path.

Under conventions: "native", the title and body omit S#<localId> and follow the host repository’s automatic pull-request template when present, otherwise a stable shape from recent merged pull requests, otherwise Shipper’s generic summary and test-plan fallback without Shipper identifiers. Native PR bodies never contain an ## Agent Feedback section. Reportable friction is kept in the local stage-comment event instead of being copied into the host pull request.

Template framing does not change pull-request titles under either posture. It also does not apply to prepared local change descriptions created when no pull request will be opened.

Store-only pull-request bodies prohibit GitHub closing references under both conventions postures. Shipper/mirror-on retains its required closing trailer. Native additionally scans every full commit message in origin/<base>..HEAD before PR creation and rejects any closing reference in a subject or body.

Pull-request linkage is an explicit association written to the local issue-state store. Adoption, implementation review-rework, and desktop flows do not recover that linkage by parsing pull-request markers or commit history, so native’s marker-free content does not weaken traceability.

Review output uses result validation and inline diff-location validation, then Shipper retains a held review round. Shipper creates no review, comment, pointer, or placeholder on the pull request, performs no reviewer/author identity lookup, and records the agent’s intended disposition verbatim. Invalid agent output still consumes the ordinary correction allowance, but retention failure is a storage failure and is not sent back to the agent for correction.

Each successful local round appends a non-projectable held_review V1 event to the per-issue workflow log. The local issue ID plus event ID identifies the round; the event ID is a UUID. Its write-once payload contains:

  • the exact reviewed revision supplied by the acquisition step, rather than the agent-controlled review payload commit;
  • the overall review body and intended disposition;
  • an optional pull request number;
  • every inline finding with a stable UUID, body, path, end line and side, and paired start line and side for multiline findings; and
  • structured path, addition count, and deletion count for every diff file omitted because of size.

The canonical feedback uses reviewBody, which is not a workflow-comment projection field. A held round therefore has no expected projection body and cannot implicitly become an issue comment or a browse surface. Later rounds append separate events and do not mutate earlier rounds.

Review allowance renewal is an additive optional field on a preserving reset event:

{
"reviewAllowanceRenewal": {
"schemaVersion": 1,
"change": { "kind": "pull-request", "number": 77 },
"source": "operator-reset"
}
}

change is either the stable PR identity above or { "kind": "local-change", "id": "<UUID>" }; source is operator-reset or packaged-recovery. The reset and snapshot commit atomically. The boundary is the latest established action in raw append order, never timestamps or revisions. A reset and its companion operator Rework record in one mirror group are one action; the last qualifying record sets the boundary, with the explicit reset ID preferred. Matching explicit identity is authoritative. Legacy attribution requires affirmative matching package evidence in the same group or uninterrupted package segment, and rejects conflicting evidence. Package clearing, replacement and fresh delivery bound inference; retained continuation does not. Completed rounds count by their original held_review position after that boundary; later handling updates never relocate a review. This view does not filter approval baselines or remediation history.

Every successful explicit preserving reset and eligible active packaged-change recovery restarts the five-round review allowance, including repeated Rework resets, unchanged revisions, and exhausted allowances. Package identity, full review history, findings, dispositions, and Rework context are preserved; renewal resolves no findings and grants no approval. Eligibility, destinations, and independent retry limits are unchanged. Count completed approved, addressed, superseded, and zero-finding reviews since the latest established renewal in append order; attempts without a completed review consume nothing. The first five completed reviews may reject into Rework; a sixth rejection enters durable Failed over Rework and retains its round, while a sixth approval may advance under existing approval rules. Automatic transitions and rejections, ordinary retries, restart/resume, grooming recovery, superseded-branch restoration, refused actions, and cancellation do not renew. Renewal belongs to the local logical commit even when projection repair is pending; repair grants no second renewal. A committed pending action reports its operation ID and says not to repeat it: store access finishes the existing operation. Recovery uses Recovery committed; local recovery pending for this outcome. For pre-release retained packages, affirmative historical preserving reset or packaged-recovery evidence, including recorded Rework operator-reset history, establishes the latest renewal. Without such evidence the existing full count remains. Upgrade alone leaves Failed intact. Current-allowance progress can be 0 of 5 or 6 of 5; historical round numbers and order never change and may exceed six. Replacement package identities start independently.

Review-rework handling uses a separate additive held_review_update V1 event stream. A reply update names its round and finding and stores the reply body; a resolve update names its round and finding; and a supersede update names the selected round plus older round IDs. The immutable held_review payload is never rewritten or deleted. A finding is handled once it has at least one reply, whether or not it is resolved, and a round is addressed once every finding is handled. A zero-finding round is addressed immediately.

For an accepted local review, Shipper appends held_review before the ordinary stage-comment and stage-transition events and before writing the advanced issue snapshot. Only after that transaction succeeds does it emit the fixed terminal notice and project the issue-facing stage summary, which names the held round and states that Shipper keeps it internally. A retention error follows the normal failed stage path: the issue does not reach PR Open and no output claims successful retention.

Review retention is the same in every posture. Human GitHub issue and pull-request conversation is still refreshed into the external advisory snapshot. Issue-summary projection and pull-request workflow-label mirroring still follow their existing mirroring rules after a successful local commit.

PR review and implementation rework input and reply delivery

Section titled “PR review and implementation rework input and reply delivery”

With pull requests enabled, revision-pinned review pre-flight reads the exact held-review round named by current review/rework history and every unresolved GitHub review thread with its complete all-author conversation. It does not select or supersede rounds. The acquired round, threads, external advisory snapshot, diff, metadata, and settled or explicitly limited CI record all belong to one verified base/head pair; movement discards the attempt before reviewer launch.

Implementation review rework consumes a round pinned by current-package rework history rather than selecting another round. The run receives only unhandled held findings and supplied unresolved GitHub threads, plus the held round’s one-based ordinal out of five; this is review-round context, not an implementation retry counter. Each target requires one nonblank fix or reasoned won’t-fix reply. A missing round or incomplete replies after one correction fail without advancing.

The existing agent contract does not vary by delivery posture. Shipper adapts the held summary into the existing appended PR <reviews> block, including the structured omitted-diff disclosure when the reviewed diff was reduced. It adapts findings into the existing .shipper/input/review-threads.json array and places them before the GitHub threads. Local comments receive deterministic negative integer IDs, while their stable UUIDs remain internal durable identity. Local findings always use isOutdated: false; Shipper does not infer staleness from the stored reviewed revision.

Every inline thread returned from the pull request follows the local threads unchanged. Those PR threads are external feedback regardless of author, including a stale Shipper-authored thread posted before the repository changed policy. The ordinary issue/PR conversation refresh remains a separate frozen advisory snapshot and is not suppressed.

The agent continues to write each reply as the exact Markdown body in .shipper/output/replies/<numeric-comment-id>.md. Negative IDs resolve only through the current pass’s local origin map and append reply events to the held finding; they never fall back to a GitHub API. Positive IDs post only to their GitHub review comments. Ordered local reply history and resolved state are rendered again on subsequent passes so already handled feedback is not re-litigated. Review-rework GitHub replies carry a stable hidden round/thread/target marker. Shipper records their human body separately, reconciles the marker before posting after a crash, and exposes delivered and resolved external dispositions through review-round inspection. Body equality alone never suppresses a reply.

autoResolveReviewThreads applies to implementation review-rework. Replies are always delivered. When enabled, each successfully handled local finding is resolved locally and each replied-to PR thread is resolved on GitHub. When disabled, neither destination receives a resolution mutation. Resolution failures are best-effort warnings and do not undo replies.

No selectable local round is a successful empty-local-input case. Every local-delivery run prepends an orchestration-owned disclosure to terminal, interim, and crash output naming the selected round (or stating that no unaddressed round was available), the number of findings consumed, and stage-cumulative successful local reply and resolution counts.

After a successful push, Shipper attempts both local and GitHub reply destinations before deciding the pass result. A local reply-recording failure is fatal, distinguishes the completed push from the failed recording, and names each privately preserved draft without logging its body. A local resolution-recording failure only warns.

For result-protocol stages, agents do not transition workflow state directly. The agent writes the result file. Shipper reads that file, resolves the verdict through STAGE_TRANSITIONS, and then applies the transition.

executeTransition() in packages/core/src/lib/output-protocol/protocol-actions.ts commits workflow and failed-state changes to the local issue-state store first. In mirror-on repositories, Shipper then mirrors the exact desired workflow labels to GitHub. store-only repositories keep the local issue-state store authoritative for workflow state and do not attempt GitHub workflow-label projection. A mirror-on projection failure does not roll back the local transition; the recovery path is shipper issue reconcile --repair.

The same result handling records an append-only workflow event before posting the Shipper-authored stage or workflow GitHub comment projection for that stage in mirror-on repositories. The local workflow event remains authoritative in both mirror modes. In store-only repositories, Shipper does not project those issue-side comments to GitHub. If mirror-on comment projection fails after the local event and state transition are committed, the event remains authoritative and reconcile repair can post or patch the GitHub comment later.

Shared agent consumers can also append an issue note with a body and explicit attribution. The required non-blank author identifies the originating agent or role; an optional non-blank stage adds workflow context. A successful append returns the durable note event ID. Retraction accepts that ID and appends a second audit event: an unknown or non-note ID is an error, while repeating an already-completed retraction succeeds idempotently. There is no edit operation or edited-note representation; correction means retracting the old note and appending a new one.

Both operations are store-first. Append commits the note before attempting a mirror-on ## Shipper Note comment, and retraction commits before attempting mirror deletion. Post or delete failure is recorded as projection state and does not change the operation’s local success; a later shipper issue reconcile --repair pass converges the GitHub mirror. A retracted note remains in raw audit history alongside its retraction but is absent from every rendered issue-history context, without a withdrawn marker or placeholder.

In mirror-on, PR-stage labels are mirrored onto the pull request with a separate best-effort gh pr edit call. The local issue-state record is the source of truth for issue workflow state, and store-only repositories do not receive GitHub issue-side projections.

shipper new is the narrow exception to the normal result-file shape because it runs before a GitHub issue number exists. The new agent writes pre-create draft files under .shipper/output/: result.json contains issue_draft, which points at issue-draft.json; that draft contains a structured title and an issue-body.md path. The new draft does not use verdict or comment, and comment is optional only for this stage. Shipper validates the draft with the standard correction retry budget and writes the full title/body to the local issue-state store. In mirror-on, it also creates the GitHub mirror and applies shipper:new there. In store-only, it creates no GitHub issue mirror or issue-side projections and returns the local S#<localId> identity as the final created_issue.

Grooming still updates issue content and workflow comments as part of its workflow. Title/body edits are store-first and mirrored to GitHub only in mirror-on; Shipper-authored grooming comments are recorded in the local workflow event log before their mirror-on GitHub comment projection. Those edits are separate from the result-file transition contract used by downstream stages. Before the groom agent starts, Shipper writes local scan context under .shipper/input/; the agent writes only final protocol outputs under .shipper/output/.

Groom orchestration, not the grooming agent or cross-issue scan, prepares one frozen github-pr-overlap-snapshot with advisory-only authority. It evaluates every open pull request in the same repository, including drafts and pull requests targeting non-default branches. GitHub’s open-state result excludes closed and merged pull requests. Retained evidence contains only number, title, link, draft state, source and target branches, exact matching paths, reported rename pairs, and capture time. It excludes authorship, review state, checks, labels, diff content, and raw API rows.

Eligibility comes only from recognizable repository-relative individual files explicitly named in the issue’s first Starting Point section. Inline code, relative Markdown-link destinations, and bare tokens can name a file. Optional :line[:column] and #Lx[-Lx] references do not affect the path. Directories, globs, URLs, absolute or backslash paths, traversal segments, missing files, and prose that does not name an inventory file are excluded. Shipper neither expands a directory or glob nor adds files found during later exploration. Comparisons are exact and case-sensitive: prefix, basename-only, fuzzy, and case-folded similarities do not match. For a reported rename, either the old or new path can match and the evidence always identifies both.

Every snapshot has one of four truthful coverage states:

  • complete — acquisition finished, including the valid case where no overlaps were found;
  • not evaluable — the issue has no eligible Starting Point files, which is not a no-overlap result;
  • unavailable — inspection could not begin or produce a usable first page, with the reason; or
  • partial — all known overlaps plus every acquisition limitation and reason.

Later-page failures, malformed rows, and GitHub’s 3,000-file pull-request endpoint ceiling produce partial evidence rather than hiding earlier matches. Effective transport: none or pullRequests: none policy prevents inspection before origin or pull-request calls and produces an unavailable result with the policy reason. Unresolved policy or origin, reachability, quota, and bounded API failures likewise become unavailable or partial evidence without stopping grooming. Permitted mirror-on and store-only grooming receive the same snapshot shape; this narrow read-only GitHub inspection does not change store-only issue-lifecycle authority.

A fresh grooming session captures once. Ordinary resume, question answering, correction, takeover without refresh, next, automatic grooming, ship, and auto-ship reuse the active snapshot unchanged. Only direct shipper groom S#395 --refresh-pr-overlaps or shipper_groom({ issue: "S#395", refresh_pr_overlaps: true }) can replace it, and only when the named issue already has a valid saved grooming session. Missing or inapplicable sessions refuse before hooks, worktree preparation, run history, GitHub inspection, or workflow mutation. An authorized takeover may accompany refresh; ownership is persisted before acquisition.

Refresh recalculates eligible paths from the current issue body and makes every truthful result—even partial, unavailable, or not evaluable—the active snapshot. It preserves the transcript, pending batch, grooming notes, answered and maximum round counts, owner, creation time, original issue fingerprint, and prior product decisions. The receipt reports the new capture time and coverage, the immediately prior capture as superseded, and sorted added and removed eligible paths. Only that one-hop supersession is retained. Material old evidence already recorded in the transcript or notes remains historical decision provenance with its original capture facts; it is not current evidence.

Every overlap remains advisory. Detection alone cannot block or sequence the issue, remove scope, or change a requirement. Grooming asks the product owner only when an overlap could plausibly affect a premise, requirement, or delivery order. If evidence materially informs a decision, the grooming record discloses provenance, capture time, and advisory standing. The final summary retains only overlaps that prompted a product question or materially informed a decision; benign overlaps remain orientation-only, and pull requests never appear under Related Issues.

The rendered snapshot is normally inline prompt context. Above the shared context-size threshold, Shipper writes the complete block to the conditional, gitignored runtime file .shipper/input/groom-pr-overlap.md and supplies a pointer. That spill is absent from a clean worktree, is not always present, and is not the durable source of truth.

An open groom manifest can declare independent ungroomed siblings with an optional top-level followups array. It can also declare a separately groomed prerequisite in a top-level prerequisites array and reference it from a parent or child blocker. A follow-up does not block the origin; a prerequisite does. Both are normal-priority shipper:new issues and use the same grooming-filed request body.

A prerequisites declaration is available only after the product owner explicitly directs the creation during the authoritative current grooming session. The agent may suggest filing one, but a stage’s blocker report, its own suggestion, and any historical prerequisite claim or determination never supply that authority. Creation and blocker attachment travel together: the same accepted manifest that declares the prerequisite must also reference it from the complete ordinary blocker set of the issue it blocks.

A follow-up declaration is available when the origin remains open, with or without children, and when a full decomposition replaces the origin:

{
"followups": [
{
"title": "Concise related follow-up",
"body_file": ".shipper/output/followup-s42-1-body.md"
}
]
}

Each follow-up and prerequisite body is a plain request rather than a groomed issue:

# Request
<The unchanged plain request.>
This work was identified during grooming of S#<origin> and is separate because <brief split rationale>.

Keep # Request as the unchanged concise request. Follow it with one or two unheaded sentences that name the originating S#<origin> issue and explain why the work is separate. Do not add an inherited-context heading, advisory marker, relevant-context list, provenance fields, or empty-state text.

A decomposition child’s grooming comment is a scoped grooming summary with a parent back-reference and any settled decisions relevant to that child’s scope, written as ordinary prose. It does not include structured origin, date, or record-kind provenance rows.

Each declaration entry has exactly title and body_file. The first non-empty body line must be # Request. For follow-ups, level-one # Requirements and # Acceptance Criteria headings and Shipper’s reserved attribution markers are invalid. Both the concise shape and historical inherited-context bodies remain accepted without migration because validation remains limited to the established leading-heading and forbidden-heading rules. Duplicate and not-planned closed outcomes cannot declare follow-ups. Shipper loads and validates every declared file before any post-flight write, so an invalid manifest creates no follow-up and does not mutate the origin.

Only product-owner answers obtained through the current grooming session’s question mechanism settle product decisions. Decisions recorded in another open issue, workflow note, or other outside record are advisory: they may inform orientation and questions, but they never bind the result. When one is needed to settle the current result, ask an ordinary question in the current session. Shipped behavior is different from a recorded decision and remains a current fact; # Request is authoritative only for the unchanged record of what was asked.

After prerequisites succeed, Shipper creates follow-ups sequentially through the ordinary request path. Each starts as an unblocked, normal-priority shipper:new issue with no explicit priority label, blocker, or decomposition relationship. Shipper appends a stable local-ID attribution to the stored request before creating any optional GitHub mirror:

<!-- shipper:follow-up-attribution:start -->
---
> **Shipper follow-up:** Filed during grooming of S#42.
<!-- shipper:follow-up-attribution:end -->

The marked body is byte-identical in the local authority and GitHub projection, is visible in the follow-up’s later grooming context, and is preserved exactly once when that grooming replaces the request with a groomed body. Shipper also appends an actual-created-identity section to the origin’s grooming summary:

## Follow-up Issues Filed
- S#51: Concise related follow-up

These rows come from successfully created local records rather than agent prose or manifest indexes. If creation fails, post-flight stops before changing the origin’s content, stage, priority, blocked state, children, or closure; the origin remains unfailed and retryable. Follow-ups created earlier in the same run are retained and reused by exact attributed request on retry instead of being filed again.

Persistent settings use one least-to-most-specific order: built-in defaults -> user-global -> committed repository -> per-repository personal. The most-specific eligible value wins once. See Settings for per-setting source eligibility. Eligible nested commands and merge values participate in the same settings reduction.

Shipper-dispatched sessions for groom, design, the adversarial design review, plan, implement, and pr_review default to MCP loading disabled. Assisted setup (setup) and issue intake (new) default to MCP loading enabled.

One precedence rule decides MCP policy, with no stage exception: a one-run flag beats all settings, an explicit commands.<stage>.disableMcp beats an explicit commands.default.disableMcp, and an explicit value at either scope beats the shipped default. An explicit default-scope value therefore reaches all eight prompt-running stages in both directions — commands.default.disableMcp: true disables setup and new, and commands.default.disableMcp: false enables groom. The adversarial design review resolves its policy through design.

This policy governs only the sessions Shipper spawns. Sessions an operator launches from their own agent CLI, editor, or external orchestrator are unaffected, and the set of tools Shipper’s MCP server exposes is unchanged.

The --disable-mcp and --enable-mcp flags are one-run invocation overrides, not persistent settings layers. Either flag overrides the stored MCP policy for that invocation only.

When MCP loading is enabled, Shipper logs:

MCP loading enabled for stage <name>.

When MCP loading is disabled, Shipper prints no MCP line and instead injects the empty MCP configuration or disable flags appropriate to the selected agent. For setup or tool-loading symptoms, see MCP tool loading issues.