Skip to content

Common Errors

A terminal publication report is written only after an attempted Git push remains unsuccessful under the existing retry policy. An observed nonzero pre-push hook status establishes local pre-push rejection. A Git rejection naming the attempted destination establishes remote rejection; GitHub is named only when the repository/destination evidence supports that attribution. Affirmative transport evidence establishes transport failure. Otherwise the cause is unavailable. Executor failure or capacity denial alone does not establish transport or hook rejection. Preparation, PR-service, teardown, and local-only failures remain separate. A failed publication does not establish missing or invalid agent output or invent a failing check.

The report captures independently available source branch, checkout path, full commit ID, and observation time immediately before the terminal attempt. Its approximate plain reachable-commit count compares the tip with the locally available destination remote-tracking ref. Only proven local absence selects the labelled configured-base fallback. A failed comparison leaves the count/ref/fallback unavailable with separate reasons. These diagnostic reads perform no remote lookup, fetch, staleness analysis, topology assessment, or uncommitted-file inventory.

The combined raw excerpt, including omission markers, is limited to 4096 UTF-8 bytes without splitting characters. Metadata, full paths, tips, and unavailable reasons sit outside that limit. Publication evidence files are under ~/.shipper/logs/ and show their actual path plus complete, incomplete, or unavailable status. An available path alone does not imply complete evidence. Agent/session logs are separately labelled and are not the publication evidence log.

Destructive reset discovers exactly the selected/invoking checkout plus the existing named managed clone. Local lineage repositories have only their selected checkout. Missing managed clones are not created or refreshed, and arbitrary clones are not searched. Checkouts sharing a canonical Git common directory are one branch set with their aliases displayed; separate clones with identical branch names remain distinct plans. Existing branch-recognition tiers are unchanged.

Before deletion, the preview shows each branch’s owning checkout, full tip or tip-unavailable reason, delete/retain/skip disposition, aliases, and discovery limitations. Normal main/user/linked checkouts holding a recognized branch are protected and retained; eligible issue worktrees are removed before branch deletion. Unknown identity, eligibility, or safety is skipped with a reason and reported as incomplete discovery. An unavailable tip alone does not prohibit an otherwise eligible deletion. CLI force skips confirmation only and still prints this preview. MCP dry-run is read-only; direct confirmed execution displays the same captured scan before its outcomes and preserves existing confirm_discard semantics. Desktop confirmation consumes its displayed plan; a missing or consumed preview requires a fresh preview.

Operations identify their owning checkout and distinguish successful deletion, intentional retention, already-absent/missing skips, and failed discovery or cleanup. Safe cleanup continues in other known locations after failures; scan, prune, deletion, or required-holder removal failures prevent full-success wording and the final success transition. PR/remote cleanup remains once per issue under existing guards. A later reset invocation rescans for retry. There is no added local tip/identity/safety re-verification between a captured preview and deletion; existing operation-level reclamation and PR guards still apply.

New validated publication-failure comment groups and captured run-history diagnosis survive reset as historical evidence. A reset, later transition, or newer different failure makes the older report historical rather than restoring current Failed state. Older Agent Failure headings are not inferred or rewritten. Preserving Rework keeps its existing eligibility, token, package, branch, and history behavior with empty destructive plans. These observations and best-effort cleanup provide no new retention or recovery guarantee.

Issue-worker identity check failed appears when a worker cannot verify its own exact process incarnation. The worker retains its launch identity assessment and refuses its first lock-taking action without taking a lock. Earlier non-locking admission refusals can still appear before this failure. A verified worker keeps the same identity throughout later stages and lock-taking actions.

Read the displayed diagnostic before drawing a conclusion:

  • Process ID identifies the receiving worker process.
  • Liveness is alive, dead, or indeterminate. An alive result alone is insufficient: the check also requires a process start token to identify that exact incarnation.
  • Start token available: yes/no states whether inspection returned a token.
  • Envelope validation gives the separate validation reason when launch data was malformed.
  • Inspection evidence contains the inspection result verbatim, including whitespace and long output. If inspection supplied no evidence, the message says No inspection evidence was available.

EPERM, a timeout, or empty output alone does not establish an environmental or sandbox cause. Validation evidence and inspection evidence describe separate parts of the identity check.

As independent platform context, macOS Seatbelt cannot execute /bin/ps inside any sandbox because that program is setuid and entitled. A sandbox allowlist cannot change this constraint. The Seatbelt backend limitations explain the platform restriction on executing setuid programs. This fact does not diagnose the cause of an individual identity failure.

If the worker began inside a sandbox, relaunch it from a normal Shipper surface, such as the terminal, desktop, or Shipper MCP workflow. Otherwise, report the failure with all of the displayed evidence, including the process ID, liveness, token availability, and any validation reason.

Workers newly launched from the merged revision receive this behavior. Already-running workers must be relaunched to activate it.

shipper groom exits 75 and reports that the issue is awaiting answers after the answer-capable orchestrator disconnects or restarts.

This is a clean pause, not a failed issue. Shipper checkpoints answered rounds and, when durable, the pending question batch. Its message truthfully distinguishes full, partial, or zero preserved progress. Saved sessions survive CLI and MCP host restarts and have no age-based expiry.

On the session’s recorded owner surface, run the same ordinary headless shipper groom S#123 command; no resume flag is needed, and repeated suspension is safe. A fresh MCP client counts as the same surface, and MCP answer submission can reconstruct its runner from the durable pending batch. Desktop, MCP, or terminal callers on another surface are refused with a content-free owner summary. Use the explicit CLI --takeover, MCP takeover: true, or Desktop Take over gesture only after the operator chooses to transfer the saved answers. A live issue run makes takeover unavailable.

Shipper preserves the original round limit and transcript while regenerating cross-issue and blocker context. It discards and starts fresh only if the record belongs to another repository or issue, the issue left New, or the record is unreadable or invalid. If a durability failure lost answered rounds, the issue enters failed state: use shipper recover S#123 to clear the failure while retaining the last checkpoint and its owner. That owner can resume normally; another surface must take over, and an unowned legacy session must be claimed through takeover. Reset-to-New is the destructive alternative.

An attended groom, design, plan, implement, or pr review session exits 82. The CLI reports Operator cancelled <stage> ...; the issue remains at <prior stage>. Desktop shows a finished neutral Cancelled by operator activity instead of a failure or pause.

The interactive TTY agent session actually started and ended without complete valid artifacts. Shipper validated once, retained the issue’s stage from session entry, and recorded a neutral operator cancellation. It did not run output correction or retry the agent, mark the issue failed, create recovery state, or count a repository-safety condition.

No recovery action is required. Run the ordinary stage command again when ready. A saved headless groom question session remains intact; ordinary shipper groom S#123 resumes it only from its owner surface, while another surface must explicitly take it over. For design, plan, implement, and PR review, committed workflow-branch work is retained and reused with continuation context when the stage and branch still match; uncommitted work was removed during worktree teardown. Compound groom, next, and ship runs deliberately stop before starting another issue or stage.

Grooming ends in shipper:failed with a report that the output remained invalid and was preserved. Desktop may show the same issue with Recover change as the primary action.

The result-bearing output from a non-interactive groom failed all three normal validation passes. Shipper retains the newest complete input/output tree as one durable bundle for the issue; it does not expire. An attended session with invalid or missing artifacts is operator cancellation instead and creates no recovery bundle.

Use shipper recover S#123, MCP shipper_recover, or Desktop Recover change. Recovery validates the preserved artifacts first and uses one unattended correction only when still necessary. It does not restart discovery or re-ask questions already answered in the preserved transcript. Another invalid result replaces the bundle and remains recoverable. Use Reset-to-New only to intentionally discard the preserved grooming work; every surface discloses that destruction first.

An issue keeps a stale local lock: shipper status, shipper issue list, or desktop shows it as locked, but no active Shipper run appears to be working on it.

Shipper uses a machine-local issue lock file to prevent two clients on the same machine from advancing the same issue at the same time. The lock is normally reclaimed automatically when the owner PID exits. Corrupt, unreadable, or process-less records are also reclaimable. Live or indeterminate run owners remain active indefinitely regardless of lock age. A valid operator-pause record is intentionally process-less: it never expires and is excluded from stale cleanup.

shipper status reports only the aggregate number of operator-paused issues; it does not list which issues are affected. Find them from the operator hold badge on Desktop issue cards or the per-issue <issue>: operator-paused - skipped lines printed by shipper unlock --stale. An operator hold you do not remember setting is state retained from an earlier build; use Resume to clear it. Existing holds are not expired or migrated automatically.

Choose recovery from the ownership state instead of treating every refusal as stale:

  • Running, queued, or reserved workflow: wait for it to finish or use Pause/Stop/Cancel in the client that owns the controllable session. Issue details identify a reservation and its conflicting command.
  • External live or indeterminate run: wait for it to finish or stop it from the owning client. Shipper reports the exact issue, PID, and acquisition time. Do not force-unlock healthy or indeterminate work.
  • Operator hold: use Resume. Force unlock is available only as a deliberate, reviewed override when you intentionally want to discard the hold; it reports pause time without a PID.
  • Confirmed dead run owner: use shipper unlock S#123 or Desktop’s Release lock action.
  • Invalid or malformed record: use shipper unlock S#123 or Desktop’s Unlock action.

The stale sweep clears only confirmed dead run and invalid records and leaves operator pauses, healthy or indeterminate runs, queued work, and reservations untouched.

