feat(cli): minimal hosted-first v0 command surface - #65
Closed
jiashuoz wants to merge 1 commit into
Closed
Conversation
The CLI had grown twenty-odd commands, and `rainier --help` listed all of them: transfers, snapshots, secrets, environments, contexts, workspaces and the self-hosted credential vault sat beside the four things a hosted developer actually does. A new developer could not read the product off one screen, which is the only test of a command surface that matters. The public surface is now sign in, check readiness, authenticate a coding agent, and create and manage sessions — 23 lines of default help. Everything else still dispatches, under `rainier help all`, labeled Advanced. A session has three independent facts and this CLI keeps them apart: the sandbox's lifecycle (`state`), the process inside it (`child_exit_code`), and whether the control plane can reach it (`reachable`). They fail separately, so they are three columns. A session whose agent exited is still Running — it holds its filesystem and it is still attachable — and a runner that dropped its link makes a session Unavailable, not Failed. Action eligibility comes from the raw API's own transition rules and never from the display word: `SuspendSession` accepts `running` and nothing else, so Stop is not offered for a queued session merely because queued and creating share the label Starting. docs/cli-v0-contract.md is the authority for what each command promises and which Cloud APIs each promise depends on. Rainier Cloud 81d1ad3 shipped the compute enrollment and `workspace_not_ready`, so `status` reads the workspace's own compute state — both `status` and `health`, because ready capacity nobody can reach is not ready. Two contracts are still outstanding and both are isolated in cmd/rainier/readiness.go: a bearer-reachable onboarding destination, and a per-environment agent launch catalog. Neither is guessed at. New: `logout`, `status`, `info`, `stop`, `delete`, the `current` selector, and `--json` on everything a script reads — carrying the canonical API facts verbatim beside the derived ones. `diff` is removed completely: git inside the session is the source of truth. `doctor`, `suspend`, `rm` and `agent ls` remain as hidden aliases for the commands that replaced them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3GPB683VghXL2XA4XWK7Y
jiashuoz
force-pushed
the
feat/cli-v0-interface
branch
from
September 8, 2026 02:16
b13efe1 to
5a9346f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
rainier --helplisted twenty-odd commands. Transfers, snapshots, secrets, environments, contexts, workspaces and the self-hosted credential vault sat beside the four things a hosted developer actually does, so a new developer could not read the product off one screen — which is the only test of a command surface that matters.Hosted v0 serves one developer, one workspace, one Dedicated plan, and several persistent sessions. The CLI is for four jobs: signing in, checking readiness, authenticating Claude Code or Codex, and creating and managing sessions. Payment, compute selection, workspace management, GitHub connection and environment administration belong on the web.
docs/cli-v0-contract.mdis the contract this PR implements.Session, Process, Connection — three facts, kept apart
A session has three independent facts, and the CLI never collapses them. The control plane reports them separately because they are separate, and each fails on its own:
statechild_exit_codereachableSession lifecycle:
statequeued,creatingrunningchild_exit_codesuspended_warm,suspended_coldfailed,deadcanceleddestroyedUnknown (<the server's own word>)Process:
Exited (N)whenchild_exit_codeis present;Runningwhen absent and the sandbox exists;-otherwise.Runningmeans the child exists — never that it is doing anything rather than waiting at a prompt.Connection:
Available/Unavailable. Never used as a lifecycle state.canceledanddestroyedare terminal records: absent from the default list, present under--all, and labelled accurately when inspected — a session somebody cancelled and one somebody deleted are different events, and neither is "finished".Unknown future states stay unknown, carry the server's own word through, and are never translated into another dimension's vocabulary. A reachable session in an unknown state is still reachable.
Action eligibility comes from raw API states
Never from the display label. Two states that share a label can differ at the endpoint, which is exactly why the label cannot decide this. Read off the control plane:
running;failedonly while its runner is connectedcontrolapp/attachments.goattachablesuspended_warm,suspended_coldResumeSessionrunningonlySuspendSessioncreating(409) anddestroyedDeleteSessionConsequences:
queuedandcreatingshare that label andSuspendSessionrefuses both.attachis offered forqueued/creating(the CLI waits) and the suspended states (the CLI resumes first) — each is a real API path the CLI drives.reachable, the one place a connection fact legitimately decides an action, because the endpoint says so.unknown— no local refusal, no invented claim, the server decides.JSON keeps the canonical facts
stateis the server's own string and is never replaced by a derived word.reachableandchild_exit_codeare the boolean and the nullable integer the API sent, with the key always present. Derived fields are additive and separately named:{ "schema": "rainier.v0.session", "version": 1, "id": "sess_b", "name": "done", "state": "running", "reachable": true, "child_exit_code": 0, "lifecycle": "running", "process": "exited", "connection": "available", "actions": {"attach": "yes", "stop": "yes", "delete": "yes"} }actionsis three-valued (yes/no/unknown) because an unknown state admits no honest boolean.ls --jsonandinfo --jsonemit the same entry shape from the same builder.Command inventory
Before:
logindoctornewlsattachsuspendresumesnapshotrmdiffpushpullcredsconnectionagentsecretenvcontextworkspaceversionAfter (default help — 23 lines, 78 columns):
Aliases retained (hidden, documented under
help all):doctor→status --verbose,suspend→stop,rm→delete(keeping its no-prompt behavior for scripts),agent ls→agent status.Removed completely:
diff. Git inside the session is the source of truth; the e2e suite's assertion is now an in-sessiongit diff --stat origin/BASE...HEAD.Advanced (
help all):resumesnapshotpushpullcredsconnectionsecretenvcontextworkspace, plus self-hosted login flags and--env/--image/--egress/--since.Stop vs Delete, and billing
rainier stopis the cold suspend;rainier attachresumes a stopped session automatically. Its help now says plainly:deleteremains permanent and distinct: it asks on a terminal, requires--yeswithout one, and never blocks on an answer that cannot arrive.Cloud API contract, corrected against
rainier-cloud81d1ad3The previous revision's §5 was written before Cloud PR #53 merged and was stale. Corrected:
GET /v0/workspaces/{id}/computeneeds_planfor an unenrolled workspaceworkspace_not_ready(409)/v0/web/bootstrapstatusnow reads the workspace's own compute enrollment and uses the server's vocabulary (needs_plan,awaiting_payment,provisioning,ready,failed,cancelling,cancelled) withhealthalongside it.ready+health:"unavailable"is not ready — capacity that exists and cannot be reached; the entitlement is intact and a session will not start, so calling it ready would send somebody torainier newto be refused. It gets its own sentence, since the recovery is nothing like selecting a plan.The launch catalog now targets
GET /v0/environments/{id}/agents— launch capability is a property of the environment's image, which is regional tenant state — instead of the stale proposal to putlaunch_cmdon/v0/agents./v0/agentsstays credential custody, account-scoped, with nowhere to put a credential and nothing to say about an image.argvis a structured array from a closed server catalog, never a shell string, and never carries an image tag, digest, package name or version.Both missing contracts are isolated in
cmd/rainier/readiness.go.statusprints noContinue:line without one, and drops any destination that is not an absolutehttpsURL — the browser bootstrap serves a relative path today, and a relative path in a "go here" message is worse than none.Verification
Rebased cleanly onto
origin/main@68ac6fe(includes #66 and #67 — no CLI overlap)../scripts/check-module-path.sh./scripts/check-public-protocols.sh./scripts/check-public-control.shgo test ./...bash scripts/build.sh -o bin/ ./cmd/...go vet ./...go test -race -count=2 ./cmd/rainier/ ./internal/cli/go test -race ./internal/e2e/ ./controlapp/ ./v0wire/ ./control/gofmt -lon every changed fileRoot help reviewed manually: 23 lines, longest line 78 columns.
Regression tests, and proof they catch the old behavior
cmd/rainier/sessionstate_test.gocovers running+exit-code, reachable=false while running, both suspended states, failed/dead, canceled/destroyed, an unknown future state, the human columns, JSON fidelity, and action eligibility.The old behavior was reintroduced into the real code and the new suite run against it. It failed on exactly the defects:
Remaining server dependencies
GET /v0/web/onboarding(edge) — a bearer-reachable, absolute-URL onboarding map keyed by the compute vocabulary. Until it shipsstatusprints noContinue:line.browserhttp.onboardingFor()and this route must be generated from one shared table or they will drift.GET /v0/environments/{id}/agents(cell) — the per-environment launch catalog. Until it shipsrainier new --agent Xfails with one message naming it."default": trueonv0wire.EnvironmentView— so the default environment is a server fact rather than the CLI's "there is only one" heuristic. Decoded forward-compatibly today.v0wire.SessionView—infoomits repository metadata rather than reconstructing it.Not in this PR
No
rainier githubgroup. No server changes. Not merged.🤖 Generated with Claude Code
https://claude.ai/code/session_01Q3GPB683VghXL2XA4XWK7Y