Skip to content

Architecture

Shipper exposes one local-store-backed workflow through three entry points: the CLI, the desktop app, and the MCP server. This page explains the runtime boundaries and shared state model behind those entry points; command usage, desktop walkthroughs, and MCP client setup live in the task-focused guides and reference pages.

Shipper exposes one local-store-backed workflow through the CLI, Desktop, and MCP, and all three call shared core helpers instead of carrying separate workflow logic. That shared model stores authoritative workflow state, workflow history, and issue title/body content in the local issue-state store and append-only workflow event log, optionally projects workflow labels and workflow comments to GitHub, coordinates guarded work with machine-local issue lock files, and writes stage outcomes through the verdict-based Protocol result contract. Issue locks live under the per-repo local issue-state root, are keyed by Shipper local issue ID, and are shared by the CLI, desktop app, and MCP server on the same machine only. When projection is enabled, GitHub issue title/body content and Shipper-authored workflow comments are mirrors of local state; direct GitHub edits are repaired store -> GitHub.

Workflow history is forward-only from the point the local workflow event log became authoritative. GitHub workflow comments created before that activation point are not backfilled into workflow history, not imported into the local event log, and not automatically cleaned up.

The same append-only log can hold attributed issue notes: ad-hoc decisions or findings whose author identifies the originating agent or role and whose optional stage supplies workflow context. Appending returns the note event ID. Notes cannot be edited in place; withdrawing one appends a separate retraction audit record. Folding keeps both raw records but removes a retracted note entirely from rendered issue history, with no tombstone or superseded placeholder.

The log also holds versioned, issue-scoped verification_run events for observed orchestrated pre-push hooks. These events are bodyless and readable through the generic local workflow-history readers. They are never projected to GitHub and never appear as ordinary local comment history; their structured payload is the consumption surface for local readers and downstream features.

In mirror-on, each live note is projected as a visibly marked ## Shipper Note comment with a versioned hidden ownership marker. Reconciliation uses that exact marker to adopt or recreate a missing projection, repair its body, remove duplicates, and delete every mirror of a retracted note. In store-only, notes remain in local agent-facing history and no GitHub comment is written; existing mirrors stay frozen until mirror-on reconciliation resumes. GitHub post and delete failures never roll back the authoritative local append or retraction.

Agent-facing comments use three distinct channels:

  1. shipper_comment notes are the durable, attributed, retractable record for steering and decisions. They live in the append-only workflow event log and may be projected to GitHub.
  2. External GitHub issue and pull-request conversation comments are transient advisory input. Shipper snapshots their current text at agent-stage entry, labels them as external advisory context, and never interprets them as workflow state or a durable note.
  3. Inline pull-request review threads retain their path and line anchors and flow only through the existing review-rework implementation review-thread path.

The bounded external-comment read does not reverse local-store authority. Advisory text has no path to update issue title/body, workflow stage, blocked or failed status, or priority. Refresh excludes every tracked Shipper workflow-comment and note-projection ID so projected output cannot be ingested again. A stage keeps one snapshot for its lifetime; edits and deletions become visible only at the next stage entry.

Shipper accepts advisory comments from every author GitHub permits to comment, including public commenters when repository interaction settings allow them. There is intentionally no author permission filter in this path. This trust assumption is explicit and revisitable; repositories that need a narrower boundary should use GitHub’s interaction controls until Shipper provides one.

Configuration is shared through Settings at .shipper/settings.json, while repo-local artifacts are documented in the .shipper directory. Bundled prompt bodies live under packages/core/src/prompts/templates/, with per-agent wording and launch settings supplied by capability and invocation tables in packages/core/src/lib/. Stage work runs in ephemeral worktrees stored in ~/.shipper/worktrees/ so entry points reuse the same prompt, worktree, GitHub, and result machinery.

Before a groom prompt is delivered, assembly resolves repository mode, command mode, and session situation alongside agent capability, repository conventions, and delivery context. The resulting prompt therefore contains the reviewed rulebook for that launch rather than deferring mode or situation selection to the agent.

flowchart LR
  CLI[CLI] --> Core["@baremetallabs-ai/shipper-core"]
  Desktop[Desktop] --> Core
  MCP[MCP] --> Core
  Core --> DesiredState["Machine-global auto-ship desired state"]
  Core -->|optional issue projection / PR operations| GitHub[GitHub]
  GitHub -->|bounded stage-entry external advisory read| Core

CLI, desktop main/preload, and MCP production code import the stable @baremetallabs-ai/shipper-core root package. The @baremetallabs-ai/shipper-core/internal entry exists only for Shipper workspace test/support harnesses, has no stability guarantee, and is not a production dependency.

Core’s stable startOrchestratedShipperRun() boundary launches a configured command and environment and returns an independent per-run handle. Its launch-time UUID and operating-system PID exist before any structured question arrives, so several handles can run concurrently without sharing state. Each handle streams marker-filtered stdout and separately identified stderr, surfaces the existing defer/answer question lifecycle, optionally enforces an overall time limit, and reports exit code, signal, consumer-stop, time-limit, and unexpected-error completion facts.

Consumers attach output destinations at launch. A graceful stop signals the process tree first and uses forced termination only after the caller’s grace period; an immediate forced stop is also available. On POSIX, each child has its own process group so tree signals reach its descendants; hosts must therefore stop active handles during shutdown because signals aimed only at the host’s process group do not reach those children. Core keeps no run registry, retained-output replay buffer, lookup-by-ID operation, or durable run record. This shared boundary does not change the desktop execution path; desktop adoption and any late-attaching output replay belong to later work.

Substantive outer owners acquire independent idle-sleep protection leases around progress-capable work. On Darwin, each active lease owns one /usr/bin/caffeinate -i -w <owner-pid> child so macOS can aggregate overlapping assertions. -w is the crash backstop; there is no process-local reference count, helper daemon, IOKit dependency, or display assertion. Non-Darwin platforms return a strict no-op lease that does not spawn, write state, or change output.

Orchestrated parent code alone suspends a lease at an observable headless operator-answer wait and resumes it before writing an answer or classifying a terminal outcome. Attended native questions have no such seam and keep the lease for the live session. A spawned Shipper child under an effective outer owner receives an internal delegated activity id so it does not create a duplicate CLI lease; that marker is spawn metadata, not a setting.

A guarded machine-wide ledger under ~/.shipper/run/ (under the Shipper home) records only live activities and an optional current loss occurrence. Dead owner and assertion PIDs are pruned on every transaction. The ledger never retains completed occurrences. Warning history lives in the dedicated persistent log under ~/.shipper/logs/ and in existing run records, session metadata, and status surfaces. Only the transaction that creates a loss occurrence elects one proactive emitter; other affected workflows receive the same UUID-tagged event through history. Failure to coordinate is retried while the lease remains active and never fails healthy workflow work.

Core owns one machine-global Auto-ship repository control record for each stable repository identity. It is shared by every Shipper process for the current operating-system user that resolves the same Shipper home and survives process exit, restart, host replacement, and machine reboot. The version 6 record contains the canonical repository identity, the desired-state fields enabled, workerCount, and haltRequested, a separate durable safetyStop latch and bounded engagement history, and the enablement generation, per-enablement launchFailureRetries ledger, and per-issue saturated worktree-setup contention attempts. An enabled repository with safetyStop.current present remains durably enabled but automatic dispatch is suppressed; engagement and resume never rewrite the desired-state fields. Each new supervision host reloads the latch before candidate inspection rather than treating process restart as a clear.

