> ## 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.

# Operations

# 运维

正常运行完全自动：timer 触发 tick，tick 最多做一件事，进度自己发布到
journal 和 GitHub。正常路径永远不需要 status 命令、轮询或督工——下面的
命令只用于首次验证、排查和恢复。

## Timers

timer 模板 `systemd/muyan-pilot@.timer` 部署为**两个实例**：
`muyan-pilot@1.timer` 和 `muyan-pilot@2.timer`。每个实例每 5 分钟触发
一次，全天 24 小时（`OnCalendar=*-*-* *:00/5`、`AccuracySec=30s`、
`Persistent=false`——错过的 tick 被丢弃，从不排队），并各自启动自己的
service 实例（`muyan-pilot@1.timer` → `muyan-pilot@1.service`，
`muyan-pilot@2.timer` → `muyan-pilot@2.service`），所以可以同时运行
两个独立的 Runner 实例。每个 tick 的 service 实例：

1. **先 fast-forward 代码**（`ExecStartPre`，在 Python 进程外）：
   `git fetch origin main && git merge --ff-only origin/main`。checkout
   不干净、fetch 失败或无法 fast-forward 时 preflight 失败：service 不
   启动，原因写入 systemd journal（fail fast）。正在运行的长任务从不被
   热更新或杀掉——一个 service 实例 active 时 systemd 忽略对**该实例**
   的后续 start 请求，下一次真正启动取到最新代码。两个实例可能在同一
   tick 运行 `ExecStartPre`，所以 fetch + fast-forward 包在一个短生命
   周期的 `flock`（共享状态目录下的 `base-sync.lock`，Python 侧同步取
   同一把锁）里：main worktree 不会被并发写入。自 Issue #171 起，所有更新
   共享 remote-tracking ref 的 fetch 都走同一把锁：Runner 侧的
   `freeze_base` / `verify_pr` / `merge_gate` / `confirm_merged` fetch 统一
   经 `fetch_base_ref`（先取锁 → fetch → 释放，锁或 fetch 出错即 fail
   fast），implement/review 两个 prompt 指示 Pi 用
   `flock <BASE_SYNC_LOCK> git fetch origin <base_branch>` 执行 base
   新鲜度 fetch（锁路径从配置的 `repo_dir` 渲染进两个 prompt）。两个并发
   Runner（或 Runner 与 Pi 会话）因此不再争抢共享 ref（不会出现
   `cannot lock ref ... is at <X> but expected <Y>` 失败）。
2. **运行一个 tick**：恢复一个已打开的 PR（review/fix/merge），或领取
   一个 `ai-ready` Issue，然后退出。在任何 slot 或领取之前，tick 还
   运行启动前 **git transport 检查**（Issue #114）：部署 checkout 的
   **配置的** `origin` remote 必须是第一个配置 source repo 的 SSH
   形式，且 `git ls-remote <ssh-url>` 退出 0（SSH 可达且已认证）。
   传输损坏记录结构化 `transport_check_failed ... reason=...` 行并让
   启动失败——不取 slot、不领取、不改标签，**没有 HTTPS 回退**（git
   数据操作，包括 `.github/workflows/*.yml` 推送，永远走 SSH；GitHub
   API 操作留在 `gh` token 上）。

```bash theme={null}
systemctl --user list-timers 'muyan-pilot@*.timer'
systemctl --user status 'muyan-pilot@*.service'
```

## Unit 模板变更后（Issue #131、#142）

unit 模板（`systemd/muyan-pilot@.service` 和 `systemd/muyan-pilot@.timer`）
是部署配置，不是普通代码——但模板变更合并后**不需要任何人工步骤**。下一次 timer 触发时 `ExecStartPre` 同步 checkout，启动前
`unit_drift` 检查自动自愈：用同一个幂等安装（把两个模板复制到用户
unit 目录、daemon-reload、enable 两个 timer 实例——从不启动、停止或
重启运行中的 Runner）、再用同一个哈希检查复核，tick 继续，每个 unit
记录一行结构化 `unit_drift auto_synced`（before/after sha256、部署
commit）。不再出现“每 5 分钟重复同一个 `unit_drift` 错误直到人工
介入”的循环（#131/#140 实例：每次启动都因 `unit_drift` 失败，直到
人工运行同步命令）。同一个安装还会**一次性迁移** #149 之前的非模板
unit（`muyan-pilot.service` / `muyan-pilot.timer`）：停掉旧 timer（停
的是 timer，绝不停 service）并删除旧文件，旧单实例调度不会再拉起旧
service。

