Metadata-Version: 2.5
Name: graphban-cli
Version: 0.8.4
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 shipped skills into `.claude/skills/` and `.agents/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. When the vault secret is already set it is left alone;
`gban swamp setup --replace` mints a new gate credential and overwrites that vault entry.

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 stack map`

```bash
gban stack map --dry-run
gban stack map --project <id>
```

Reads this checkout and posts one app-topology record to `POST /api/platform/stack/app`.
`--dry-run` prints that record and posts nothing; it needs no server and no key.
Two runs of the same commit print the same JSON. There is no timestamp in it.

What it reads:

- Compose files (`compose.yml`, `docker-compose.yml`, and the `compose.*` / `docker-compose.*` variants): a service with `build` is a deployable, a service with only `image` is infrastructure, and `depends_on` is an edge. `environment` contributes variable **names** and, when a value is a literal URL, the bare host.
- Dockerfiles (`Dockerfile`, `Dockerfile.*`, `*.Dockerfile`), for the deployable they sit in and for a `FROM` image the table knows.
- Dependency manifests (`package.json`, `pyproject.toml`, `requirements*.txt`, `go.mod`, `Cargo.toml`, `Gemfile`). Every `package.json` and `Cargo.toml` is a component named from the manifest's `name`. The known-SDK table adds edges; it is not what decides whether the component exists. `file:` and `workspace:` dependencies (and a Cargo `path` or `workspace = true`) are internal edges. Other manifests still become a component only when a dependency hits the table, and that edge is attributed to the component whose directory contains the file.
- Deploy manifests (`vercel.json`, `railway.json`, `railway.toml`, `fly.toml`, `netlify.toml`, `render.yaml`) are a platform node. The file body is not stored.
- `.env.example` only. A real `.env` is not a template and is not opened. A commented `KEY=` is read and the edge is `state: documented` rather than `set`. `*_TOKEN`, `*_CLIENT_ID`, and `*_CLIENT_SECRET` count, and so do `*_BASE` and `*_ISSUER` when the value is a URL. A passphrase (`AUTH_SECRET`, `ENCRYPTION_KEY`) is not an edge unless a table row names it. Values stay out of the record.
- Source files are not scanned. There is no source-HTTP pass in this command. stderr always prints a coverage line: files read, files that produced nothing, commented template keys skipped, and that source was not scanned. Exit 0 means the record was built.

The known-SDK table ships at `cli/src/gban/data/stack_sdks.toml` and is versioned with `gban`. A checkout may add rows in `.graphban/stack-sdks.toml` (same shape); those rows are consulted first.

Compose is parsed by a subset, not by PyYAML: block mappings and lists, single-line `[...]` / `{...}` flow, and `|` / `>` scalars. Anchors and aliases are not expanded (`*name` stays text). A `<<` merge key is ignored. A file the subset cannot read is skipped with a warning on stderr; the rest of the tree is still mapped.

Env values are never stored and never posted. A host taken from a URL is the bare host (`api.stripe.com`), because the posted shape rejects any string containing `://`. An image the table does not know is infrastructure named after the **compose service**, kind `unclassified image` — the image string is not stored. A `depends_on`, `external: true`, or `extends.file` that names something this file does not define becomes an `unresolved` node. It is not dropped and it is not a failure.

A real post needs a git commit (there is nothing to send for a repo with no HEAD) and a project-write API key: `$GBFLEET_API_KEY`, then `$GRAPHBAN_API_KEY`, then the key `gban setup` stored for this project. The login session is a different credential and is not sent. Pass `--project` (or `$GRAPHBAN_PROJECT`) so the record names the project; without one, `--dry-run` still prints and a post is refused.

## `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.