Classified automatic pre-start launch failures (repository preparation, issue reservation, launch setup, session admission, and dispatch) share one per-enablement retry ledger. The first failure is attempt one; waits of 30, 60, 120, and 240 seconds follow attempts 1–4; the fifth failure exhausts short retries and enters recovery-only quiescence. A later sparse probe is authorized only by a cooled-scope observation change or the ordinary heartbeat, never by the four-second control cadence. Failed probes stay recovery-only. Issue reservation cools only that issue; every other or unknown classification cools the repository. Auto-ship stays enabled, work stays eligible, and there is no Resume or clear-retry action. Scheduled short retries are progress-capable for sleep prevention; recovery-only is healthy idle supervision. Manual launch failures, post-start merge retry, pending-check deferral, worktree-setup contention, explicit halt, and the repository safety stop remain separate.

The ledger survives daemon/Desktop handoff, process or machine restart, elapsed time, notice dismissal, and a reason change within the same cooled scope. A future saved deadline waits only its remaining time; an elapsed or implausibly distant deadline authorizes at most one prompt retry, and missed slots are never replayed. A successful automatic or manual start clears the repository episode plus that issue’s episode while preserving unrelated issue episodes. Authoritative inspection retires issue episodes that are no longer eligible, and a genuine disable followed by re-enable starts a new enablement with an empty retry ledger. CLI Launch retries, MCP and JSON launchFailureRetries, the Desktop repository status line, issue cards, and the consolidated notice all report the same scope, posture, attempt count, and next-attempt time or recovery triggers.

The stable core package exposes typed read, inspection, and patch operations so adopting surfaces do not access the stored bytes directly. Patches serialize read-modify-write operations across processes, return previous and resulting desired state plus per-field applied/unchanged outcomes from the same guarded write, and merge only the fields a patch actually supplies. haltRequested is therefore literal: { enabled: true } preserves an outstanding halt, and only an explicit haltRequested: false clears it, including as part of an atomic combined enable-and-clear patch. An absent record is measured against the effective safe default, so clearing an already-clear halt reports unchanged. Classification is ordered: unsupported positive schema version first, then valid-envelope repository identity mismatch, then strict known-version parsing, then replaceable corruption or absence. Absence and replaceable corruption can create a safe current record, but compatibility conflicts are authoritative retained state, never effective defaults. A malformed safety snapshot invalidates the whole record, so corrupt data can never be interpreted as enabled without suppression.

All ten writers—desired-state patch, ordered safety-event accounting, dedicated safety resume, automatic launch-failure recording, launch-failure retry consumption, successful-launch clearing, launch-failure retry reconciliation, launch-failure acknowledgement, and worktree-setup contention record/clear—classify under the per-repository state.lock before atomic whole-record replacement. Version 1 through version 5 records retain every known desired-state, halt, safety, generation, and contention value and migrate contiguously to strict version 6 on the next successful real mutation, initializing empty launchFailureRetries because the old notice cannot establish scope or posture. Historical engagement tuples become explicit legacy-unknown conditions rather than invented independent issue identities. The result discloses migration even when every requested desired value is unchanged. An already-current exact no-op performs zero physical writes. Replaceable corruption can be recreated, while unreadable storage is reported distinctly.

An unsupported future schema remains byte-for-byte unchanged and returns its retained and current versions, readable versions, path, and staleSide: "client", with only the current-client or installation remedy. A valid version 2, 3, or 4 envelope for another canonical identity likewise remains unchanged and returns the requested and retained identities plus the path, with only an external-file resolution remedy. Shipper offers no repair, reset, discard, or automatic rewrite for either unsupported-schema-version or repository-identity-mismatch compatibility conflict.

The durable safety ledger retains pending strikes from strike one, a progress generation, the ISO engagement time, the lifetime engagement count, and the exact three condition instances that engaged the stop. CLI and supervisor submit ordered events through the same state.lock; no host keeps an authoritative shadow streak. Identity uses qualifying kind, scope, condition type, and subject, with revision and progress epoch separate. The same unchanged revision in one progress epoch is a duplicate; changed revisions, A → B → A, and recurrence after successful advancement append. Admission generation fencing discards late completions after resume or re-enable.

Exactly two atomic state transitions clear safetyStop.current: a genuine durable not-enabled-to-enabled transition, or the dedicated safety-resume operation. Both advance the accounting generation and clear the prior generation’s pending condition ledger; the engagement being cleared becomes the one retained previous entry. Repeating enabled: true, a successful manual run, elapsed time, daemon exit or restart, and host replacement do not clear the latch. Engagement and dedicated resume preserve enabled, workerCount, and haltRequested, and manual operator-initiated runs remain admissible while automatic work is suppressed.

Halt, disable, and the safety stop are three distinct states. A halt is a persistent operator-requested, repository-scoped, machine-local hold stored only in haltRequested. It preserves enablement and worker settings, withdraws queued automatic starts, drains running automatic Ships to their safe checkpoints, blocks every later automatic stage and merge, and survives an idle repository, wind-down completion, and daemon, Desktop, CLI, or machine restart. It ends only through an explicit clear. Disable is enabled: false: it drains current automatic work and prevents dispatch until Auto-ship is enabled again, but neither disable nor enable alone clears a halt. The safety stop is the automatic failure safeguard described above, with its own durable latch and its own resume gesture, and safety recovery never clears a halt. Each machine honors and clears its own Auto-ship desired state independently; a halt is never synchronized across machines.

The supervisor consumes this state and never writes it. Draining, Halted, and unknown-runtime Halt requested are presentations derived from live runtime evidence, not persisted phases: a surface reports Draining only when represented runtime facts show draining automatic work, Halted when represented facts show none, and Halt requested when those facts are unavailable. No surface describes a halt as consumed or automatically cleared.

MCP and the CLI are thin adapters over that surface-neutral foundation. MCP derives the active repository’s canonical identity, passes one guarded patch to core for shipper_set_auto_ship, and composes core repository-state inspection into shipper_status. The visible shipper auto-ship enable|disable|drain|clear-halt|resume group instead always derives the current checkout’s stable identity with no repository-target, all-repositories, settings, or worker-count surface. Its five gestures use only the shared durable desired-state, safety-resume, registry, and dispatch-owner operations; they neither create a daemon client nor launch or attach to a supervisor. shipper auto-ship clear-halt and the no-input MCP tool shipper_clear_auto_ship_halt each perform exactly one { haltRequested: false } patch, and Desktop exposes the same operation as a dedicated Clear halt board action. shipper_resume_auto_ship and shipper auto-ship resume each make exactly one atomic resumeAutoShipSafetyStop call and never turn Auto-ship on. Neither adapter reads the machine-global state file directly or exposes its path, format, or lock. Core still provides no watcher or change-notification mechanism, and core StatusSnapshotSchema, buildStatusSnapshot(), and CLI shipper status --json remain unchanged.