`muyan-pilot install-units --config muyan-pilot.toml` 保留为手工入口
（首次 setup、需要立即同步时）。自愈后仍漂移（安装步骤失败、模板
缺失、复核前 unit 再次漂移）时，启动仍然 fail fast（结构化
`unit_drift` 行、非零退出、不取 slot、不领取 Issue）：哈希校验和
哨兵边界不变。

## 日志（journal）

journal 是本地记录。一个 run 的每行都以 run id 前缀 `[<run_id>]` 开头，
一条 grep 还原完整时间线：

```bash theme={null}
journalctl --user -u muyan-pilot@1.service -u muyan-pilot@2.service -f
journalctl --user -u muyan-pilot@1.service -u muyan-pilot@2.service | grep e07383c2
```

你会看到的稳定 `key=value` 行：

* `run_start` / `run_end` — 开始时的完整现场（branch、worktree、
  session 文件），结束时的结果（PR URL、commit）；
* `activity` / `heartbeat` / `model_wait` / `resumed` — session 运行
  期间的实时 Pi 活动（phase、最近动作、elapsed、idle）；
* `pi_idle` — 超过 300 秒（`PI_IDLE_WARN_SECONDS=300`）没有
  model/session 活动且模型不期望回复时的一次 WARNING；活跃的慢模型
  （`model_wait`）从不告警；
* `run_failed` — 完整现场加原因（`pi_exit_N`、`timeout_...s`，或
  `upstream_dead_stale_...s`——冻结的 `model_wait` 超过
  `PI_MODEL_WAIT_DEAD_SECONDS`（默认 600 秒）判定上游模型已死，Runner
  杀掉 Pi）。

idle 告警和上游已死 kill 是日志/健康阈值——它们不是每 5 分钟调度，也
不是业务任务 timeout。

## CLI（`muyan-pilot`）

正式入口是安装后的 `muyan-pilot` console script，以 **editable**
`uv tool` 安装（官方本地部署——tool 环境直接从 clone 目录导入
`muyan_pilot`，`ExecStartPre` 的 checkout 同步会被下一个 CLI 进程自动
取到，普通源码/模板变更无需重装或升级；`uv tool install --force --reinstall --editable --python /usr/bin/python3 <clone 目录>`——见
[快速开始](/zh/getting-started)）。`muyan-pilot doctor` 报告一致性
（`cli_source: clean` 或 `cli_source: DRIFT` + 结构化
`cli_source_drift` 行：实际导入路径、期望 repo\_dir、精确的 editable
重装命令），`muyan-pilot setup` 校验或重装。
`muyan_pilot.py` 的直接执行入口保留为开发/兼容路径。

```bash theme={null}
# 在配置的 source repo 创建 Issue 并加 ai-ready 标签
muyan-pilot add "task title" --body "task body" --config muyan-pilot.toml
muyan-pilot add "task title" --repo OWNER/BACKLOG-REPO --config muyan-pilot.toml

# 只读队列视图：当前（ai-in-progress）任务带实时 Pi 活动、下一个 ready
# Issue、每个 source repo 的最近结果（ai-pr-opened / ai-fix-needed /
# ai-merged / ai-blocked）
muyan-pilot status --config muyan-pilot.toml

# 只读部署/健康报告：repo commit、unit drift、CLI 源码（cli_source:
# clean / DRIFT + 结构化 cli_source_drift 行）、git transport（配置的
# origin URL、protocol、期望 SSH URL、SSH 探测——传输失败报告为
# `transport: FAILED ...`，不抛出）、timer/service 实例状态、slots、
# Pi session、当前 Issue、最近 journal
muyan-pilot doctor --config muyan-pilot.toml

# 一次性、幂等初始化（新机器/新仓库）：gh auth + 仓库权限、平台 labels、
# systemd user units、checkout 检查（含 git transport：已有 HTTPS
# `origin` 迁移为 `git@github.com:owner/repo.git`（人工授权的迁移路径；
# Runner 本身从不改写 remote），并探测 SSH 连通性（fail fast，没有
# HTTPS 回退））
muyan-pilot setup --config muyan-pilot.toml

# 打印当前 run 的 Pi session JSONL 路径（没有 session 时 fail fast）
muyan-pilot session --config muyan-pilot.toml
muyan-pilot session --follow --config muyan-pilot.toml   # tail -f
muyan-pilot session --pretty --config muyan-pilot.toml   # 一行摘要
```

