Skip to content

State Machine

The six pipeline names are Design, Plan, Implement, Review, Rework, and Ready. They describe the next work, derived immediately from existing records without migration or reprocessing:

Completed stageNext step
shipper:newGroom, in the separate New attention area
shipper:groomedDesign
shipper:designed, simple routeImplement
shipper:designed, complex or undecided routePlan
shipper:plannedImplement
shipper:pr-open, with or without a pull requestReview
shipper:reworkRework; implementation runs next
shipper:readyReady for final integration

Ready is not shipped and does not prove that a merge is currently eligible. A rejected simple implementation that escalates to complex appears in Plan. Auto-ship says Review in either PR mode; Rework names the workflow position while running activity still names implementation.

CLI/MCP retain six next-step groups. For the same issue population, the five desktop columns are Design · Plan · Implement · Review · Ready: desktop Implement equals CLI/MCP Implement plus Rework. New stays in a separate attention area. Closure-held and failed issues are excluded; closure hold takes precedence over failure. Blocked, locked, operator-paused, and running issues retain their route-aware column membership. Attention counts overlap those memberships. MCP blocked rows remain in their next-step groups with blocker and ownership details; only stageless or unknown-stage blocked rows use Blocked attention — no next-step group.

Current open responses add nextStep, and status adds nextStepPipeline. Existing completed-stage pipeline JSON fields, workflow labels, status filters, commands, and reset arguments keep their meanings. Closed records have no future next step, and historical records are not rewritten.

Maintained GitHub label descriptions retain completed-work meaning and add next-step guidance. New repositories receive them during setup; existing repositories automatically refresh differing descriptions on their next normal label setup or maintenance, without separate initialization. A successful catalog cache skips repeat maintenance; a changed catalog or force refresh reads again. Existing colors and unrelated labels are preserved. Read-only status, list, and view make no label writes.

Reset choices, previews, successes, and refusals lead with the next step, then the literal completed-stage target and any route clearing. An eligible Design drop targets groomed and clears the route to undecided. An eligible Plan drop targets designed for complex or undecided routes; a simple-route Plan drop is refused with the route unchanged because designed would next Implement. An eligible Implement drop selects preserving rework when offered by the existing reset options. Otherwise it targets designed for simple and planned for complex or undecided. The existing main-process preview validates the package; missing, stale, closed, merged, unreachable, or unverifiable evidence refuses confirmation without falling back to destructive cleanup. To deliberately discard work, choose Start over — in the card Reset menu; the existing cleanup confirmation applies. The Plan column is different from --to planned, whose next step is Implement. A successful --to designed says Implement for simple and Plan for complex or undecided. New (--to new) also clears the route.

These translations do not override backward-only eligibility, failed-stage retry, preserving Rework exceptions, active-work restrictions, confirmation, destructive work-loss warnings, or work-preservation guarantees. Review and Ready are not drag-reset destinations. A non-failed same-column drop does nothing, including Rework already in Implement; repeat Rework menu resets remain real preserving operations. Dragging cannot force a route change beyond Design’s existing route clearing.

Shipper’s V7 issue-state store records workflow stage, priority, failed state, and any external closure hold. GitHub issue labels can mirror that state for people and integrations when issue projection is enabled. Each initialized local record has one workflow stage at a time, plus control and priority fields that project to zero or more GitHub labels in mirror-on repositories.

LabelDescription
shipper:newNew issue, awaiting grooming
shipper:groomedProduct-groomed, awaiting design
shipper:designedDesign-reviewed; next Implement (simple), Plan (complex or undecided)
shipper:plannedImplementation planned, awaiting coding
shipper:pr-openImplementation packaged; next Review, in either PR mode
shipper:reworkPackaged change preserved; implementation re-delivers it
shipper:readyReview accepted; next final integration

The complete managed label set consists of the seven workflow labels above, shipper:blocked, shipper:failed, shipper:priority-high, and shipper:priority-low. If that set drifts from the local store mirror, run shipper issue reconcile to inspect its exact extra and missing labels and shipper issue reconcile --repair to repair it. Check output identifies this as managed label-set drift. For example:

For one compatibility release, raw shipper:implemented is treated as Rework and raw shipper:pr-reviewed is treated as PR Open. Reads preserve either raw label; the next ordinary transition replaces it with the current destination.

managed label-set drift (extra: shipper:new; missing: none; repair direction: store -> GitHub)

Repair output names the Managed label-set drift repaired issue count. For older GitHub-only label problems, see missing or duplicate workflow labels. Manual GitHub edits to known managed labels are overwritten from the local store; ordinary labels and unknown labels with a shipper: prefix are neither reported nor changed. The obsolete shipper:locked label is also ignored and preserved. The obsolete label has no lock meaning, does not itself cause repair refusal, and may be deleted manually. The desired managed set converges in one operation, so an extra stage or opposite priority label is removed while a missing label is added. Blocked state uses that same convergence path, while canonical blocked/unblocked workflow comments are repaired separately. Issue locks remain machine-local filesystem coordination rather than GitHub-owned state.

Numeric snapshots in issues/<GitHub-number>.json or the historical pending-import directory refuse the entire store unchanged. Use a Shipper release from before this change to migrate it before retrying this version; no specific release is promised. Supported local-ID V3 upgrades and first-reconciliation blocked-state imports for fresh or recreated stores, including interrupted imports, remain available. See retired snapshot recovery.

LabelDescription
shipper:blockedIssue has unmet dependencies. Prevents advancement (except new -> groomed).
shipper:failedOperator action is required. Blocks next and auto-selection until reset or eligible preserving recovery clears it.
LabelDescription
shipper:priority-highHigh-priority issue. Processed first by auto-ship.
shipper:priority-lowLow-priority issue. Processed last by auto-ship.

Normal priority is the default when neither label is present.

When a mirrored GitHub issue closes while its saved stage is between shipper:new and shipper:pr-open, or at shipper:rework, Shipper preserves the active V7 record and enters an operator closure hold. A legacy reviewed label receives the same protection as PR Open. The hold is local authoritative state, not a GitHub label. It retains the stage, failed, blocked, priority, linked-PR, and blocker fields, so a failed issue can also be held without appearing twice in attention lists.

Evidence records the GitHub close reason, actor when available, the closing pull request or commit when GitHub supplies one, whether a closing PR is the issue’s own linked PR, and whether attribution is stable or retryable. Closing by the issue’s own merged PR before Ready still enters the hold; it does not skip review. Missing or ambiguous attribution remains retryable and can be enriched by a later refresh without producing another first-entry notification.

While held, Shipper refuses groom, ship, next, merge, change recovery, reset scan/execute, auto-ship, and Desktop drag/drop before side effects. Issue details and comments remain readable, and priority, block, ordinary unblock, and force-unblock remain available. Choose exactly one resolution; there is no default:

  • Not planned accepts withdrawal after showing a token-bound dependent recovery preview. It writes not-planned history and runs the recovery rules below without releasing blockers.
  • Reopen and resume requires GitHub to confirm the mirror is open, restores the saved workflow, failed, blocked, and priority labels, then clears the hold. A remote failure leaves the local snapshot unchanged.
  • Complete asserts that the work shipped. It requires a fresh preview-bound confirmation token, names every dependent that would be rechecked (or explicitly says there are none), then completes the issue and releases eligible dependents.

If someone externally reopens the issue first, refresh or reconcile restores projected labels and clears the hold automatically. shipper:ready remains outside this hold path: an externally merged stored PR follows Ready completion, while other deliberate closure handling remains governed by its existing terminal rules.

Every failure path makes two independent decisions:

  1. Issue disposition: whether the issue enters durable shipper:failed or retains its current workflow stage.
  2. Repository safety disposition: whether the occurrence counts toward the repository-wide auto-ship safety stop.

Durable shipper:failed is allowed only when an operator must act before the issue can progress and a recovery that preserves already-completed work exists in every repository posture where the path is reachable. If either condition is false, the issue retains its current workflow stage, is skipped for the rest of the current run, and remains eligible for selection in a later run without any operator command.

A leave-in-place outcome may carry an explicit null repository-safety decision and remain neutral. A failure path may instead carry a repository-scoped designation when the repository, rather than the individual issue, needs the circuit breaker. That designation deliberately and explicitly records that the occurrence counts toward the safety stop even though the issue retains its workflow stage. It is never inferred from the issue disposition or from diagnostic text.

Every failure path must explicitly decide both axes. A path may not reach durable failure, leave-in-place, or safety-stop behavior by falling through an unclassified default.

The published qualifying safety-stop identifiers and their observed outcomes are:

  • terminal-stage-failure — a terminal stage, merge, worker, or Desktop command-result infrastructure failure.
  • merge-retry-exhausted — the automatic merge retry ceiling is reached while the issue retains its stage.
  • pending-checks-exhausted — the configured pending-check window expires and its durable report is saved.
  • groom-output-invalid — Groom accepts without the required valid manifest, so the issue remains recoverable at its current stage while the repository circuit breaker counts the occurrence.
  • worktree-setup-contention — the third consecutive automatic setup contention leaves the issue at its current stage and records one stable repository-safety condition; earlier occurrences are silent and neutral.
  • upstream-merge-wait — concurrent parked not-attemptable Ready pull requests share one repository-scoped occurrence for that upstream wait. The first park contributes the condition; later parks add members without a new strike. The occurrence closes only after every included pull request becomes attemptable, merges, or changes head. The issues remain Ready, and no agent stage runs.

