Operations
Normal operation is fully automatic: the timer triggers a tick, the tick does at most one thing, and progress is published to the journal and GitHub by itself. You never need a status command, polling, or supervision in the normal path — the commands below are for first verification, troubleshooting and recovery.The timer
systemd/muyan-pilot.timer fires every 15 minutes, 24 hours a day
(OnCalendar=*-*-* *:00/15, AccuracySec=30s, Persistent=false — a
missed tick is dropped, never queued). Each tick starts
muyan-pilot.service, which:
- Fast-forwards the code first (
ExecStartPre, outside the Python process):git fetch origin main && git merge --ff-only origin/main. A dirty checkout, a failed fetch or a non-fast-forwardable state fails the preflight: the service does not start and the reason lands in the systemd journal (fail fast). A currently running long task is never hot-updated or killed — while the service is active, systemd ignores the timer’s start request, and the next real start picks up the latest code. - Runs one tick: resume an opened PR (review/fix/merge) or claim one
ai-readyIssue, then exit.
Logs (journal)
The journal is the local record. Every line of a run starts with the run id prefix[<run_id>], so one grep reconstructs the full timeline:
key=value lines you will see:
run_start/run_end— the full scene (branch, worktree, session file) at start, and the result (PR URL, commit) at the end;activity/heartbeat/model_wait/resumed— live Pi activity while a session runs (phase, last action, elapsed, idle);pi_idle— one WARNING when there is no model/session activity for more than 5 minutes (PI_IDLE_WARN_SECONDS=300) and the model is not expected to reply; a slow active model (model_wait) never warns;run_failed— the full scene plus the reason (pi_exit_N,timeout_...s, orupstream_dead_stale_...swhen a frozenmodel_waitpastPI_MODEL_WAIT_DEAD_SECONDS(default 600 s) declares the upstream model dead and the Runner kills Pi).
The CLI (muyan_pilot.py)
--config or the
MUYAN_PILOT_CONFIG environment variable (default
muyan-pilot.toml). status and session are debug attachments — the
journal and GitHub remain the normal observability path.
Worktrees and base freshness
Each claim freezesorigin/<base_branch> (fetched first) and creates the
task worktree and feature branch from that exact SHA — never from the
main worktree’s current HEAD. Branch and worktree names carry the run id
(e.g. .worktrees/<...>-issue-14-e07383c2), so a retried Issue gets a
new independent run and the old scene is preserved. .worktrees/ is
gitignored.
Before creating the PR, the implementer re-fetches the base: if
origin/<base_branch> advanced, it merges the latest base into the task
branch, resolves conflicts manually, reruns the full test suite, and only
then pushes. The Runner verifies with
git merge-base --is-ancestor origin/<base_branch> HEAD and rejects a
delivery whose head does not contain the latest remote base.
Failure recovery
The run artifacts (plan, test log, session JSONL) stay in the task
worktree as the local record; GitHub carries the delivery record.
Concurrency
max_concurrency (default 1) bounds the number of concurrent deliveries
on the machine. A slot is an exclusive flock(2) lock on
<repo_dir>/.muyan-pilot/slots/slot-N, taken before any claim and held
for the whole delivery lifecycle (implement → review → merge); the kernel
releases it when the process exits, however it exits. A Runner that
cannot take a slot logs capacity_full and exits without claiming an
Issue.