On Desktop, unavailable or stale supervision claims do not by themselves disable otherwise-valid Groom, Ship, Next, Pause, Reset, or drag for an idle issue with no known owner; Merge keeps its existing supervision requirement. A known reservation or external owner makes mutation entries visible but disabled without unavailable tooltips or explanatory text and prevents drag. Ownership guidance is available in issue details. Queued Ship keeps Pause and Stop, queued Next keeps Stop without Pause, and Stop appears only for a queued or running session Desktop can actually control. Logs, issue details, and focus for an active Desktop Groom session remain available while mutations are blocked.

A hidden shipper daemon run invocation printed a one-line startup decision, or exited successfully or unsuccessfully, and you need to know what that line means.

Direct daemon startup reports the evidence behind its final decision. It never starts a competing daemon unless the endpoint is locally proven unreachable. Compatible peers, concurrent launches, and uncertainties after a peer accepted the connection are successful no-ops. An incompatible peer, an unanswered connect error without proven absence, and every explicit refusal fail without authorizing startup.

shipper daemon run is a hidden operator and troubleshooting command. It is deliberately absent from CLI help. As soon as the decision is known it writes exactly one concise line naming the coarse outcome and discriminator, then exits 0 on success or 1 on failure.

Successful decisions go to standard output:

  • started (newly-started)
  • already-running (confirmed-compatible-peer)
  • already-running (concurrent-convergence)
  • already-running (negotiation-timeout)
  • already-running (negotiation-closed)
  • already-running (protocol-error)

Failed decisions go to standard error:

  • already-running (incompatible-peer)
  • already-running (connect-error) and, when the runtime supplied one, code=<CODE>
  • refused (<refusal-code>), including socket-path-too-long, non-socket-endpoint, owner-guard-retained, and startup-refusal

Accepted-peer uncertainty is a safe successful no-op: a short negotiation timeout can happen against a healthy but busy daemon. Unanswered connect errors and refusals fail honestly and do not start a competing daemon. The optional connect error code is the only low-level detail on this line.

Use the hidden shipper daemon inspect command when you need current daemon state. Inspection is diagnostic only.

You need to distinguish a healthy absent daemon from a reachable daemon or an unreachable endpoint whose live owner has published advisory state.

The daemon manages its own lifecycle and exits when idle, so its absence is not itself a failure. Live inspection and advisory fallback also provide different levels of detail.

The intentionally hidden, read-only shipper daemon inspect command is a troubleshooting aid. It is deliberately absent from CLI help. Inspection is diagnostic only: it does not mean the daemon should be kept alive, and an absent daemon requires no lifecycle intervention. The command writes compact JSON on one line; the examples below are formatted for readability.

A healthy result when no daemon is running is:

{
"running": false,
"supervisionDeliveryHealth": {
"status": "unknown",
"reason": "daemon-not-running",
"message": "Supervision delivery health is unavailable because no daemon is running."
}
}

This is a normal state, not an error or a prompt for recovery. When the daemon is reachable, the result contains live service information:

{
"running": true,
"reachable": true,
"generation": "4.2.0-1a2b3c",
"version": "4.2.0",
"connectedClientCount": 3,
"supervisionDeliveryHealth": { "status": "degraded", "degradedConnectionCount": 1 },
"attachments": [
{
"client": { "kind": "desktop", "instanceId": "desktop-1" },
"alive": true,
"facts": [],
"supervisionDelivery": {
"status": "degraded",
"activeScopes": ["sessions"],
"outstandingStateDeliveryCount": 2,
"oldestOutstandingStateDelivery": {
"scope": "sessions",
"acceptedAt": "2026-08-20T12:00:00.000Z",
"ageMs": 10000
}
}
}
]
}

The connected-client count includes the inspection command’s own short-lived connection, so it may be one greater than the number of other attached clients. Because inspection attaches as a client, it also restarts the client-free grace window when it disconnects; repeatedly inspecting an idle daemon therefore delays its designed idle exit.

Delivery health is independent from reachability. A connection is degraded only when a supervision state write accepted by the daemon remains incomplete for at least 10 seconds. Initial and resynchronization snapshots plus live state events on every supervision channel participate; pings, service-information and command replies, shutdown/generation notices, and transient live output do not. Equal accepted and completed progress stays healthy indefinitely while idle or remotely quiescent, and an open socket cannot override an overdue delivery. Connections without an active supervision subscription are not-applicable. Aggregate health is degraded when any connection is overdue, and degradedConnectionCount is the number of independently degraded connections. Oldest timing is omitted when no delivery is outstanding.

Delivery degradation is advisory: shipper status, shipper status --json, shipper daemon inspect, and MCP shipper_status continue to succeed. Completion, unsubscribe, subscription replacement, or connection removal clears current degradation immediately without acknowledgement, disconnect, restart, or stopped supervised work. A legacy peer with no health fields is explicitly unknown (legacy-peer), never healthy by omission. Durable start, restore/removal, and immediate rejection records are in the rotating daemon diagnostic log described below.

If a live owner exists but its endpoint is unreachable, inspection falls back to the advisory record:

{
"running": true,
"reachable": false,
"advisory": true,
"generation": "4.2.0-1a2b3c",
"version": "4.2.0",
"connectedClientCount": 2,
"supervisionDeliveryHealth": {
"status": "unknown",
"reason": "daemon-unreachable",
"message": "Supervision delivery health requires a reachable daemon."
}
}

The version, generation, and connectedClientCount fields appear only when matching advisory facts are available. Advisory fallback therefore is not available for every unresponsive daemon. A socket that accepts a connection but cannot complete negotiation instead reports a Daemon endpoint state is indeterminate: ... error; Shipper does not silently replace that error with advisory data.

A failed read of local issue eligibility makes the observation panel unverified immediately and writes a diagnostic. It warns after 30 seconds and becomes an error after 120 seconds from the first failure, regardless of the number of retries. Inspect the reported local cause; this is separate from a GitHub outage. Unrelated unexplained observation keeps its five-minute warning policy.

Local checks retry approximately every four seconds. The first successful local check, including an empty result, clears the condition automatically without Resume, acknowledgement, or restart. Later GitHub failure does not undo that local recovery. The board, terminal status, and MCP status show the same first/latest failure times, count, cause, severity, last success, and scheduling impact.

The original heartbeat deadline survives local failure. A due observation attempts safely permitted current work; if blocked, it shows the cause and next remote opportunity separately from the next local check. Recovery services overdue work once without catch-up bursts. A blocked heartbeat is not a completed one. Automatic starts wait for verified eligibility while queued reservations and running work remain intact; quota, ownership, safety, halt, pause, and capacity gates still apply.

Disabling Auto-ship shows inactive health and removes retry promises. Restart or re-enable starts unverified with a fresh episode and count until a fresh local success. Diagnostic history is retained. A halt does not stop local failure escalation. See Observation health and recovery.

Desktop shows that automatic retries are exhausted for one supervision state area. shipper status reports the same exhausted condition and directs retry to Desktop. Healthy areas remain current. Socket-write delivery health may still be healthy: this is receiver schema rejection, not a 10-second incomplete write.

A supervision subscriber rejected an application payload. Automatic recovery makes five total attempts at approximately 0, 1, 3, 7, and 15 seconds. Reconnects and Desktop restarts do not reset that budget while the same background service is active. After exhaustion, the daemon probes once every five minutes. Identical rejections produce one start record, at most one aggregate update per minute, and one exhaustion or recovery record. Diagnostics retain payload byte size, a sanitized structural path, and a bounded summary; they exclude full state values, cursors, and delivery identifiers.

Use Desktop’s Retry control for one additional attempt. shipper status is read-only and does not retry. Only the smallest affected area is unavailable; starts are withheld only for a repository whose live operational state is unavailable. Last-known values are not shown. If Desktop is wholly unusable, restart the background service. Daemon restart starts a new daemon-lifetime episode.

Those references own the locations and lifecycle behavior.

shipper next, shipper ship, or the desktop app cannot tell which stage an issue is in because the issue has no workflow label or more than one workflow label.

Every Shipper issue must have exactly one workflow label such as shipper:new, shipper:rework, or shipper:ready. Missing labels usually mean the repository was not initialized or the issue was not adopted. Duplicate labels usually mean labels were edited outside Shipper.

Run shipper setup if the repository has not been configured or if the workflow labels need to be synced. Use shipper adopt https://github.com/owner/repo/issues/42 for a single existing GitHub issue, shipper adopt https://github.com/owner/repo/pull/17 for a single existing GitHub PR, or shipper adopt --all for existing GitHub issues that should enter the Shipper workflow. --all remains issue-only. PR adoption rejects closed or merged PRs, draft PRs, fork PRs, PRs whose backing issue still resolves to a different open PR, and PRs with ambiguous closing references with actionable messages. Desktop users can perform equivalent label-state recovery through the app without duplicating the CLI steps here.

Shipper reports that the local issue-state replica is missing, malformed, pending a GitHub mirror, has older local issue-state metadata that cannot be reconstructed locally, or has content drift. A run may fail with recovery guidance to run shipper issue reconcile --repair, repair may report that older metadata was recovered, or shipper issue reconcile may report that GitHub title/body content differs from the store. Null-mirror drift is reported per record as Mirror pending (repairable) when repair can reconnect an eligible origin, create a mirror, or attach an exact match, and as Mirror pending (not currently repairable) when a remembered origin is missing, closed, labeled, or claimed. The latter names the origin and creates no replacement. Existing closed-mirror drift is reported as closed GitHub mirror (the GitHub mirror exists but is closed; repair direction: reconcile closure by lifecycle state). Either condition is real drift, so check mode exits 1.