Historical safety records may contain merge-remediation-exhausted. Readers preserve that token and its original meaning, but no current producer emits it and it is not a current qualifying choice.

Every producer supplies either literal null or one of those repository-scoped classifications. Omission is invalid at runtime sidecar and IPC protocols and is a compile-time error on typed stage and ship paths. Missing information never supplies an issue disposition or repository-safety decision.

The exempt outcomes are active merge retries, review/Rework routing, active pending-check deferrals, intentional parked work, operator cancellation or pause, returned-for-grooming outcomes, stage-entry refusals, selection-time Ready exclusions, and manual runs. Blocked entry refusal and a blocked post-work stop both carry explicit null and are neutral. When the post-work stop also reports successful advancement, that advancement increments the repository progress generation before the neutral blocked disposition is processed. A later observation of an existing condition can therefore accrue as a post-progress recurrence; the earlier strike remains durable. Other exempt outcomes record neither progress nor a qualifying condition and leave the safety ledger unchanged. A pending-check deferral qualifies only when its configured window expires. An exempt outcome may still be a durable per-issue failure and produce a nonzero command exit; repository safety disposition is independent of issue disposition and process status.

S#232 removes the unused exhausted per-issue stage-retry class as an explicit amendment to the taxonomy published by S#233; no runtime path produced that class. The rest of S#233’s taxonomy remains unchanged.

This contract is S#232’s explicit amendment to, and supersedes, S#53’s blanket rule that every no-verdict stage crash enters failed state after retries and S#149’s blanket rule that exhaustion of either merge ceiling is durable failure. The lane-specific behavior described elsewhere on this page applies this governing rule.

The general stage-scaffold retry budget is three attempts and applies only to non-zero agent exits from non-interactive design, plan, implement, and PR review runs. An attended operator cancellation is terminal for that command and is never retried. Groom has no general stage retry budget. Setup and orchestration exceptions do not inherit the agent-exit budget. Specialized merge, content-validation, GitHub-transport, and integration loops are independent mechanisms with their own boundaries.

Each scaffold attempt starts from an attempt-local workflow-branch checkpoint. If either the initial agent invocation or an output-correction invocation times out after changing the committed HEAD, Shipper keeps that committed tip for the next attempt. Any other agent exit resets only the commits made since that attempt began, so commits preserved from an earlier timeout survive a later crash. Attended cancellation is the exception: it retains the complete committed HEAD for a later invocation while forced worktree teardown discards staged, unstaged, and untracked changes.

This branch-state rule is uniform across design, plan, implement, and PR review, although it is a no-op when an attempt makes no commits. Design and plan normally reset a reused workflow branch to the resolved base before their first attempt. A branch retained by cancellation is reused without that reset when its stage and transition evidence still match, and the next agent receives continuation context. An unrelated reset or stage transition, or a moved branch, disables continuation. Only explicitly validated continuation-lookup failure/recovery groups are transparent to that rule. Implement and PR review otherwise leave the branch unchanged before their first attempt.

When the attempt budget is exhausted, any commits preserved from a timeout remain on the workflow branch. The durable failure reports the branch and its current full commit ID so the operator can inspect that state before choosing recovery or reset. Attempt counts and downstream result, review, verdict, pull-request-content, and continuous-integration gates are unchanged.

Groom, design, plan, and implement expose one stageExecutionOutcome on every unsuccessful run. A leave-in-place outcome is unsuccessful, excludes the issue for the rest of the current run, retains its workflow stage, remains selectable in a later invocation, and is neutral for the repository safety stop. A durable-failure outcome commits shipper:failed to the local store before best-effort GitHub comment and label projection, names the operator action, and is terminal for repository safety.

Preparation uses positive evidence rather than a broad “network” guess. DNS, connection, transport-shaped timeout, unreachable-network, TLS/proxy, HTTP 408/429/5xx, and equivalent registry or fetch evidence leave the issue in place. Operator cancellation during a blocking preparation hook also leaves the issue in place. A blocking hook’s own timeout is not transport evidence; like a proven missing base or workflow ref, externally checked-out branch, refused worktree reset, missing install executable, broken manifest/lockfile/configuration, or unknown nonzero preparation command, it is durable. This classification does not add retries.

A GitHub read that fails before the stage mutates state leaves the issue in place regardless of which read site failed. A configured model or reasoning effort rejected by the selected agent does the same: correcting the setting lets a later invocation select the stage without resetting the issue. Once durable failed state exists, ordinary groom, next, or ship commands never clear it; recovery must be explicit.

Implementation delivery also has an explicit pr-creation-unconfirmed leave-in-place outcome. After GitHub reports that a pull request already exists, or after an ambiguous protected create result, Shipper performs a guarded confirmation restricted to an open, same-repository pull request whose head is the packaged branch and whose base is the attempted base. An eligible candidate follows the normal implementation-to-PR-Open success path, including the ordinary association record, without a recovered/adopted distinction.

If that confirmation cannot be verified or returns no eligible candidate, a pull request may already exist but could not be confirmed. The issue stays at its current stage, does not enter failed state, and remains eligible for an ordinary later run. The outcome carries literal null repository safety: repeated occurrences neither record a qualifying condition nor advance the repository progress generation and never escalate through a per-issue consecutive-unconfirmed counter. The existing protected outage allowance and repository safety stop remain the only backstops. Confirming-read exhaustion uses exit 79; a completed no-candidate result or other unverifiable confirmation uses exit 1. Both are unsuccessful and require no reset or recovery command. Once the outcome is classified as unconfirmed, Shipper does not close, edit, or recreate a pull request. Branch publishing and the attempted create may already have changed GitHub.

Validated accepted work survives later advisory-note cleanup failure. Deterministic orchestration faults instead become durable: a missing mirror under mirror-on policy, missing or unrecognized workflow label, successful stage whose label did not advance, or exhausted transition cap.

Operator cancellation is a first-class leave-in-place outcome only when an interactive TTY agent session actually started. Prompt resolution, setup, or spawn failures cannot claim cancellation. After the attended process returns normally, closes its tab, or quits the application, Shipper validates the complete result artifacts exactly once. Complete valid artifacts remain authoritative and finish the stage normally even when the process exit was nonzero. Missing, partial, unreadable, or invalid artifacts produce one typed operator-cancelled result and exit 82.

Cancellation retains the workflow stage from session entry, records literal null repository safety, creates one neutral operator_cancellation workflow-history event, and creates no failed state, correction pass, agent retry, recovery bundle, or recovery instruction. Saved headless groom question state is not consumed and remains resumable. This is neither a successful completion nor the resumable exit-75 pause; it is also distinct from a supervisor-requested process stop.

For design, plan, implement, and PR review, committed branch work survives cancellation and is offered to a later matching invocation with continuation context. Uncommitted work is discarded when the worktree is torn down. shipper groom --auto, shipper next, and sequential shipper ship start no later issue or stage after cancellation. Parallel automatic shipping stops refilling work and drains only attempts that were already running.

S#352 explicitly amends S#251. Bounded correct-and-revalidate handling and the counted groom-output-invalid durable failure remain the contract for non-interactive runs; neither applies to an attended operator cancellation.

When Shipper exhausts its clean GitHub-outage waiting allowance before committing a stage outcome, the command exits 79. No failed state is committed: the issue retains its workflow stage, is skipped for the rest of the current run, and can be selected normally by a later run. This leave-in-place outcome is neutral for the repository safety stop and does not advance its consecutive-failure count. Exit 79 is still an unsuccessful command result; it distinguishes this retry-later outcome from both success and durable failure.

Work-preserving recovery by repository posture

Section titled “Work-preserving recovery by repository posture”

Preserved groom output is posture-independent: every row below can use shipper recover S#123, MCP shipper_recover, or Desktop Recover change. Shipper validates the stored tree first and runs at most one unattended correction without restarting discovery or re-asking answered questions.

Repository postureRecovery from durable shipper:failed
GitHub-backedUse shipper recover S#123 or Desktop’s Recover change for an open stored pull request or a reachable pinned local change retained from an earlier posture. Recovery preserves completed local review rounds. For a stored PR confirmed closed without merging, use shipper issue reconcile --repair to preserve the implementation branch and return the issue to shipper:rework; shipper next then delivers the replacement PR. Use reset only when recovery is ineligible or its cleanup is acceptable.
Remote git without pull requestsUse shipper recover S#123 or Desktop’s Recover change for a reachable pinned local change or an open stored pull request retained from an earlier posture. A pinned revision remains eligible when its source branch has advanced, provided the revision is still reachable. Use reset only when recovery is ineligible or its destructive cleanup is acceptable.
Fully localUse shipper recover S#123 or Desktop’s Recover change for a reachable pinned local change or an open stored pull request retained from an earlier posture. Recovery preserves the packaged record, prior review rounds, branches, worktrees, and workflow history while returning the issue to Change Packaged. Use reset only when recovery is ineligible or its destructive cleanup is acceptable.

