← All churts

uBixShepherd — The Place I Actually Work Now (0.2 beta, updated for v0.4.0-beta.1)

uBixShepherd — The Place I Actually Work Now (0.2 beta, updated for v0.4.0-beta.1)

From a terminal to a page I leave open

When I introduced uBixShepherd, the one place to talk to a swarm of agents was a terminal chat. It worked, and then I noticed what I was doing with it: running shepherd lane list to find out what was stuck, and scrolling to find the decision I owed somebody.

The thread was never the problem. The problem was the question every morning: what needs me, and what has gone quiet? So 0.2.0-beta.1, the next beta, is mostly an answer to that. The daemon now serves a web UI of its own, and the chat pane is where I spend the day.

Update: v0.4.0-beta.1 (2026-10-12)

I wrote this post for 0.2.0-beta.1, and I’d rather revise it than write a third one. v0.4.0-beta.1 is the next beta after v0.3.0-beta.1, whose update lives on the introduction.

The chat page, retaken for v0.4.0-beta.1, with the topic pills described below:

The chat page: a front-desk thread with topic pills, a decision to answer, and the backlog rail with its topics

Lane diff grew a verdict. A lane’s Changes tab, and a run’s own range of it, now carry a gate badge: passed, failed, stale (the gate last ran on an earlier commit) or not run. Shepherd records each gate run against its commit, so “it passed” always means “on this head”.

A lane's Changes tab: commits, a file diff, files marked outside the scope, and the gate badge

A run's Changes: back link, the run's own commits and files, and the gate passed badge

The front desk files its own turns under topics. It used to need to be told which topic a turn belonged to. Now a turn is filed under the topic of the decision, run or lane it’s about, and each message in the thread wears the topic’s pill, so a long day’s thread can be read one matter at a time.

The desk can widen a lane’s scope, but only on my say-so. lane_widen adds paths to an open lane once I’ve answered a decision asking for it, in a turn I started. A path another lane holds is still refused, naming that lane, and each change is logged and put in the feed. The scope lease stays a lease.

Smaller, and mostly fixes:

  • “Finished” no longer lies about a running lane. The board judged a clean lane with nothing unlanded as finished, even when a continued run was working in it. A lane with a run in flight is judged live.
  • OpenCode can run npm, so it can build and test a web repo. The repo’s gate is allowed exactly as written, not as a prefix, so make check no longer lets make check build through.
  • A back link on the run and lane pages returns to where I came from, scroll position and all, and g t is in the Help dialog next to the other keys.

The chat pane and its backlog rail

The chat page: the front desk's thread on the left, the backlog rail on the right

Stub data, not my repos. The tool that takes these screenshots runs the UI against made-up lanes.

The thread is the same front desk conversation as in the terminal, one per workspace, kept by the daemon. The new part is the rail beside it: the work that waits on me or has gone quiet, sorted by urgency and, inside a group, longest-waiting first.

  • Needs you: open decisions, then requests that need routing.
  • Broken: a failed or interrupted run, or an open merge request whose pipeline failed.
  • Idle: an open lane with no run in flight and nothing it’s waiting for.
  • Working: a run in flight, with its progress line.
  • To review: an open merge request whose pipeline passed.

Each row says how long it has been that way, and links to the lane or straight to the place in the thread where that work last spoke. A decision can be answered in the thread or from the rail.

The detail I like most is the idle row. It doesn’t just say idle; it says what the lane last did (last: commit) and how long ago, because “quiet for 25 hours since a commit” and “quiet for 25 hours since a failed run” are different problems. Those times come from the lane’s real activity and the forge’s own merge request and pipeline timestamps, not from when the last agent happened to exit.

The rail’s header carries three things I used to have to go and look up:

  • Spend against budget. Today’s spend against the daily budget, turning amber from 80%.
  • Forge health. When the forge was last polled, or its error. If polling is failing, the merge request badges may be stale, and the rail says so rather than letting me trust them.
  • Quota holds. When an agent is out of quota, the rail says which one and until when, so an idle lane has an explanation.

The rail only reads what the board already polls. It’s a view, not a second source of truth, and the board and the rail use the same rules, so they can’t disagree about what is broken.

The board and the lane

The board: lanes grouped by what they need

The board is the same grouping without the thread: needs you, broken, to review, working, then what finished today. A lane’s page has its runs with cost, a timeline, and its scope lease and worktree.

A lane: runs, timeline, scope and the merge request

There’s a dark mode too, and it follows the system.

The chat page in dark mode

How you get in

The binary embeds the UI. shepherd web asks the daemon for a sign-in link that works once, for a minute, and opens it. The browser gets a session cookie and never sees the operator token. A browser session can use the front desk and answer decisions, but can’t change the daemon’s setup. Remote use is an SSH tunnel to the loopback port; the daemon doesn’t speak TLS yet, and I’d rather say so than have you assume it.

OpenCode, local or hosted

OpenCode runs got more honest. Point opencode.endpoint at any OpenAI-compatible host (Ollama, say) and a bare model name runs there: small mechanical edits, on a model that costs nothing. A model written as openrouter/qwen/qwen3-coder goes to OpenRouter through OpenCode’s own login instead. Before this, a hosted model name could be quietly redirected to the local endpoint, which is exactly the kind of bug that costs you an afternoon.

Hosted runs are billed, and Shepherd sums OpenCode’s reported cost per run into the same daily total and budgets as everything else. A local model records zero. The timeouts are now settable (opencode.request_timeout, opencode.chunk_timeout) because a 30B model on a laptop is slower than the old hard-coded 15 minutes assumed.

The docs are blunt about the limit: on the model I checked, OpenCode was fine for typos, renames and one-line fixes, and unusable for writing tests in a typed codebase. Use it for the small jobs.

Still being polished

This is a beta, and the UI is the part most in motion. Radius, elevation, focus states and the tab icon (it grows a dot while something waits) were just finished, and a few things in the rail’s review list are deferred on purpose: a progress bar on running rows, grouping by project, per-lane spend. The screenshots above are of the UI as it stands today; expect to see them replaced.

Get it

Binaries for Linux, macOS and Windows are on the v0.4.0-beta.1 release page, with checksums and a signature. They’re not code-signed, so the install notes cover the macOS quarantine flag.

Two honest caveats. macOS and Linux are what I test. Windows builds, but it’s untested and has no login service yet, so treat it as an experiment. And it’s a beta: interfaces can still change, and each change is listed in the changelog.

Source and issues are at github.com/ubixsys/ubixshepherd.

← All churts