Routine comment convergence handles outstanding delivery. Use shipper issue reconcile --repair for already delivered comment content drift and unknown duplicate discovery or cleanup. A warning names S# identity and the GitHub parent/comment ID when known; retry or run repair after fixing the lookup failure. Local stages and committed note UUIDs remain authoritative while delivery is pending. A saved comment is replaced only after direct comment 404 plus successful matching parent access; listing absence and a bare 404 are inconclusive.

Recovery covers stage comments, notes, PR flake comments, Unblocked and Blocked comments. Completed explicit repair posts wanted legacy work once when no attributable copy is found and no concrete ambiguous-send evidence exists. Superseded or retracted absent work is retired without posting. Closure alone does not retire historical comments, including post-closure notes. Legacy work without an attempt token directs the operator to shipper issue reconcile --repair.

Uncertain delivery retains automatic duplicate protection: another send cannot be promised automatically. Use shipper issue recover-comment S#42 to inspect without posting, then authorize one exact current token with all four options:

Terminal window
shipper issue recover-comment S#42 --group <id> --attempt <token> --retry --accept-duplicate-risk

The earlier attempt may already have posted. Authorizing another attempt can create a duplicate comment.

A found attributable copy wins over authorization. Incomplete, unavailable, malformed or exhausted searches consume no token and never establish absence. Saved authorization, recovered receipt identity, verified body acknowledgement, retirement and cleanup are separate outcomes. Ineffective PATCH and residual cleanup remain pending. Obsolete uncertain work is retired only after evidence or authorization. Routine convergence retains conclusive absence for its matching attempt. Transient unavailable discovery retries at the next due ordinary synchronization without requiring inspection or repair. Explicit repair reuses a completed catalog per actual issue/PR parent and directly validates selected identities before mutation.

shipper issue recover-blocked-comment remains a Blocked-restricted compatibility wrapper using the same flags and engine. Blocked ownership and slot ordering remain stricter: saved ID, heading or body alone never establishes ownership. Store-only policy refuses GitHub recovery without writes. A live stage defers authorization without recording it; rerun after the stage finishes.

Repair changes only Shipper-owned Blocked comments: a currently validated marker belonging to a known local Blocked group or explicit POST provenance for that same comment ID establishes ownership. A heading, matching body, author account, saved ID, or body hash alone does not. An unmarked legacy linkage is not acceptance proof. Unverifiable old copies remain for manual removal, including human Blocked text on an unblocked mirror with no Shipper history. Ordinary non-Blocked legacy-body adoption remains available in explicit repair.

Automatically eligible known-unsent workflow-comment delivery retries at the saved quota deadline or after 1, 2, 4, 8 and 16 seconds, up to six failed passes. Definitive permission/validation rejection stops immediately. Rate-limit evidence takes precedence over rejection and consumes no failed attempt. Uncertain delivery never automatically resends merely because discovery is empty. Existing unrelated GitHub errors keep their pending-mirror treatment without a new generic retry loop.

Plain repair may service stopped work once without resetting its automatic budget. If it still fails, its stopped state and attempt count remain; timers and priority changes do not restart a full cycle. Active GitHub holds still apply, including after a priority change. “GitHub delivery held before launch” means no invocation launched; “GitHub rejected delivery; automatic retries stopped” requires matching rejection evidence. Unknown delivery does not prove that a POST was rejected or absent.

A corrupt derived pending-work index produces issue-specific stopped repair guidance while healthy peers remain usable in status, lists, the desktop board and catch-up. Explicit repair reconstructs the index from local records. Malformed authoritative records or history require correction and are retained unchanged. After successful authorized recovery, an immediate pending read reflects verified completion before ordinary synchronization; a recovered receipt alone does not verify body or cleanup. Unrelated/newer work and size-limit debt remain pending.

Authorization-required work displays the exact group/token command above instead of promising automatic resumption. Duplicate-risk warnings apply only to unresolved attempted POSTs; proven-unsent/rejected posts, completed/retired work and accepted-edit observation do not receive them. Definitive causes appear once per target. Desktop shows stopped retries as attention rather than success.

V20 retains a one-time blocked-import sweep for affected V16–V19 stores, supported legacy V1–V10 stores and unreconciled V11–V15 stores. Reconciled V11–V15 stores retain their exclusion. The first mirror-on repair stages unclaimed managed blocked issues before clearing eligibility; ordinary reads do no remote discovery. Incomplete discovery remains pending for later repair.

Notes, CLI/desktop priority changes, and explicit mirror sync succeed locally during a running stage. Automatically eligible work reports:

Saved locally; GitHub delivery pending while a stage owns this issue. Delivery resumes at the next ordinary synchronization after the stage finishes.

Do not unlock a healthy stage for these operations. Lock release performs no GitHub work and never waits. Work resumes at the next ordinary issue synchronization. Later retry deadlines remain durable and the runtime services them automatically. If all processes exit, the next runtime startup resumes requests even with Auto Ship disabled. Current store-only policy cancels queued obligations. Newer request generations and independent error debt survive completion of an older generation. Upgrade all CLI, Desktop, and MCP clients sharing a V20 store before writing new recovery evidence; do not delete or downgrade stored evidence.

Shipper-managed issue title/body content is authoritative in the local issue-state store. GitHub is the visible mirror for users and integrations. Direct title/body edits in GitHub are not imported back into the store after adoption or migration; repair treats that as store -> GitHub drift. Pending local records can also exist when Shipper wrote the store record but failed before creating or attaching the GitHub mirror. A missing-mirror condition means the issue mirror is genuinely absent. A closed-mirror condition means the issue mirror still exists on GitHub but is closed while the local record remains active. Older metadata can exist on machines that ran earlier Shipper versions before issue-state schema bumps. Locally reconstructable older replicas migrate automatically on first issue-state access and emit a one-time informational notice. Older replicas with local-ID V3 blocked records that lack a stored local blocker list still need GitHub data and stay on manual repair. GitHub-number snapshot filenames are retired and refuse the store. Newer metadata means the local store was written by a Shipper CLI newer than the one currently running.

If Shipper can reconstruct an older replica from local data, retry the original operation; Shipper migrates that replica automatically on first access. Run shipper issue reconcile --repair for supported local-ID V3 blocker imports needing GitHub data, malformed current stores, uninitialized or recreated stores, pending GitHub mirrors, or content/label drift. Repair preserves the next-local-ID floor from metadata, snapshots, tombstones, and pending identities. Fresh or recreated stores retain first-reconciliation blocked-state imports from GitHub comments. Durable unfinished imports survive interruption and retry even after metadata becomes reconciled. Local-ID V3 imports with stored blocker lists complete locally in store-only mode; GitHub-derived debt waits for a mirror-on repair. Current authoritative records do not reimport GitHub blocked state. Repair recreates genuinely absent GitHub mirrors from the local store and overwrites GitHub title/body content from the local store when drift is detected.

Closed mirrors are reconciled by lifecycle state instead of being retired indiscriminately. Ready records with a confirmed externally merged stored PR complete through the existing Ready path. Mirrored records from New through Rework enter a closure hold that preserves stage, failed, blocked, priority, PR association, and blockers for a deliberate not-planned, reopen, or complete decision. Ambiguous actor or closer evidence remains retryable for later attribution. If the mirror is externally reopened, refresh or repair restores projected labels and clears the hold. Complete requires an assertion/dependent preview plus its current confirmation token; not planned does not release dependents. Reset does not clear a closure hold; choose one of the three closure outcomes instead. In store-only, reconcile does not fetch, report, update, close, or create issue mirrors.

Records named issues/<GitHub-number>.json or _pending-blocker-imports/legacy/<GitHub-number>.json refuse the entire store before repair, journal recovery, or writes, including orphaned records and malformed JSON or metadata. The message names the repository, absolute store root, and every old record. Existing local issue state is left unchanged. This Shipper version no longer migrates these records. Use a Shipper release from before this change to migrate the store before retrying this version; no specific release is promised. Supported local-ID V3 upgrades and old metadata alone are not retired, nor is a current record merely because it has a githubIssueNumber field.

The obsolete GitHub shipper:locked label has no lock meaning. Repair ignores and preserves it without refusing solely for its presence. Operators may delete it manually.

When the observed issue-state schema is greater than the supported schema, the running reader is stale. Restart or reconnect the MCP client and server. If a freshly launched server remains incompatible, update the installed Shipper release or rebuild the source checkout before reconnecting. Preserve the issue-state store unchanged. Use shipper_status after reconnecting to compare both schema versions and verify that server.startedAt and server.buildIdentity identify the new process and loaded bundle.

Make future content changes through Shipper intake, grooming, or adoption flows instead of editing the GitHub issue title/body directly.

shipper next, shipper ship, shipper merge, or the desktop Ready merge action reports that a linked PR was closed without merging or could not be found.

Native and adopted foreign-branch PRs are tracked through the same durable stored Shipper association. The association survives PR body rewrites and closing-reference edits. Shipper only follows the recorded PR while it is open. If a ready issue’s recorded PR was already merged outside Shipper, the merge command, merge queue, MCP merge tool, desktop refresh, and reconcile repair complete the issue without performing another merge or mutating the PR. Earlier PR stages are not eligible for merged completion.