shipper:failed and an external closure hold are distinct durable operator-attention states. Eligible groom/package recovery, reconcile recovery where applicable, or an acceptable reset clears failed state. A closure hold is cleared only by its Not planned, Reopen and resume, or Complete resolution (or the equivalent confirmed external reopen); reset never clears a hold.

Every path that records a Shipper-managed blocker as not planned—direct close, held-closure resolution, and duplicate or not-planned grooming—evaluates open dependents whose stored blockers reference either the target’s Shipper local ID or its mapped raw GitHub issue number. Duplicate references identify one dependent and produce one disposition.

The impact preview and applied report use these stage rules:

Dependent stateRecovery disposition
shipper:newNo action. The issue stays at New and remains eligible for grooming.
shipper:groomed through shipper:plannedReturn to shipper:new.
shipper:pr-open, shipper:rework, or shipper:readyRemain at the current stage because a packaged change is in flight.
Failed at any workflow stageRemain failed at the current stage; later clearing failed state does not revive this recovery.
Active issue lockDefer durably and reclassify after that run releases its lock.
External closure holdDefer durably until the hold is resolved by reopening; terminal hold resolutions discard the pending return.

A return to New preserves the complete blocker set byte-for-byte and leaves shipper:blocked authoritative. It also preserves failed and closure-hold fields; the movable classification requires those fields to be clear before the transition. Clean recovery returns consume no failure, crash, or retry budget. A raw legacy reviewed label follows the PR Open row without being rewritten by the read.

Deferred recovery is derived from unmatched not_planned_dependent_recovery workflow events, not from snapshot flags. Each queued event names the not-planned target and the reason for waiting. Issue-lock release and hold reopen automatically drain the relevant issue; repository auto-ship selection also sweeps outstanding events. Draining re-reads current state, combines multiple queued targets into one reset, and writes a resolved history record. If the dependent became failed or entered a PR stage, it remains there with that durable reason. If it closed, the pending work is discarded durably.

Recovery deliberately does not decide that the not-planned blocker is satisfied. The dependent remains blocked so grooming can decide whether to retain, replace, or release that prerequisite. Completed closure still follows the existing dependent-release path, and ordinary unblock and operator-only force-unblock behavior—including clearing all overridden blockers and its audit comment—are unchanged.

stateDiagram-v2
  direction LR

  state "shipper:new" as New
  state "shipper:groomed" as Groomed
  state "shipper:designed" as Designed
  state "shipper:planned" as Planned
  state "shipper:pr-open" as PrOpen
  state "shipper:rework" as Rework
  state "shipper:ready" as Ready
  state "operator closure hold" as Held
  state "issue closed" as Closed

  [*] --> New
  New --> Groomed: groom
  Groomed --> Designed: design accept
  Designed --> Planned: plan accept
  Planned --> PrOpen: implement accept
  Rework --> PrOpen: implement accept
  Designed --> PrOpen: implement accept (simple route)
  PrOpen --> Ready: derived pr-review accept
  Ready --> Closed: PR merged or observed merged
  New --> Held: external GitHub closure
  Groomed --> Held: external GitHub closure
  Designed --> Held: external GitHub closure
  Planned --> Held: external GitHub closure
  PrOpen --> Held: external GitHub closure
  Held --> Closed: not planned or confirmed complete
  Closed --> [*]

  Groomed --> New: design reject
  Designed --> Groomed: plan reject
  Planned --> Designed: implement reject
  Rework --> Designed: implement reject
  Designed --> Designed: implement reject (simple route, escalates to complex)
  PrOpen --> Rework: derived pr-review reject (every route)
  PrOpen --> Rework: reconcile closed-unmerged recovery
  Ready --> Rework: reconcile closed-unmerged recovery

  state "Control labels" as Controls {
    state "shipper:blocked" as Blocked
    state "shipper:failed" as Failed

    [*] --> Blocked: dependency found
    Blocked --> [*]: unblock accept
    [*] --> Failed: classified durable failure
  }

  Failed --> New: reset --to new
  Failed --> Groomed: reset --to groomed
  Failed --> Designed: reset --to designed
  Failed --> Planned: reset --to planned
  Failed --> Rework: reset --to rework (with packaged evidence)

  note right of Blocked
    Blocks advancement except new -> groomed.
  end note

  note right of Failed
    Requires operator action and a posture-valid, work-preserving recovery.
    reset can target new, groomed, designed, planned, or rework;
    if failed state still carries a workflow stage, that same stage is a recovery target.
  end note

  note right of Held
    reopen exits the hold and restores the saved stage.
  end note

Every issue carries a durable route: undecided, simple, or complex. The route decides which stages the issue passes through after design.

Design assigns the route. Every accepted design result must declare a route and a nonblank one-line reason. A design result that omits either is invalid stage output — the design stage’s bounded correction pass retries with feedback naming what is missing, and exhausting it fails the issue with the ordinary operator guidance. There is no setting that disables routing and no silent default. Route data on a rejected or failed design result is ignored; only an accepted design changes the route.

Design defaults to simple and routes complex only when an exclusion clearly applies. A clear exclusion needs specific evidence tied to its rule: named artifacts, an identified seam or new test infrastructure, or a concrete open question or alternative. General uncertainty is not an exclusion. Only a rejected combined implementation permanently escalates to complex; findings after an accepted implementation follow normal review and implementation rework. The exclusions are:

  • Production code changes span more than one package or component, or alter a cross-package contract. Documentation, tests, fixtures, and generated reference output do not count toward this: one production package plus its docs and tests is still one package.
  • It changes durable saved workflow data, how stages exchange results, Shipper’s bundled stage prompts, user-visible configuration or stages, or the set of available agent tools. Runbooks, guides, and other documentation are not stage prompts.
  • It adds a dependency or changes build or continuous-integration configuration.
  • The expected change is large: many files, a new architectural seam, or coordinated edits whose ordering matters.
  • Verifying it requires new test infrastructure. Post-merge, live-environment, or operator acceptance steps do not exclude simple: neither route performs them, and PR review applies equally to both.
  • Design cannot state the implementation approach in a few sentences, or would leave open questions or alternatives to implementation.

Routes never revert to simple. An undecided route may become either simple or complex, and simple may escalate to complex, but complex never returns to simple. A design result declaring simple for an issue already stored as complex is not accepted, and the correction feedback names the stored route.

What each route means for the stage sequence. A complex-route issue runs the full chain: shipper:designed to shipper:planned, then one implementation-and-delivery step to the review-entry state. A simple-route issue never records shipper:planned. It runs one combined implementation-and-delivery step from shipper:designed; on acceptance it moves straight to the review-entry state, and on rejection it stays at shipper:designed while its route escalates permanently to complex, so the next dispatch runs planning.

Where the decision is recorded. The accepted design review states the route and its reason under an ## Issue Route heading, in the same place on every issue. The authoritative audit trail is the ## Issue Route Changed entry in the issue’s workflow history, written whenever an accepted design changes the stored route.

A ready issue also reaches the same completed tombstone when Shipper observes that its stored linked pull request was already merged outside Shipper. This recovery is ready-only: an externally merged pull request linked to shipper:pr-open does not skip the remaining review stages. Shipper performs no merge or remote-branch deletion on the recovery path, but it does run the same guarded local branch cleanup and immediately re-check blocked dependents. Resetting first moves the issue out of ready and clears a closed or merged PR association, so later external-merge recovery is intentionally no longer eligible.

There is a separate reconcile recovery edge for a linked pull request positively confirmed closed without merging. At shipper:pr-open, shipper:rework, or shipper:ready, shipper issue reconcile --repair may move the issue backwards to shipper:rework, clear the dead association, and clear failed state. Recovery requires an open or absent issue mirror, the exact surviving PR head ref, no active issue lock, and lock-time stage/link revalidation. It preserves the implementation branch; the next shipper next runs implementation from that branch.

This differs from ready externally-merged completion: a merged ready PR completes the issue, while a closed-unmerged PR returns surviving work to implementation. It also differs from manual reset cleanup. Reconcile recovery does not scan or delete artifacts, synthesize or delete a branch, adopt a replacement PR, close or mutate a PR, or rerun implementation.

For result-protocol stages after grooming, workflow state is transitioned by Shipper from the agent’s result file. The agent writes result.json; Shipper resolves the verdict to a target stage, commits that stage to the local issue-state store first, then mirrors the desired workflow labels to GitHub when issue projection is enabled. If the GitHub mirror fails, the local transition remains committed and shipper issue reconcile --repair is the recovery path. For example, after a successful design the store stage changes from shipper:groomed to shipper:designed; a mirror-on repository then mirrors GitHub to the same label set:

Terminal window
gh issue edit <ISSUE> --add-label "shipper:designed" --remove-label "shipper:groomed"

