Skip to main content

The board

The board is a local web page that shows every repo you have configured, every tracked stack in those repos, and the current status of every branch in those stacks, all in one view. Right-clicking a stack offers four actions, and each one spawns an agent pane running the matching gitq:* skill against that stack, with the board watching its progress live.

It is a companion to the CLI, not a replacement for it. Everything it displays comes from gitq diagnose and gitq preflight; everything it does is delegated to an agent that runs the same CLI you would.

Configure it​

The server reads config.json from the app root -- ~/.mattstack/gitq, from a checkout as much as from the compiled binary, unless GITQ_APP_ROOT moves it. Start from the example:

bash
mkdir -p ~/.mattstack/gitq
cp config.example.json ~/.mattstack/gitq/config.json
json
{
"repos": [{ "path": "/Users/matt/Documents/GitHub/example", "name": "example" }],
"port": 11008,
"herdrWorkspace": "gitq"
}
FieldMeaning
reposRequired, and must be non-empty. One entry per repo to display. path is required. name is optional and defaults to the last path segment; it is the label shown on the board.
portPort to listen on. Defaults to 11008. The PORT environment variable wins over this, so a launch agent can pin the port without editing the file.
herdrWorkspaceThe herdr workspace label that action tabs are launched into. Defaults to gitq.

There is no forge field. Each repo's provider, instance and token come from that repo's own git remote, so a board-wide host could never be right for more than one of them; a self-hosted instance is named once in settings.json's forges for the whole of gitq. A gitlabHost key used to sit here validated and unread; an existing config.json still carrying it loads fine, ignored.

Config is read once at startup. Editing config.json needs a restart. See Configuration.

Run it​

bash
bun run serve

It prints one line and keeps running: gitq board on http://localhost:<port>, where <port> is whatever config.json (or PORT) resolved to, 11008 by default. It shuts down on SIGTERM/SIGINT, releasing the port before it exits, so a process manager can restart it immediately.

bun run build:binary compiles a standalone dist/gitq with the client bundle embedded, and gitq board runs the same server from it with no checkout on the machine. That binary is the form mattstack.app ships.

The first page load is the slow one: it runs the git queries for every configured repo, plus a forge fetch per repo whose remote names one it has a token for. After that the snapshot is cached for 60 seconds and served stale while it refreshes in the background, so a reload during that window is instant. The refresh button in the top bar forces a fresh collection.

The page polls every 60 seconds, immediately when the tab becomes visible again, and every 4 seconds while any job is live, so a running action updates without you touching anything.

EndpointWhat it is
/The board itself.
/data.jsonThe snapshot the page renders. ?fresh=1 forces a refetch.
POST /actionLaunches an action. Body: repoPath, stack, action, and optionally sourceSlot.
/healthzReturns ok.

Reading the board​

Each repo gets a section headed by its name, followed by one work-slot chip per work worktree reading either <slot>: free or <slot>: <action> on <stack>, or <slot>: parked on <stack> when a cascade is paused there. Below that, the repo's stacks on the left and an activity feed on the right.

A stack panel shows the stack name, then the root branch, then each branch in tree order. Per branch you may see:

  • A status badge, a short label of its own (not the situation string lowercased, behind is not behind-parent): behind, local, needs sync, drift, diverged, merged, deleted, conflicts, ci failed, or <n> threads. Hovering shows the full status line. See Reading the tree for what each one means.
  • conflict predicted, shown only when the branch has no status badge of its own and gitq preflight predicts a rebase conflict for it. It is a hint, not a verdict.
  • A worktree chip naming the worktree the branch is checked out in, highlighted when that worktree is dirty. A dirty checkout is what makes a cascade refuse to move that branch.
  • An MR link in the forge's own notation, !42 on GitLab and #42 on GitHub, opening the merge request there. MR and pipeline data need the same token as publish, resolved per repo; without one that repo still renders from the last known MR fields in the store.

Above the branches, a stack shows a chip per live or recent job, reading <action> <status>, coloured for conflict, live, error, and done, and a line of global blocks (an uncommitted working tree, or a rebase in progress) when they apply.

The activity feed lists live and errored jobs first, then entries from the operation log, each with a relative timestamp. If a snapshot refresh fails, a banner appears at the top and the board keeps showing the last good data rather than going blank.

The actions​

Right-click a stack (or one of its branches) for the menu:

  • sync, publish, and restructure each launch against the whole stack.
  • absorb is listed once per dirty worktree, as absorb from <worktree>, so you choose which set of uncommitted changes to distribute. With no dirty worktree, the item is present but disabled.
  • open !<iid> in gitlab (or #<iid> in github, following the repo's forge), when you right-clicked a branch that has an MR.

Each action spawns a herdr tab running claude with the matching skill and the --state / --status-bin contract, which is how the job chips update live while the agent works. Relaunching an action that is already live refocuses its tab instead of spawning a second pane. Agent skills covers what each skill actually does once it starts.

Local only, by Host header​

POST /action is accepted only when the request's Host header is local: localhost, 127.0.0.1, or any *.localhost domain. Anything else gets a 403.

The same check is reported to the page, and the client hides every action menu item when it is false. So a board reached through a tunnel is genuinely read-only: the menu offers nothing but the MR link, or the line read-only over the tunnel, and the server would reject the request anyway.

Restarting​

The React client is bundled in memory when the server starts, so any change to the client code needs a restart to show up. Two exceptions: style.css is re-read on every request from a checkout, so CSS edits are live; and config.json, like the client bundle, is read at startup and needs a restart. The compiled binary serves both the client bundle and style.css from copies embedded at build time, so nothing there is live-editable.