Use the recovery that matches the observed state:

  • Merged: leave a ready issue in place and rerun shipper merge, refresh desktop, or run shipper issue reconcile --repair; Shipper completes it automatically.
  • Closed without merging: run shipper issue reconcile --repair. When it reports [closed-unmerged:recovered], run shipper next; Shipper revalidates the surviving implementation branch and delivers a fresh PR through implementation.
  • Not found: investigate whether the PR was deleted, moved, or recorded for the wrong repository. Shipper preserves the association and stage because not-found is not abandonment evidence. Re-establish the reference before retrying. Use shipper reset S#42 --to rework only after independently confirming preserved work is available.
  • Status lookup failed: retry after GitHub access recovers. Shipper preserves the existing link and does not advise re-linking when it could not determine the pull request state.
  • Head branch gone: restore the exact PR head branch, then rerun reconcile. Do not reset to Rework until the implementation branch exists again.
  • Issue mirror closed: resolve or reopen the GitHub issue mirror before retrying. Do not reset while the issue-side closure remains unresolved.
  • Active Shipper run: wait for the run holding the issue lock, then rerun reconcile.
  • No link recorded or a known replacement: re-deliver with shipper implement S#42, or run shipper adopt https://github.com/owner/repo/pull/<current-pr-number> for the intended replacement. The desktop adoption modal provides the same adoption path.

Shipper cannot read issues, update labels, create pull requests, or clone GitHub-backed worktrees because GitHub API or git authentication fails.

Interactive runs need an authenticated GitHub CLI session. Containers and CI usually rely on Auth tokens, and git operations also need a usable credential helper.

For local interactive use, run gh auth login and confirm gh auth status passes. In noninteractive environments, export one of the supported Auth tokens in the same environment that runs Shipper.

If Shipper can authenticate but GitHub remains unavailable for the full clean-outage waiting allowance, the ship command exits 79. That result is unsuccessful but non-durable: no failed state is committed, the issue retains its workflow stage, the current run skips it, and a later run can select it normally. It is neutral for the repository-wide safety stop. Restore GitHub availability and run again; no reset or recovery command is required.

The same leave-in-place behavior applies to any GitHub read that fails before groom, design, plan, or implement has mutated stage state. A model or reasoning value rejected by the configured agent also retains the stage and needs only a settings correction; a later command selects it normally. Neither condition creates failed state.

Pull Request Creation Could Not Be Confirmed

Section titled “Pull Request Creation Could Not Be Confirmed”

Implementation delivery reports: “A pull request may already exist on GitHub, but Shipper could not confirm it. The issue stays at its current stage and will be retried in a later run.”

GitHub either refused creation because the packaged head already has a pull request, or a protected create attempt ended without confirmed pull-request metadata. Shipper then performs a guarded lookup restricted to an open, same-repository pull request with the packaged head and attempted base. A failed lookup, an empty filtered result, or only an ineligible candidate is treated conservatively because GitHub may still contain the just-attempted pull request.

Restore GitHub availability when needed and rerun the ordinary ship command. The issue stays at its current stage, and on re-entry the initial lookup reconciles a now-visible eligible pull request without creating another one. No reset or recovery command is required.

This outcome adds no issue comment, pull-request association, or failed state. Once it 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. The outcome is explicitly neutral for repository safety. Exit 79 means the confirming read exhausted the protected outage waiting allowance; other unconfirmed results use exit 1. Neither result asks the operator to reset.

In fully local mode, a stage reports that a configured local base branch does not exist, or that automatic detection could not find any of the listed candidate branches.

When transport is none, Shipper uses exact local branch references and deliberately ignores remote and remote-tracking references. A configured defaultBaseBranch may have been deleted locally, or none of the automatic candidates (such as main or master) may exist.

Create or restore the named local branch, or update defaultBaseBranch at the settings source named in the error. For an automatic-detection failure, create one of the listed candidates locally or configure defaultBaseBranch. A proven missing base is durable, so after fixing the branch use the explicit reset command named in the diagnostic before retrying.

In fully local mode, a stage reports that its required workflow branch does not exist locally.

The workflow branch may contain commits from an earlier stage attempt. Shipper refuses to recreate it from the base or substitute another branch because doing so could hide lost workflow history. Remote and remote-tracking references are ignored when transport is none.

Restore the exact named local branch from the source that contains its workflow commits, then retry. Do not create a replacement branch from the base unless you have independently confirmed that no workflow history needs to be recovered.

Worktree Creation and Install Command Failures

Section titled “Worktree Creation and Install Command Failures”

A stage fails while creating its isolated worktree, preparing dependencies, or running the configured dependency bootstrap.

Shipper creates ephemeral worktrees under ~/.shipper/worktrees/. Dependency bootstrap consists of the optional installCommand followed by executable .shipper/hooks/worktree-setup. If an initial install fails while using Shipper’s marked shared npm, Turbo, uv, pnpm, Cargo, Playwright browser, Electron runtime, or electron-builder packaging cache, Shipper leaves every shared cache intact, rewrites only the marked variables to worktree-private paths, and retries once before classifying the preserved command evidence. Positive registry transport evidence such as DNS failure, connection reset, timeout, TLS/proxy failure, or HTTP 408/429/5xx retains the workflow stage and needs no reset. A missing command (exit 127 or ENOENT), broken manifest/lockfile/configuration, or unknown nonzero exit enters durable failed state with a concrete repair and explicit reset action. A non-zero .shipper/hooks/worktree-setup exit still warns and continues so an agent can inspect and repair the worktree. Timeouts still abort bootstrap when installCommand or worktree-setup exceeds hookTimeoutMinutes.

Shipper also runs installCommand after it integrates the base branch by merge or rebase. If this post-integration install exits non-zero for any reason, it first falls back from marked shared paths, then clears eligible private or custom worktree caches and retries once before the existing three-attempt agent remediation path. Shared sentinels survive both retry paths, so a failing run cannot delete cache data still used by concurrent worktrees. The automatic reset and retry still happen even on code paths where no agent remediation is available.

The effective NPM_CONFIG_CACHE, TURBO_CACHE_DIR, UV_CACHE_DIR, PNPM_CONFIG_STORE_DIR, NPM_CONFIG_STORE_DIR, CARGO_HOME, PLAYWRIGHT_BROWSERS_PATH, electron_config_cache, ELECTRON_BUILDER_CACHE, and XDG_CACHE_HOME locations are eligible anywhere they safely resolve inside the affected worktree. Any other configured worktreeEnv location is eligible only when it resolves strictly below that worktree’s .shipper/tmp/ directory. The rule is the same whether the effective worktreeEnv map came from user-global, repository, or personal settings.

Shipper removes only the locations named by qualifying entries, never .shipper/tmp/ itself and never an active machine-wide or legacy shared-cache root. Question audit trails, rendered grooming bodies and comments, failure markers, and other scratch content therefore survive. Locations outside the worktree are never removed, and non-qualifying custom entries are silently ignored. A cache-clear failure is reported but does not replace the install error or prevent the single retry. This automatic recovery has no flag or setting to tune or disable. sharedCache: false controls only the sharing performance optimization; it does not disable local cache recovery.

Shared-cache preparation itself degrades per cache. If one child cannot be created, secured, or probed, Shipper records one key-specific informational reason in run output, keeps that cache private, and leaves every other eligible cache shared. A common-root failure reports one reason and keeps all covered caches private. Neither case fails the stage, increments a failure counter, creates an alert or notification, or changes board state. A string-valued operator override is reported once per run in an aggregate notice that names every blocking worktreeEnv setting and says removing it restores sharing. A deliberate effective sharedCache: false opt-out is silent; user-global false applies machine-wide, while repository or personal true overrides it.

When neither installCommand nor executable .shipper/hooks/worktree-setup is configured, Shipper prints a visible advisory warning and continues without installing dependencies. An empty installCommand means setup deliberately recorded that no bootstrap is needed.

Inspect the effective bootstrap command, timeout, and their origins first:

Terminal window
shipper settings get installCommand
shipper settings get hookTimeoutMinutes

If a Shipper-owned shared cache is suspected of containing poisoned artifacts, the explicit clear action removes both the active machine-wide root and all orphaned legacy per-repository roots. It affects every repository on this machine, so coordinate with other workers before running it:

Terminal window
shipper cache clear

Normal worktree preparation, install recovery, target reclamation, and orphan sweeps never clear, prune, evict, size-cap, seed, or copy the active or legacy shared roots. The explicit command also excludes worktree-private caches and operator-configured paths.

Check that the repository can create git worktrees, that the failing path under ~/.shipper/worktrees/ is writable, and that the effective bootstrap is valid. For multi-package repositories, verify that installCommand and .shipper/hooks/worktree-setup cover every area needed by the first build or test. If a post-integration install remains broken after automatic and agent remediation, inspect the final command output rather than deleting additional worktreeEnv locations. When the shared command is wrong, correct it in committed repository settings and commit the result. If the diagnostic says the initial failure is durable, run its explicit reset only after repairing the command:

Terminal window
shipper settings set installCommand 'npm install' --repo

If the operation is legitimate but slow, choose a timeout scope deliberately. Use --user when the preference should apply across repositories, or --personal for only the current repository:

Terminal window
shipper settings set hookTimeoutMinutes 30 --user
shipper settings set hookTimeoutMinutes 30 --personal

See the settings precedence for how these scopes combine.

A detached dev server, file watcher, editor, or shell can keep its working directory inside a stage workspace after the stage process tree exits. If it writes after Shipper removes the workspace, it can recreate that generated directory without .git linkage or live ownership evidence. In older versions, every later stage needing that path stopped immediately, and manually deleting the directory helped only until the leaked process wrote again.

Teardown now runs worktree-teardown first, then finds every process whose working directory is the workspace or a descendant. Shipper sends SIGTERM, waits on a fixed two-second rescan window, and sends SIGKILL to matching identities that remain, with one final second for exit. It reports all stopped PIDs and commands in one worktree line. Open files alone do not make an outside-cwd process a target. If inspection, signalling, or exit is incomplete, Shipper emits one warning, records retired parent provenance when possible, removes the workspace, and lets the stage retain its original outcome. Use the worktree-teardown hook to stop repository services gracefully before enforcement begins.