A returned fail verdict uses that result-file transition path. No-verdict crashes and exceptions are classified under the failure outcome contract, not by one universal retry rule. Design, plan, implement, and PR review receive the scaffold’s three attempts for non-zero agent exits. Groom has no general stage retry budget, and setup or orchestration exceptions do not consume or inherit that agent-exit budget. Specialized merge, content-validation, GitHub-transport, and integration loops remain separate.

When stage execution records durable failure, Shipper commits the authoritative local failed state before attempting the failure comment and failed-label projection in mirror-on mode. Either GitHub projection can be repaired later without undoing that local state. A merge failure that successfully routes the issue to Rework or PR Open is a normal workflow outcome and does not enter failed state.

Agent timeouts follow the same rule for direct and MCP-started issue stages. The configured limit is per agent attempt, not per stage call; scaffold retries, grooming correction, question waits, projection, run terminalization, cleanup, and ownership-safe lock release remain outside that attempt timer. A terminal timeout records agent_timeout history with the stage, limit, exhausted attempts, available session log, exact reset target, and either no preserved commit or the workflow branch plus full preserved revision. It always records that staged, unstaged, and untracked work was discarded. Mirror projection debt is repaired with shipper issue reconcile --repair without rolling back failed state or retaining the run lock.

Ready still has deterministic prerequisites. With pull requests disabled it requires a packaged local change; with pull requests enabled it requires an open linked PR or an externally merged PR that can be completed. Mirror-on also requires a GitHub issue mirror. A missing package, mirror, association, or PR record records one durable failure with exact reset, PR-open, adopt, or reconcile guidance and is not selected by a later queue/run. A temporary linked-PR lookup failure is an untimed run skip instead.

Merge handling distinguishes whether Shipper obtained a durable determination about the pull request:

Action or deferralCausesWhat happens next
retryBusy merge queue, local repository contention, temporary GitHub failure, or rate limitThe issue remains Ready. Auto-ship retries after per-issue delays of 30, 60, 120, then 240 seconds. Attempt five skips it for only the current invocation/enabled run; no failed state is persisted.
reworkMerge conflictHandling returns the issue to Rework with a current-package conflict record. The next dispatch runs implementation in the merge-rework situation.
reviewFailed checks, branch restrictions, branch still behind, failed branch update, or a determined unknown failureHandling returns the issue to PR Open (Change Packaged without PRs). The next dispatch runs normal review pre-flight.
fail-nowNo usable repository merge method or a non-fast-forward/fetch-first local-base publication rejectionThe issue enters durable failed state immediately with concrete repository-setting or reconciliation guidance.
Selection-time excludedCLI auto-ship in a pull-request-disabled repository finds a Ready issue without a packaged local changeThe issue enters the same durable failed state as the attempted prerequisite path, but no attempt, worker, worktree, log, token accounting, or merge starts. The run exits 1; the outcome records neither a qualifying safety condition nor repository progress.
Deferred: checks pendingGitHub reports one or more CI checks still runningThe issue remains at its current stage. Only this deferral owns a SHA-keyed deadline. Single-issue CLI runs and targeted or one-shot merges exit 78; --auto runs still exit 0. Desktop records Deferred.
Deferred: untimed run skipRequired approval, temporary Ready-prerequisite lookup failure, dirty local base, transient publication/handling failure, unconfirmed merge, or another current-run blockerThe issue retains Ready, is skipped for the current drain, and remains eligible in a later run. It consumes no retry count, creates no deadline, and is neutral for the repository safety stop.

Typed cause or deferral fields select these actions; diagnostic reason text never does. Required approval therefore retains Ready and consumes no retry attempt. A later capability snapshot with APPROVED or CLEAN can merge. No usable merge method fails immediately and directs the operator to enable a supported repository merge setting.

Pending checks are a typed deferral, not a retryable or remediable failure. Shipper visibly names the PR and currently incomplete checks on each CLI deferral; Desktop keeps each command as a Deferred issue run status in issue details and updates one informational notice. For one PR head SHA, a process or Desktop application session measures total elapsed deferral time against merge.pendingChecksTimeoutMinutes (default 60). A new commit changes the SHA and starts a full new window; restarting the CLI process or Desktop application also resets the in-memory clock. Passing checks permit the merge without intervention.

At the configured deadline, Shipper records that CI checks never completed, names every currently incomplete check, and moves the issue into durable failed state. Recover an open linked PR with shipper recover S#123 or Desktop’s Recover change. In store-only mode the same wait, timeout, and recovery reporting remain local without GitHub writes. A required check that never reports any status is not visible as pending and is outside this timeout bound.

Method selection and the single fallback after a rejected rebase update stay inside one merge attempt. A successful fallback emits no merge failure, so it does not advance the five-attempt retry count. A final command using the resolved method that fails and cannot be confirmed merged remains an unrecognized merge failure; Shipper does not choose another method from its diagnostic text.

A determined unrecognized failure routes to review; uncertainty after a merge request instead produces the special unconfirmed-merge Ready deferral because the change may already have merged. Failure classification comes from typed cause rather than diagnostic wording. Review and Rework routes do not create merge-loop counters. Only retryable failures use the bounded retry sequence.

shipper new is Shipper-owned after draft validation: the agent writes draft files under .shipper/output/, then Shipper writes a local issue-state record and creates a GitHub mirror only when issue projection is enabled. Grooming updates authoritative title/body content in the local store first, optionally projects it to GitHub with workflow comments, and commits the initial shipper:groomed transition through the same local-authoritative workflow path. The result-file transition contract owns the downstream workflow stages.

Feature-branch integration points use the resolved baseIntegration strategy before a stage result is processed. The default rebase behavior replays feature commits and retains the existing per-commit recovery budget. With merge, Shipper integrates base or remote feature updates with a normal merge, preserves existing commit IDs, and creates no commit when the branch already contains the ref. Each distinct merge receives one independent three-attempt conflict episode. Exhaustion aborts that merge and restores the exact pre-integration branch and tracked worktree state.

When the implementation agent resolves and stages conflicts successfully, Shipper completes the selected integration, reruns PR preparation against the final branch tip, and then processes the resulting pr_spec as the normal implementation acceptance transition. In rebase mode, each stopped commit receives up to three conflict-agent invocations. Git-observed advancement to the next replay step starts a fresh three-attempt budget, with no overall cap across the finite replay. Shipper-owned git rebase --continue calls and managed-path-only recovery do not consume an agent attempt.

After each agent-assisted advancement, Shipper reports the original commit and its n/m replay position. If three agent invocations leave the same Git step unchanged, the current implementation identifies that stuck commit and position before entering its existing shipper:failed recovery flow. Unresolved or unstaged conflicts and non-zero agent exits still end the current integration command unsuccessfully; their durable disposition remains subject to the governing contract. See PR preparation rebase recovery.

Publication follows the same strategy. Rebase mode retains its existing force-with-lease recovery for computed non-fast-forward topology. Merge mode integrates an advanced remote branch and retries only an ordinary fast-forward push; it never rewrites published feature history. Every local, ordinary, and forced finalization path keeps the no-content publication guard, including for a merge-only branch, but the guard is not itself a stage outcome. If a strict result refresh finds a valid reject or fail, that verdict controls the outcome. A no-content accept, or absent, unreadable, or invalid output, retains the publication refusal and durable failure path. Pull-request completion strategy and local-delivery semantics are unchanged by this feature-branch integration setting.

For no-PR local delivery, the base branch may already have advanced locally when publication runs. A non-fast-forward or fetch-first rejection means the remote base diverged, so Shipper records a durable fail-now outcome with reconciliation guidance. Other transport errors are untimed run skips. A later attempt recognizes the already-advanced base and retries publication idempotently instead of reintegrating the packaged change.

Mirror-mode flips do not rewrite workflow state. Stage labels, stageEnteredAt, completion history, tombstones, PR associations, title/body content, and workflow events remain in the local store and event log; reconcile only changes GitHub projections when mirror-on policy permits issue-side effects. Open local-only records become explicit reconcile backfill candidates when mirroring is enabled: eligible same-repository issue origins reconnect, origin-free or ineligible PR/foreign origins receive new mirrors, and unsafe closed, missing, labeled, or claimed origins stay Mirror pending (not currently repairable). This changes only mirror identity; workflow state and history do not transition. Records completed during store-only remain tombstoned. On re-enable, reconcile closes any old open mirror for a tombstoned local record instead of recreating a current snapshot.

A reject verdict rolls back to an earlier stage if the work is insufficient:

StageCan roll back to
designshipper:new
planshipper:groomed
implement (every route)shipper:designed
pr-review (every route)shipper:rework

Implementation rejection returns to shipper:designed on every route, including Rework, and never closes the pull request or deletes the branch.

Completed reviewer work reports an accepted agent result, while Shipper independently derives the workflow gate. REQUEST_CHANGES, any canonical must-fix finding, or any known failed hosted check rejects; pending, unavailable, passing, and deliberately absent local-only checks do not. Each failed check is retained as a distinct locationless must-fix finding with available step and test evidence. Acceptance retains the complete round and advances directly to shipper:ready. Rejection retains and pins the round, records every cause, supersedes only older active rounds for the current package, and returns every route to shipper:rework without changing route or package identity. Work carrying legacy shipper:pr-reviewed is read as PR Open and enters normal review.

