Skip to main content

orion v0.5.1

August 27, 2026

A cleanup release. Everything here was found by running Orion on Orion, which is why so much of it is about the pipeline's own housekeeping rather than about what agents produce.

The one entry that affects you even if you ignore the rest: orion watch --max-jobs N now waits for the work it started. Before, a run that started a job and put it into CI reported started 1 job(s) and finished them and exited, abandoning it — the drain learned "something is in flight" only from a reconciliation pass that ran before the job existed. Nothing was lost, but nothing was finished either, and a later orion collect had to pick up the pieces.

Merged branches are now actually deleted, locally and on the remote. Until now, none of them were.

orion collect removed the worktree and then ran git branch -d, whose safety check is an ancestry test: is the branch tip reachable from the base. Orion merges by rebase, which replays the commits as new objects, so the originals are never reachable and -d refused — every time, for every ticket, however cleanly it landed. The refusal was caught and downgraded to a warning on the reasoning that git disagreeing about merged-ness was worth surfacing. That is right for a merge-commit workflow and wrong for ours, where the disagreement is the guaranteed consequence of the merge strategy we chose. So the happy path printed a warning that meant nothing, and every branch survived. Two tickets left two orphans; a week of orion watch would have left dozens.

The pull request is now the authority on merged-ness, which it always should have been: Prune is only ever reached from the merged path, so the forge has already answered the question before git is asked. The remote branch is deleted too, which nothing did before. Finding the remote already gone is treated as success rather than failure, because it is now the expected case — see the next paragraph.

orion init sets delete_branch_on_merge on the repository, so GitHub deletes the head branch at merge time. This is deliberate belt-and-braces: the situations where collect never runs are exactly the messy ones — a watcher killed mid-run, a merge done by hand in the web UI, a network failure between merging and pruning — and server-side deletion is the only cleanup that survives all of them.

New: orion protect. Applies branch protection using the checks the repository is observed to run, read from real check runs rather than guessed. The rule that carries the weight is "require branches to be up to date before merging", which is the server-side form of the staleness gate added in v0.4.4. Both are worth having and they are not redundant: the gate needs no admin rights and works anywhere, but it can only warn a human, who can skim the warning and merge anyway. This refuses the merge.

It is a separate command rather than part of init for a reason worth knowing before you run it: a required status check that never reports blocks every pull request forever, with no timeout and no recourse short of an admin editing the settings by hand. At adoption time no CI has ever run, so any check name chosen then would be a guess. Run orion protect once CI has run at least once. --dry-run shows what it would require.

Provisioning no longer hardcodes one required approving review. On a solo repository that rule can never be satisfied — GitHub does not let anyone approve their own pull request — and you discovered it at the moment you tried to merge your first change, with the branch already protected. The count is now derived from who can actually push, and both the number and the reason for it are printed, because a value that silently differs between two repositories is worse than either default.

Note for private repositories on the free plan: branch protection is a paid feature there, and GitHub's error says only "upgrade". Making the repository public also enables it, at no cost, and Orion's message now says so.

orion init now prepares the sandbox's Python environment once, rather than leaving every ticket to work it out again.

A sandbox is a fresh clone, and a fresh clone has no .venv. scripts/test.sh already looks for one in the main worktree — a git worktree does not carry ignored files — but there was never anything there to find, so every ticket fell through to bootstrapping a virtualenv inside its own worktree and threw it away afterwards. On one measured run that discovery was 17 of 31 shell calls. Worse, when the bootstrap failed the script exited 1, which is indistinguishable from a failing test: the branch went red for a reason unrelated to the change, and with ci.auto_fix on, Orion paid an agent to react to an environment problem it could not fix from inside a worktree.

The virtualenv is now built in the sandbox clone at adoption, only when the project declares dependencies (pyproject.toml or requirements.txt), and reinstalled when a manifest is newer than the last install. Re-running orion init is how you repair or refresh it. It is non-fatal throughout: if it cannot be built, runs still work exactly as they did before.

The other half is that the agent was never told any of this. scripts/test.sh was named only in the CI-fix prompt, which a ticket run never sees, so the implementer found the entry point by guessing and then found out how to make it work by trial and error — rediscovered from zero on every ticket. The implementer prompt now names the command, and names the provisioned interpreter when there is one. Both lines appear only when the thing they name exists: a prompt that confidently names a missing script is worse than silence, because the agent runs it, watches it fail, and goes exploring anyway.

Changed​

  • orion init creates and refreshes .venv in the sandbox clone for projects that declare Python dependencies, so per-ticket worktrees find one through the fallback scripts/test.sh already has
  • the implementer prompt names ./scripts/test.sh and the sandbox's interpreter when they exist, instead of leaving each ticket to discover both

Fixed​

  • orion watch --max-jobs N waits for the jobs it started instead of abandoning them. oneTick learned whether anything was in flight from a reconciliation pass that runs before a job is started, so the one job it had just launched was invisible to it and the run exited announcing started 1 job(s) and finished them. The job was fine; nobody was watching it.
  • orion sandbox prune --force does something. The flag was parsed and then ignored — the call site passed a hardcoded false — so the escape hatch for a worktree prune refused to remove silently did nothing, twice as confusingly because the command reported success.
  • CI no longer runs twice per push. Without a concurrency group a force-push left the superseded run burning through all three OS legs while its replacement queued behind it, and a rebase is a force-push, so every branch that went stale under ci.require_up_to_date paid for itself twice. Orion scaffolds this into the workflows it writes for adopted projects and did not have it itself: the repository that generates the good default was the one missing it.
  • collect, work and watch now refuse a key that cannot exist, before any Jira or GitHub call. orion collect or or-39 used to take or as a ticket key and report that no pull request was found for branch orion/or — an accurate sentence about a key no branch or PR could ever carry — then process or-39 and exit 0, so a typo in a cron line warned once per tick forever. A bad token now fails the whole invocation with a usage error naming it and the shape expected; a mix of valid and invalid keys does no partial work. watch's positionals are project keys, so it rejects the inverse mistake (orion watch or-39) and its usage line now reads [PROJECT...].
  • orion init no longer leaves its .claude/settings.json backups for git to pick up. It writes a timestamped .bak before rewriting the file, and init is not a once-ever command — it re-runs on adoption, on --force, and after any change to the hook wiring — so those files accumulated in the working tree, were swept up by a git add -A, and landed in history permanently, in whatever repository adopted Orion. A run that keeps a backup now ensures .gitignore covers .claude/*.orion-*.bak, appending only when the line is absent and creating the file when there is none, and says so in its output rather than editing a file you did not name in silence. If .gitignore cannot be written it warns and finishes: working hooks are worth more than a tidy tree.

Download v0.5.1 · Staying current