Every worktree-backed stage first evaluates only the exact branch and generated path blocking that stage. This synchronous target recovery runs inside the target transaction. After target preparation releases its path guard, Shipper opportunistically schedules trigger-driven background maintenance across every repository represented in the machine’s shared worktree root. The caller never waits for machine-wide repository inspection, even when inspection later fails.

Concurrent triggers coalesce within a process. Across processes, each task tries a fixed machine-wide lease once and skips immediately when it is contended; it does not wait or queue. Failed or contended attempts clear the in-process task, and failed tasks release any lease they hold. An abandoned lease ages out after a generous bound, so later triggers retry naturally. Global success, zero-reclamation runs, contention, skips, and failures produce no normal log or aggregate summary. They produce no recovery record, toast, notification, prompt, or board state. Debug-only diagnostics remain available for internal troubleshooting. There is no polling interval, per-second tick, or timer-driven directory scan.

A registered Shipper-managed target can be recovered regardless of age when its repository and branch match. An unregistered target with valid .git linkage remains eligible when the link names the current canonical Git common directory and the canonical path and generated name match. The exact generated target can also self-heal when .git is truly absent and no live-owner record exists, provided it lies inside the managed root and the current parent repository registers no worktree there under any branch. Empty, ignored, unignored, and otherwise non-empty contents are treated alike.

Before clearing that linkless target, Shipper runs the same cwd-holder termination sequence under the path guard. If inspection is incomplete, a signal fails, or any selected or newly discovered holder survives, the stage refuses deletion and names each known PID, command, and diagnostic so you can stop it directly. Existing recovery lanes that still have ownership or parent attribution do not run this new termination step. Each successful synchronous target recovery prints exactly one stable record, [shipper] worktree-recovered {"path":"/canonical/path"}, and Desktop turns that record into one transient Recovered abandoned stage workspace notification. Linkless recovery does not add a second stopped-process success line.

Missing ownership evidence, Shipper’s own process, and the current run’s token-matched issue lock do not block target recovery. A live or indeterminate third-party owner, unreadable or malformed evidence, or mismatched repository, branch, or path does block it. Recovery discards uncommitted and untracked files without enumerating or preserving them, then continues the stage in the same run. The branch ref, committed history, and pull request state survive unchanged.

The global sweep remains narrower: recorded ownership still requires a confirmed-dead PID, and its legacy fallback still requires a record-less, confidently attributed, unregistered directory strictly older than 14 days. A linkless recreation reaches that same unchanged age-qualified lane only when matching retirement provenance was written before removal, its named parent is resolvable and no longer registers the path, the basename uses the current hashed --wt-- grammar, and a complete inspection finds no cwd holder. The sweep stays silent, trigger-driven, non-blocking, machine-wide, leased, and single-flighted.

This widened sweep lane is future-only. A recreation made before this behavior shipped has no retirement provenance and is left alone regardless of age; the exact-target path clears it the next time a stage needs that location, or you can remove it manually after confirming it is unused. Malformed, unreadable, mismatched, or missing records, registered paths, holders, incomplete inspection, and directories at or under 14 days remain untouched. There is no flag, environment variable, setting, manual age override, command option, or shipper unlock --stale workflow that disables, tunes, or loosens teardown enforcement, exact-target recovery, retirement evidence, or the sweep.

A stage stops before the agent runs with one of these message shapes:

Branch "shipper/s42-add-feature" is already checked out at /path/to/checkout
Refusing to recover worktree /path/to/checkout: the branch is checked out outside Shipper's
managed workspace root. Switch that checkout to a different branch and rerun this stage.
Refusing to recover worktree /path/to/worktree: <ownership or attribution reason>.
<The single action the operator should take.>

Here, shipper/ is the configurable default. The actual error shows the repository’s resolved generatedBranchPrefix, including an empty or custom value. Inspect it when you need to interpret the branch name:

Terminal window
shipper settings get generatedBranchPrefix

Git allows a local branch to be checked out in only one checkout or worktree at a time. Shipper needs to create a stage worktree on that branch. Before surfacing this failure, Shipper evaluated the named holder and refused to recover it because it was outside the managed workspace root or carried live, indeterminate, malformed, unreadable, or mismatched ownership evidence.

Follow the single action named in the error:

  • If a live or indeterminate third-party process owns it, wait for or stop that owner, then rerun.
  • If the branch is checked out outside ~/.shipper/worktrees/, switch that named checkout to a different branch, then rerun.
  • If managed ownership or repository attribution is unverifiable, inspect the named directory and remove it manually only after confirming it is unused.

Shipper never switches, removes, detaches, or otherwise alters a main checkout, a folder-registered developer checkout, or another holder outside its managed root. Changing generatedBranchPrefix does not release the existing branch and is not a valid way to evade the conflict.

A shipper settings set ... --repo or shipper settings unset ... --repo command fails with:

Repository settings change was not applied because repository setup has not created committed Shipper configuration. Run shipper setup --repo first, then retry the settings change.

Repository setup has not created .shipper/settings.json yet. Any repository-scoped set or unset would otherwise create the first committed settings file without the CLI fingerprint recorded by setup. Shipper therefore refuses every repository key and both mutation actions without writing the settings file or its .shipper/ parent directory.

Run shipper setup --repo to create the committed repository configuration, then retry the original settings change. Use the deterministic command shown here rather than shipper setup --assist --repo, which delegates artifact creation to the setup agent and may not create the settings file. The GitHub-backed setup route requires GitHub authentication and a GitHub remote. A committed no-remote setup is also supported and needs neither. An origin on a non-GitHub host remains unsupported by repository-target setup; use --user or --personal instead when the setting and license key permit that scope. Commit the resulting .shipper/ changes according to the setup guidance.

A settings save is refused because a model or reasoning-effort value was rejected, or because verification could not run. The message names the affected stage and resolved agent. Rejections include the offending value and the agent’s response; a manual check also names the dotted key and settings file that supplied it. In Desktop, the message appears beside each refused control while unrelated edits from the same save still apply.

Changing an effective agent, model, or effort makes Shipper run a minimal real invocation for every affected stage. A rejected value is not written. Shipper also fails closed when the resolved agent is not installed, is not authenticated, is offline, or does not finish within the single 30-second overall verification deadline, including command preparation. Claude and OpenCode may accept an effort without confirming that they used it; this is a warning, the value is written, and the agent may ignore it.

Run shipper settings check to check the stored combinations. Correct the named value at the supplying scope, or install and authenticate the resolved agent, restore network access, and retry when it is available. The refused settings file remains unchanged. Desktop retains refused drafts, offers Check agent settings, and has no save-anyway control.

For an intentional unattended or recovery write, the CLI alone can bypass the probe:

Terminal window
shipper settings set commands.implement.model emergency-model --personal --skip-agent-verification

The command reports that the setting was stored without a passing verification. Follow it with shipper settings check when the agent is available again.

Shipper warns or refuses to run because the repository’s recorded cliVersion does not match the CLI that is running now.

shipper setup --repo records the Shipper CLI version in .shipper/settings.json. If the installed CLI is newer only within the same major version, Shipper allows the command with a warning. Missing or unreadable fingerprints are blocked when the repository has committed Shipper settings, and installed CLI downgrades or major-version drift are blocked because the repository may have been initialized against incompatible or newer Shipper behavior. Repositories configured only with user-level settings do not require a committed fingerprint.

Rerun shipper setup --repo from the repository root to refresh Shipper-managed files and the recorded cliVersion. This also clears repeated same-major warnings by refreshing the fingerprint. Use the SHIPPER_SKIP_VERSION_CHECK runtime override only as a temporary escape hatch when you must run before refreshing the repository configuration.

A stage stops with an agent timeout, or a hook, worktree setup, teardown, or install phase times out before it finishes.

Headless agent processes are bounded by agentTimeoutMinutes. File-based hooks and the worktree dependency install command are bounded by hookTimeoutMinutes.

For issue-backed shipper_advance, shipper_groom, and resumed grooming through shipper_answer_question, MCP gives agentTimeoutMinutes the same per-attempt meaning as the CLI and imposes no earlier whole-stage deadline. Time paused for product-owner answers does not consume an agent attempt’s allowance. A terminal MCP timeout returns only after local timeout history and failed state are committed, the MCP-attributed run is terminal, and that run’s own issue lock is released.

When an initial or output-correction agent invocation times out after making commits, the retry notice says that the next attempt is continuing from preserved committed work. The next agent is instructed to inspect those commits and continue instead of restarting. Staged, unstaged, and untracked files are still discarded. A timeout that adds no commits uses the ordinary retry notice and reports nothing as preserved.

A retryable non-timeout agent exit removes only commits made by that failed attempt. If an earlier attempt timed out and preserved commits, those earlier commits therefore survive a later crash in the same stage run.

Inspect both effective timeout values and their origins:

Terminal window
shipper settings get agentTimeoutMinutes
shipper settings get hookTimeoutMinutes

For a cross-repository preference, set the timeouts once at user-global scope:

Terminal window
shipper settings set agentTimeoutMinutes 0 --user
shipper settings set hookTimeoutMinutes 30 --user

For a preference that should affect only the current committed repository, use personal scope:

Terminal window
shipper settings set agentTimeoutMinutes 0 --personal
shipper settings set hookTimeoutMinutes 30 --personal

Raise agentTimeoutMinutes for stages that need more runtime and hookTimeoutMinutes for hooks or install steps that need longer. A value of 0 disables the corresponding timeout. See the settings precedence to choose the appropriate layer.