For implementation and review-rework implementation, these rollback targets also apply when publication reports no content and the stage has a valid reject. The rejection record contains the agent’s reason and explicitly says that nothing was published. It is an ordinary interior event: the issue remains unfailed, the command exits successfully, and the occurrence does not increment the repository-wide auto-ship safety stop.

A valid no-content fail instead remains a durable terminal-stage-failure: it marks the issue failed, ends the command unsuccessfully, counts toward the safety stop, and uses the agent’s reason rather than the publication condition. Missing or invalid output remains distinguishable because its record uses the publication-condition diagnostic and agent-crash path. These transition, diagnostic, and safety rules are identical with transport: "origin" and transport: "none". Contentful rejects retain their stage-specific behavior: implementation publishes before rollback, while review-rework implementation rolls back without publishing rejecting work.

Design, plan, and simple-route implementation can reject because of an external impediment. The stage must name a specific, separately identifiable external action that would make work proceed and that neither the stage nor implementer can perform in the sandbox. Orchestrator-owned merge/rebase transport and an absent deliverable from another issue qualify. Difficulty, size, unfamiliarity, missing context, perceived risk, or wanting more investigation do not.

The rejecting stage explains the impediment in ordinary prose in its workflow comment: the concrete blocking condition, why that condition is outside the stage’s control, and the external action or outcome needed to proceed. It may name a known related issue when that helps the operator act, but no reference is required. There is no prescribed heading, field list, claim identifier, exact wording, or validity vocabulary, and grooming does not adjudicate the report.

The report is recorded with the stage outcome and triggers only that stage’s ordinary rejection and its normal one-step rollback — design to shipper:new, plan to shipper:groomed, and simple-route implementation back to shipper:designed with its existing permanent escalation to the complex route. By itself the report never routes the issue to grooming, marks it durably blocked, creates another issue, or selects the next action. The operator remains free to rerun the stage, return the issue to grooming, attach an ordinary blocker, direct prerequisite filing, or respond another way, without first classifying the report as valid or invalid.

Only on explicit operator direction does grooming file a prerequisite and attach it as an ordinary blocker of the origin in that same directed action. It either blocks on an existing Shipper issue or creates a plain normal-priority shipper:new request and blocks on that issue’s local Shipper identity. The new-stage issue is ungroomed and therefore excluded from auto-ship until a human grooms it. The blocked parent is also excluded while the prerequisite remains unmet, so it cannot re-enter design or plan.

Ordinary rejection, rollback, durable blocker, and blocker-reconciliation behavior are unchanged, and stages outside this set keep their established rejection and failure behavior. Historical out-of-authority prerequisite claims and grooming determinations remain readable as advisory context: their wording, validity labels, dependency fields, and claim identities inform judgment but never bind or independently determine a later outcome.

Grooming can create three distinct issue forms. Their similar creation point does not give them the same workflow meaning:

FormInitial stage and priorityRelationship to the origin
Follow-upIndependent normal-priority shipper:new issue with no explicit priority labelNo blocker, blocked state, dependency, scheduling, ordering, or decomposition link
PrerequisiteNormal-priority shipper:new issueBlocks the origin through its ordinary local Shipper identity
Decomposition childshipper:groomed with its declared priorityParticipates in the partial or full split and carries child grooming context

Filing a follow-up never changes the origin’s blockers or blocked state. It is available when the origin remains open, whether or not that run also creates children, and when full replacement closes the origin after creating its children. The attribution naming the origin provides traceability only; it does not create a parent/child or scheduling relationship.

Like every shipper:new issue, a follow-up remains outside auto-ship while ungroomed. Its own grooming independently decides scope and priority and advances it to shipper:groomed; from then on, ordinary stage, priority, failed, blocked, and lock selection rules apply without reference to the origin.

shipper ship S#123, shipper ship --auto, and shipper next treat a stage reject verdict as an orderly rollback, not as a terminal command failure. After Shipper applies the rollback transition to the local store and projects it to GitHub only when enabled, the ship loop re-reads the local workflow stage and continues from the corresponding stage.

The existing safety rails still apply:

  • Every forward or backward label change counts toward the per-run transition cap (MAX_TRANSITIONS = 15). The current implementation records shipper:failed when the cap is hit; its lane classification remains subject to the governing failure contract.
  • If a reject does not produce an observable local workflow-state move, the run still aborts to avoid an infinite loop.
  • Crashes (no verdict) and explicit fail verdicts still end the current command unsuccessfully; their issue and repository safety dispositions come from the failure contract.
  • A no-content reject from implementation or review-rework implementation remains safety-stop neutral; the same publication condition with a valid fail keeps its normal terminal-stage-failure classification.

For recovery from failed issues, reject loops, or transition-cap exhaustion, see failed issues and rollback loops.

Rolling back to shipper:new is the one special case:

  • Single-issue, worker, parked, and auto runs stop before running interactive groom, leave the issue at shipper:new, apply no failed state, and exit zero.
  • Auto reports ↩ returned — grooming required distinctly from pass and fail, and adds the issue to the in-run skip set. The final return happens before transition-cap accounting; unrelated workflow cycles remain protected by MAX_TRANSITIONS = 15.

Auto-advances an issue by reading its current workflow stage from the local issue-state store and dispatching to the corresponding stage through the shared in-process dispatcher. next rejects issues whose local snapshot is marked failed, and refuses blocked issues except shipper:new, which can always be groomed. If the dispatched stage rejects, next logs the rolled-back local stage and exits zero. Crashes and explicit fail verdicts still exit non-zero.

Processes issues in priority order. Auto-ship chooses the highest-priority available issue first, then prefers the most-complete stage within the same priority tier:

shipper:ready > shipper:rework > shipper:pr-open > shipper:planned
> shipper:designed > shipper:groomed

shipper:new is excluded from auto-ship because grooming is interactive by default.

Issues are ordered by priority first: shipper:priority-high, then normal priority, then shipper:priority-low. Within the same priority tier, auto-ship prefers the most-complete stage using the ordering above. Within the same stage and priority tier, issues are processed FIFO by the local issue-state replica’s stageEnteredAt timestamp. Issues with recorded timestamps sort before issues whose timestamp could not be resolved, and issue number is the stable tiebreak.

After a qualifying merge, Auto-ship immediately re-checks blocked, non-New direct dependents of the completed issue. CLI Auto-ship uses each successful merged result as the trigger. Daemon-hosted Auto-ship, including manually and automatically started ship and merge sessions, triggers only when Shipper actually performed the merge. Completions observed outside Shipper keep their existing behavior.

The targeted pass includes dependents whose stored blockers name the completed issue as a native Shipper reference or as a GitHub issue number mapped to an active or tombstoned managed record. Transitive dependents and unmapped external GitHub blockers are excluded. The pass does not wait for repository idleness or ordinary worker capacity, does not preempt running work, and does not give a released issue queue-jumping preference. On both surfaces it remains subject to normal safety, lock, pause, quota, and dispatch controls. Daemon-hosted Auto-ship additionally honors repository enablement, halt, and the bounded launch-failure retry ledger. An issue that still has another unresolved blocker stays silently Blocked. Targeted attempts reuse the existing automatic-unblock status.

After exhausting available issues, auto-ship still evaluates all store-blocked issues under the repository’s normal projection policy when the repository is idle. This untargeted pass remains the backstop for unmapped external GitHub blockers and for dependents that a targeted attempt did not resolve. If any issues are unblocked, auto-ship loops back to process the newly available work under ordinary priority, stage, and FIFO ordering.

Sequential auto-ship and single-issue shipper ship S#123 runs execute stages in-process through the same dispatcher that powers shipper next. Parallel auto-ship keeps one OS process per active issue for fault isolation and communicates with each worker through a small fork/IPC protocol.

A non-NEW reject stays inside that per-issue ship loop. A reject chain that reaches shipper:new records the successful returned outcome above. Auto-ship records the command outcome when the run ends in an error such as a crash, an explicit fail, or transition-cap exhaustion. Issue disposition and repository safety disposition are then evaluated independently under the failure outcome contract.

When the current implementation records durable failed state, later auto-ship sessions omit the issue even with a fresh in-memory exclusion set. The current run’s skip set remains an additional same-session mechanism and is also the complete disposition for leave-in-place outcomes.

In a pull-request-disabled repository, CLI auto-ship prefilters a Ready issue whose packaged local change is missing. It reports ⊘ excluded with the recovery instruction, records the canonical durable prerequisite failure, and continues selecting without launching issue work. The exclusion exits 1 but is neutral for the repository safety stop and leaves the durable condition ledger unchanged. If the final selection pass excludes the remaining Ready tail, the closing report says that Ready candidates remain but none is shippable; a mixed run that later drains shippable work uses the ordinary completion headline.