所有命令通过 `--config` 或 `MUYAN_PILOT_CONFIG` 环境变量接收配置
（默认 `muyan-pilot.toml`）。`status` 和 `session` 是调试附件——
journal 和 GitHub 仍是正常可观测路径。

## Worktree 与 base 新鲜度

每次领取先 fetch 并冻结 `origin/<base_branch>`，任务 worktree 和
feature branch 都从那个精确 SHA 创建——绝不来自主工作区当前 HEAD。
branch 和 worktree 名带 run id（例如
`.worktrees/<...>-issue-14-e07383c2`），所以重试的 Issue 得到新的独立
run，旧现场保留。`.worktrees/` 已 gitignore。

任务 worktree 共享部署 checkout 的单一 `origin` remote（`git worktree
add` 创建的 worktree 继承主仓库的 remote 配置），所以 git transport 只
在 checkout 上配置一次，所有 worktree 继承：新 bootstrap worktree
天然有 SSH `git remote -v`，它们的 fetch/push——包括
`.github/workflows/*.yml`——走 SSH（Issue #114）。

创建 PR 前，实现者重新 fetch base：如果 `origin/<base_branch>` 前进了，
它把最新 base 合入 task branch、手工解决冲突、重跑完整测试，然后才
推送。Runner 用 `git merge-base --is-ancestor origin/<base_branch>
HEAD` 验证，拒绝 head 不包含最新远端 base 的交付。

## 故障恢复

| 状态                      | 发生了什么                                   | 怎么办                                                                                                                     |
| ----------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `ai-blocked`            | Runner fail fast（命令错误、现场无法恢复、审查超轮）      | 读 Issue 评论（现场 + 原因）和 journal。修环境或修任务，然后把同一 PR 路径重新标为 `ai-fix-needed`（同一 run 继续），或移除标签重新派发为全新 `ai-ready`（新 run）。它从不自动恢复。 |
| `ai-fix-needed`         | PR head 尚不可合并（review finding 或 base 冲突） | 什么都不用做——下一 tick 在同一 PR 上启动下一个审查会话，会话内吸收最新 base。                                                                         |
| 被杀后残留的 `ai-in-progress` | Runner 在任务中途被 SIGKILL                   | 下一 tick 的重启扫描接回（仅当没有其他 Runner 活着时）：同一 run id、同一 worktree、同一条进度评论——不新建 run。                                              |

run 产物（plan、test log、session JSONL）留在任务 worktree 作为本地
记录；GitHub 承载交付记录。

## 并发

`max_concurrency`（默认 1）限定本机并发交付数。slot 是
`<repo_dir>/.muyan-pilot/slots/slot-N` 上的排他 `flock(2)` 锁，在任何
领取之前取得，整个交付生命周期（implement → review → merge）持有；
进程无论如何退出，内核都会释放它。拿不到 slot 的 Runner 记录
`capacity_full` 后退出，不领取 Issue。两个 timer 实例可能在同一 tick
启动两个 Runner 实例，它们竞争同一组 slot——容量仍是
`max_concurrency`（默认 1 时行为与 #149 之前完全一致），而不是实例数。


## Related topics

- [Testing](/zh/testing.md)
