Review workspaces

View as markdown

Most real review of an app or a site means running it: booting the dev server, hitting a URL, clicking through the change, not just reading a diff. The routing that gets an issue to a review slot is ordinary tag routing: a status requires tags, a workspace carries tags, and the issue loads onto a free slot whose effective tags match. On top of that generic routing, Shelbi ships three pieces of review-specific machinery: a workflow review: serve recipe that boots the branch, a review sidebar listing issues Ready and Queued for review, and a review interface for running the change and accepting or rejecting it.

This page covers the review tag, the serve recipe, and the review surfaces in the TUI. The generic tag and slot mechanics they build on — effective tags, superset routing, and $SLOT — live in Workspaces.

The review tag

Review routing uses one tag by convention: review. A workflow's review status requires it, and every slot meant to run review work carries it:

# workflows/default.yaml — the review status requires the tag
statuses:
  - { id: review, owner: user, agent: review, tags: [review] }
# project.yaml — tag the machine (or individual slots) so its slots carry it
machines:
  - name: hub
    kind: local
    work_dir: ~/Workspaces/myapp
    tags: [review]              # every slot on hub inherits `review`

When an issue enters the review status, the orchestrator routes it to a free workspace whose effective tags are a superset of {review} — the same superset routing every tag uses. Nothing branches on the literal word "review"; it is just the tag this workflow happens to require. If no declared workspace carries it, the load fails loudly rather than running review work on a general slot.

Serving the branch: the review: block

Routing gets an issue onto a review slot. Serving the branch — booting the dev server so a human can click through it — is best expressed with the workflow's dedicated review: block. This is the recommended way to stand up a review server. Shelbi resolves the recipe's $SLOT / $PORT placeholders against the review slot's port and injects the resolved recipe into the Review agent's dispatch prompt; the agent runs it verbatim, health-checks it, and hands back a URL.

# workflows/default.yaml
review:
  workdir: site                                # relative to the worktree root
  setup: npm install --no-audit --no-fund      # one-shot; must exit 0 before serving
  serve: npm run dev -- -p $SLOT               # binds the dev server to the slot's port
  ready: curl -sf http://localhost:$SLOT       # readiness probe, polled until it exits 0
  url: http://localhost:$SLOT                  # reviewable URL; gates the "Open Browser" action

The review: block carries exactly these fields — matching the ReviewServe struct in source:

  • workdir — subdirectory to run the recipe in, relative to the worktree root. Omitted, it runs at the root.
  • setup — a one-shot install/build command that must exit 0 before serving. Omitted, setup is skipped.
  • serve (required) — the command that starts the dev server, bound to the slot's port.
  • ready — a readiness probe polled until it exits 0. Omitted, there's no HTTP probe.
  • url — the reviewable URL handed to the human once the server is up. It also gates the review interface's Open Browser action.

Because the recipe lives on the workflow, a monorepo's app / site / docs workflows each serve their own subdirectory on the review slot's port without colliding. When a workflow declares no review: block, the Review agent does a diff-only review: it does not auto-detect a framework or boot a default-port server.

$SLOT (transition env) vs $PORT (review template)

Both spellings exist, and they are not the same mechanism:

  • In a transition's run / ready commands, $SLOT is a shell environment variable holding the workspace's numeric slot. You derive a port from it yourself with shell arithmetic ($((3000 + $SLOT))).
  • In the review: block, $SLOT and $PORT are interchangeable template placeholders, both resolved by Shelbi to the review slot's port before the recipe reaches the agent. Both the $X and ${X} spellings are substituted (see substitute_review_url in source); a fixed URL with no placeholder round-trips unchanged. There is no shell arithmetic here — the agent receives the already-substituted string.

Transition commands: run, ready, teardown

The review: block is the preferred way to serve a review branch, but a status's transitions can also run arbitrary commands, and that's the right tool for hub-side side-effects that aren't the review server itself. A transition can carry:

  • run — shell commands executed in order, in the issue's worktree, on the assigned workspace's machine. They run after the edge's git actions and share the same short-circuit contract.
  • ready — a command polled until it exits 0 (bounded by ready_timeout, default 90s), so a launched server can be confirmed up before the human is handed the URL.
  • teardown — expressed as the run of the exit transition; there is no separate teardown hook.

