Skip to main content

运维

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

Timers

timer 模板 systemd/[email protected] 部署为两个实例[email protected][email protected]。每个实例每 5 分钟触发 一次,全天 24 小时(OnCalendar=*-*-* *:00/5AccuracySec=30sPersistent=false——错过的 tick 被丢弃,从不排队),并各自启动自己的 service 实例([email protected][email protected][email protected][email protected]),所以可以同时运行 两个独立的 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 上)。

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_Ntimeout_...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_pilotExecStartPre 的 checkout 同步会被下一个 CLI 进程自动 取到,普通源码/模板变更无需重装或升级;uv tool install --force --reinstall --editable --python /usr/bin/python3 <clone 目录>——见 快速开始)。muyan-pilot doctor 报告一致性 (cli_source: cleancli_source: DRIFT + 结构化 cli_source_drift 行:实际导入路径、期望 repo_dir、精确的 editable 重装命令),muyan-pilot setup 校验或重装。 muyan_pilot.py 的直接执行入口保留为开发/兼容路径。
所有命令通过 --configMUYAN_PILOT_CONFIG 环境变量接收配置 (默认 muyan-pilot.toml)。statussession 是调试附件—— 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 之前完全一致),而不是实例数。

相关主题

Testing