Terminal and MCP enable apply identical desired-state semantics, including halt preservation and transition-only safety-stop clearing, and both retain the resolved checkout in the machine-global repository registry. Terminal enable registers the checkout first, then records intent; it reports that the checkout is now supervisable and visible to Desktop and the daemon, but it starts or contacts no supervisor. Explicit MCP enable records intent first, records the resolved checkout, upserts the retained local entry, and then makes a bounded supervisor-availability attempt. It reports exactly one of available, not-started, incompatible, or indeterminate; an incompatible outcome carries client, daemon, and minimum-daemon versions plus the stale side and asks a human operator to re-run Shipper installation. Other mutations do not start or attach to a supervisor, though a running supervisor normally observes them within roughly five seconds.

The repository registry’s v2 schema keeps separate desktop and mcp source partitions. The retained wire name remains sources.mcp, but both terminal and MCP enable upsert local checkouts in that partition; Desktop replaces only its full configured-repository projection. Local upserts use the active canonical storage key and resolved absolute checkout path. Both changes use the same inter-process lock and atomic replacement, so either writer preserves the other source and consumers deduplicate by stable identity while combining Auto-merge eligibility. A v1 registry migrates under the same lock.

The desired-state model retains important limits. It is runtime state, not configuration, and is not part of shipper settings. Halted or disabled state arms repository-control Stops for running automatic Ships at safe checkpoints and withdraws queued automatic work. An enabled, unhalted observation retracts only unconsumed repository-control requests; explicit per-run Pause and Stop gestures win. Consumed stops and withdrawn entries are not revived, and clearing a halt never replays stopped runs, restores withdrawn queue entries, or dispatches replacement work; only ordinary Auto-ship eligibility may select later work. Explicit manual advances and per-run Pause and Stop actions remain available under a repository halt. Non-enable changes to untracked repositories persist, but no supervisor acts on them and there is no board surface. Enabled means requested, not dispatching, because an availability result, a persistent halt, or durable safety suppression may show that dispatch is inactive without changing the recorded value. Auto-merge remains a separate operator setting. Finally, this state is machine-global operator state, not committed repository configuration and not shared with collaborators.

The CLI runtime starts in command modules under packages/cli/src/commands/<name>.ts. Each command validates input, performs the needed prerequisites or issue selection, and then either runs command-local orchestration or calls shared helpers from packages/core.

Prompt-driven stages cross into core at runPrompt() in packages/core/src/lib/prompt-runner.ts. That boundary renders the bundled prompt template for the resolved agent and workflow step, reads structured invocation metadata, appends requested issue or PR context, and spawns the configured agent CLI. For groom, rendering also resolves repository mode, command mode, and session situation before delivery, alongside agent capability, conventions, and delivery context.

The new, groom, design, plan, implement, and PR commands run in fresh ephemeral worktrees. withWorktree() in packages/core/src/lib/worktree.ts handles creation, callback execution, and cleanup under ~/.shipper/worktrees/. The stage runs from the configured base branch or PR branch so the agent does not operate in the caller’s dirty checkout.

Cleanup runs worktree-teardown first so a repository can stop its services gracefully. Shipper then discovers every process whose resolved working directory is the worktree or one of its descendants, regardless of who started it or whether it remains in the stage process tree. Open file descriptors alone do not qualify a process. Remaining holders receive SIGTERM, are rediscovered during a fixed two-second grace period, and then receive SIGKILL with one further second allowed for exit. Shipper writes one worktree line naming stopped PIDs and commands. Incomplete inspection, signalling, or exit produces one aggregated warning, but cleanup still removes the worktree and does not change stage success.

Inside the target worktree transaction, core first checks only the exact branch and generated path that would block the current stage. This synchronous target-scoped recovery finishes while the target path guard is held. After the transaction releases that guard, core uses a trigger-driven scheduler to start a machine-wide background sweep and never awaits it. The caller therefore does not wait for any machine-wide repository inspection, whether target preparation succeeds or fails.

The background sweep still spans Shipper worktrees for every repository represented under the shared ~/.shipper/worktrees/ root. Concurrent triggers coalesce into one in-process task. Before inspection, that task tries once to acquire a fixed machine-wide lease; a contended lease is skipped without waiting. A failed or contended attempt ends silently and clears the in-process task so a later trigger may retry. A failed task releases any lease it holds, while an abandoned lease ages out after a generous bound. Candidate path guards remain the active-use boundary. There is no polling interval, per-second tick, timer-driven directory scan, or renderer state. The global task produces no normal log, aggregate summary, recovery record, toast, notification, prompt, or board state.

Every new worktree has a durable owner record under ~/.shipper/worktree-owners/ containing the creating process ID and canonical parent Git common directory. The global path removes a recorded worktree only after that exact PID is confirmed dead. Elapsed lock time is not a reclamation gate: a live or indeterminate PID remains protected indefinitely. Issue-lock ownership uses the same liveness-only rule.

When teardown cannot completely inspect or stop holders, it also writes sparse retirement provenance outside the worktree. This record contains only the worktree path, canonical parent Git common directory, and retirement time; it is not live ownership. Matching provenance is removed after recovery or successful path reuse. If its workspace remains absent, the record itself expires after 30 days. A record is retained while its workspace exists regardless of age.

A fixed legacy fallback applies only to pre-existing worktrees that have no owner record. It requires all three conditions: the directory is older than 14 days, its parent repository can be attributed confidently, and the directory is no longer registered with that parent. The fallback age and the global sweep have no setting or opt-out. Ambiguous ownership or parent metadata remains in place.

The same unchanged, strict older-than-14-days lane can reclaim a future linkless recreation only when matching retirement provenance names a resolvable parent repository, that parent registers no worktree at the path, the directory uses the current hashed --wt-- name grammar, and a complete cwd inspection finds no holder. A missing, malformed, unreadable, or mismatched record; an unavailable or registering parent; any holder or incomplete inspection; a legacy or Desktop-groom name; or a younger directory is skipped silently. Recreations from before this behavior shipped have no retirement provenance and remain sweep-ineligible. The sweep remains unconditional, machine-wide, silent, trigger-driven, non-blocking, leased, and single-flighted.

Target-scoped recovery accepts missing evidence, Shipper’s own process, and the current run’s token-matched issue lock as neutral. A third-party owner that is live or whose liveness is indeterminate blocks recovery, as does malformed, unreadable, or mismatched ownership evidence. A registered checkout is eligible only when it belongs to the same canonical Git common directory, names the exact branch, and lives inside ~/.shipper/worktrees/; a main or developer checkout outside that root is never altered.

An unregistered target with a valid .git parent link still requires that link to resolve to the same canonical Git common directory and its canonical directory to be the exact generated path and name for the branch. In addition, the exact current target can recover with no Git linkage when it is inside the managed root, the current parent registers no worktree there under any branch, .git is truly absent, and no live-owner sidecar exists. Empty, ignored, unignored, and otherwise non-empty contents are treated alike. Under the target path guard, Shipper runs the same cwd-holder stop sequence and refuses deletion with PID, command, and diagnostic details if inspection or termination is incomplete. Existing malformed or unreadable linkage, mismatched or unreadable ownership, different parents, live owners, and outside-root checkouts remain protected. A successful recovery emits one worktree-recovered record, which produces one transient Desktop notification; it does not emit a separate stopped-process success line. Age is irrelevant for synchronous target recovery. Neither target recovery, teardown enforcement, retirement evidence, nor the global policy can be disabled, tuned, or loosened by a flag, environment variable, setting, age override, or command option.

