Metadata-Version: 2.5
Name: graphban-cli
Version: 0.5.0
Summary: gban — a Graphban client for the human at a terminal
Project-URL: Homepage, https://github.com/asc-me/graphban
Project-URL: Repository, https://github.com/asc-me/graphban
Project-URL: Documentation, https://github.com/asc-me/graphban/blob/main/cli/README.md
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.12
Provides-Extra: dev
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# graphban-cli

**`gban`** — the client for a human at a terminal.

Five surfaces existed before this and none of them was for a person at a shell prompt:
`graphban` talks to the database from inside the container, `gbfleet` supervises processes,
`gbagent` is a spawned child, the web app is a browser, and `/api/mcp` is for agents. Issuing
a seat, seeing why an agent is stuck, or re-tasking one meant opening a browser.

Specified by [PRD-40](https://github.com/asc-me/graphban/blob/main/docs/prd-40-gb-cli.md).

## Install

```bash
uv tool install graphban-cli
```

or, on macOS:

```bash
brew install asc-me/tap/gban
```

The formula carries no `resource` stanzas — `graphban-cli` has no runtime dependencies, so
there is nothing to vendor and nothing to regenerate when a transitive moves. The supervisor
is deliberately not in the tap; see below.

Add the supervisor too if you run waves — a separate package, and `gban fleet` hands off to
it. At a terminal, `gban fleet` will offer to run this for you when it finds no supervisor;
it asks first and never installs on a bare return, because a person who typed a read-only
command did not consent to software being installed:

```bash
uv tool install graphban-fleet
```

That also gives you `gbagent`, the first-party coding agent: it is an entry point of
`graphban-fleet`, not a package of its own, because the supervisor resolves it on PATH like
any other vendor binary.

`uv tool update-shell` once, if uv says the bin directory is not on your PATH.
`uv tool upgrade --all` to move both forward.

Two commands rather than one, and an extra (`graphban-cli[fleet]`) is deliberately not
offered: `uv tool install` exposes only the REQUESTED package's executables, so an extra
installs `gbfleet` into `gban`'s environment and puts it on no path at all. Measured — with
the extra, `gban fleet` reported "gbfleet is not installed here" while `gbfleet` sat in the
very environment it was running from.

`gban` pulls **nothing**: `client.py` is `urllib.request` throughout, and the install lands
exactly one distribution. `gbfleet` brings httpx and its transitives, which is why they are
separate packages and not one.

With pip instead, into an environment you already have:

```bash
pip install graphban-cli
```

`gban` looks for `gbfleet` beside its own interpreter before falling back to PATH, so that
shape works with neither on PATH.

To run an unreleased change, install from the repository instead — the same spec the release
builds from:

```bash
uv tool install "git+https://github.com/asc-me/graphban.git#subdirectory=cli"
```

Releasing is [docs/releasing.md](https://github.com/asc-me/graphban/blob/main/docs/releasing.md).

```bash
gban login --server https://cloud.agentldgr.dev
gban doctor                       # both halves: the ledger, and the local fleet
gban agents                       # the roster, and why an agent is stuck
gban agents role SA-A4 planner    # what used to need a browser
gban seats issue worker worker planner                  # one entry per agent
gban keys                                               # which key is that agent on
gban fleet up --seats-file seats.txt --adapter claude   # hands off to gbfleet
```

`seats issue` takes **one role per agent, repeats included**, because that is the server's
own shape: two agents on one seat share a session and cannot review each other. Each code is
printed once and written nowhere — a CLI that helpfully saved them would invent a second
credential at rest that no route and no test knows about.

`agents` prints what an agent was **last refused, and why**. That line is the reason the verb
exists: a roster saying "idle worker" for an agent being told no on every call it makes is
what made the Super-Arc diagnosis take a database query.

`agents role` re-tasks a live agent **within its credential's ceiling** and never past it. A
role the key does not permit is the server's refusal, printed in the server's own words;
widening a ceiling means minting a different credential, and keeping those two acts apart is
the point of having a ceiling. It lands on the agent's next poll.

## Enabling delegation on a project

```bash
gban login          # once, at a terminal
gban setup          # everything mechanical between that and a delegating agent
gban setup --auto   # …or every project whose repository sits here or beside here
```

An agent with the delegation skill runs all of this for you except `gban login`, which it
hands back as a `! gban login` line to type — it needs a terminal, and no agent has one — and
then carries on from where you left it.

**The directory names the project.** Run `setup` from inside the repository the work belongs
to and it matches that directory against the projects you can read. An explicit `--project`
wins; nothing else does. In particular the default `gban login` stores is *not* used here and
is named in the refusal when it exists — logging in once inside one project must not quietly
mint a credential for it while you are standing in another repository, and a key in the wrong
project is not a mistake anybody notices quickly.

`setup` mints a project-scoped credential, writes the `graphban` and `gbfleet` MCP entries
into every parent harness that would actually read them, stores that key next to the login
session (a `gb_sk_…`, never the refresh token), installs the supervisor if it is missing,
drops the delegation skill into `.claude/skills/`, and then **verifies** rather than
asserting: it asks the new credential what it can actually see. Restart the session that will
call `delegate`/`spawn` afterwards — MCP servers are read at startup.

`gban fleet` and `gban doctor`'s local half then use that minted key. They do not feed the
login session to the supervisor. The order is `$GBFLEET_API_KEY` if you set it, then
`$GRAPHBAN_API_KEY`, then the key `gban setup` stored — and, for a machine that already ran
setup, the one already in the harness dest.

Three properties worth knowing, each of which is a bug this command exists to not have:

- **The credential does not expire.** `gban keys mint` produces a *wave* key, which lasts a
  day (`FLEET_KEY_DAYS`); that is right for a wave and wrong for a project. Only seats expire.
- **It writes where the harness will actually read.** There is more than one. Claude Code
  reads `~/.claude.json`'s per-project `mcpServers` (JSON), which outranks a repository
  `.mcp.json` — writing the repository file under a stale entry leaves the agent on the old
  key, and surfaces as a JSON parse error because the harness is parsing a 401 body. Grok
  reads `~/.grok/config.toml`'s `mcp_servers` (TOML, snake_case; `mcpServers` parses and
  loads nothing). Writing only Claude's file while Grok holds a different key reports
  success and leaves `delegate` unadvertised (GRPH-825). Grok's `gbfleet` MCP also
  gets `--workspace ~/.grok/gbfleet-wt/<repo>`: the sibling default is a path Grok's
  sandbox cannot write, and spawn then dies as `git worktree add` 128 (GRPH-826).
  Claude keeps the sibling default. `--scope user` is the default because a credential
  outside the repository cannot be committed. Re-running a working setup still
  *repairs* a missing `gbfleet` entry and a missing `--workspace`; reuse skips a mint,
  never a write.
- **`--auto` matches, and says so.** A project carries no repository link — no remote, no
  path — so `--auto` compares your project ids and names against this directory, what is in
  it, and its siblings. One level, never a recursive walk. Two directories answering to one
  project, or one directory answering to two, are **refused rather than guessed**: a
  credential minted into the wrong repository is not a mistake anybody notices quickly.
- **It refuses to write a key into a file git tracks.** `--scope project` on a tracked
  `.mcp.json` or `.grok/config.toml` is refused rather than warned about, because a warning
  attached to committing a credential still commits it.
- **It gitignores the files it and `gbfleet` write into the checkout.** `.mcp.json`,
  `.cursor/mcp.json`, `.grok/config.toml`, `.gbfleet-*`, `.swamp/`, `graphban-swamp/`.
  A warning that said "make sure it is gitignored" still wrote the key; `gban setup`
  now adds the lines, and `gban doctor` FAILs if a credential file would still be
  committed. It asks git (`check-ignore`), so an equivalent pattern already in the
  file is left alone.

## Wiring a checkout to Swamp

```bash
gban swamp setup
```

Steps 3 and 5 of [the Swamp runbook](https://github.com/asc-me/graphban/blob/main/docs/swamp.md):
`repo init`, `extension source add`, `vault create`, and the **gate** credential — minted,
piped to `swamp vault put` on stdin, then checked for the scopes it actually came back with.
Every step is skipped when already done.

Two things it will not do. It does not install Swamp, because that install pipes a remote
script into a shell. And it never writes the gate key into an MCP config: `gban setup`'s agent
key must not carry `gate`, or the agent doing the work attests its own completion — which
fails silently, since a gate that always says yes looks exactly like a gate that held.

## `gban login` wants a real terminal

It refuses without one, rather than prompting. `getpass` falls back to a plain **echoing**
read when it cannot turn echo off — it warns, but the warning arrives after the person has
decided to type — so a login through a pipe, a heredoc or an editor's command runner would
put the password in the scrollback. There is no non-interactive login yet (PRD-40 open
question 2: an API key cannot reach the JWT routes, so CI would need a service session).

## Why not `gb`

Because `gb` is already `git branch` on a large share of developer machines, and **an alias
beats a binary on `PATH`**. The deployed walk hit it on the very first command and got git's
usage text; nothing inside the process can detect that, because by the time `gb` would have
run, the alias did not.

It is not one alias but a whole namespace. oh-my-zsh's git plugin — which is where most of
these come from — defines sixteen `gb*` aliases and ten `grb*`, so `gb`, `gba`, `grb` and
`gbl` are all spoken for. `gban` is outside it, still short, and still says which product it
belongs to.

## Licence — Apache-2.0, deliberately not the repository's FSL-1.1

The repository is [FSL-1.1-Apache-2.0](https://github.com/asc-me/graphban/blob/main/LICENSE.md). This directory is
[Apache-2.0](https://github.com/asc-me/graphban/blob/main/cli/LICENSE), for the reasons PRD-22 §8 gives for `fleet/` — every one of which
applies here identically. `gban` is inert without a Graphban server and holds no authority of
its own, so FSL's Competing Use clause protects the server and protects nothing here. It is a
laptop-installed developer CLI, which is exactly the kind of dependency that has to clear a
corporate licence policy scanner.

## Why it is in this repository

**Not a second repository**, for the reason [`fleet/README.md`](https://github.com/asc-me/graphban/blob/main/fleet/README.md) gives for
the supervisor, with more force: the client↔server contract has no schema anywhere, and a
cross-repo break would present as absence reading clean — `gban` still runs, nothing errors, the
verb quietly stops meaning what it said. The evidence is recent and specific: `ROLES` lost
`reviewer` in one PR while another added a test naming it, and CI caught the pair inside
seventeen minutes because both lived in one repository. Split across two, that lands as a bug
report from somebody whose `gban agents role ... reviewer` started refusing.

**Not inside `backend/`**, because `graphban-api` pulls fastapi, sqlalchemy, pgvector, psycopg,
alembic, redis and cryptography, and this installs on a laptop. `tests/test_packaging.py`
derives its forbidden set from the backend's own dependency list rather than a denylist
somebody maintains.

## What it is not

It is **not a second web app**: no board, no PRD editor, no search. Every verb is either
something a human currently opens a browser for, or a diagnosis nothing else gives.

It is **not `graphban`**, which talks to the local database from inside the container and
stays exactly as it is. Mixing "against the DB in the container" and "over HTTP from a laptop"
into one command is an ambiguity that ends with somebody purging the wrong instance.

It **holds no state the server does not** and computes nothing the server computes. Every verb
is one endpoint, called once (PRD-40 D11); an ordering between two calls would be a rule, and
a rule in the client is a second definition of something the server already enforces.
