运维
正常运行完全自动:timer 触发 tick,tick 最多做一件事,进度自己发布到 journal 和 GitHub。正常路径永远不需要 status 命令、轮询或督工——下面的 命令只用于首次验证、排查和恢复。Timers
timer 模板systemd/[email protected] 部署为两个实例:
[email protected] 和 [email protected]。每个实例每 5 分钟触发
一次,全天 24 小时(OnCalendar=*-*-* *:00/5、AccuracySec=30s、
Persistent=false——错过的 tick 被丢弃,从不排队),并各自启动自己的
service 实例([email protected] → [email protected],
[email protected] → [email protected]),所以可以同时运行
两个独立的 Runner 实例。每个 tick 的 service 实例:
- 先 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_mergedfetch 统一 经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>失败)。 - 运行一个 tick:恢复一个已打开的 PR(review/fix/merge),或领取
一个
ai-readyIssue,然后退出。在任何 slot 或领取之前,tick 还 运行启动前 git transport 检查(Issue #114):部署 checkout 的 配置的originremote 必须是第一个配置 source repo 的 SSH 形式,且git ls-remote <ssh-url>退出 0(SSH 可达且已认证)。 传输损坏记录结构化transport_check_failed ... reason=...行并让 启动失败——不取 slot、不领取、不改标签,没有 HTTPS 回退(git 数据操作,包括.github/workflows/*.yml推送,永远走 SSH;GitHub API 操作留在ghtoken 上)。
Unit 模板变更后(Issue #131、#142)
unit 模板(systemd/[email protected] 和 systemd/[email protected])
是部署配置,不是普通代码——但模板变更合并后不需要任何人工步骤。下一次 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 还原完整时间线:
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)。
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 目录>——见
快速开始)。muyan-pilot doctor 报告一致性
(cli_source: clean 或 cli_source: DRIFT + 结构化
cli_source_drift 行:实际导入路径、期望 repo_dir、精确的 editable
重装命令),muyan-pilot setup 校验或重装。
muyan_pilot.py 的直接执行入口保留为开发/兼容路径。
--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 的交付。
故障恢复
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 之前完全一致),而不是实例数。