Reclamation deletes the entire confirmed orphan directory. Uncommitted and untracked files are discarded without being inspected, listed, previewed, or preserved. The branch ref and every commit remain in the parent repository, and pull request state is unchanged; reclamation never deletes branch refs, resets commits, or mutates a pull request.

shipper next and sequential shipper ship dispatch the next stage in-process through packages/cli/src/commands/stage-dispatch.ts. Parallel shipper ship --auto --parallel <n> uses packages/cli/src/commands/ship-auto.ts to fork packages/cli/src/ship-worker.ts, with the parent and worker exchanging one run message plus one result message over IPC for each isolated job.

All GitHub interaction goes through the gh CLI. Structured gh --json payloads are checked through packages/core/src/lib/gh-schemas.ts and packages/core/src/lib/gh-json.ts so command code works with parsed shapes instead of unchecked JSON.

The desktop app is an Electron runtime split between renderer code, the preload bridge, and the main process. Renderer code calls the preload API exposed by packages/desktop/src/preload/index.ts, and the main process handles those calls through ipcMain.handle(...) handlers under packages/desktop/src/main/handlers/.

Desktop is a client of the per-user Shipper daemon (one daemon per Shipper home). Its main-process controller starts or connects to that daemon before accepting background work and relays current sessions, activity tasks, Auto-ship state, narration, durable run records, and retained output pages to the renderer. If the daemon is unavailable for the bounded recovery window, the controller starts the same BackgroundSupervisionRuntime in process and keeps that fallback for the rest of the Desktop process lifetime. Shared run/task state feeds repository tabs, boards, issue details, setup, and New Issue placeholders. The daemon lifecycle and dispatch section owns the shared lifecycle, coordination, and version-compatibility behavior.

BackgroundSupervisionRuntime is the shared policy and process owner in both modes. It launches all six background workflows—New, Ship, Next, Init, Unblock, and Merge—through the same core prompt, worktree, reservation, lock, and result-protocol paths used by terminal commands. Desktop-owned PTY Setup and Sign in sessions remain exceptions. Desktop grooming also remains Desktop-hosted: an interactive PTY groom or a headless groom holds the ordinary issue lock, and the headless child uses the desktop-headless-groom marker while it runs or awaits an answer.

The host-agnostic Auto-ship supervisor in packages/core is the single policy owner for each canonical repository identity. It selects and orders candidates, admits manual and automatic work, tracks retry, safety, and drain state, derives the host projection, and chooses which narration events are delivered. Only the committed safety latch and its bounded history are persisted; partial failure accounting and the other policy structures remain local to the current host.

A host supplies durable control, repository snapshots, run observations, explicit time and wakeups, repository inspection and execution effects, and narration-event delivery. The supervisor has no DOM, renderer, Electron, ambient clock, timer, focus, or visibility dependency. Presentation remains a host responsibility after the supervisor has selected an existing narration event.

Every control cadence re-runs the canonical record inspection. A compatibility conflict closes new automatic dispatch and polling without cancelling queued or running work, clearing a halt, resuming a safety stop, acknowledging a launch notice, or otherwise rewriting retained bytes. Already admitted work drains normally; dispatch ownership remains held until that work settles, then is released. Runtime, daemon, and Desktop projections use an explicit unavailable arm containing the inspection and independently known repository facts, with no desired state, canDispatch, safety suppression, or safety-stop values. Once an operator externally restores an interpretable record, the next cadence recovers normal supervision without restarting the host.

The daemon is the normal host. It owns workflow children, automatic scheduling, repository-scoped dispatch leases, retry and pending-check waits, and partial safety accounting until its drain settles. A child—not the runtime or supervisor—holds each issue run lock and merge lock. Manual work remains guarded by launch reservations and those child-held locks even when another live process holds a repository’s automatic dispatch lease.

Background supervision is host-neutral. Its host injects both the pause-signaling storage root and a synchronous destination for output, session-status, activity-task, projection, condition, narration, and run-record events. The runtime itself has no Electron or renderer dependency. Desktop attaches through revisioned daemon subscriptions and hydrates current state before live events. Retained output pages can be fetched after attachment; live output chunks are transient and are never journaled.

Initial runtime startup publishes host-neutral progress for run-history recovery, aggregate and per-repository checkout resolution, dispatch-ownership acquisition, and the first policy-bearing fill. Desktop and the daemon translate the same events into the shared supervision-recovery diagnostic schema. Desktop uses only the aggregate checkout event for operator progress while the per-repository events remain diagnostic detail. It subscribes before local startup so it can report progress, but it deliberately does not request hydration snapshots until runtime.start() has resolved; one first snapshot then materializes readiness.

The prolonged-recovery investigation established one contained checkout coupling. A Desktop headless grooming session is a separate child process, so the former process-local managed-clone leases did not coordinate it with in-process fallback startup. Startup previously refreshed every named checkout, including disabled repositories. The unconditional startup refresh is removed: startup now resolves or creates the managed checkout without reset, clean, or remote synchronization. Ordinary automatic candidate selection reads locally stored issue facts as a proposal. Only after that proposal is actionable and both dispatch and merge quota admissions are open does one authoritative pre-dispatch inspection synchronize the repository and reselect from current facts. That current-policy decision is fill-local, may be reused for every automatic enqueue for less than 30 seconds, and is never reused across a fill boundary.

Authoritative reselection can replace the local proposal or abandon it neutrally. A current-policy failure prevents dispatch. Automatic Ship, Merge, and Unblock reuse the checkout established by the inspection instead of refreshing again while enqueueing. Manual commands and non-Auto-ship callers retain their refreshed checkout or current-policy behavior. Managed clones now use a repository-scoped, cross-process reader/writer lease for that remaining boundary. Simultaneous setup readers proceed together, a waiting refresh writer blocks later readers, and every wait is silent and bounded by one five-second deadline. A lease holder is reclaimed only when process liveness proves its PID dead; age alone and indeterminate liveness never authorize reclamation. Worktree setup releases its read lease before dependency bootstrap, repository hooks, or agent work, so unrelated and long-running user work is not serialized. This shared guard completes the focused follow-up across S#206’s process-local coordination boundary without adding a second checkout resolver or a machine-wide lock.

The governing rule for shared setup resources is consistent: Shipper serializes or hardens access when the owning tool hard-fails on concurrency without internal retry, and relies on tool-native concurrency safety when the tool already provides it. Git common-config updates are read-first and retry a contended config.lock within the same five-second bound because Git fails that write immediately. In contrast, npm and Turbo shared caches retain no Shipper lock because those tools support concurrent clients themselves. The cache no-lock decision and managed-clone/config hardening are therefore applications of one rule, not exceptions to one another.

S#321 (GitHub #1773) records this narrow exception as the second amendment to #1528 (S#206) R6, after the once-per-fill amendment in #1689 (S#278), and as an amendment to #1652 (S#255) R9. Only automatic candidate selection may propose work from local state; dispatch, auto-unblock, and auto-merge reconciliation still fail closed unless current committed policy is established at the action boundary. This is not a general time-window freshness cache.

Repeating snapshot hydration did not contribute to slow local startup. Before progress reporting, Desktop subscribed to local runtime events only after runtime.start() resolved, so runtime events could not cause snapshots while startup was pending. After readiness, runtime events can still trigger full snapshots and retained narration replay. That post-start replay is the S#315 reappearing-toast lead; this recovery work does not change toast behavior or claim to reproduce the original ten-minute incident.