The merge-retry counter is per issue and lives only for the current CLI run or Desktop session. A retryable issue is excluded for 30, 60, 120, then 240 seconds while other eligible issues continue. CLI does not sleep when only delayed work remains; sequential and parallel drains end and a later run can try again. Attempt five retains the issue’s stage and failed flag, skips it for the rest of that invocation or Desktop enabled run, and emits one issue-scoped merge-retry-exhausted safety occurrence. It does not persist failed state. A fresh CLI invocation or an explicitly new Desktop enabled run starts a new retry sequence and can select the issue without recovery. Review and Rework destinations are ordinary workflow routing. They create no merge streak, per-issue ceiling, durable failure, or safety-stop occurrence; the next selection observes the new stage and dispatches review or implementation normally. Retry exhaustion counts once toward the repository-wide safety stop; every other stage-retaining merge outcome is safety-neutral.

Pending-check deferrals do not participate in the merge-retry counter. Their separate SHA-keyed elapsed-time bound and recovery behavior are described in the merge outcome table above.

CLI and Desktop use the repository safety axis of the failure outcome contract for automatic shipping. Three distinct qualifying condition instances engage the stop. Manual runs do not participate. Repeated observations of one unchanged condition share one strike, including a repository condition observed by several issues. Issue-scoped conditions on different local IDs remain distinct. A changed revision or an intervening successful repository advancement creates a new condition instance.

The pending ledger is repository-scoped, durable from strike one, and shared by CLI and Desktop. A restart cannot add another strike for an unchanged condition or forget an existing strike. On the third condition instance, sequential CLI stops selecting work; parallel CLI refuses to refill slots, drains work already in flight in receipt order, and abandons parked work. Both print the triggering conditions and any eligible unattempted issue, then exit 1. Desktop keeps desired state enabled while reporting Auto-ship suppressed.

This shared policy deliberately supersedes the earlier #1427 no-CLI-stop decision, #1428’s exhaustion exemption, and the stuck-only safety rationale inherited from #1413.

The local failed snapshot survives restarts and excludes the issue from later auto-ship sessions. Mirror-on projects the failed label only after the local commit; store-only performs no GitHub write. When the packaged artifact remains recoverable, use shipper recover S#123 or Desktop’s Recover change. This preserves the packaged work and clears Desktop’s session guard. Use Reset only when change recovery is ineligible or its cleanup is acceptable. See the posture-specific recovery table for the available work-preserving paths.

The shared per-run transition cap (MAX_TRANSITIONS = 15) prevents infinite loops, including tight review/Rework reject and redispatch cycles.

Grooming’s deferred question-file exchange checkpoints its coarse owner surface—Desktop, MCP, or an orchestrated headless terminal—alongside answered rounds, pending ordinary authored questions with stable IDs, grooming notes, and the question-round limit captured at session creation. Attended terminal grooming does not create these durable checkpoints: it asks natively inside one live agent session. When the answer-capable surface stops, grooming exits 75 as a clean awaiting-answers pause and stays at shipper:new; it does not enter failed state. A later ordinary headless groom resumes only from the recorded owner surface. A foreign surface is refused until an explicit takeover permanently transfers the conversation, while a legacy record with no owner is unowned and requires the same takeover gesture. Repeat suspension is unlimited and sessions have no age-based expiry. Current cross-issue and recorded-blocker inputs are regenerated, and issue title/body drift is disclosed so affected decisions can be revisited.

A saved session is discarded as invalid only when it belongs to another repository or issue, the issue has left the New grooming entry stage, or the record is unreadable or invalid. A saved session that still holds the removed compact-confirmation format is separately incompatible: Shipper discards only that session file, plainly explains that grooming must restart for the issue, and never transforms or resumes the removed format. Ordinary-question-only saved sessions remain resumable, and historical session logs, transcripts, and grooming decision metrics are never rewritten or reclassified. Surface mismatch is not invalidation: the non-owner sees only issue identity, owner or unowned state, last-saved time, answered-round count, and pending-batch presence. Question, answer, transcript, and note content stay hidden. Attended terminal grooming creates no new question checkpoint; an existing terminal-owned record is instead materialized into the worktree as read-only context that the live session resolves through its native questions, is retained on cancellation or failure, and is cleared only after successful owned completion. Desktop and MCP use the same owner rule. Automatic groom --auto, next, ship, MCP advance, and auto-ship stand down neutrally at New for a foreign or unowned session, repeat the visible refusal on later passes, and do not mutate failure or repository-safety accounting.

Invalid result-bearing output from a non-interactive groom is captured before correction begins. Those runs allow three validation passes. One atomic bundle per issue retains the newest complete .shipper/input and .shipper/output trees, has no expiry, and survives worktree cleanup. A successful fresh groom or groom recovery deletes it. Reset-to-New discloses and deletes it only after the local reset transition commits. An attended groom instead follows the cancellation contract above: it validates once and creates no recovery bundle when artifacts are invalid or missing.

When a failed issue has a saved question session, recovery clears failed state, returns it to New, reports the preserved answered-round count, and retains the conversation with its recorded owner unchanged. It asks no questions and runs no agent. The owner can resume normally; another surface must take over first, and an unowned session must be claimed through takeover. A question session takes precedence over a simultaneous groom-output bundle and retains both; combining a session with packaged work is ambiguous. Groom-output recovery validates first, skips the agent if validator changes made the output valid, and otherwise allows exactly one headless correction. Another invalid result replaces the bundle and keeps the issue failed. Simultaneous groom output and packaged artifacts are also ambiguous and disable recovery. Invalid groom output is an ordinary counted groom-output-invalid auto-ship failure, and failed recoverable issues remain suppressed from auto-selection.

A failed issue with a stored packaged artifact has one non-destructive recovery edge back to shipper:pr-open. Use shipper recover S#123 in the CLI, shipper_recover through MCP, or Recover change in Desktop. The retired shipper pr recover tombstone fails with guidance to use the top-level command and performs no recovery mutation.

Recovery dispatches from the stored artifact rather than the repository’s current posture. A pinned local change remains the source after pull requests are enabled, and an open linked pull request remains the source after they are disabled. Eligibility requires failed state, no closure hold, and a packaged artifact. It is independent of whether a crash cleared the stage label or left shipper:pr-open, shipper:rework, or shipper:ready. A raw legacy reviewed label is eligible as PR Open.

For a pinned local change, the exact packaged revision must still exist and be reachable from the stored source branch. The branch may advance beyond that revision. A missing source branch, invalid revision, or rewound/divergent branch makes recovery visibly ineligible and points the operator to Reset. A linked artifact must still identify an open pull request.

Except for Rework, recovery clears failed state and stores shipper:pr-open, displayed as PR Open when pull requests are enabled and Change Packaged when they are disabled. It preserves the exact packaged revision, source and target branches, title, body, branch and worktree state, workflow history, and every completed or held review round. The next review appends a new round. Recovery performs no reset cleanup, event supersession, or artifact mutation. Projection failures do not roll back the local transition; each surface warns to run shipper issue reconcile --repair.

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.

Rework is the recommended preserving reset from PR Open, Ready, or Rework, and from a failed issue without a stage. It requires an open linked PR under GitHub PR policy or a reachable pinned local change under no-PR policy. All routes retain their route; early stages do not qualify merely because they hold a package. Missing, closed, merged, or unverifiable evidence refuses the reset without changes; inspect or repair the association with shipper issue reconcile --repair, or recover the local package first. Rework preserves the package, association, branches, worktrees, route and review history, and clears failure. Repeat Rework resets are real operations. Next, Ship, and auto-ship dispatch Rework to implementation, which re-validates and re-delivers the preserved branch and reuses its open pull request.

Every successful Rework reset records operator-reset; automatic review rejection records review-reject with the new round and rendered reason. It supersedes older unaddressed, unsuperseded rounds only for the current package while preserving all history. Every completed current-package round since the latest renewal counts toward the current five-round allowance regardless of handling or supersession; an addressed zero-finding round still counts, while replacement packages start fresh. The first five completed reviews in the current allowance may reject into Rework. A sixth rejection atomically retains and pins its round, establishes review rework, and enters durable Failed over Rework. shipper recover clears that failure while preserving Rework and the retained pin and restarts the allowance. Manual reset selects the newest eligible round, while a later roundless entry takes precedence, and never invents rejection. PR identity and the durable local package UUID scope retrieval. History survives restart. A committed operation awaiting journal recovery is reported as Reset committed; local recovery pending; store access rolls it forward once.

Failed Rework recovery retains its stage and existing pin, clearing failure only after the same package checks. Existing recovery destinations for other stages remain unchanged.

Rework execution. Next, Ship, auto-ship, and shipper implement S#123 run implementation for an issue at shipper:rework. Under the stage lock, implementation freezes one situation from current-package history: a held-round pin selects review rework; otherwise a newer roundless merge-conflict entry selects merge rework; no entry and every other roundless entry selects fresh. Review rework supplies the pinned round, all outstanding diff and locationless check findings, unresolved external threads, and the current-package round ordinal out of five. Every target requires one fix or reasoned won’t-fix reply. An unreadable pin remains review rework and fails without advancing if one correction cannot restore it. Merge rework requires no reply inputs. Acceptance reuses the existing branch and package association. A legacy shipper:implemented issue is handled the same way.