Each command runs synchronously, so a long-lived server must background itself (… &, nohup, a detached pane). The edge is entered the moment the launcher returns; ready is what confirms the server actually answers.

Unlike the review: block, a transition run: server runs hub-side and declaratively — Shelbi executes it, not the Review agent — so it can't health-check, summarize a failure, or apply a human's tweak. Prefer review: for the serve recipe; keep transition run: for the side-effects around it.

A review column, from these pieces

Here is a complete review column: a tagged machine, a tagged status, a review: serve recipe, and an accept edge that merges. Only the routing is generic workflow config; the serve recipe and the review interface are the review-specific machinery layered on top.

# project.yaml — tag the machine (or individual slots)
machines:
  - name: hub
    kind: local
    work_dir: ~/Workspaces/myapp
    tags: [review]
 
workspaces:
  - { name: alpha,    machine: hub }                 # dev slot
  - { name: bravo,    machine: hub }                 # dev slot
  - { name: review-0, machine: hub, tags: [review], slot: 3000 }
# workflows/default.yaml — the review status requires the tag, and the
# review: block tells the Review agent how to boot the branch.
statuses:
  - { id: in-progress, owner: agent, agent: developer }
  - { id: review,      owner: user,  agent: review, tags: [review] }
  - { id: done,        owner: user }
 
transitions:
  - from: review, to: done, actions: [merge, delete_branch]
 
review:
  setup: npm install --no-audit --no-fund
  serve: npm run dev -- -p $PORT
  ready: curl -sf http://localhost:$PORT
  url: http://localhost:$PORT

With review-0 on slot 3000, $PORT resolves to 3000 and the agent serves the branch on :3000; a second review slot set to 3010 would serve there. The port is deterministic and collision-free because it's derived from the slot.

The status's agent runs the recipe

Routing does two things when an issue enters the status: it loads the issue onto the matching workspace running the status's agent, and it hands that agent the resolved review: recipe. Shelbi ships a built-in review agent whose charter is "make the change runnable for a human, don't code" — name it here, or point agent: at any agent you've authored. See agents.

The review sidebar

Review-status issues surface in the sidebar under two dedicated sections, split by whether an issue is loaded on a review slot yet:

 — Ready for Review —
 ✓ Polish dark-mode toggle              hub:review-0
   jlong/dark-mode-toggle-polish
 
 — Queued for Review —
 · Fix cookie domain bug
   jlong/fix-cookie-domain-bug
  • Ready for Review (, cyan) — the issue's assigned_to names a review-tagged workspace, so the branch is loaded on a review slot and serving. Each entry is a two-line row: line 1 is the issue title with a right-aligned location badge (the machine:workspace it's loaded on, e.g. hub:review-0); line 2 is the branch, dim.
  • Queued for Review (·, dim) — every other Review-status issue (unassigned, or still pinned to the dev workspace that produced it), waiting for a free review slot. No location badge, because nothing is serving yet.

A review-tagged slot never appears under the — Workspaces — section; its capacity surfaces exclusively through these two review sections. Dev workspaces list under — Workspaces — as usual.

The daemon auto-loads queued review-column issues onto idle review-tagged slots each poller tick (autoload_review_queue), so a queued branch normally lands on a free slot without any human action. The confirm popup is the manual path: selecting a Queued row raises a "Load onto a review workspace?" prompt and, on confirm, loads the branch onto a free review-tagged slot (never the dev pane that built it) — useful for loading a queued issue while the slots are otherwise busy. Selecting a Ready row opens its review interface directly.

Sidebar collapse persists

Machine groups under — Workspaces — can be collapsed (Space / Enter on a machine header). The set of collapsed machine names is persisted to ~/.shelbi/state.json under sidebar.collapsed_machines (the SidebarPrefs struct), so the choice survives a sidebar respawn and follows you across projects that share a machine name. Machine names not present in the current project are ignored, not dropped, so re-adding the machine restores its prior state.