Quitting Desktop closes its daemon connection but does not stop daemon-hosted work. Queued and running work, retry and pending-check waits, review/rework transitions, post-merge unblocks, run records, and shared run/task state survive until the drain settles. A committed safety stop and remote quiescence are distinct conditions. The durable stop remains enabled, reported, and unchanged, while that repository’s process-local supervisor can quiesce remote polling; resuming the safety stop wakes the repository on the next control cadence. A clean daemon generation retirement and successor takeover are invisible to Desktop. If a daemon dies mid-run, startup reconciliation marks only runs whose prior supervisor is proven dead as orphaned; Desktop presents those durable outcomes as Interrupted.

This durable repository-level contract explicitly supersedes the previously published process-local, run-scoped, and daemon-lifetime safety-stop contract recorded by S#233, S#263, S#286, and S#290. Host exit is no longer a resume gesture.

The runtime also uses filesystem-based IPC for flows that need process-to-process coordination beyond stdout and stderr. Native Desktop grooming launches a Desktop-hosted headless child and completes through its grooming session and result flow. Interactive Setup passes a temporary control directory as SHIPPER_DESKTOP_CONTROL_DIR and uses the finalize-requested request for terminal close through packages/core/src/lib/desktop-control.ts and packages/core/src/lib/prompt-runner.ts. Background ship halt signaling is allocated beneath ~/.shipper/pause-sentinels/<hostPid>-<uuid> and passed to the child only as SHIPPER_PAUSE_SENTINEL_FILE; the file contains a schema-v1 origin record. Desktop best-effort removes the abandoned pause-sentinels directory from its legacy Electron user-data location during startup, without making that old location part of the supervisor boundary. Each host removes its own exact files; another PID/UUID-owned file is reclaimed only when that PID is proven dead, while alive or indeterminate owners are retained and timestamps never authorize deletion.

Desktop distribution is currently unsigned macOS arm64 only; see the Desktop install guide for current distribution status.

The MCP server uses stdio transport only, reserves stdout for MCP protocol messages, and routes ordinary logs to stderr. At startup, createServer() captures the launch cwd and creates one inert active-repository object, then runs repository-directory selection, settings loading, identity resolution, and preflight. Those repository-startup steps resolve into ready or degraded readiness. One stable real-schema catalog is then registered exactly once. A failure to construct that protocol surface still fails server construction; repository-startup failure does not.

SHIPPER_REPO_DIR is resolved relative to the startup working directory when set; otherwise the startup working directory is used, and a successful startup changes into that repository as the initial active repository. Startup calls loadSettings(), which returns the one authoritative repository identity for the selected root, then runs the local preflight. A usable named remote identity is preferred when present; otherwise commit lineage supplies a stable identity. A committed local repository without a remote is supported. If that identity is absent, startup reports the resolved path and exactly one observed cause: Git is unavailable to the MCP process, so install Git and make it available to that process; the path is not a valid inspectable Git repository, so use a valid repository or repair its accessibility or integrity; or the repository has no commits, so create an initial commit. After correcting the external condition, recover in band with shipper_switch_repo, or restart or reconnect the MCP client. Startup does not run a global GitHub authentication or gh repo view gate. Local tools therefore register while GitHub is unreachable or unauthenticated; GitHub-dependent tools surface external failures only when invoked.

While degraded, repository reads without an explicit repository_path and all mutations return the immutable original startup error. shipper_switch_repo remains callable for recovery, and a valid explicit repository_path on a repository-scoped read bypasses that active-readiness gate for one isolated call. Target preparation validates the local checkout, resolves identity with ambient repository selectors ignored, and resolves target-local settings. It performs no preflight, GitHub call, daemon start, cwd change, settings commit, or active-context mutation. The selected handler then retains its ordinary behavior, including live external inspection where applicable. Targeted reads do not count as active-repository work and preserve successful response schemas. A failed target reports its own error while leaving the degraded active state and original startup error unchanged.

shipper_switch_repo can also change the active repository after a healthy startup. Targets may be absolute paths or paths relative to the server’s current working directory. A target must already be an existing readable local Git checkout with at least one commit that passes the same settings, policy, and preflight readiness as a normal startup. A committed local repository without a remote is a valid switch target; a remote is not required. The tool does not clone or initialize repositories. A switch updates process.cwd(), the active repository identity used by later tools, and the loaded repo-local settings. This is runtime-only state: restarting the MCP server returns to the repository chosen by startup resolution rather than a prior recovery target, and the server still has one active repository at a time. Pending shipper_answer_question sessions remain pinned to the repository where they were created, while new sessions after a switch use the new active repository.

Switch coordination is per server and fail-fast. After canonicalizing the requested path, a healthy request for the already-active checkout succeeds before busy checks. A different target is refused immediately when the exact sampled active-operation count is nonzero; no pending switch is installed and no later state change can occur. Otherwise preparation acquires a non-queuing switch guard while unrelated calls continue. A concurrent different switch is refused. Immediately before commit, the server samples the active count again and refuses if work began during preparation. With a zero count, cwd, committed settings, active context, and readiness change synchronously with no await in that critical section.

After a successful degraded-to-ready recovery the server emits a tools/list_changed notification so capable clients can refresh. Normal invocation does not depend on a client relist: the stable catalog already advertised the real names and schemas, so clients that do not refresh can still call them. The recovered selection does not persist across a server restart.

Tool registration builds one documentation corpus context with buildDocsCorpus() in packages/mcp/src/docs/corpus.ts. resolveDocsCorpusRoot() first looks for workspace docs, then bundled docs, then an absolute SHIPPER_DOCS_PATH fallback. The Environment variables reference owns details for SHIPPER_REPO_DIR, SHIPPER_DOCS_PATH, and the desktop control variables.

The tools in packages/mcp/src/tools.ts fall into three implementation classes. Direct helpers call core utilities without spawning shipper, including shipper_status, shipper_list_issues, shipper_get_issue, shipper_get_pr, shipper_adopt, shipper_set_priority, shipper_set_auto_ship, shipper_resume_auto_ship, shipper_close_not_planned, and shipper_unlock. shipper_get_pr omits mode for the one-shot comprehensive call graph: summary and merge state, one checks observation, and bounded review submissions and inline threads. Its mode: "checks" branch reuses the retrying checks helper without loading summary, reviews, or threads. shipper_set_priority calls the shared store-backed priority operation directly: it does not spawn the CLI, and GitHub priority labels are projections rather than control inputs. Direct in-process helpers include shipper_docs_search, shipper_docs_get, shipper_reset via executeReset, deterministic shipper_unblock, shipper_switch_repo, and shipper_answer_question for resuming pending headless sessions. shipper_advance and shipper_groom adapt core’s orchestrated-run handle into MCP’s protocol-session map and unchanged tool presentation. Issue-backed MCP runners do not supply a stage-wide deadline: each headless agent attempt independently enforces agentTimeoutMinutes, while retries, corrections, answer waits, projection, cleanup, run recording, and lock release remain owned by the child. Stable displayed-ID answers are forwarded without charging time paused for product-owner answers against an agent attempt. One-shot workflow-changing tools keep their existing subprocess helper: shipper_create_issue uses headless mode, while shipper_merge runs shipper merge --once for ready PRs. The MCP reference owns the per-tool surface, while MCP setup owns client setup.