reset --to rework preserves eligible packaged work as described above. Earlier targets are destructive; an eligible Planned reset closes the PR and deletes managed branches under existing safeguards. Foreign adopted branches remain protected.

  • Without --to, it presents an interactive picker of valid earlier targets. If the local snapshot is failed and still carries workflow stage label X, the picker also includes X.
  • With --to <stage>, it resets directly to the specified target. Ordinary issues must target an earlier stage. A failed issue that still carries workflow stage label X may target X as recovery.
  • Valid backward reset targets are new, groomed, designed, and planned, as long as the target is behind the current stage or matches the failed issue’s retained workflow stage label.
  • Reset closes matching open PRs and attempts to delete recognized remote branches. Recognition includes local-ID branches under the current generatedBranchPrefix and the built-in shipper/ fallback; legacy numeric branches are recognized only under shipper/. Historical custom prefixes and numeric branches under a custom prefix are never selected. Reset also removes matching local branches and local worktrees, records later-stage workflow events as superseded, deletes only their GitHub comment projections, commits the target stage to the local store, and projects workflow labels plus a reset notice comment only when issue projection is enabled.
  • Bodyless verification_run history survives reset supersession just like stage_timing history; moving an issue backward does not hide earlier observed verification runs.
  • Deleting a remote branch that reset has already selected for cleanup skips Git’s pre-push hook, so pre-push verification never begins for that deletion and no verification_run is recorded for it. A verification_run always means a hook actually started, so the absence of a record here is the correct reading, not a missing entry. Reset reaches that point only after its own safeguards pass: adopted foreign PR heads are never selected, linked PRs must be closed first, other branches are re-checked for open PRs, and remote cleanup does not happen at all when remote transport is disabled. This applies identically on the CLI, Desktop, and MCP reset surfaces.
  • Nothing else changes. Branch creation, branch updates, and branch deletion outside reset keep their existing pre-push verification, and the exception is not exposed as an option, flag, or any other reusable way to skip verification.
  • Before discarding a pinned local change, every surface names its full packaged revision and source branch and recommends shipper recover or Recover change when eligible.
  • Reset-to-New names preserved grooming output and saved question sessions (including answered-round counts) before confirmation, then deletes them only after the local state transition commits.
  • Before closing an open linked pull request, every surface names it, warns that reset will close it and clear the stored association, and recommends change recovery when eligible. MCP and Desktop require explicit acknowledgement before mutation. MCP uses confirm_discard: true as its sole destructive acknowledgement for groom output, saved question sessions, local changes, and open pull requests.
  • Same-stage failed recovery preserves work already completed at X, including X-stage comments and links, while removing artifacts after X and posting the standard reset notice.
  • For an agent_timeout failure with committed preservation, the exact matching same-stage reset keeps only the recorded local branch ref when it still equals the full recorded revision. The physical worktree and all uncommitted work are removed. The reset event retains the timeout, stage, target, branch, and revision identity so the next groom or scaffold launch continues from that commit. A different target, missing ref, or moved ref uses ordinary cleanup or fails safe rather than claiming continuation.
  • Reset always clears local failed state and removes the mirrored shipper:failed label.
  • Reset preserves local priority and the mirrored shipper:priority-high / shipper:priority-low labels.
  • Non-failed same-stage reset and all forward targets are rejected.

For symptom-first reset guidance, see failed issues and rollback loops.

Pre-push verification duration is separate from stage timing and shipping duration. It measures only the observed hook interval and excludes transport, remote work, complete-evidence finalization, workflow-history persistence, and queue time.

Verification is split three ways, and each owner runs a different scope.

Agents verify the work handled in their own session. Planning, implementation (including review and merge rework), and pull-request opening run the narrowest credible checks mapped to the plan steps, acceptance criteria, review findings, and changed code they handled, plus lint and type-check when the repository’s instruction file names them. Any other command qualifies as a default fast static check only when that file explicitly designates it as one.

Format checks, builds, full test suites, and combined complete-verification commands are reserved. An agent runs one only when a concrete obligation cannot be credibly verified without that exact check. A combined command is never a way to obtain lint or type-check: when either is not separately available, the agent records that limitation and leaves the combined command to Shipper and CI.

Agent checks run in the foreground. Every check an agent starts must finish before the session ends, and the session still produces its required result artifact. A session never ends merely to wait on a running check.

Failure and deferral handling is unchanged:

  • A prescribed check that starts and fails is actionable. It is fixed or reported, never deferred.
  • A prescribed focused or fast-static check the sandbox refuses before it starts is recorded as deferred to review.
  • A check that needs genuinely unavailable external infrastructure — a service, secret, or CI-only environment — is recorded as deferred to CI.
  • A sandbox-incompatible test fails loudly, is recorded as deferred to review, and receives its first execution during Shipper’s complete verification.

Shipper performs complete pre-push verification once per eligible tree. An existing green receipt satisfies a repeat on an identical tracked tree, as described in reusing green pre-push verification. When complete verification rejects a publication, its failure output returns to the agent in a follow-up session.

CI verifies every push. Nothing an agent defers, and nothing a receipt satisfies, removes a push from CI’s own complete run.

Review keeps its separate targeted-verification and environment-limitation behavior; this ownership split does not change it.

The first real affected-stage session after a change to this split is the representative transcript check for it. That sample is a post-merge observation, not a pull-request gate.

Participating pre-push hook executions use one exclusive slot per canonical repository. Different repositories can run concurrently. The same named or lineage identity is serialized across ordinary clones, the managed clone, moved checkouts, and worktrees. Named identities reserve ~/.shipper/run/verification/named/<owner>/<repo>/capacity.lock; lineage identities reserve ~/.shipper/run/verification/lineage/<lineage-token>/capacity.lock.

Capacity is acquired independently for each hook execution attempt, immediately before that hook starts, and released by the same owner as soon as the hook completes or is interrupted. Remote upload, server response, fetch, base integration, transport recovery, and post-hook repair run entirely outside the reservation and are never counted as queue or verification duration. Active verification duration begins only after capacity is granted.

Only a positive wait produces a verification-queue-wait record. An uncontended acquisition creates no wait record at all. When the publication also records executed-verification evidence, a granted wait correlates with the verification_run for the same issue and stage immediately afterward. Complex-route implementation publications still participate but do not create that execution record. A timed-out wait has no verification run and no hook-rejection evidence, because the hook never started. Both outcomes feed the verificationWait phase below.

For participation and exclusion rules, holder diagnostics, the cumulative 45-minute failure, conservative abandonment, identity-resolution failure, former machine-wide reservations, the SHIPPER_VERIFICATION_SLOT=exclusive hook contract, and worker-count precedence, see waiting for repository-scoped verification capacity.

Per-stage shipping durations measure active processing only. Whenever a stage suspends to wait on something outside its own work, the active segment stops and the suspended interval is excluded from that stage’s total. Waiting has never been counted as stage time and still is not.

Selected waits and every executed pre-push verification run are recorded separately as durable local timing history:

  • Check settling belongs to pr_review pre-flight and is recorded as a ci-checks wait. No other stage records check settling: an implement run at Rework delivers its change and exits, and the next review pre-flight waits on that revision’s checks.
  • Failed-check rerun waits belong to pr_review pre-flight and are recorded as flake-rerun waits. No other stage repeats that gate.
  • Timer-mode review-age waits and minimum-review-window waits belong to pr_review, occur before reviewer launch, and are recorded as review-window waits.
  • Agent retry backoff is recorded as an other wait with detail agent retry backoff.
  • Every other sleep — check polling, queue-lock polling, quota handling, and any unlabeled wait — is not classified and produces no timing history.

Each classified wait is finalized exactly once, whether the waited operation succeeded, failed, or timed out; the record carries only timing, never an operation outcome. Recording is best effort: if it fails, the operator sees a warning while the waited operation’s value, error, and the overall command outcome are unchanged. Environments that disable timing record nothing and behave exactly as before.

Shipping-duration results carry an optional phase breakdown alongside the unchanged per-stage breakdown:

  • verification — the total of executed pre-push-hook verification runs, of every result.
  • verificationWait — the total of separately recorded verification queue waits, whether the queue was granted or the wait timed out.
  • ciWait — the total of ci-checks and flake-rerun waits.
  • reviewWait — the total of review-window waits.
  • agentRetryWait — the total of other waits whose detail is agent retry backoff.

The five phases never overlap: each recorded entry contributes to exactly one of them, so no elapsed interval is counted twice. Receipt-triggered skipped verification and in-progress waits contribute to none of them. Malformed timing entries are rejected, excluded from every total, and reported as warnings; valid per-stage and phase results remain available.

Absent, zero, and missing evidence are distinguished deliberately. With no valid qualifying record, the entire phase breakdown is omitted. With qualifying records that all measured zero, the breakdown is present but empty. Otherwise it contains only the phases whose recorded total is positive — a missing phase means “no recorded evidence,” never “zero.”

Timing history is prospective: waits that happened before this feature shipped are not backfilled or inferred from timestamps or wall-clock gaps. Completed wait and verification timing survives later workflow resets under the existing local workflow-history lifetime, and there is no separate retention control. The CLI prints no phase output. Desktop adds ciWait, reviewWait, verificationWait, and agentRetryWait to the existing badge total and renders them as CI wait, Review window, Verification queue, and Agent retry rows. It excludes verification because verification execution is already included in stage time.

