> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbi.build/llms.txt
> Use this file to discover all available pages before exploring further.

# Setup

# One-time setup

`muyan-pilot setup` is the one-time, config-driven initialization
entry for a new machine or a new task-pool repository. It verifies or
reinstalls the editable CLI install, verifies the local prerequisites,
aligns the platform labels, installs the systemd user units, checks
the checkout, and reports the optional model proxy — in one command,
with a stable machine-readable result.

```bash theme={null}
muyan-pilot setup --config muyan-pilot.toml
```

Run it once per machine (and once per new task-pool repository). It is
**idempotent**: re-running it never creates duplicate labels, units or
timers, and never touches business Issues or business labels.

## What it does, in order

Setup is **fail-fast**: a core prerequisite failure stops the run
before any later mutation, with the concrete reason on stderr and a
non-zero exit code.

1. **Commands** — `git`, `gh`, `python3`, `uv` and the installed
   `muyan-pilot` CLI on the PATH (you install these yourself — setup
   only checks them; a missing one fails fast with the actionable
   install guidance, e.g. `required command missing: uv (not on PATH)
   — install uv first: curl -LsSf https://astral.sh/uv/install.sh | sh`),
   and a reachable `systemctl --user` user bus (a container or headless
   session without a user bus fails with the bus error).