If the stage exhausts its attempts, read the failure’s workflow branch name and full commit ID before repairing or resetting. Commits preserved from a timeout remain on that branch after both all-timeout and mixed timeout/non-timeout exhaustion; the failure report also confirms that uncommitted work was discarded. The report includes the stage, per-attempt limit, exhausted attempt count, available session log, and exact supported reset action, such as shipper reset S#123 --to planned. If it reports no preserved commit, reset performs ordinary cleanup. If it reports a branch and full revision, that exact same-stage reset retains the unchanged ref while removing the worktree; the next stage run continues from it. A moved or missing ref fails safe. In mirror-on mode, a missing GitHub timeout comment or failed-label projection does not undo local failure or keep the lock; run shipper issue reconcile --repair to repair that explicit projection debt.

During shipper implement, a long rebase can report progress such as rebase advanced past abc1234 resolve generated files (2/6). A failed recovery retains the established three-attempt summary and adds the original commit and n/m replay position that remained stuck.

Shipper owns the fetch, rebase, and push steps around delivery. The implementation agent resolves and stages rebase conflicts when asked, but it does not run git rebase --continue or push the branch. Each stopped commit receives three agent resolution attempts. When Git’s rebase state advances to the next commit, that later step receives a fresh three-attempt budget; there is no overall attempt cap across a progressing finite replay. Shipper-owned continuation and managed-path-only conflict recovery do not consume agent attempts.

If the agent resolves and stages the conflicts successfully, no user action is needed. Shipper continues the rebase, reruns delivery preparation against the final branch tip, and proceeds like a clean shipper implement run. Each agent-assisted advancement line, including its commit and n/m position, appears in the CLI run log and the desktop board’s stage detail.

If one replay step remains unchanged after three agent invocations, the failure identifies the stuck commit and position. The issue’s local ship run log retains the complete terminal detail even when the published or stored Agent Failure record is shortened. Use that local log as the authoritative diagnostic for the failure.

If the stage is marked failed, use the failed-issue reset and recovery path instead of manually continuing the rebase.

Merge-Mode Integration or Publication Failure

Section titled “Merge-Mode Integration or Publication Failure”

With baseIntegration: "merge", Shipper reports a merge conflict episode, refuses protected .shipper/ scratch-history rewriting, or says that merge mode preserves feature history and no forced update was attempted. With baseIntegration: "rebase", a terminal publication error may instead include a line suggesting baseIntegration: merge.

Merge mode preserves existing feature commit IDs and permits only ordinary fast-forward publication. Each distinct merge has one three-attempt conflict episode; if it cannot progress, Shipper aborts that merge and restores the pre-integration branch and tracked worktree state. Protected scratch paths cannot be removed by rewriting history in this mode.

The rebase-mode discoverability line appears only when Shipper computes from branch refs that the attempted publication is non-fast-forward. It is omitted for a fast-forward attempt and for merge mode. Shipper does not read rejection wording to make this determination, does not infer that a remote forbids force pushes, and never changes the configured strategy automatically.

Covered issue-aware simple implementation, PR-opening, and review-rework implementation publication calls write one bodyless local issue-history verification_run record for every pre-push hook invocation whose start Shipper observes. Each retry is a separate record. No record is written for no-transport publication, when no executable hook or hook-start observation exists, or for pushes outside those orchestrated workflows.

Only the hook’s aggregate status colors verification: 0 is green, another status from 1 through 255 is red, and an absent, malformed, or out-of-range status is unobservable. Remote rejection is separate publication context, so a green hook followed by remote rejection stays green. Ordered step observations and unavailable, partial, or reported coverage never override that color. The record measures only time inside the hook wrapper, excluding transport, remote work, evidence finalization, history persistence, and queue time. Rejected-line and step-mismatch details stop at the first 20 in observed order while their totals count every occurrence. Hook output is not copied into the verification record.

When an attempted push remains unsuccessful, read the Publication Failure comment and the publication report. It names checks and statuses only when observed. The combined raw excerpt is at most 4096 UTF-8 bytes, including omission markers; markers indicate omitted output without claiming exact dropped-byte counts.

Shipper appends every failed attempt to one timestamped pre-push-evidence-*.log under ~/.shipper/logs/, including attempts without a hook or with a reused verification receipt. The report shows Full publication log (complete|incomplete): <path>, with reasons for incomplete capture, or explicitly states why the log is unavailable. A recovered retry creates no terminal Publication Failure comment; its evidence remains in the local log. Evidence files follow the existing ship-log lifecycle without a new retention or recovery guarantee. Successful recovery displays Recovered after a pre-push rejection; complete evidence: <path> or Recovered after a pre-push rejection; incomplete evidence: <path> with capture limitations.

A failed-attempt verification record can point to an available complete or incomplete evidence file; a pointer alone does not establish completeness. Recording remains best effort and cannot change hook gating, publication, retry, remediation, stage, or process results. If collection, validation, or persistence fails after a known hook start, Shipper emits one Failed to record pre-push verification for S#<id> attempt <n>: <reason> warning and does not invent a record.

Open the evidence file as plain text. Ordered attempt headers give timestamps, push commands, Git output and observed or unavailable Git/hook statuses. Optional hook streams and per-step status reports retain only supplied observations. Missing or truncated streams are disclosed as incomplete; complete hook spools alone do not establish a complete publication log.

While its wrapper runs a repository’s pre-push hook, Shipper sets SHIPPER_PRE_PUSH_STATUS_REPORT to an append-only JSONL file. Hooks may best-effort append one version-1 record for each directly observed step:

{
"schemaVersion": 1,
"type": "step",
"step": "lint",
"startedAt": "2026-08-11T12:00:00Z",
"finishedAt": "2026-08-11T12:00:03Z",
"exitStatus": 0,
"reportedResult": "unobservable",
"processes": "unavailable"
}

Every field is required. schemaVersion is 1, type is "step", and step is a nonblank name. startedAt and finishedAt are valid timestamp strings. exitStatus is a nonnegative integer or "unobservable"; reportedResult is "pass", "fail", or "unobservable"; and processes is "unavailable" or an array of objects with a nonblank process name and a nonnegative integer or "unobservable" exitStatus. No records means coverage is unavailable. A malformed or unknown-version record, a report read failure, or an unobservable step exit status makes coverage partial; otherwise one or more valid records makes it reported.

For example, a POSIX hook can preserve its real gating result like this:

Terminal window
run_check() {
step=$1
shift
started_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
if "$@"; then status=0; else status=$?; fi
finished_at=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
if [ -n "${SHIPPER_PRE_PUSH_STATUS_REPORT-}" ]; then
printf '{"schemaVersion":1,"type":"step","step":"%s","startedAt":"%s","finishedAt":"%s","exitStatus":%s,"reportedResult":"unobservable","processes":"unavailable"}\n' \
"$step" "$started_at" "$finished_at" "$status" >>"$SHIPPER_PRE_PUSH_STATUS_REPORT" 2>/dev/null || :
fi
return "$status"
}

Reporting must remain best-effort: a report-write failure must not change hook gating. Supply only statuses the hook actually observed. Do not label unobserved descendants as observed, and do not derive reportedResult or process exit statuses from human-readable command output. Hooks that do not adopt this channel remain fully supported and produce evidence with coverage unavailable. This channel does not claim Shipper traced every descendant of the hook process.

Issue-aware simple implementation, PR-opening, and both review-rework implementation publication paths can skip an executable pre-push hook when the same local issue already has an original actual green verification_run record for the identical tracked tree. Tracked-tree identity is the only eligibility key: the earlier run’s revision, workflow stage, and remote-publication outcome never change it, so a revision-only commit over an unchanged tree stays eligible while any tracked-content change must verify again unless that new tree has its own qualifying original green run.

Shipper looks for a source independently before every publication attempt and every later publication call, after that attempt’s tracked contents are final. When one qualifies, the push runs as git push --no-verify, so the hook never executes and the publication neither acquires nor waits for repository-scoped verification capacity. The ship run log names the short tracked tree and the original event:

Reusing green pre-push verification for tree 4f2c81a09b3d from event 0d4a2f1c-9f4e-4c2a-b6c7-1f9e5a7c2b83.

Each such attempt appends one bodyless verification_run receipt that points directly at that original actual run. A receipt is never itself a source, so receipts cannot chain, and a red, unobservable, or another issue’s run can never authorize a skip. Because no hook ran, a receipt-backed attempt creates no hook status or verification/queue-wait duration. A failed receipt-backed push still collects publication evidence with its actual log availability.

Qualification is fail-safe. Any lookup, read, validation, or source-resolution doubt emits one Could not qualify a reusable pre-push verification for S#<id> tree <short-tree>: <reason> warning and then runs the hook with its normal success, rejection, capacity, and evidence behavior; a valid history with no matching run is silent. Receipt recording is best effort in the same way as executed-verification recording: once a valid source has authorized a skip, a failed receipt append emits one Failed to record skipped pre-push verification receipt for S#<id> attempt <n>: <reason> warning and never turns an otherwise successful publication into a failure. The original run stays eligible for a later attempt.

Reuse assumes that a repository’s tracked contents capture what its pre-push hook checks. A hook that depends on untracked state — machine-local files, environment values, or network results that are not part of the tree — or that inspects the commits being pushed rather than their contents, such as commit-message linting over the push range, branch-name checks, or signature requirements, can therefore be skipped when it would not actually have passed. A revision-only change keeps the tree identical, so those history-dependent hooks are skipped too. A repository-policy opt-out is the intended escape hatch for that case, but it is not part of this behavior today; there is no setting, no dedicated CLI, Desktop, or GitHub surface, and no promised latency for a receipt-backed publication.