Machine-local issue locks prevent concurrent execution on the same issue for Shipper clients running on one machine.

  • Owner union: locks/local-<id>.json contains either a run owner with PID, token, and acquiredAt, or an operator-pause owner with a token and pause timestamp. Legacy PID records still parse as run owners.
  • Acquire: A run creates a run owner. If either owner kind already exists, dispatch refuses; deliberate operator pause is reported visibly while ordinary run contention keeps its existing neutral stand-down behavior.
  • Pause handoff: Pausing a running command replaces its own token-matched owner atomically. Pausing queued work installs the pause with the matching reservation token before reservation cleanup. An idle pause refuses if a live run or queued reservation owns the issue.
  • Dead-owner reclaim: If the recorded PID is no longer running, the next client removes and reacquires the lock automatically.
  • Liveness-only run stale state: Dead run owners and corrupt or unreadable records are stale and reclaimable. A live or indeterminate run remains active indefinitely, regardless of age.
  • Durable operator pause: An operator pause has no PID, never expires, and is never removed by a stale sweep or process cleanup. Time spent only under this owner does not accrue shipping duration.
  • Resume: Resume removes only an operator-pause owner and always reports either resumed or already-resumed.
  • Release: Normal release removes the file only when the owner token still matches, so an old owner cannot delete a successor lock. shipper unlock S#123 releases reclaimable debris; a live run owner first reports its PID and acquisition time. An operator pause reports its kind and pause time without process attribution. Either requires shipper unlock S#123 --force with the matching assessed token so a successor cannot be deleted; Resume is the normal operator-pause release.

Issue locks are single-machine coordination. Two different machines operating on the same repository do not observe each other’s local lock files.

All workflow commands wrap their execution in withIssueLock(). For stale lock recovery, see stale locks.

shipper:blocked mirrors local store-owned blocked state. Blocker relationships are stored as typed local blockers: Shipper blockers keep local IDs, while external GitHub issue or PR blockers keep their GitHub number. The label and canonical Blocked by #N comment are repaired from the store.

When Shipper detects a managed issue closed externally on GitHub, it records the close reason, actor, and closer evidence. New through Rework records enter the external closure hold described above, including when the closer is their own merged pull request. Stable evidence is deduplicated; unknown, absent, or unreadable attribution remains retryable. Ready records keep their existing verified externally merged PR completion path. Existing tombstones are not reclassified or downgraded, and store-only records never enter an issue-mirror closure hold.

  • The next command refuses to advance blocked issues, except shipper:new.
  • shipper ship re-tests stored blockers at entry and every stage boundary, including before its Ready-stage merge. Unresolved blockers stop later-stage work without marking the issue failed.
  • Auto-ship selection excludes blocked issues from the local issue-state snapshot, except for shipper:new.
  • Targeted shipper merge re-tests stored blockers before merge side effects. The untargeted merge queue excludes blocked Ready issues without re-testing or mutating their blocked state.
  • The unblock command evaluates stored blockers deterministically. Shipper blockers clear only from completed tombstones. A raw GitHub number that maps to a Shipper-managed issue uses the same local tombstone rule; a number with no managed local record retains external behavior, where an issue clears when closed and a pull request clears when closed or merged.

For the short recovery pointer for blocked issues, see failed issues and rollback loops.

FileRole
packages/core/src/lib/issue-state-store.tsLocal issue-state records, tombstones, and transactional mutation
packages/core/src/lib/issue-state-authority.tsStore-first workflow/priority/reset commits and optional label projection
packages/core/src/lib/labels.tsMirrored label definitions (names, colors, descriptions)
packages/cli/src/commands/stage-dispatch.tsShared in-process stage dispatch
packages/cli/src/commands/next.tsnext validation and lock-wrapped dispatch
packages/cli/src/commands/ship-execute.tsSingle-issue ship loop and stage progression
packages/cli/src/commands/ship-auto.tsSequential and parallel auto-ship orchestration
packages/cli/src/ship-worker.tsParallel worker IPC entry
packages/cli/src/commands/reset.tsStage categorization and cleanup for reset
packages/cli/src/commands/issue-list.tsLabel display and grouping
packages/core/src/lib/lock.tsLocal issue lock acquire, ownership-scoped release, operator force handshake, and liveness classification
packages/core/src/lib/auto-ship-selection.tsAuto-ship selection and FIFO ordering
packages/core/src/lib/github.tsGitHub issue/PR helpers and association reads
packages/core/src/lib/prerequisites.tsLabel existence validation

Required-check obstruction and stalled remediation

Section titled “Required-check obstruction and stalled remediation”

New automatic check stops apply only to independently acquired hosted checks. Shipper stops in Failed over Rework when current hosted evidence establishes an external obstruction after useful same-pass fixes, or when a completed, answered check-only review repeats with the same failing evidence on the exact unchanged revision. Both stops retain the package, branch, PR association, unpublished fixes, canonical answers, evidence and outstanding findings. Pending remote answer delivery remains visible separately from handled and resolved findings.

An external obstruction records concrete evidence, causal reasoning, an external remedy and why the failure still applies to final HEAD. For example, a required service explicitly rejects an expired organization credential while local code fixes pass: its owner must rotate the credential. A red check, a matching failure on main, or a missing tool alone does not establish that cause. Uncertainty calls for investigation; sandbox limitations and transient failures retain their normal verification and rerun opportunities.

Stalled remediation makes no external causal claim. For example, a completed V4 review’s only blocker is a required hosted verification failure, its canonical answer is retained, no code changes follow, and a fresh hosted acquisition repeats the identical identity and diagnostic evidence. Detection runs after existing refresh/rerun/flake handling and before another review agent. Nits are neutral. Changed HEAD, a passing check, a new blocker, materially changed evidence/procedure/source, pending or unavailable outcomes, incomplete evidence, legacy baselines, unanswered blockers, or request-changes review bodies prevent this mechanical stop. It does not execute a saved arbitrary local command.

Local verification is reported in existing prose summaries and relevant comments, without a structured local evidence report. Every attempted non-permission local failure, including an apparently unrelated or pre-existing failure, requires an anchored 🔴 must-fix: comment on the most relevant changed line and REQUEST_CHANGES, followed by ordinary Rework fixes or prose replies. Permission-denied and unattempted checks are limitations, never passes or waivers. All previously required checks stay required. Local-only workflows have no local-evidence automatic stop; the ordinary review-round cap still applies.

Historical local observations and failure records remain readable without migration. Outstanding historical local findings receive ordinary prose replies and never authorize a new automatic stop. Upgrade alone does not clear findings, renew allowance, or recover Failed. Publication and hook diagnostics and required hook execution remain unchanged.

Each applicable stop records one terminal safety occurrence. A skipped review creates no held round and consumes no review allowance. All required checks remain required: answers, reports, thread resolution and retry actions cannot authorize shipping or merging red checks.

Applying a remedy or restarting Shipper alone leaves the issue Failed. Inspect the durable diagnosis with shipper issue view S#123 (or --json), then explicitly retry with preserving shipper reset S#123 --to rework, or shipper recover S#123 when recovery is eligible. Existing eligibility, locks, closure holds and confirmation rules apply. Both explicit paths renew the five-round review allowance on unchanged HEAD without a dummy commit, preserve history and useful unpublished work, and start a fresh attempt. Missing or divergent retained source refs refuse retry; restore the source history first. Normal integration, hooks, verification and review must confirm the remedy before advancement. Retained failure evidence becomes historical after renewal or completion and remains available on Desktop, CLI and MCP, including failures with zero rounds.

Preserved-work continuation lookup failure

Section titled “Preserved-work continuation lookup failure”

Continuation lookup uncertainty stops the stage before preparation or agent launch, records ordinary Failed state when available, and returns nonzero. Diagnostics identify the issue, stage, and underlying history, state, or ref problem and say “continuation could not be determined.” Repair that problem, then explicitly run shipper recover S#123 before retry. Repair alone or restart does not clear persisted Failed. Ordinary Reset, including Rework and --force, is refused while this failure is active. Continuation-lookup recovery takes precedence over grooming and packaged recovery and restores the original raw workflow label (including PR Open or Ready), branch, evidence, route, and associations without renewing the review allowance. If failure state could not be recorded, explicit Recover verifies the unchanged stage and branch context read-only and acknowledges a definitive lookup without writing a reset event. Uncertainty still refuses recovery. Confirmed non-applicability permits ordinary execution without broadening eligibility. When applicable work was recorded but its branch is confirmed absent, run output states exactly: “S#<id> <stage>: preserved work was recorded on <branch>, but that branch was not found.” This notice makes no claim of loss or safety. There is no automatic repair or new flag.

Only explicitly validated continuation-lookup failure and recovery groups are transparent to the original continuation classifier. All unrelated failures, resets, transitions, and completion barriers remain effective, and cancellation or timeout-reset continuation still requires the exact recorded commit revision. Recovery never copies preservation evidence or refreshes its revision.