2. **CLI editable install** (Issue #152) — the official local
   deployment is the EDITABLE `uv tool` install: the tool env imports
   `muyan_pilot` from the deployment checkout, so the `ExecStartPre`
   checkout sync is picked up by the next CLI process automatically.
   When the running process already imports from the configured
   `repo_dir` the step verifies it (no uv call, `cli=verified`);
   otherwise it runs the exact force editable reinstall
   (`uv tool install --force --reinstall --editable --python
   /usr/bin/python3 <repo_dir>`, `cli=installed`). A failing install
   fails fast — no unit install, no half-initialized state. The step
   never touches a running Runner process (the new source is loaded by
   the next CLI start).
3. **Auth** — `gh auth status` (logged in with a usable token).
4. **Per target repository** (every configured `source_repos` entry by
   default; exactly one with `--repo OWNER/REPO`):
   * the repo exists and the viewer has write permission
     (`gh repo view` → `viewerPermission` must be `WRITE`, `MAINTAIN`
     or `ADMIN`);
   * the eight platform labels are aligned **declaratively** from the
     repo-managed `labels.toml` (repo root) — the single source of
     truth for label name, color and description: a missing label is
     created, a drifted label is updated, nothing is deleted, and
     business labels (`bug`, `enhancement`, ...) are never touched.
5. **Systemd units** — the repo templates
   (`systemd/muyan-pilot@.service`, `systemd/muyan-pilot@.timer`) are
   installed idempotently into the user unit directory (the same
   install `install-units` performs: copy, migrate the pre-#149
   non-templated units away once, `daemon-reload`, enable the two timer
   instances `muyan-pilot@1.timer` / `muyan-pilot@2.timer` — the
   service is never started, stopped or restarted), then each timer
   instance's enabled/active state and next trigger time are reported.
6. **Checkout + git transport** — check of the configured `repo_dir`:
   the `origin` remote's **transport** (Issue #114): git data
   operations (fetch, push — including `.github/workflows/*.yml`) must
   go over SSH (`git@github.com:owner/repo.git`), so an existing
   HTTPS `origin` is migrated here with the plain
   `git remote set-url origin git@github.com:owner/repo.git` (setup is
   the human-authorized migration path — the Runner itself never
   rewrites a remote), the SSH URL must match the first configured
   source repo — a remote pointing at a DIFFERENT repo is never
   migrated (the rewrite would re-target the checkout at another
   repository) and fails with `setup_failed reason=... origin remote
   repo mismatch ...` — and `git ls-remote <ssh-url>` must exit 0
   (SSH reachable and authenticated — a failure is `setup_failed
   reason=... ssh_unreachable ...`, no HTTPS fallback). Then the
   read-only parts: current branch, clean worktree (a dirty checkout
   fails: the timer's `ExecStartPre` fast-forward refuses a dirty
   worktree, so the Runner could never start), and base freshness
   (local `HEAD` vs freshly fetched `origin/<base_branch>` —
   reported, not a failure: the timer fast-forwards a clean checkout
   at each start).
7. **Optional model proxy** — the `local-llm-kv-cache` proxy health
   endpoint (`http://127.0.0.1:18082/health`) is checked and reported
   as **optional**: its absence or unhealthiness is a warning in the
   output and never blocks the core GitHub/Pilot setup.

Setup never creates business Issues, never claims a task, never starts
Pi, and never modifies a protected branch.

## Options

| Option                 | Meaning                                                                                     |
| ---------------------- | ------------------------------------------------------------------------------------------- |
| `--config PATH`        | The `muyan-pilot.toml` configuration (default: `$MUYAN_PILOT_CONFIG` or `muyan-pilot.toml`) |
| `--repo OWNER/REPO`    | Initialize exactly this configured source repo (default: every configured source repo)      |
| `--installed-dir PATH` | User unit directory (default: the standard `~/.config/systemd/user`)                        |
| `--json`               | Print the result as JSON instead of `key=value` lines                                       |

## Output

The default output is stable `key=value` lines (one per concern; values
containing spaces are quoted), so agents and scripts can parse it:

```text theme={null}
setup=ok version=2 base_branch=main
cli=verified source=/path/to/checkout/muyan_pilot.py
repo=xqliu/muyan-pilot permission=ADMIN default_branch=main labels=8/8
service=installed path=~/.config/systemd/user/muyan-pilot@.service sha256=9f2c...
timer=muyan-pilot@1.timer enabled active=true next="Thu 2026-08-27 10:00:00 +08"
timer=muyan-pilot@2.timer enabled active=true next="Thu 2026-08-27 10:05:00 +08"
checkout=remote=origin branch=main clean=true base_fresh=true remote_url=git@github.com:xqliu/muyan-pilot.git protocol=ssh migrated=false ssh_reachable=true
model_endpoint=optional optional_proxy=healthy url=http://127.0.0.1:18082/health
```

`cli=verified` means the running CLI already imports `muyan_pilot`
from the deployment checkout (the editable install is in place);
`cli=installed` means the force editable reinstall ran. `labels=8/8`
means all eight platform labels match `labels.toml`;
there is one `timer=` line per timer instance and its `next` is that
instance's next trigger time (`-` when it has none);
`optional_proxy` is `healthy`, `unhealthy` or `unavailable` and never
changes the exit code. `migrated=true` means setup rewrote an HTTPS
`origin` to the SSH URL (a re-run then reports `migrated=false`);
`ssh_reachable` is the `git ls-remote` probe result.

`--json` prints the equivalent JSON document:

```bash theme={null}
muyan-pilot setup --config muyan-pilot.toml --json
```

## Success and failure examples

Success (exit code `0`):

```bash theme={null}
$ muyan-pilot setup --config muyan-pilot.toml
setup=ok version=2 base_branch=main
cli=verified source=/path/to/checkout/muyan_pilot.py
repo=xqliu/muyan-pilot permission=ADMIN default_branch=main labels=8/8
service=installed path=~/.config/systemd/user/muyan-pilot@.service sha256=9f2c...
timer=muyan-pilot@1.timer enabled active=true next="Thu 2026-08-27 10:00:00 +08"
timer=muyan-pilot@2.timer enabled active=true next="Thu 2026-08-27 10:05:00 +08"
checkout=remote=origin branch=main clean=true base_fresh=true remote_url=git@github.com:xqliu/muyan-pilot.git protocol=ssh migrated=false ssh_reachable=true
model_endpoint=optional optional_proxy=unhealthy url=http://127.0.0.1:18082/health
```

(The optional proxy is down here — note `optional_proxy=unhealthy` —
but the core setup still succeeds with exit code `0`.)

Failures (non-zero exit code, `setup_failed reason=...` on stderr):

```text theme={null}
setup_failed reason=required command missing: uv (not on PATH) — install uv first: curl -LsSf https://astral.sh/uv/install.sh | sh (https://docs.astral.sh/uv/getting-started/installation/)
setup_failed reason=systemctl --user user bus unavailable (is a systemd user session running?): ...
setup_failed reason=editable tool install failed for /path/to/checkout: ... (fix: uv tool install --force --reinstall --editable --python /usr/bin/python3 /path/to/checkout)
setup_failed reason=repo not accessible: nobody/no-such (gh repo view failed: ...)
setup_failed reason=insufficient permission for owner/repo: viewerPermission='READ' (one of ['ADMIN', 'MAINTAIN', 'WRITE'] required to manage labels)
setup_failed reason=label alignment failed for owner/repo: ai-ready (gh label create failed: ...)
setup_failed reason=checkout is not clean: /path/to/checkout (uncommitted changes: ' M bootstrap_runner.py') — commit or stash them first: ...
setup_failed reason=checkout check failed for /path/to/checkout: ssh_unreachable: git ls-remote git@github.com:owner/repo.git failed: ... stderr=git@github.com: Permission denied (publickey). — fix SSH and retry
```

A missing prerequisite command (e.g. `uv`), a wrong repo, missing
labels that cannot be created (permission), a dirty checkout, an
unreachable SSH (no HTTPS fallback) or a missing systemd user bus all
stop the setup with the concrete reason — nothing is half-initialized
without a clear signal.

## Idempotency

Re-running setup on an already-initialized machine is a no-op for the
external state: labels that already match are not rewritten, the unit
templates are re-copied from the repo (drift repair), the timer stays
enabled, and no Issues or business data are created or modified. The
reported hashes and states are stable across runs. The HTTPS→SSH
migration is one-shot: after the first run the `origin` remote is SSH
and re-runs only re-verify it (`migrated=false`).


## Related topics

- [Getting started](/getting-started.md)
- [Optional kv cache](/optional-kv-cache.md)
- [Release v0.1.2](/release-v0.1.2.md)
- [Operations](/operations.md)
- [Testing](/testing.md)