The review interface

Opening a Ready issue launches the review interface — a two-column tmux layout: the review panel on the left and the swappable review content on the right. The panel is the review window's own navigation, so the global nav sidebar stays docked in the dashboard. The panel is where you drive the review:

  • A square back button at the top (a back-arrow glyph) that switches focus back to the dashboard window. It leaves the review interface loaded, so you can return to it from the sidebar.
  • A header with the review status (Ready for review) and the review worktree's folder (📂) — click it to reveal the worktree in your OS file manager.
  • A view switcher that swaps the right-hand content pane:
    • 🤓 Chat with Reviewer (default) — talk to the Review agent.
    • 🔀 View Diff — open your system diff tool over the review branch's changes (the same merge-base(base, HEAD)..HEAD range shelbi diff shows) in the main pane. By default it launches git's configured diff tool (diff.tool, or diff.guitool for a GUI tool) in directory-diff mode; with none configured the panel reports a short error rather than launching. A tool that reviews a revision range instead of two directory trees (such as skim) needs a review.diff_command override in project.yaml — see Project config. A diff tool that exits without rendering leaves a short message in the pane rather than only git warnings.
    • ✍️ Edit in <editor> — open the worktree in your configured editor (see below).
    • 🌐 Open Browser — open the resolved review url in your system browser. This entry renders only when the workflow declares a review URL.
  • An Actions group:
    • ✅ Approve — move the issue one column forward along the normal accept edge (review → done in the default workflow), firing that edge's actions (e.g. merge, delete_branch), then tear the interface down.
    • ❌ Reject — open a type-the-reason dialog; on submit, the reason is appended to the issue body and the issue is bounced back to the workflow's ready status for another pass. An empty reason can't submit.

The review editor

The Edit in <editor> view launches a hub-wide editor, configured once in ~/.shelbi/config.yaml under the editor key, so a reviewer's editor choice follows them across every project. The value may be a bare command (hx) or a command with flags (code --wait). Resolution order is:

  1. ~/.shelbi/config.yaml's editor,
  2. the $EDITOR environment variable,
  3. vim.

The switch label is derived from the program's basename, first letter upper-cased — hx shows as Edit in Hx, code --wait as Edit in Code.

The load → serve → inspect flow

End to end, a finished issue reaches a human like this:

  1. A dev workspace finishes. It writes its review-ready marker; the hub poller rebases the branch onto the base branch and moves the issue into the review status. The finishing workspace closes its own session and returns to idle (see Workspaces).
  2. The orchestrator routes it. The review status requires tags: [review], so the issue loads onto a free review-tagged workspace and the Review agent boots the branch from the resolved review: recipe. Queued review issues are paired with idle review slots in declaration order — the dev workspace that built the branch is not preferred (it is typically not review-tagged); the only reuse is resuming a slot the issue is already serving on. If every matching slot is busy, the issue sits in Queued for Review until one frees — nothing is preempted.
  3. A human inspects. You open the issue's review interface from the sidebar, click through the running app at the served URL, and decide: Approve (move to Done) or Reject (bounce back with a reason). Each move fires that edge's actions and run.

The board's category vocabulary is unchanged: the issue sits in the handoff-category review status throughout.

Review and Zen Mode

Routing an issue to a review workspace is the human path — the whole point is human eyes on the running app. Whether such an issue is eligible for Zen Mode's auto-merge is governed by the ordinary action-based rule: Zen's high-confidence bar fires on any transition whose actions include merge. Keep merge off the enter-review edge and on the accept edge (as above) so the orchestrator holds the issue for you rather than crossing the accept boundary on its own.

See also

  • Workflow config — the status tags, the review: serve block, and transition run / ready / ready_timeout.
  • Project config — machine and workspace tags, and workspace slot.
  • Global config — the hub-wide config.yaml editor the review interface's "Edit in <editor>" view uses.
  • Workspaces — the pool model these slots extend.
  • Agents — the role that runs inside a workspace, including the built-in review agent.
  • Set up review workspaces — the step-by-step walkthrough.