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 initcreates and refreshes.venvin the sandbox clone for projects that declare Python dependencies, so per-ticket worktrees find one through the fallbackscripts/test.shalready has- the implementer prompt names
./scripts/test.shand the sandbox's interpreter when they exist, instead of leaving each ticket to discover both
Fixed
orion watch --max-jobs Nwaits for the jobs it started instead of abandoning them.oneTicklearned 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 announcingstarted 1 job(s) and finished them. The job was fine; nobody was watching it.orion sandbox prune --forcedoes something. The flag was parsed and then ignored — the call site passed a hardcodedfalse— 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_datepaid 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,workandwatchnow refuse a key that cannot exist, before any Jira or GitHub call.orion collect or or-39used to takeoras a ticket key and report that no pull request was found for branchorion/or— an accurate sentence about a key no branch or PR could ever carry — then processor-39and 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 initno longer leaves its.claude/settings.jsonbackups for git to pick up. It writes a timestamped.bakbefore 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 agit add -A, and landed in history permanently, in whatever repository adopted Orion. A run that keeps a backup now ensures.gitignorecovers.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.gitignorecannot be written it warns and finishes: working hooks are worth more than a tidy tree.