Start here

Run bay without a runner

A small team can merge with bay from its own machines, with no runner to host or keep running.

A small team can use bay with no runner at all. bay merge --wait runs the whole merge on the machine that asks for it. It builds the candidate, runs the checks, and pushes the result to main. Nothing else has to stay on, and your clone stays on your own machines.

This suits a few people with a few changes a day. A fleet of agents that should submit and leave still needs a runner.

When to use it

  • A small team: two or three people, each merging their own work from their own machine.
  • A private repository: you would rather not clone it onto a shared machine.
  • A first trial: try bay before you decide where a runner should run.

Merge from your machine

Run bay merge --wait from the change's view. It returns the real outcome: merged, refused with a reason, or failed. Checks run in the sandbox on your machine: bwrap on Linux, sandbox-exec on macOS. Without a key registry, the merge is unsigned, and results say trust: unsigned. Leave [runner] command out of .bay/config.toml. With it set, bay merge --wait first submits to that command, and fails when the command cannot take it.

sh
bay merge --wait

Save edits from your main checkout

bay save <intent> merges the edits in your main checkout, the folder you cloned, as one change. It suits a person who edits files in place, with no bay start and no view. It takes every changed, new and deleted file, except files Git ignores. It opens a change with those files as its paths, and never commits on your branch. Then it runs bay merge --wait, so it merges the same way, with or without a runner. After the merge, your checkout fast-forwards to the new main when nothing is in the way.

sh
bay save "fix the footer" --message "Footer links fit on a phone"   # --message is optional

When bay refuses a save

A refused save leaves every file in your checkout exactly as it was. When it opened a change, the result names it under change, and the change stays open. Run bay save again with the same words to go on. It reuses that open change. A change awaiting approval returns APPROVAL_PENDING and detail.queued. Once approved, a runner merges it; without one, save again. When main changed the same lines, it refuses with MERGE_CONFLICT. Bring those files up to main, then redo your edits.

  • It runs only in the main checkout, never in a change's view or another worktree. In a view, run bay merge.
  • It refuses while a merge, rebase, cherry-pick or revert is under way.
  • It refuses a file with an unresolved conflict, and a submodule.
  • It refuses when your HEAD holds a commit that main lacks.
  • Each of these fails before bay opens a change, and changes nothing.
  • With nothing to save, it returns ok and merges nothing.

If your machine sleeps

The push to main is one atomic step, so main never gets half a merge. When the machine wakes, bay merge --wait reads the change's record and goes on. If main moved, it checks again. If its own lease ran out, it takes it back. If it slept after the push, bay reads the first result and never merges twice. Until the machine wakes, the change waits, unless a runner also runs, which may finish it for you.

Approvals without a runner

An approval counts only with a key registry and an enrolled approver key. With [policy] signing = "approvals" in .bay/policy.toml on main, only approvers need keys. Then merging machines need none, and merges and check records run unsigned. Keys and the registry explains this mode. Require a person's approval shows how a person approves.

When to add a runner

  • Agents should leave: a runner finishes each merge after bay merge returns at once.
  • Machines sleep or move: an always-on runner keeps merges going.
  • More changes a day: the throughput settings need runners to run checks side by side.
  • Targets that follow main: only bay runner run moves a target that sets advance = true.

Limits

  • An operation's lease lasts [runner] lease_s, 60 seconds by default.