MCP and desktop both call closeIssueAsNotPlanned() in core; MCP does not invoke desktop IPC, and neither entry point spawns the CLI for this action. The operation resolves the local identity, acquires the issue lock, validates the required non-blank reason and attribution, and re-reads current state under that lock. A terminal-transition journal stages the exact existing history plus one ordinary reason note and the not_planned tombstone, commits that logical unit, then rolls it forward by installing history and tombstone before removing the active snapshot. Prepared journals abort without changing live state; committed journals finish on the next read, so recovery produces exactly one note and one tombstone. Store-only stops after that authoritative local commit. Mirror-on then converges the note comment, managed labels, and issue lifecycle from the tombstone. An already-closed mirror is not reopened or re-closed, but still receives the normal note-projection attempt. Projection failures are retained as mirror-pending repair debt without rolling back the local closure; shipper issue reconcile --repair retries the identity-owned comment and other surfaces without duplication. An authoritative local completed tombstone is the only outcome-conflict rejection. This closure path is unrelated to reset, development-artifact cleanup, and dependent-issue completion inference.

shipper_status returns structured, counts-only repository orientation data except for the three distinct qualifying conditions that engaged a safety stop. Issue records contain localId, nullable githubIssueNumber, and title; repository records contain only the affected object, and migrated legacy records say identity is unavailable; inventory, run, session, lock, stage-list, and other-repository identities remain excluded. It and shipper_list_issues consume the same local issue-inventory projection and the same blocked/failed definitions. Status counts stage, blocked, and failed dimensions orthogonally; the listing assigns each issue to one exclusive section using failed, then blocked, then stage precedence. A numerical difference is intentional. The shared core status snapshot uses explicit machine-readable unavailable markers when local issue state cannot be read. The CLI renders that same unchanged snapshot but exits 1 for degraded issue state; MCP returns it successfully with the shipper issue reconcile --repair recovery pointer.

MCP extends the snapshot in packages/mcp/src/tools.ts with independent top-level autoShip, daemon, and automaticWork sections. The active-repository operation guard holds the captured repository stable while MCP builds the core snapshot, inspects the full durable repository state, reads advisory daemon status, and strictly inspects dispatch ownership. Available repository status exposes enabled, workerCount, haltRequested, and required suppression. The false suppression variant contains only { suppressed: false }. The true variant projects suppressed: true, engagedAt, exactly three distinct qualifying failures with strict issue, repository, or legacy-unknown scope, operator context, kind, conditionType, summary, cause, and relation, lifetime engagementCount, and resumeTool: "shipper_resume_auto_ship". It does not expose revisions, raw observation counts, the accounting generation, previous engagement, or persistence shape. Because all values come from one record, an unreadable record degrades the whole section behind { status: "unreadable", message }. A retained unsupported-schema-version or repository-identity-mismatch record instead returns { status: "compatibility-conflict", conflict, message }, with the same version facts and remedy as the canonical core classifier. Neither unavailable branch fabricates desired-state, suppression, dispatch, or safety defaults, and neither offers repair, reset, or discard. They do not reuse the issue-reconcile command or hide healthy issue-state fields. Conversely, degraded issue state does not hide readable auto-ship state. A never-recorded repository and an explicitly disabled/default record produce the same effective observation. Direct durable inspection keeps suppression accurate after daemon idle exit.

The advisory daemon view validates the owner record, status record, and PID liveness without opening a daemon socket. A running value exposes instanceId, version, generation, superseded, and updatedAt; plain not-running is ordinary, while unreadable or inconsistent facts use an explicit unavailable marker. For a running v2 status record, automatic work selects only the active storage key and publishes the counts-only inFlight and drainInProgress values. Legacy status, a missing repository row, non-daemon ownership, and uncertain ownership remain distinct unavailable states rather than fabricated zeroes. When both daemon status and ownership prove nothing is running, zero and false are accurate. Outside the bounded suppression failures, no issue, stage, run, session, lock, or other-repository identity is exposed. shipper_status may briefly attach to a reachable daemon for current supervision-delivery health, then closes that read-only connection. It never starts or mutates the daemon, makes no GitHub request, and may reset the existing client-free idle grace period. CLI status uses the same live inspection and normalization.

shipper_set_auto_ship uses the same operation guard, derives one canonical repository identity, and performs exactly one core update. A recorded response identifies the repository and includes the full previous and resulting states, an applied-or-unchanged outcome for every field, and any lossless v1-to-v3 or v2-to-v3 migration. That one-write provenance avoids a pre-read race and lets callers confirm an atomic worker-only patch, a persistent halt, an explicit halt clear, an enabled-and-halted state, or an atomic combined enable-and-clear without a follow-up read. Any patch that sets haltRequested: true also carries the normalized automaticWork evidence the tool actually observed. A refused response contains only status, repository, and the unsupported-schema-version or repository-identity-mismatch conflict; it cannot imply an owner, success, desired-state outcome, migration, supervisor availability, or dispatch deferral. Only a recorded explicit enable then registers the checkout and makes the bounded availability attempt. Checkout, registry, launch, timeout, and daemon-protocol compatibility problems do not roll back recorded durable intent; they become the discrete availability result. Disable, halt, worker-only, and halt-clear paths never start, stop, restart, signal, or attach to a supervisor.

shipper_clear_auto_ship_halt derives the same active canonical identity and performs exactly one { haltRequested: false } patch. It maps the core outcome to cleared or already-clear, uses the same structured compatibility refusal, and never pre-reads state, registers a checkout, alters enablement or worker count, resumes a safety stop, or starts or attaches to a daemon.

shipper_resume_auto_ship derives the same active canonical identity and calls core’s atomic resume operation once. A recorded response maps resumed and already-resumed to cleared and not-engaged without a pre-read. A compatibility refusal contains only status, repository, and conflict. It does not rewrite desired state, register a checkout, touch the repository registry, or connect to, launch, or coordinate a daemon. Consequently a recorded operation can clear an engaged stop while the repository remains disabled and preserves enabled, workerCount, and haltRequested atomically.

The daemon lifecycle and dispatch section owns the shared lifecycle, coordination, and version-compatibility behavior behind those MCP outcomes.

While an MCP mutation is awaiting asynchronous work, every read-only MCP request is admitted and allowed to progress, including during a long shipper_reset cleanup transport or stage run. The MCP operation coordinator never queues a read behind a mutation merely because the mutation is in flight. This admission boundary does not make synchronous work concurrent: a request sharing the Node.js host is dispatched after the current synchronous work returns control to the event loop.

A read that Shipper can answer locally is served from durable state and finishes on its own, and that holds even when it concerns the very issue being mutated. A read that depends on GitHub or another external service still pays that dependency’s ordinary latency — that cost belongs to the dependency, not to the mutation.

Consistency comes from the issue-state store boundary rather than from waiting. A read that happens before a mutation commits logically sees the previous complete snapshot. A read that overlaps the short atomic commit may briefly wait on the existing store lock, but it never observes prepared or partially installed state: individual files are installed by atomic rename, and an outstanding journal is recovered by ordinary reads. A read after the logical commit sees the committed result. Each read therefore returns the latest coherent durable state available at the moment it runs.

