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.
Shared model
Section titled “Shared model”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:
shipper_commentnotes 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.- 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.
- 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.
Orchestrated child runs
Section titled “Orchestrated child runs”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.
Idle-sleep protection leases
Section titled “Idle-sleep protection leases”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.
Auto-ship repository control state
Section titled “Auto-ship repository control state”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.
Desktop
Section titled “Desktop”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.
Auto-ship supervisor
Section titled “Auto-ship supervisor”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.
Read-only concurrency during mutations
Section titled “Read-only concurrency during mutations”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.
Daemon lifecycle and dispatch
Section titled “Daemon lifecycle and dispatch”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.
Supervision delivery health
Section titled “Supervision delivery health”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.
Residency while enabled
Section titled “Residency while enabled”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.
Generation retirement
Section titled “Generation retirement”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.
Dispatch ownership and operator recovery
Section titled “Dispatch ownership and operator recovery”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.
Version compatibility
Section titled “Version compatibility”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.
Workflow state
Section titled “Workflow state”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.