Waiting for repository-scoped verification capacity

Section titled “Waiting for repository-scoped verification capacity”

One exclusive slot per canonical repository serializes Shipper-triggered pre-push hook executions across every checkout and worktree of that repository. Different repositories proceed independently and do not wait for, block, reclaim, or alter one another’s reservations. Named identities use ~/.shipper/run/verification/named/<owner>/<repo>/capacity.lock; lineage identities use ~/.shipper/run/verification/lineage/<lineage-token>/capacity.lock. Same-repository serialization covers ordinary clones, the managed clone, moved checkouts, and worktrees.

Only an actual Shipper-managed executable pre-push hook invocation participates, in an issue-aware implementation, PR-opening, or review-rework implementation publication attempt. Local-only (transport: "none") publication, a repository with no pre-push hook, a hook file that is not executable, a publication whose hook Shipper skips or that git never invokes, direct git push outside Shipper, standalone npm test or agent check commands, and hosted continuous integration never acquire or wait for this capacity.

Capacity is acquired once per hook execution attempt, immediately before that hook starts, and released as soon as it completes or is interrupted. Remote upload, server response, fetch, base integration, transport recovery, and remediation never hold the reservation and are never counted as queue or verification time. A retried publication acquires capacity again for each new hook attempt. No first-in-first-out or other acquisition order is promised; every waiter receives the same timeout and holder visibility.

An uncontended acquisition is silent and creates no wait record. On first encountering a holder, a waiting publication prints one line naming the holder PID plus every repository, issue, and stage value it can recover, and it prints that line again only if the holder changes. Waiting never repeats periodically. After a contended grant the publication prints its total queue time once:

Waiting for repository-scoped Shipper verification capacity for owner/repo; holder PID 40321, repository owner/repo, issue S#412, stage implement.
Acquired repository-scoped Shipper verification capacity for owner/repo after 3m 12s in queue.

A reservation is reclaimed only when its owner process is proven dead. Age never permits reclamation: a live, indeterminate, malformed, unreadable, or otherwise unverifiable owner is retained no matter how old the file is, and the waiter reports that holder once and continues toward its own timeout.

If Shipper cannot establish canonical repository identity, publication fails before the pre-push hook starts. The diagnostic states that repository identity could not be resolved and no pre-push verification was attempted. Shipper produces no verification evidence and does not fall back to machine-wide or checkout-local capacity.

A leftover former machine-wide reservation at ~/.shipper/run/verification.lock with no live older owner is silent, harmless, and requires no cleanup. If a strict version-1 record references a currently live PID, stage output warns with that PID and repository, advises restarting the daemon and Desktop to end the mixed-version overlap window, and repository-scoped publication proceeds without waiting or failing.

Each publication invocation may queue for a cumulative 45 minutes of same-repository waits. On exhaustion the publication ends with the same durable issue and stage treatment as any other terminal publication failure, reporting Verification never started because repository-scoped capacity for owner/repo timed out after the cumulative 45-minute queue limit. together with the last observed holder context. That timeout produces no verification_run record, no hook-rejection evidence, no in-command requeue, and no agent remediation, because the hook never started. It still collects a publication log with its actual availability and completeness; capacity timeout alone leaves the push cause unavailable rather than proving transport failure. A later publication invocation receives a fresh 45-minute limit. Waits in other repositories do not consume this budget.

Every positive wait is recorded separately from active verification as a verification-queue-wait record with outcome granted or timed-out, and both outcomes feed the verificationWait shipping phase. See State transitions for how that phase relates to verification duration.

A hook granted exclusive Shipper capacity receives SHIPPER_VERIFICATION_SLOT=exclusive in its environment. Repositories may use that marker to relax concurrency limits they impose only to protect against overlapping local hooks. In this repository the test-worker precedence is: an explicit positive whole number written using decimal digits alone in SHIPPER_TEST_MAX_WORKERS always wins, and no ceiling is imposed on it, so a valid value greater than two is used as requested; without one, an exclusive Shipper run uses exactly two test workers, while a direct non-Shipper push exports 2. An empty, malformed (including signed or whitespace-padded), fractional, zero, or negative value during an exclusive run is rejected with exactly one warning naming the rejected value and the two workers actually used, and verification then continues normally, so a worker-setting typo alone never fails a publication. Standalone test runs and hosted CI never interpret the slot marker.

For other merge or publication conditions, resolve the reported merge conflicts and retry, or remove protected scratch files without rewriting existing feature commits. Choose baseIntegration: "merge" explicitly only when its preserved commit IDs, merge commits on the feature branch, and ordinary-only publication match the repository’s requirements. Otherwise keep the default rebase strategy and repair the remote branch or branch-protection condition.

For no-PR local delivery, a non-fast-forward or fetch-first rejection while publishing the already-advanced base is a different durable outcome: the remote base diverged and must be reconciled before recovery. Other transport failures are untimed skips. Fix transport and rerun; Shipper recognizes the advanced local base and retries publication idempotently.

shipper pr review or shipper implement reports that the reduced review diff exceeds its effective byte ceiling. The diagnostic gives the exact reduced byte count, effective ceiling, supplying settings layer, reviewDiffCeilingBytes, the five largest file contributions with their additions and deletions, and the exact personal-scope command that can raise the ceiling. It also states that the setting is editable in the Desktop repository settings dialog. The review or implementation agent does not run, and the issue enters durable failed state instead of being selected again by auto-ship.

Shipper applies fixed size policy to complete unified-diff file sections. A section larger than 50,000 bytes is replaced with an explicit changed-file marker containing its path and GitHub’s addition/deletion counts. Selection is based only on section size, not filenames, so the same rule applies to lockfiles, generated schemas, snapshots, vendored bundles, minified assets, and ordinary source files. Shipper measures the resulting reduced diff afterward. Review and implementation rework proceed normally when that reduction brings the artifact within the effective ceiling.

The post-reduction ceiling defaults to 500,000 bytes and is controlled by the finite positive whole-byte setting reviewDiffCeilingBytes. It has no force flag, skip-to-merge path, unlimited sentinel, or other bypass. Raising the ceiling causes the complete reduced input to be reviewed; it does not bypass review.

Choose either equally valid remedy:

  • Raise the finite ceiling for this repository with the exact value shown by the diagnostic, for example shipper settings set reviewDiffCeilingBytes 750000 --personal, or edit the same setting in the Desktop repository settings dialog.
  • Shrink or split the change so the post-reduction review diff fits the current ceiling.

Then run shipper recover S#123, followed by shipper pr review S#123. Recovery always stores shipper:pr-open, so review must run again before Rework or Ready transitions can continue. In a local posture that stage displays as Change Packaged. Recovery preserves the exact packaged revision, source branch, stored artifact, and completed review history.

If the stored pull request is closed without merging, first run shipper issue reconcile --repair; a reported recovery returns it to shipper:rework while preserving the branch. For a merged, missing, or absent link, use an ordinary implementation-level reset to an earlier active stage, or adopt the current open pull request when that is the intended replacement. See the state-machine reset contract for destructive cleanup rules.

An issue is marked shipper:failed, repeatedly rolls back after reject verdicts, stops after too many transitions, or posts an Agent Failure comment after a stage exhausts its retries without a verdict.

Shipper treats reject as a rollback event, not an immediate command failure. It can keep advancing after rollback until the work reaches a terminal point, but the shared transition cap MAX_TRANSITIONS = 15 prevents infinite loops and marks the issue shipper:failed when exhausted. No-verdict crashes do not share one universal retry path. Design, plan, implement, and PR review use the stage scaffold’s three attempts for non-zero agent exits. Groom and review-rework implementation have no general stage retry budget, and setup or orchestration failures do not inherit that budget. Specialized merge, remediation, content-validation, GitHub-transport, and integration loops are separate. The canonical failure outcome contract determines whether each exhausted or immediately unsuccessful path may enter durable failed state.

Choose recovery from the canonical posture-specific recovery table. If reset cleanup is acceptable, shipper reset S#123 moves to an earlier valid stage and shipper reset S#123 --to <stage> chooses the target explicitly. A failed issue that still shows or stores workflow stage label X may target X to preserve X-stage work, but Reset still removes artifacts after the target. In either no-PR posture it clears the packaged LocalChangeRecord, so it is not a work-preserving recovery for that artifact.

After a terminal crash, fix the underlying credentials, dependencies, repository state, or other root cause. If the failed issue carries a pinned local change or an open linked pull request, run shipper recover S#123. Recovery dispatches from that stored artifact regardless of current posture, accepts a source branch that advanced beyond the pinned revision, always stores shipper:pr-open, and preserves completed review rounds and workflow history. A missing source branch, invalid revision, divergent or rewound branch, closed linked pull request, or absent artifact makes recovery ineligible and points to Reset. For a closed-unmerged stored pull request, shipper issue reconcile --repair may first return it to shipper:rework while preserving the branch. Fixing the cause does not itself clear durable failed state.

For an exhausted design, plan, implement, or PR-review attempt sequence, inspect any workflow branch and full commit ID named in the failure before choosing Reset. A prior timed-out attempt’s commits remain on that branch even when a later attempt failed for another cause. Reset may intentionally discard that retained progress.

Ordinary stage-selection commands never auto-clear durable failure, including failures previously caused by agent configuration. Missing mirrors, missing or unrecognized workflow labels, accepted stages whose label did not advance, and transition-cap exhaustion also record durable local stage_failure history with a named repair. If a design result was already accepted and only adversarial-note cleanup failed, the accepted transition is preserved and the cleanup diagnostic is reported instead of replacing completed work with failed state.