Overlapping reads keep their ordinary response shape and their normal success and error semantics. Active work stays observable through the existing lock, ownership, and activity indicators; no generic staleness warning is added just because a mutation happens to be running.

None of this widens any other boundary. Mutation-versus-mutation concurrency is unchanged, and issue locks still exclude a second mutation on the same issue. Repository switching never drains or holds new work: busy requests refuse immediately, preparation permits unrelated calls, and the final pre-commit check refuses if any active-repository operation is then in flight.

The Shipper daemon is one cross-repository, machine-global service for the current operating-system user’s Shipper home; a relocated SHIPPER_HOME runs its own independent daemon. It hosts Auto-ship supervision. Desktop launch and explicit MCP Auto-ship enable are the only operations that automatically start it. Status reads and the hidden inspection command never start a daemon, but they briefly attach when one is reachable; other read-only MCP queries, ordinary CLI commands, and all four shipper auto-ship gestures neither start nor attach. See Inspect Daemon State.

The Shipper directory reference is the authority for the daemon endpoint, advisory runtime state, and lifecycle diagnostic log locations.

The adapter records acceptance immediately before enqueueing each state-bearing snapshot-start, snapshot-item, snapshot-end, or event envelope and observes completion from the socket write promise. This includes initial snapshots, resynchronization snapshots, replay, checkpoints, and live events across every supervision channel. Control/lifecycle traffic and transient live output are excluded.

One timer targets the earliest accepted write’s 10-second boundary. Before every projection the adapter also refreshes classification, so delayed timer dispatch cannot return a false healthy result. A subscribed connection is healthy while no accepted write is overdue and degraded when at least one is; an unsubscribed connection is not applicable. Aggregate health is degraded when any connection is degraded. Socket reachability, time since publication, and remotely quiescent idle time are independent inputs: static equal accepted/completed progress remains healthy indefinitely.

The detector is advisory and has no recovery authority. It does not detach clients, restart or stop the daemon, or stop supervised work. Current degradation clears as soon as no overdue write remains or the connection is removed. Additive wire fields preserve older consumers; a new consumer reading an older peer reports health as unknown (legacy-peer) while retaining independently usable client identity, socket-alive, attachment facts, connected-client count, and daemon facts. Inapplicable timing is omitted. Current health is live-only; durable rotating diagnostics retain exactly one episode start and one restore/removal record, plus one coalesced immediate-rejection record.

The daemon remains resident while at least one repository has Auto-ship enabled, including when all enabled repositories are remotely quiescent and its ordinary work inventory is empty. This takes up the resident-daemon decision that S#290 deliberately deferred: enablement now means continuous watching, while disabling every repository remains the operator gesture that restores normal idle retirement.

Remote quiescence is process-local and independent per repository. After a fill finds no admitted work, no actually running workflow, and no Ready pull request whose checks are still in progress, that repository skips standing managed-checkout refreshes and open-pull-request/check reads. Local issue state remains readable without remote work. Any local issue-state change capable of changing what is dispatchable wakes only the affected remotely quiescent repository on the roughly four-second local control cadence. Non-exhaustive examples include issue adoption, issue creation, grooming completion, unblocking, workflow-stage changes, issue pause or resume, safety resume, desired-state updates, and registry membership changes. Each quiescent repository also receives a deterministic, storage-key-spread heartbeat every 20–25 minutes so externally originated pull-request and check changes are observed without a synchronized request burst. This S#319 slow heartbeat is the sole no-proposal authoritative observation exception. It remains supervision-quota gated, and work discovered by it cannot dispatch unless both dispatch and merge admissions are open. Quiescence is not durable, is re-derived after restart, and has no operator resume gesture.

After the last client detaches, normal idle exit requires both an empty work inventory and zero enabled repositories after the existing grace period. Explicit graceful shutdown and superseded generation retirement remain governed by the real work inventory, so residency and heartbeat state do not prevent either operation from draining cleanly.

When a running daemon detects that its generation has been superseded, it immediately closes automatic admission, begins releasing automatic dispatch authority, and cancels queued automatic work. During this retirement window, the reachable daemon is deliberately running older code and deliberately not starting new automatic work. It remains reachable and continues serving attached and newly connecting clients, and it may still admit manual requests.

After its entire work inventory becomes quiescent, the retiring daemon stops accepting connections, announces supersession and going-away to attached clients, drains protocol writes, and exits. A launch-capable client’s next connection attempt starts the successor running the new code. Clients treat going-away as an orderly transition: they reconnect instead of reporting a crash.

New automatic work cannot perpetuate retirement because automatic admission is closed and queued automatic work was cancelled. Manual requests can extend the retirement window, so its bound is on automatic work rather than elapsed time.

The per-repository dispatch-owner.json record is a two-way live-owner interlock shared by Terminal auto-ship, daemon supervision, and Desktop’s in-process fallback. Each participant atomically obtains the same repository-scoped authority before it may start an automatic drain. The first running or could-not-determine holder wins, so at most one supported automatic drain runs for a repository at a time. A proven-dead holder becomes not held; no timestamp expires dispatch ownership.

For each enabled repository, the daemon normally acquires and retains that authority while automatic admission is open, including when the repository is remotely quiescent. Desktop fallback follows the same rule. If Terminal already holds ownership, that repository’s supervisor defers only new dispatch: in-flight workflows, non-dispatch control work, and supervision of other repositories continue. The deferral is repository-local and self-resolving; the supervisor retries on its roughly four-second control cadence and resumes admission after Terminal releases its exact ownership. Enabling Auto-ship while Terminal is active records the desired state normally and begins supervision as soon as ownership becomes available.

Terminal auto-ship likewise refuses to begin when Desktop, the daemon, another Terminal drain, or an unrecognized compatible holder is running or has could-not-determine liveness. CLI status, MCP status, and Desktop project the same surface, PID, acquisition timestamp, liveness, diagnostic-only last refresh, and repository consequence. Unavailable inspection is unknown, names no holder, and offers no recovery gesture.

The operator clear is a guarded compare-and-delete, available through Desktop’s Clear dispatch ownership, shipper auto-ship clear-dispatch --force, and the two-call MCP tool shipper_clear_dispatch_ownership. Its token covers stable repository and holder identity but excludes renewedAt, liveness, schema version, and future fields. Same-owner refresh does not make a preview obsolete; a successor owner returns changed and nothing is deleted. The operation does not change desired state. Discarding dispatch ownership held by a genuinely running holder means two schedulers will dispatch for this repository concurrently.

A halt or disabled state requests safe-checkpoint Stops for admitted automatic Ships. Returning to enabled and unhalted retracts unconsumed repository-control requests, but never revives work that already stopped or queued work already withdrawn. Cause coalescing applies: clearing a halt retracts an owned, unconsumed repository-control signal only while enablement is also open, because a simultaneous disabled state still justifies the same signal. A persistent halt is released through shipper auto-ship clear-halt, the Desktop Clear halt action, or shipper_clear_auto_ship_halt, each of which preserves enablement, worker count, and the safety stop. An engaged safety stop is separately cleared through shipper auto-ship resume, Desktop Resume, or shipper_resume_auto_ship without changing enablement, worker count, or the halt.

--ignore-dispatch-lease remains an unsupported inspection-only bypass: it publishes no ownership, releases nothing, and does not cause any supervisor to stand down.

Release clients and daemons with different major versions cannot interoperate; the version 4.2 Auto-ship wire-contract boundary also rejects 4.x release peers from opposite sides of that version. Development builds remain compatible. When a release pair is incompatible, the product reports the client version, daemon version, and minimum required daemon version and identifies the stale side when the versions are comparable. The operator resolves the mismatch by re-running Shipper installation. Shipper never kills, evicts, restarts, or replaces an incompatible peer.

The desktop board displays five forward-progress columns: Design · Plan · Implement · Review · Ready. Cards currently in Rework appear in Implement with a Rework badge, which disappears on return to Review. CLI/MCP status retains six next-step groups, including Rework; desktop Implement combines their Implement and Rework groups, with attention exclusions unchanged. Underlying workflow states remain New, Groomed, Designed, Planned, PR Open, Rework, and Ready.

Completed review is a derived gate: acceptance retains its round and moves PR Open directly to Ready, while rejection on every route atomically retains the round, supersedes eligible current-package history, pins review rework, and moves to Rework without replacing the package. Next, Ship, and auto-ship dispatch Rework to implementation. A sixth rejection within the current five-round allowance commits that same event group with a Failed-over-Rework snapshot. Explicit preserving reset or eligible active packaged recovery restarts the allowance while preserving all history; automatic cycles continue consuming it. Rework receives the same in-flight association and external-closure protections as PR Open. The retired raw shipper:pr-reviewed label is read as a legacy alias for PR Open and is normalized only when a later transition writes workflow state.

Workflow state is stored locally and optionally projected to GitHub labels. The stage sequence branches on the issue route design assigns. A complex-route issue runs the full chain shipper:new -> shipper:groomed -> shipper:designed -> shipper:planned -> shipper:pr-open -> shipper:ready, while a simple-route issue runs one combined implementation-and-delivery step from shipper:designed and never records shipper:planned. The State Machine page owns the full transition table, but every entry point uses the same state model before deciding what work can run next. Priority labels affect auto-ship ordering as mirrors of the local priority field.

Auto-ship candidate selection and FIFO ordering read the local issue-state store, including each issue’s workflow stage, priority, blocked/failed flags, local lock overlay, and stageEnteredAt timestamp. If that replica is absent or unreadable, selection fails with guidance to run shipper issue reconcile --repair instead of falling back to GitHub timeline reads.

Grooming also prepares one local index for its cross-issue scan before the groom agent starts. The index has two entry kinds: every other live managed snapshot, at every open workflow stage, and, in mirror-on repositories only, open unadopted GitHub-only issues. The issue being groomed, closed managed issues, and tombstones are not candidates and have no detail file. Every entry carries the same bounded 512-character body skim, and every detail file — managed or GitHub-only — contains exactly that issue’s full body and nothing else. A managed candidate’s current title, stage, and blockers stay in its index entry, and its accumulated workflow-event history is never supplied; a GitHub-only candidate contributes no comments or invented Shipper workflow fields. The agent reads only that index and its referenced local details and performs no live scan lookup.

Product decisions recorded on another open issue or in workflow notes are advisory. When one is needed to settle the current result, ask it as an ordinary current-session question. Behavior observed directly in the current product is a factual baseline, and unchanged observed behavior is never reconfirmed merely to establish it. Explicit prerequisite reconciliation is separate from this discovery: the current issue’s recorded blockers arrive in groom-recorded-blockers.json, a completed prerequisite is satisfied automatically, a not-planned prerequisite stays unresolved until the product owner chooses a replacement or removal, and neither is reported as a discovered related issue.

The mirror-on unmanaged tier is an advisory GitHub read prepared alongside the authoritative local records. Shipper suppresses current mirror identities and remembered same-repository import origins, preserves most-recently-updated order, and exposes at most the newest 100 unmanaged issues. The index states whether that tier is complete, truncated, or unavailable. A supplemental read failure discards any partial unmanaged population and continues with explicit managed-only coverage; a missing or unreadable local replica remains fatal with shipper issue reconcile --repair guidance. Store-only never performs the unmanaged read or origin lookup and retains its unchanged offline managed-only path.

Issue content reads use the same local store. shipper new, grooming parent edits and child issue creation, and PR adoption-created tracking issues write the store first, then mirror the fitted projection to GitHub. Adoption and first reconciliation of fresh or recreated stores provide supported GitHub -> store seed paths. After that, shipper issue reconcile --repair creates missing open mirrors, repairs local-owned workflow/title/body/priority/failed/blocked drift store -> GitHub, repairs missing or drifted workflow comment projections from the event log, and reconciles initialized records whose GitHub issue is closed. GitHub shipper:blocked labels and canonical ## Blocked comments are imported into typed store blockers during first reconciliation of fresh or recreated stores. Supported local-ID V3 sources retain blocker imports too. Durable pending imports retry after interruption, even after metadata becomes reconciled; stored V3 blocker lists can complete locally in store-only mode, while GitHub-derived debt waits for mirror-on repair.

GitHub-number snapshot migrations are retired. Numeric filenames in issues/ or _pending-blocker-imports/legacy/ refuse the entire store before recovery or writes, even if JSON or metadata is malformed. The refusal identifies the store and every old record and leaves existing local issue state unchanged. Use a Shipper release from before this change to migrate the store before retrying this version; no specific release is promised. Local-ID V3 and later locally reconstructable upgrades remain supported, and a current record’s githubIssueNumber field does not make it retired. The obsolete shipper:locked GitHub label has no lock meaning: reconcile ignores and preserves it, and operators may delete it manually.

Mirror mode changes which GitHub projections are allowed, not which local state is authoritative. In mirror-on, reconcile reconnects a read-only same-repository issue origin when it is open, unclaimed, and has no shipper: label, then converges the local authoritative state onto it. PR-only, foreign, and origin-free records receive fresh mirrors. Closed, missing, labeled, or claimed origins remain unmirrored and are reported as Mirror pending (not currently repairable). In store-only, existing mirrors are frozen and reconcile does not report or repair issue-side mirror drift. When mirroring is re-enabled, reconcile realigns frozen mirrors from the local store and closes old open mirrors for tombstoned records. Local ordering, stage-entry timing, workflow/completion history, PR associations, and tombstones are preserved across those round trips.

Manual GitHub edits to workflow stage, failed, or priority labels no longer change Shipper behavior. Those labels are optional mirrors of the local store and are restored to the store value by reconcile repair when issue projection is enabled; blocked state and blocker relationships are also local-store owned. The shipper:blocked label and canonical Blocked by #N comment are human-facing mirrors repaired from the store only in mirror-on repositories. Lock state is local runtime state and is not mirrored through GitHub labels.

Control labels change selection and safety behavior across the whole workflow. shipper:blocked prevents normal advancement except the allowed new-to-groomed path, and shipper:failed blocks next and auto-selection until reset or recovery.

Local issue locks prevent concurrent workflow access on the same machine. Acquiring a lock writes locks/local-<id>.json under the per-repo issue-state root with the owner PID, token, and acquiredAt timestamp. Dead, corrupt, unreadable, or process-less records are reclaimable. Live or indeterminate owners remain active regardless of age and expose their PID and acquisition time before an explicit force unlock. Two different machines do not observe each other’s locks.

Stage output is reduced to a verdict described by the Protocol. accept advances to the next state, reject rolls back according to the transition table, and fail or a crash terminates the run as a failure.