Do not recover by manually editing GitHub workflow or failed labels. Shipper mirrors those labels from the authoritative local issue-state store, so manual label edits are overwritten by the next store mirror and do not make the issue eligible for automatic selection.

Blocked issue recovery starts with shipper unblock. Desktop users can perform equivalent label-state recovery through the app.

An external closure hold is not shipper:failed. Reset does not clear a hold; use its not-planned, reopen, or confirmed-complete resolution. See the canonical closure-hold distinction.

An agent does not see expected Shipper MCP tools. For a Shipper-dispatched worker stage this is the expected outcome rather than a fault. It is only a problem when a stage you opted back in still sees no tools, or when a run loads tools where the stage should avoid MCP access.

Shipper-dispatched worker stages — groom, design, the adversarial design review, plan, implement, and pr_review — default to MCP loading disabled. Assisted setup (setup) and issue intake (new) default to enabled. One precedence rule decides the rest, with no stage exception: a one-run flag beats all settings, an explicit commands.<stage>.disableMcp beats an explicit commands.default.disableMcp, and an explicit value at either scope beats the shipped default. Shipper logs MCP loading enabled for stage <name>. only when a stage resolves to enabled; a disabled stage prints no MCP line.

Re-enable one stage with shipper settings set commands.<stage>.disableMcp false, or every stage with shipper settings set commands.default.disableMcp false. For one run, use --enable-mcp to force loading or --disable-mcp to prevent it. Read a stage’s effective value with shipper settings get commands.<stage>.disableMcp; the global key reports <unset> because the shipped default varies by stage.

Automation or an operator needs to distinguish a merge waiting on checks, a failure that will retry, a durable merge condition that returned to PR review, an untimed skip, and an immediate durable failure.

Typed causes—not reason or console text—select four actions plus typed deferrals:

  • Deferred: checks pending (exit 78): GitHub reports checks still running. The issue remains at its current stage without moving retry or the shared repository safety counter. CLI and Desktop name the PR and currently incomplete checks on every deferral; Desktop records a Deferred issue run status in issue details and updates one informational notice with the fixed deadline. Later passing checks permit merge without intervention.

  • Will retry (exit 76): Shipper obtained no determination because the merge queue was busy, the local repository was busy, or GitHub failed temporarily. The issue retains its stage without a failure comment so the next dispatch retries the merge. Desktop keeps automatic retry/backoff quiet in issue run details.

  • Returned to Rework (exit 77): a merge conflict atomically records the reason and returns the issue to Rework for implementation.

  • Returned to normal review (exit 77): failed checks, a blocked merge, a still-behind branch, a branch-update failure, or an unknown determined condition atomically records the reason and returns the issue to PR Open (or Change Packaged). See the state machine’s canonical merge failure taxonomy for the complete list.

  • Untimed run skip (exit 78 for a targeted Ship or Merge): required approval, temporary Ready-prerequisite lookup failure, dirty local base, and transient publication or failure-handling trouble retain the stage and skip the issue for the current run. They create no deadline or merge-loop count and are safety-neutral. Required approval consumes no retry; an approved later attempt can merge.

  • Fail now (exit 1): no usable repository merge method and divergent local-base publication enter durable failed state immediately with setting or reconciliation guidance.

Ready prerequisite failures also exit 1 durably: a missing packaged change under the no-PR policy, missing mirror under mirror-on, missing PR association, or missing/closed PR prints the exact reset, PR-open, adopt, or reconcile recovery. A temporary PR lookup failure is the untimed skip above.

Pending checks have a separate total-elapsed bound configured by merge.pendingChecksTimeoutMinutes (default 60). The in-memory clock is keyed by PR head SHA and retains its first timestamp across repeated observations in one CLI process or Desktop application session. A new commit starts a full new window. A process or application restart also resets the window. At the exact deadline, Shipper durably reports CI checks never completed within N minutes for PR #P: CHECKS, marks the issue failed, and suppresses Retry. Store-only repositories persist the same local report and failed state without GitHub label or comment writes. A required check that never reports any status is not visible as pending and is not covered by this bound.

Auto-ship bounds the retry loop per issue. A no-determination failure waits 30, 60, 120, then 240 seconds between attempts while other issues continue. CLI ends instead of waiting when only delayed work remains. Attempt five retains the issue’s stage and failed flag, skips it only for the current invocation or Desktop enabled run, and emits one merge-retry-exhausted safety occurrence. It never calls durable issue-failure persistence. A fresh CLI invocation or new Desktop enabled run can select it without recovery.

Determined conditions do not enter a merge cycle. They leave Ready immediately for either Rework or normal review and remain eligible through that ordinary workflow. A successful merge clears the temporary retry count.

Notification and summary causes are concise and long check-name lists are truncated. The ship log and existing PR failure comment retain the full diagnostic. Store-only repositories record the same reporting locally without GitHub writes.

Retry exhaustion counts once toward the current automatic run’s repository safety stop. Pending-check expiry remains durable across later sessions and requires recovery; retry exhaustion does not. Every other stage-retaining merge outcome is safety-neutral.

For shipper ship --auto, a run whose only unresolved outcomes are deferred or will retry exits 0 because those outcomes are not terminal failures. Any returned-to-review or fail-now outcome still exits 1.

CLI and Desktop share the canonical failure-outcome and repository-safety contract. Three distinct qualifying condition instances engage the stop. An unchanged repeat contributes one strike total. A changed revision or a successful repository advancement before the repeat creates a new instance; advancement does not erase pending strikes.

The six current qualifying identifiers are terminal-stage-failure, merge-retry-exhausted, pending-checks-exhausted, groom-output-invalid, worktree-setup-contention, and upstream-merge-wait. Producers must send one of those classifications or explicit null; omission is rejected rather than defaulted. Active retry and pending-check loops remain neutral until their specific ceiling is reached. Concurrent parked not-attemptable Ready pull requests share one upstream-merge-wait occurrence until every included pull request becomes attemptable, merges, or changes head. Blocked entry refusal and blocked post-work stop are also explicit neutral outcomes, and an eligible advancement before a blocked stop advances the repository progress generation before the neutral blocked outcome is processed. Desktop’s leftStuck value describes card presentation and retry behavior, not repository safety.

The pending ledger is durable and shared by CLI and Desktop from strike one. A later invocation, Desktop relaunch, or daemon restart neither forgets strikes nor recounts an unchanged condition. At the threshold, CLI stops selecting or refilling, drains work already in flight, abandons parked work, reports the triggering conditions and any eligible unattempted issue, and exits 1. Desktop leaves desired state enabled and reports Auto-ship suppressed.

Auto-ship launch retries and recovery-only waiting

Section titled “Auto-ship launch retries and recovery-only waiting”

When automatic work fails before its run starts, Auto-ship remains enabled and recovery is automatic. The Launch retry status reports repository or issue scope, consecutive attempts, the latest command and classified reason, and either the scheduled next-attempt time or recovery-only wake conditions. Short waits are 30, 60, 120, and 240 seconds after attempts one through four; the fifth failure enters healthy recovery-only quiescence. In that posture, unchanged four-second polls launch nothing. A cooled-scope change or ordinary heartbeat permits one probe, and another failure increments the visible count without restarting the short schedule.

Inspect the latest run-history or supervision diagnostic error for the underlying checkout, reservation, setup, admission, or dispatch failure. No Resume or clear-retry action is required. Restart and notification dismissal preserve the episode. A successful automatic or manual start, issue ineligibility for issue scope, or disable followed by re-enable applies the documented selective reset rules. These states are separate from pending-check deferral, post-start merge retry, worktree contention, explicit halt, and the repository safety stop.

Handle 75 from shipper ship as the pause sentinel, 76 as a will-retry merge failure, 77 as a durable merge condition successfully returned to PR review, 78 as a typed deferral (pending checks or an untimed skip), 79 as exhausted clean GitHub-outage waiting, and 80 as a startup refusal caused by a running or could-not-determine dispatch holder. Exit 79 retains the workflow stage, skips the issue for the current run, remains eligible in a later run, and is neutral for the safety stop. Exit 80 attempts no issue and is also neutral for the safety stop. Handle 1 as an immediate durable or ordinary failure and 130 as interrupted shipper implement check polling.

For pending-check expiry or another durable failure whose packaged artifact remains recoverable, use shipper recover S#123 or Desktop’s Recover change. Retry exhaustion needs no recovery; start a later invocation or explicitly enable a new Desktop run after the transient condition clears. If the source branch is missing or no longer contains the pinned revision, the linked pull request is no longer open, or no packaged artifact exists, use Reset instead. Do not edit GitHub labels by hand. Recovery commits locally first; projection failures warn to run shipper issue reconcile --repair and do not undo recovery.

The ready-issue externally-merged recovery remains an exception: merge-lock contention there is quiet and the next refresh retries it without surfacing a failure.

Rework execution or local recovery pending

Section titled “Rework execution or local recovery pending”

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 selects review rework, merge rework, or fresh delivery from the current package history, then re-validates and re-delivers the preserved branch and reuses its open pull request.

If Reset committed; local recovery pending appears, the reset already committed. Do not retry reset. The next store access finishes the existing operation once; if access still fails, repair local store access first. Failed Rework recovery retains the current review pin and stage.

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.

CLI-only pending work survives process exit. Run shipper issue reconcile —repair to service it now; repair respects GitHub holds. Plain repair comes before duplicate-risk authorization when sufficient. No new CLI background process is required.