Skip to main content
The Reliant CLI provides commands for running server components, managing the local tools daemon, authenticating, and working with workflows.

Quick Reference

Global Flags

These flags are available on all commands.

reliant auth

Manage authentication Authenticate the CLI with a Reliant server. A login is an rlat_ access token issued by the control plane, stored in the credentials file shared with forge (see ‘reliant auth login —help’), keyed by the server it is for.
Subcommands:

reliant auth login

Log in to a Reliant server Logs the CLI in to the resolved server (—server, else RELIANT_SERVER_URL, else the built-in default) with the OAuth 2.0 authorization-code flow + PKCE. The server names its authorization server (the control plane) at /.well-known/oauth-authorization-server. Your browser opens there; you sign in with your normal Reliant account and approve; a one-time code comes back to a temporary listener on a loopback port, and is exchanged for an rlat_ access token (90 days, scope reliant:api, plus the deploy, secret and domain permissions your organization grants you — see below). The token is stored in the credentials file shared with forge (~/.config/forge/credentials.json), under this server. There is no “current” server: a later command against another —server finds no login there and says so, rather than sending this token somewhere it was not issued for. FORGE NEEDS NO SEPARATE LOGIN. ‘reliant forge …’ (and every shell a Reliant agent runs) asks ‘reliant auth forge-credential’ for a control-plane token, which exchanges this login — or the daemon’s credential — for one that holds only deploy/secret/domain authority, for at most an hour. This login never leaves this machine’s Reliant files. For CI, skip login and set RELIANT_TOKEN.

reliant auth logout

Forget the login for the resolved server Removes the resolved server’s entry from the shared credentials file. Every other entry (other servers, and forge’s logins) is untouched. The token stays valid server-side until it expires or you revoke it in the web app.

reliant auth serve

Start a local OAuth helper server Starts a lightweight HTTP server on localhost that handles OAuth callback flows for Claude and Codex authentication. USUALLY YOU DO NOT NEED THIS. reliant daemon start serves the same endpoints when it runs on the machine your browser is on, so a local daemon already covers it. Run this when the daemon is REMOTE (or not running) and you still want to connect an account from this machine — the OAuth provider redirects to localhost, which only exists where your browser is. The server exposes: GET /health — identity + readiness (service, version), for the web app’s probe POST /oauth/start — Start an OAuth flow (opens browser, waits for callback) Example: reliant auth serve reliant auth serve —port 19284
Flags:

reliant auth status

Show the login for the resolved server Shows which credential the CLI would use for the resolved server (and where that server came from: the —server flag, RELIANT_SERVER_URL, or the default). Purely local: no network call.
Flags:

reliant auth token

Manage API tokens (rlat_ access tokens acting as you) API tokens authenticate automation against the Reliant API without a browser login. ‘reliant auth login’ already stores one for this CLI; ‘create’ mints an additional, separately named token to hand to a script or CI (RELIANT_TOKEN). Creating a token is a consented browser login: a token never mints a token. Listing and revoking use the resolved credential (RELIANT_TOKEN or your login).
Subcommands:

reliant auth token create

Create a new API token Mints an API token (reliant:api, 90 days) named reliant-cli@<name> through a browser login you approve, and prints it once. It is NOT stored: this CLI keeps using its own login. Hand the printed token to automation as RELIANT_TOKEN. Re-running with the same —name replaces that token.
Flags:

reliant auth token list

List API tokens (metadata only, never secrets)
Flags:

reliant auth token revoke

Revoke an API token by name or ID

reliant daemon

Manage the local tools daemon The tools daemon runs on your local machine and provides tool execution capabilities (shell, file operations, MCP servers, terminal sessions) to the Reliant cloud platform via a bidirectional gRPC stream.
Subcommands:

reliant daemon logs

Tail daemon logs Streams daemon log output. Defaults to the last 50 lines with live follow.
Flags:

reliant daemon ls

List every daemon instance on this machine Lists every daemon instance directory under ~/.reliant/instances and reports, for each, whether a daemon is alive in it and what its runtime record says. Liveness comes from the instance’s advisory lock, not from the runtime record and not from the process table: the kernel releases the lock when the holding process dies, so RUNNING cannot be stale and does not depend on what the daemon binary is named or where it lives. RUNNING and the record are reported separately because they can disagree, and the disagreement is the diagnosis. A running instance with no record is a daemon that died before publishing one, or one still starting up. A record with no running daemon is a leftover from a process that is gone.

reliant daemon register

Register this machine as a daemon Registers this machine to connect to the Reliant cloud platform as a daemon. If not already logged in, opens your browser for OAuth authentication. Creates a long-lived access token for the daemon and stores it locally. After registering, run ‘reliant daemon start’ to connect.
Flags:

reliant daemon start

Start the tools daemon Starts the tools daemon in the foreground (default) or background. The daemon connects to the Reliant cloud platform and provides local tool execution capabilities. Credential resolution order:
  1. Daemon credentials file (created by ‘reliant daemon register’)
  2. If logged in but not registered, auto-registers and creates credentials
  3. If not logged in, prompts for login and then auto-registers — unless —non-interactive (or RELIANT_DAEMON_NON_INTERACTIVE) is set, in which case the daemon never opens a browser or runs the login flow itself. It instead stays resident and idle, publishing “awaiting_credentials” in its runtime state, and polls for a credentials file to appear on disk (see ‘reliant daemon status’ and internal/toolexec/daemonstate). This is the mode Electron spawns in: its own login page owns interactive sign-in, and the daemon must never pop a second one.
Flags:

reliant daemon status

Check daemon status Reports whether a tools daemon process exists, whether its gateway stream is actually established, and which binary it is running. Tool execution happens inside the daemon, over that stream — a daemon process whose stream never came up serves nothing. “Running” therefore means “connected”, not “a PID exists”, and this command exits non-zero whenever the stream is not established.
Flags:

reliant daemon stop

Stop the tools daemon Sends a graceful shutdown signal to the running tools daemon and waits for it to actually exit. Use —force to SIGKILL immediately. If the daemon does not exit within the grace period this escalates to SIGKILL, and if it survives that, the command exits non-zero and leaves the runtime record in place. A daemon reported as stopped while it is still running keeps its gateway registration, and the next ‘daemon start’ then registers a second daemon under the same identity — the two evict each other until one is killed.
Flags:

reliant db

Database schema commands Apply or inspect the migrations embedded in this binary. DATABASE_URL (required) and DATABASE_DRIVER are read from the environment — the same variables the servers read, so a migration Job and the api-server cannot disagree about which database they mean.
Subcommands:

reliant db migrate

Manage database migrations
Subcommands:

reliant db migrate status

Print which embedded migrations the database is missing Report how many of this binary’s embedded migrations the database has applied, and list any it has not. Opens the database WITHOUT migrating and WITHOUT waiting, so it stays usable to diagnose the very deadlock the wait produces.

reliant db migrate up

Apply every pending migration Apply every migration this binary embeds that the database has not recorded applied, then exit. Exits 0 when the schema is already current, so it is safe to run on every deploy and safe to re-run after a failure.

reliant forge

Connect RPC development framework for LLM-optimized applications Forge is a development framework where everything communicates via Connect RPC interfaces, purpose-built for LLM-driven development. It enables easy mocking, middleware injection, spec-driven development, and component swapping - all while maintaining a single, consistent interface pattern throughout the entire stack. New here? Run ‘forge start’ — the whole path from an empty directory to authored protos, in one call.
Subcommands: Flags:

reliant forge api

Inspect and exercise Connect RPC endpoints over plain HTTP+JSON Connect handlers accept plain HTTP/1.1 POST requests with Content-Type: application/json — no gRPC tooling required. Sub-commands surface that capability for ad-hoc debugging from the shell.
Subcommands:

reliant forge api curl

Print a copy-pasteable curl command for a Connect RPC method Print a curl invocation that exercises a Connect RPC endpoint over plain HTTP+JSON. The URL is derived from the proto package + service + method name; the request body is a zero-value skeleton populated from the method’s input message fields. Arguments: <service.method> Fully-qualified service and method, e.g. “users.v1.UserService.GetUser”. Short form is also accepted: “UserService.GetUser” matches the unique service of that name across all proto packages. Examples: forge api curl users.v1.UserService.GetUser forge api curl UserService.GetUser —port 9090 forge api curl users.v1.UserService.CreateUser —body ‘{"name":"alice"}’ The command never executes — it only prints. Pipe to sh if you want to run it, or paste into a debugger / Postman / HTTPie session.
Flags:

reliant forge build

Compile the project’s binaries and frontends locally (never publishes) Compile the project’s services and frontends. A LOCAL CHECK. This command never publishes anything. Pushing images and recording a release are environment acts — a push destination is declared per workload in an env’s render, and a release’s artifact set is discovered from it — so they live on forge env build <env>: forge env build prod —push # build + push to declared registries forge env build prod —release v1.4.0 # build + push + record the release which is also why THIS command’s environment argument is optional: it scopes docker builds and tag resolution, and nothing here needs an env to compile. This command is a PURE EXECUTOR of the per-service, per-env build declaration in KCL. With an environment argument it iterates the services the rendered env declares and dispatches on each service’s build.type:
  • go → go build the declared cmd (CGO_ENABLED=0, stripped) — no hardcoded ./cmd; the target package comes from KCL
  • docker → docker build the service’s image (dockerfile/platform/…)
  • shell → run the verbatim build command
It also builds Next.js frontends (npm run build) and, with —docker, the shared project image. Output binaries land in the output dir. Examples: forge build # Compile everything forge build staging # Scope docker builds/tag resolution to deploy/kcl/staging/ forge build -t web # Build only the “web” frontend forge build -o bin # Output binaries to bin/ forge build —docker # Also build Docker images (locally; never pushed) forge build —debug # Build with debug symbols for Delve When a declared reference is a k3d-local localhost:<port>, the image is also tagged registry.localhost:<port>/<path> (LOCAL alias only — the host can’t DNS-resolve registry.localhost, so it isn’t pushed; the containerd mirror config inside k3d resolves that reference at pull time).
Flags:

reliant forge ci

CI helper commands — verify, scan, and validate in CI pipelines
Subcommands:

reliant forge ci migration-safety

Run SQL migration safety checks based on forge.yaml config Checks SQL migrations for patterns that pass on empty databases but fail or lock populated databases.

reliant forge ci run

Show one CI run’s timeline — cut, checks, promotes, rollouts — and whether it passed Read one CI run’s timeline from the hosted control plane and say whether it passed. A run is everything one pipeline attempt wrote under its run id: the release it cut, the checks it recorded (forge gate record), every promotion it made, and each promotion’s rollout. Nothing is a stored event — the control plane derives the stages from the ledger on every read, so polling this is safe. The run id is the one forge stamped on those writes: —run-id, or the CI default (github:<owner/repo>/<run>/<attempt> under GitHub Actions). WHICH CONTROL PLANE: —env names an env whose KCL declares it. Without —env, the project’s single declared control plane is used; with several, —env is required. A run can span envs, so the env only chooses where to ask. Exit codes (the hosted primitives’ shared table): 0 every stage passed (skipped stages count as passed) 1 a stage failed or errored, or the request cannot be served (a run too large to read as one timeline) 2 the control plane could not be read — unreachable, auth refused, or it does not serve run timelines 5 a stage is still running: retry this command, the run has not finished A run id nothing has written under is an EMPTY timeline and exits 5, never 0: an id nobody used is not a pass. The control plane answers another organisation’s run id the same way, so this cannot be used to probe them. Examples: forge ci run “github:acme/app/12345/1” forge ci run “$RUN_ID” —env prod —json | jq -r ‘.stages[] | select(.status != “passed”)’
Flags:

reliant forge ci summarize

Render forge —json documents as a Markdown job summary (for $GITHUB_STEP_SUMMARY) Render one or more forge —json documents as Markdown, for a CI job summary. Every hosted deploy verb’s —json carries the same envelope (ok, exit_code, error); this prints that as a one-line verdict, the document’s scalar fields as a table, and its row lists (images, workloads, stages, promotions, gates) as tables. It reads documents; it decides nothing. It exits 0 whatever the documents say — the step that wrote each document already exited with its verdict. It exits 1 only when an input cannot be read. Examples: forge env deploy prod v1.4.0 —json > deploy.json forge env status prod —json > status.json forge ci summarize deploy.json status.json >> “$GITHUB_STEP_SUMMARY”

reliant forge ci validate-kcl

Validate that every environment renders manifests kubectl will accept Renders each environment’s deploy/kcl/<env>/main.k through the embedded KCL runtime and asserts the result is DEPLOYABLE, not merely that it evaluates. An env’s applied stream is output.manifests, EXPANDED exactly as forge env deploy expands it: every Cluster-bound forge.dev Workload record is rendered through pkg/deploy.RenderWorkloads (Full profile), per (cluster, namespace) set. A record that does not render fails here. Every expanded object must carry apiVersion + kind, and no top-level key but output may hide k8s objects no deploy would ever apply. Hosting is PER WORKLOAD. The env’s hosted part (forge.OnHosted workloads, hosted databases, hosted frontends) is judged by its own deploy path: each workload must pass Workload.Validate(ProfileRestricted), and the set must render as the control plane renders it — the same plan forge env deploy runs, minus the release and the RPCs. Shares its implementation with forge doctor --signal deploy, so CI and the doctor cannot disagree about whether a project can be deployed.

reliant forge ci verify-generated

Verify generated code is pristine and up to date Three checks, all local to the checkout:
  1. Self-certification: every generated file’s embedded forge:hash marker must verify (recompute vs embedded) — catches hand-edits that were committed without —force / forge project disown.
  2. Freshness: runs forge generate and verifies no files changed — catches stale generated code after an input (proto/forge.yaml) change.
  3. Commit policy: no generated file is gitignored (check 2 cannot see an ignored file), and no machine-local state (.forge-kcl/, a frontend’s dev public/config.js) is tracked.

reliant forge ci verify-test-run

Verify a go test -json run actually ran its tests (reads output; runs nothing) Reads a go test -json stream and reports the packages that skipped so much that their pass proves nothing. This command RUNS NO TESTS. It reads the record of the run your project already did, so it costs one extra flag and no extra time: go test -json ./… | tee test.json | forge ci verify-test-run go test -json ./… > test.json; forge ci verify-test-run —from test.json go test -json swallows the human-readable output, hence the tee — keep the raw stream for a human and hand a copy to forge. TWO RULES, AND WHY NOT MORE. Skips are legitimate: -short exists, framework limitations get documented skips, and one reference package keeps a genuine unconditional skip even when fully configured. A gate that fires on every skip is a gate that gets switched off. So: zero-evidence every test in the package skipped — its “ok” is a statement about nothing. No sample-size floor; unambiguous at any size. mass-skip the package skipped more than —max-skip-ratio of its tests and has at least —min-tests of them. Healthy packages are never listed. When a package’s heavy skipping is genuinely expected — an integration-only package on a machine with no docker — declare it once in forge.yaml, with a reason: ci: test_skips: allow:
  • package: internal/dockerintegration reason: “every test here needs a live docker daemon”
The reason is required and is read by humans, not by forge: an exemption nobody had to justify is one nobody will revisit. A declaration that stops suppressing anything is reported as no longer needed rather than left to rot. WHICH RUN TO POINT IT AT. The one whose green you are treating as coverage. A deliberately-reduced run (-short, a single package, a -run filter) is not a claim about the whole suite, so gating it teaches people to ignore the gate; forge cannot see the flags a stream was produced with and will not guess. THREE STATES. Input that carries no go test -json events, or a stream that ends mid-run, is UNDETERMINED — forge could not obtain the facts. That is not a pass and it exits non-zero: this command never reports a clean run it did not read. Failures in the stream also fail the command, because go test -json ./... | forge ci verify-test-run in a shell without set -o pipefail reports only the LAST command’s status — a checker that ignored them would launder a red suite green.
Flags:

reliant forge ci vuln-scan

Run vulnerability scanners based on forge.yaml config Runs govulncheck for Go and npm audit for frontends. Defaults to scanning everything enabled in forge.yaml. This is a GATE, so it never reports a pass it did not verify. If a selected scanner cannot run — the binary is not on PATH, or the config selects no scanner at all — the command FAILS and names the missing piece, rather than exiting 0 over a scan that never happened. The success line names every scanner that actually ran.
Flags:

reliant forge cloud

Talk to the hosted control plane an environment declares Commands that reach the hosted control plane declared by an environment’s forge.ControlPlane block. The endpoint is per-environment and comes from KCL, so which server a command hits follows from its env argument — not from any stored “current context”. Authenticate with forge login, or set the declared token env var.
Subcommands:

reliant forge cloud releases

List releases from the hosted control plane List the releases the hosted control plane holds for your organization. The endpoint comes from <env>’s forge.ControlPlane declaration; the credential from —token, then the declared env var, then the credentials file entry for that endpoint (forge login). Scope is always the caller’s own organization — the request carries no organization field, so there is nothing to widen.
Flags:

reliant forge cloud status

Show the endpoint and credential source for an environment
Flags:

reliant forge cloud token

Mint, list and revoke ORG automation tokens (the CI deploy credential) Manage the organization’s automation tokens on the hosted control plane. An org token has no acting user, so it outlives any one person — which is what a CI pipeline’s FORGE_CONTROL_PLANE_TOKEN must be. Minting needs a human session (an org admin, via forge login); the control plane refuses a machine credential. Typical setup for the scaffolded release workflow: forge cloud token create —env prod —name github-actions
—scopes deploy:read,deploy:write —json | jq -r .secret
| gh secret set FORGE_CONTROL_PLANE_TOKEN
Subcommands:

reliant forge cloud token create

Mint an org automation token; its secret is printed ONCE
Flags:

reliant forge cloud token list

List the org’s automation tokens (never their secrets)
Flags:

reliant forge cloud token revoke

Revoke an org automation token, immediately
Flags:

reliant forge cluster

Manage the local k3d cluster and inspect dev state Manage the local k3d development cluster and inspect dev-loop state. forge cluster owns the universal mechanics every k8s-targeting forge project needs: k3d cluster lifecycle, ingress URLs, status, logs. Project-specific orchestration (sibling-repo deploys, helm chart bootstraps, webhook listeners) lives in your scripts/ and Taskfile.yml — composed with the forge cluster primitives. The cluster config is read from deploy/k3d.yaml (override via —config). Lifecycle subcommands pin the kubectl context to k3d-<cluster-name> as a guardrail against accidental prod-context leaks. Examples: forge cluster up # create k3d cluster from deploy/k3d.yaml forge cluster reload # re-render KCL + kubectl apply + wait rollout forge cluster urls # print the ingress URL table for the env forge cluster status # cluster + pods + ingress URLs forge cluster logs —service api # kubectl logs -f for a service forge cluster instances # list every forge dev namespace on the host forge cluster connect prod-us —context gke_acme_us-central1_prod —env prod forge cluster disconnect prod-us —env prod # a cluster YOU operate, as a deploy target
Subcommands:

reliant forge cluster connect

Register a Kubernetes cluster the control plane may deploy into Register a cluster you operate, by address, so environments can target it. Nothing is installed in your cluster beyond the RBAC the platform’s apply needs, and nothing about the cluster changes. forge reads the API server address and CA from your kubectl context, tells the control plane, and applies the in-cluster grant. forge cluster connect prod-us —context gke_acme_us-central1_prod —env prod forge cluster connect vke-prod —context vke-prod —auth token —env prod forge cluster disconnect prod-us —env prod AUTH — two values, the two ends of a real trade-off rather than two clouds: gcp GKE workload identity. The hub presents its OWN GCP identity and no secret ever crosses the boundary. forge prints the one-time IAM grant you run; it cannot run it for you. token A scoped ServiceAccount token forge mints in your cluster. Works on ANY Kubernetes cluster — EKS, AKS, VKE, k3s, bare metal — and needs no cloud identity. Strictly worse (a bearer token at rest), bounded by the RBAC forge applies, and revoked by disconnect. —auth auto (the default) picks gcp for a GKE context and token for anything else. The token path is a real answer, not a fallback: it is how a cluster with no cloud identity to trust becomes a target at all. Re-running connect UPDATES the cluster of that name, so fixing an endpoint is the same command again. —env names WHICH control plane to talk to, from that env’s forge.ControlPlane declaration.
Flags:

reliant forge cluster disconnect

Deregister a connected cluster and remove what forge created in it Deregister the connected cluster of this exact name. Two halves, and the order matters: the control plane revokes the cluster’s credential first, then forge deletes the ServiceAccount, token Secret and RBAC it created in the cluster. A token revoked server-side is already powerless, so a failure to reach the cluster afterwards leaves litter rather than a live credential. REFUSED while any live environment targets the cluster — that check is the control plane’s, and it is why this is not a local delete. forge cluster disconnect prod-us —env prod —context gke_acme_us-central1_prod —context is optional and names where to clean up. Without it, forge deregisters the cluster and tells you which objects to delete by hand.
Flags:

reliant forge cluster down

Delete the k3d cluster Delete the k3d cluster named by —config’s metadata.name, and FIRST every cluster nested on its docker network (declared with owner, so it has no config file of its own and cannot outlive its owner’s network). Given an environment, delete every k3d cluster its KCL declares instead — including secondaries declared with owner and no config file, which —config cannot name. Secondaries are deleted before their owner: k3d cannot remove a docker network a secondary is still attached to. This deletes whole clusters, and with them every namespace on them — including other environments’ and other worktrees’ stacks sharing the cluster.
Flags:

reliant forge cluster info

Print static dev-loop config (cluster name, expected context, declared ports) Print the static dev-loop config declared in forge.yaml + deploy/k3d.yaml. Static means “what the project says it expects” — cluster name, expected kubectl context, registry URL, declared service/frontend ports. It does NOT contact the cluster or check pod state. For dynamic state (is the cluster up? are pods running? what are the live ingress URLs?) use forge cluster status.
Flags:

reliant forge cluster instances

List every forge dev namespace on every reachable cluster List every forge-managed dev namespace on the host. Inspects each k3d cluster’s kubeconfig context and reports the namespaces labelled app.kubernetes.io/managed-by=forge. Useful for the multi-worktree workflow: many worktrees, each with its own namespace, all sharing one cluster (or one per worktree). Examples: forge cluster instances forge cluster instances —json
Flags:

reliant forge cluster logs

Stream kubectl logs for one or all services in the dev namespace Stream kubectl logs. Without —service, streams logs from every pod managed by forge in the dev namespace (the same label forge uses to discover deployments). With —service, scopes to one service deployment. Examples: forge cluster logs # tail every forge-managed pod forge cluster logs —service api # tail one service forge cluster logs —service api —tail 200 forge cluster logs —no-follow # one-shot, no streaming
Flags:

reliant forge cluster reload

Re-render deploy/kcl/dev + kubectl apply + wait rollout Re-render the dev KCL manifests, apply, and wait for rollout. This is the inner loop during local development: after editing code or KCL, run this to push the change into the cluster without rebuilding the docker image (the same code path forge env deploy dev uses, but skips the cluster bootstrap). Examples: forge cluster reload forge cluster reload —image-tag dev-2026-06-01 forge cluster reload —dry-run
Flags:

reliant forge cluster reset

Delete then recreate the cluster
Flags:

reliant forge cluster status

Print dynamic dev-loop state (cluster up/down, pods, ingress URLs) Print the dynamic state of the local dev environment. Dynamic means “what’s actually happening right now” — does the k3d cluster exist, what’s the current kubectl context, what pods are in the dev namespace, what ingress URLs are exposed by the dev env’s KCL gateways, what sibling dev namespaces exist on this cluster. For static config (declared cluster name, expected context, declared service/frontend ports) run forge cluster info. Examples: forge cluster status forge cluster status —json # machine-readable for scripts/dashboards
Flags:

reliant forge cluster up

Create the k3d cluster from deploy/k3d.yaml Create the k3d cluster from deploy/k3d.yaml. If the cluster already exists, this is a no-op success. With —wait, blocks until the cluster’s nodes report ready. Given an environment, the clusters come from that environment’s KCL instead of a k3d config file: every forge.Cluster the env’s bundle declares is ensured exactly as forge env up <env> ensures it — the declared pod/Service CIDRs, API port, owner network and registry-inherit included, none of which a k3d YAML carries. Use it whenever the env declares its clusters: a cluster created from the bare config file lacks those fields, and the env’s own deploy then refuses it. Examples: forge cluster up forge cluster up —wait forge cluster up —config deploy/k3d.custom.yaml forge cluster up dev-k8s —wait
Flags:

reliant forge cluster urls

Print the ingress URL table for the dev env Print the ingress URL table for the dev env. Reads deploy/kcl/dev/ via the same KCL render the deploy pipeline uses, then prints one URL per HTTPRoute/GRPCRoute grouped by gateway and listener. When features.ingress is disabled this prints a short notice and exits 0. When the dev env has no gateways declared yet it prints a pointer to deploy/kcl/dev/ingress.k. Examples: forge cluster urls forge cluster urls —json # machine-readable for scripts/dashboards
Flags:

reliant forge component

Manage UI components from the component library List, search, and install UI components from Forge’s built-in component library.
Subcommands:

reliant forge component install

Install components into your project Install one or more components from the library into your project’s src/components/ui/ directory. If —dir is not specified, the command auto-detects the nearest frontend directory.
Flags:

reliant forge component list

List all available components
Flags:
Search components by keyword

reliant forge db

Database and migration commands Manage database migrations. Forge uses a migration-first database model:
  • Checked-in SQL migrations in db/migrations/ are the source of truth
  • golang-migrate is the canonical migration runner
  • Entity types are projections of the applied schema (forge generate)
When something is wedged: A migration failed part-way and the state is marked dirty No further migration will run until the flag clears, and ‘migrate up’ refuses too. Repair the schema by hand, then: forge db migrate force <version> # record it as applied, runs no SQL forge db migrate up Or, on a scratch dev database, skip the repair entirely: forge db reset # DROP, recreate, migrate, seed (dev-only) A migration cannot apply because existing rows violate it Adding a constraint to a column seeding filled with placeholders wedges both repairs against each other: ‘seed reset’ refuses because the schema is behind, ‘migrate up’ refuses because of the rows. Discard the state: forge db reset # needs no dirty-state reasoning (dev-only) The dev database is full of bad or stale rows Do not drop the database by hand: forge db seed reset # delete seeded rows and re-seed (dev-only) forge db reset # or rebuild the whole database (dev-only) Seeded rows are rejected by their own schema ‘forge db seed apply’ names the constraint it could not place, and why. Load the db/seeding skill for the constraint shapes forge can seed. Project files (db/migrations, db/seeds/vocab.yaml, db/seeds/custom/) are read from the project root: the one -C names, or the one the current directory is in. A relative —dir resolves against that root too.
Subcommands:

reliant forge db introspect

Inspect the migrated database schema Connect to a PostgreSQL database and display the current schema. Shows tables, columns, types, constraints, indexes, and foreign keys. Examples: forge db introspect —dsn “postgres://user:pass@localhost/mydb?sslmode=disable” forge db introspect —dsn “DATABASEURL"−−tableusersforgedbintrospect−−dsn"DATABASE_URL" --table users forge db introspect --dsn "DATABASE_URL” —format json
Flags:

reliant forge db migrate

Run migration lifecycle commands with golang-migrate Apply, inspect, and repair migration state using golang-migrate. There is no “down”: forge rolls forward only. A bad migration is repaired by a new forward migration written against the state the database is actually in. Migrations are stored in db/migrations/ by default. Install golang-migrate from https://github.com/golang-migrate/migrate/tree/master/cmd/migrate. The connection string can be provided via —dsn or, if the flag is omitted, the DATABASE_URL environment variable. Examples: forge db migrate up —dsn=<dsn> forge db migrate up # picks up $DATABASE_URL DATABASE_URL=… forge db migrate status forge db migrate version forge db migrate force 20240102150405
Subcommands:

reliant forge db migrate force

Clear a dirty migration state by recording a version without running SQL Record <version> as the applied migration version WITHOUT running any SQL. Reach for this when a migration failed part-way and golang-migrate marked the state dirty. Nothing else will run until that flag clears — including ‘forge db migrate up’, which refuses on a dirty version — so this is the way out of that loop. Forcing asserts a fact; it does not verify one. Forge cannot know how much of the failed migration actually landed, so repair the schema FIRST (inspect it with ‘forge db introspect’, then finish or undo the partial migration by hand), and force only once the database matches what that version intended. Forcing past a migration whose SQL never ran leaves the schema permanently behind what forge believes is applied. Examples: forge db introspect # see what actually landed forge db migrate force 20240102150405 # then clear the flag forge db migrate up # and catch up
Flags:

reliant forge db migrate status

Show migration status
Flags:

reliant forge db migrate up

Apply pending migrations
Flags:

reliant forge db migrate version

Show the current migration version
Flags:

reliant forge db migration

Create and re-version forward-only SQL migration files Create a new forward-only SQL migration in db/migrations/, or re-version one. Versions are 14-digit UTC timestamps (<YYYYMMDDHHMMSS>_<name>.up.sql), never sequential numbers. A sequential allocator picks max+1 by reading the directory, which is collision-free on one checkout and collision-prone across branches: every branch cut from the same commit reads the same highest number and picks the same next one. A timestamp cannot collide without two branches allocating in the same second. Existing sequential files are never renamed — a 14-digit timestamp sorts after any 5-digit number, so adoption costs nothing. Commands: new allocate a fresh timestamp and scaffold the .up.sql file rebase re-version an existing file whose version the schema has passed The .up.sql file includes rich schema context so LLMs can immediately write the migration SQL. Context includes:
  • Current schema (parsed from existing migrations, or from DB with —dsn)
  • Previous migration content
  • Migration history
Examples: forge db migration new add_users_table forge db migration new add_preferences —dsn “$DATABASE_URL” forge db migration new “backfill account status” —dir db/migrations forge db migration rebase db/migrations/20260101120000_add_users.up.sql forge db migration rebase —all-pending
Subcommands:

reliant forge db migration new

Create a new forward-only migration with a fresh UTC timestamp version Create a new forward-only migration in the migrations directory. The file is named <YYYYMMDDHHMMSS>_<name>.up.sql, using a UTC timestamp allocated to sort after every version already in the directory. Never hand-type a version number: max+1 is what makes parallel branches claim the same one. There is no .down.sql. Forge rolls forward — a bad migration is repaired by a new migration written against the state the database is actually in. Examples: forge db migration new add_users_table forge db migration new add_preferences —dsn “$DATABASE_URL”
Flags:

reliant forge db migration rebase

Re-version migrations to fresh timestamps that sort after everything merged Re-version one or more migrations to fresh UTC timestamps. WHEN YOU NEED THIS. A migration whose version is below the schema’s current version can never run: golang-migrate applies only what is above its recorded high-water mark, so the file is not pending, it is invisible. That happens one way — branches A and B are cut, A allocates the earlier timestamp, and B merges and deploys first. Main now holds A’s SQL at a version the database has already passed, and every later up reports the schema current. The first symptom is a query for a column that does not exist. The file has not run anywhere, so re-versioning it is safe and is the fix. Both the migrator’s refusal and forge lint’s version rules point here. Each file keeps its name stem and gains a fresh timestamp that sorts after every version in the directory AND after the highest version on the default branch, allocated by the same allocator as forge db migration new. Tracked files are moved with git mv. A batch keeps its relative order. WHAT IT REFUSES. A migration already on the default branch keeps its version, always. Some database has recorded it as applied under its current filename, and renaming it would leave that database with a recorded version whose file no longer exists — worse than the problem, and unrecoverable without editing schema_migrations by hand. Write a new forward migration instead. Examples: forge db migration rebase db/migrations/20260101120000_add_users.up.sql forge db migration rebase —all-pending
Flags:

reliant forge db reset

DROP the dev database, recreate it, migrate to head, and seed (dev-only) DROP the dev database, recreate it empty, apply every migration, and seed. One verb for “this scratch database is wedged — just rebuild it”. This is the way out of a state nothing else can exit. When a migration fails part-way the database is marked dirty, and the two repairs block each other: ‘forge db seed reset’ refuses because the schema is behind, while ‘forge db migrate up’ refuses because the rows only seed reset can delete violate the new constraint. Adding a foreign key to a column that seeding filled with placeholders is enough to produce it. reset needs none of that reasoning because it DISCARDS the state rather than repairing it. There is no dirty flag to clear and no row to fix: the database is gone and rebuilt from db/migrations. It is destructive and dev-only, so it is gated three ways:
  • the environment must be confirmed development (from deploy/kcl/<env>/config.k); there is no override flag
  • the connection string must be the one that environment declares — a DSN forge cannot reconcile with <env> is refused, not assumed. Unlike ‘forge db seed apply’, an arbitrary loopback —dsn is NOT accepted: this command DROPs the database, and other projects’ databases live on loopback too
  • the resolved host and database name are printed and must be confirmed; pass —yes for non-interactive use
Prefer ‘forge db seed reset’ when only the ROWS are bad: it keeps your schema and migration state, so there is nothing to re-migrate afterwards. Examples: forge db reset # confirm interactively forge db reset —yes # non-interactive (CI, scripts) forge db reset —dsn “$DATABASE_URL” —yes
Flags:

reliant forge db seed

Materialize deterministic development seed data at runtime Materialize deterministic, FK-coherent development seed data directly into a dev database — no seed files are written into your project. Seeds are introspected from the APPLIED schema (db/migrations, with real foreign keys), synthesized deterministically per cell, ordered by a foreign-key topological sort, and INSERTed idempotently (ON CONFLICT DO NOTHING). apply/reset refuse any non-dev environment; seeding never runs against staging or production. Synthesized values satisfy the schema’s constraints BY CONSTRUCTION — CHECK vocabularies, char_length/varchar caps, numeric ranges — and a single-column UNIQUE column draws without replacement so it never collides. A constraint that cannot hold the configured row count (a UNIQUE column backed by a short CHECK vocabulary) caps that table at plan time with a warning, before any INSERT. apply is one transaction: it seeds everything or nothing, so a failed run never leaves a half-populated database and is always safe to retry. Values you supply in db/seeds/vocab.yaml are validated instead, and an invalid one is skipped with a warning (that column falls back to built-in synthesis). Timestamps there are written relative to now ({from: -90d, to: +30d}, -3d, now), and undescribed timestamp columns land in the four weeks before the day the seed runs. An entity that reaches one parent by TWO paths (orders.patient_id, and orders.prescription_id -> prescriptions.patient_id) carries an invariant the schema implies but does not state. Seeding it independently produces rows that contradict the rule, so apply REFUSES and prints the declaration to paste: COMMENT ON CONSTRAINT … IS ‘forge:ref derived-from=<column>’ (or ‘authoritative’, or ‘independent’). Load the db/seeding skill for the table. Which database: with no —dsn, the one the env declares (or $DATABASE_URL when it is that same database). —dsn may name ANY loopback database — localhost, 127.0.0.1, ::1 or a unix socket — so a throwaway postgres on a spare port can be seeded. A non-loopback —dsn must be the env’s own database, or carry —allow-remote-dsn. The environment must be dev either way. Seeding nothing is an error whenever there is something to seed: a database whose tables the migrations forge read do not define (the wrong -C or —dir) fails instead of printing “Seeded 0 row(s)”. database.seed.tables: [] in forge.yaml is the deliberate way to synthesize nothing and apply only db/seeds/custom/. Examples: forge db seed apply # seed the dev database forge db seed apply -C ~/src/app # from anywhere forge db seed apply —dsn postgres://postgres:postgres@localhost:55432/scratch?sslmode=disable forge db seed status # per-table seeded-row counts forge db seed reset # wipe seeded tables and re-seed
Subcommands:

reliant forge db seed apply

Materialize seed data into the dev database (dev-only)
Flags:

reliant forge db seed reset

Delete seeded rows (child-first) and re-seed (dev-only) Delete the rows forge seeded, child-first so foreign keys stay satisfied, then seed again from the applied schema. This is the command to reach for instead of dropping and recreating the database by hand. It is the supported way to get back to a clean dev dataset after bad seed data, a vocab.yaml change, or hand-edited rows — your schema and migration state are left alone, so there is nothing to re-migrate afterwards. It does NOT repair a broken migration state. reset seeds, and seeding requires a fully-migrated schema, so on a database with pending or dirty migrations it refuses exactly as ‘seed apply’ does — clear that first with ‘forge db migrate up’ or ‘forge db migrate force <version>’. Only rows matching forge’s deterministic seed data are deleted; rows you or your application created are left in place.
Flags:

reliant forge db seed status

Show per-table seeded-row counts vs the seed model
Flags:

reliant forge db squash

Collapse N migrations into one canonical baseline (.up.sql) Squash applies every migration in —from-dir against an ephemeral Postgres container, dumps the resulting schema + seed data with pg_dump, and writes a single forward-only baseline migration. Output: <out-dir>/<baseline>.up.sql (CREATE statements + INSERTs for non-schema_migrations rows) No .down.sql is written: forge rolls forward only. This is the canonical “N migrations → one baseline” workflow used when pulling a long-lived schema into a new project, or when collapsing historical migrations into a checkpoint. Paths resolve against the project root (-C, or the project the current directory is in). Requires:
  • docker (for the ephemeral postgres)
  • migrate (golang-migrate CLI) on PATH
  • pg_dump on PATH (matching the postgres image major version)
Examples: forge db squash forge db squash —from-dir db/migrations —to 00001_baseline forge db squash —to 20260506_baseline —out-dir db/baselines/
Flags:

reliant forge debug

Debug a running service with Delve Debug Go services with Delve. Session state is persisted to .forge/debug-session.json so subsequent commands (break, continue, eval, …) reconnect to the same debugger. Examples: forge debug start api-gateway forge debug break handler.go:42 forge debug continue forge debug eval “req.UserID” forge debug stop
Subcommands:

reliant forge debug args

Show function arguments in the current scope
Flags:

reliant forge debug break

Set a breakpoint Set a breakpoint at a file:line location or on a function by name. The positional argument is auto-detected: a “file.go:42” spec sets a source-line breakpoint, while anything else (e.g. “main.handleRequest”, “runtime.gopark”, ”(*Server).Serve”) is resolved as a function breakpoint via Delve’s location parser. The —func flag forces function resolution. Examples: forge debug break handler.go:42 forge debug break main.handleRequest forge debug break runtime.gopark forge debug break ’(*Server).Serve’ forge debug break —func main.handleRequest forge debug break handler.go:42 —cond “id > 5”
Flags:

reliant forge debug breakpoints

List all breakpoints
Flags:

reliant forge debug clear

Clear a breakpoint by ID
Flags:

reliant forge debug continue

Resume execution until the next breakpoint
Flags:

reliant forge debug eval

Evaluate an expression in the current scope Evaluate a Go expression in the debugger’s current scope. Examples: forge debug eval “req.UserID” forge debug eval “len(items)”
Flags:

reliant forge debug goroutines

List goroutines
Flags:

reliant forge debug locals

Show local variables in the current scope
Flags:

reliant forge debug stack

Show the current call stack
Flags:

reliant forge debug start

Start a debug session for a service Start a debug session for a Go service. The binary is built with debug flags (-gcflags=all=-N -l) and launched under Delve. If the argument contains ”/” or ”.”, it is treated as a direct path. Otherwise it is looked up by name in forge.yaml. Examples: forge debug start api-gateway forge debug start —attach 12345 forge debug start —port 2345 api-gateway forge debug start ./cmd/server
Flags:

reliant forge debug step

Step over the current line
Flags:

reliant forge debug step-in

Step into the current function call
Flags:

reliant forge debug step-out

Step out of the current function
Flags:

reliant forge debug stop

Stop the debug session (detaches from attached processes; kills only what forge launched) Stop the debug session. For sessions forge launched (forge debug start <service> / —docker), stop kills the debugged process and reaps the dlv server. For an ATTACH session (forge debug —attach <pid>), stop DETACHES the debugger and leaves the target process running. forge never kills a process it did not launch.

reliant forge doctor

Check that the PROJECT is well-formed (deployability, payload caps, tooling, cluster capability) Check that this project is healthy and well-formed. Everything doctor checks is answerable from the project’s own artefacts, across every declared environment, with nothing running:
  • deployability of the rendered manifests — probes, resource requests and limits, credentials sourced from Secrets, ServiceAccounts bound to pods, a way to apply pending migrations,
  • Connect payload caps,
  • disowned generated files and dead forge.yaml keys,
  • the host tools forge shells out to (and their minimum versions),
  • cluster CAPABILITY: does the cluster carry the GatewayClass / ClusterIssuer this project’s manifests require.
Doctor takes no —env, because it never asks about one. For “is the stack for environment X up and reachable” — the app’s /healthz, pprof, the compose infra, the telemetry backends, Delve — use: forge env status <env> # host services + frontends + runtime checks forge env status <env> —signal traces # one signal only That command resolves the ports the stack actually bound (rather than guessing a default), and reports the holder pid, whether the process is forge-owned, whether its build is stale against HEAD, and whether two processes are serving one service. Examples: forge doctor # Check the project forge doctor —json # Machine-readable output forge doctor —verbose # Show evidence for passing checks forge doctor —signal deploy # Deployability gate only (the CI arm)
Subcommands: Flags:

reliant forge doctor parity

Diff a service’s host-mode vs cluster-mode env+config Compare what env vars and config a named service WOULD see in host-mode (forge run <svc>) vs cluster-mode (forge env deploy <env>) projection. Surfaces “local wasn’t representative of prod” divergences statically without running anything — no docker, no kubectl, no Secret reads. Exits 1 when bug-class divergences exist (value mismatch, missing key without a secret-channel explanation). Exits 0 when the only divergences are secret-channel (host secrets_file ↔ cluster secret_ref — expected by design). Examples: forge doctor parity tasks # default —env=dev forge doctor parity tasks —env=staging forge doctor parity tasks —json # machine-readable output
Flags:

reliant forge domain

Manage custom domains and what they serve Manage custom hostnames as control-plane resources. A DOMAIN is claimed once for your organization and is not tied to an environment. A BINDING points it at one environment and one target inside it. Adding a domain tells you the DNS to publish; once it verifies, the platform issues its certificate and serves it. forge domain add hounders.club —env prod # claim it, print the DNS forge domain ls —env prod forge domain show hounders.club —env prod forge domain verify hounders.club —env prod # check DNS now forge domain bind hounders.club —env prod —target web forge domain bind www.hounders.club —env prod —redirect-to hounders.club forge domain unbind hounders.club —env prod forge domain rm hounders.club —env prod Domains are NOT declared in KCL. A hostname you bring needs an action at your registrar, ownership verification and a certificate — asynchronous and human-gated, none of which a deploy converges — and it binds to ONE environment while an env file renders to many. A hosted spec that carries a domains field is refused at render. (forge.OnCluster keeps Port.domains: there you own the ingress.) —env names WHICH CONTROL PLANE to talk to, from that env’s forge.ControlPlane declaration — a domain itself has no environment. Your organization comes from the credential and is never sent, so there is nothing to widen. The credential is —token, then the declared env var, then the credentials file entry for that endpoint (forge login), then the credential helper $FORGE_CREDENTIAL_HELPER (a host application’s session — Reliant sets it, so a user signed in to Reliant needs no forge login).
Subcommands:

reliant forge domain add

Claim a hostname and print the DNS records to publish Claim a hostname for your organization and print the DNS to publish. The domain starts in pending_dns: those records ARE the next step, and nothing converges until they resolve. An apex and its www are two names — add both, and bind one as a redirect to the other. Claiming does not lock the name. Any number of organizations may add the same hostname and sit in pending_dns; the first to PROVE ownership takes it, and the others move to conflict. Locking at creation would let anyone who merely types a domain deny it to its real owner.
Flags:

reliant forge domain bind

Point a domain at one environment’s workload or frontend Point a domain at a target in —env, replacing any binding it has. forge domain bind hounders.club —env prod —target web forge domain bind www.hounders.club —env prod —redirect-to hounders.club —target names the WORKLOAD OR FRONTEND as your config names it, not a deployment id: a deployment id is re-minted when a workload is replaced, which is exactly when a binding must survive. It also lets you bind a domain before its target has ever been deployed, which is the natural order for a first launch. —redirect-to serves a 308 to another hostname instead of proxying. The apex/www pair is the case for it. BINDABLE IN ANY STATE, SERVES ONLY WHEN LIVE. Bind before DNS has propagated and it starts serving by itself once verification and the certificate complete.
Flags:

reliant forge domain ls

List your organization’s domains and what they serve List every domain your organization holds, with its state and binding. Every domain, not just this env’s: a domain is org-scoped and has no environment. The BINDING column names the environment each one serves, so scoping by eye is possible without a second call.
Flags:

reliant forge domain rm

Remove a domain and stop serving it Remove a domain, its binding, and stop serving it. This FREES THE HOSTNAME for another organization to claim, which is the only self-service escape from a conflict: the org holding it lets go. To stop serving a domain but KEEP it — and keep its verification, so re-binding later costs no DNS work and no new certificate — use forge domain unbind instead.
Flags:

reliant forge domain show

Show one domain’s state, binding and required DNS
Flags:

reliant forge domain unbind

Stop a domain serving, and keep the domain Stop the domain serving, and KEEP the domain. Its verification survives, so re-binding later costs no DNS work and no new certificate. To give the hostname up entirely, use forge domain rm.
Flags:

reliant forge domain verify

Check a domain’s DNS now, instead of waiting for the poller Check DNS right now and print the resulting state. A NUDGE, NOT THE MECHANISM. The control plane polls regardless, so this changes only latency — it exists because someone who has just saved a record wants an answer in a second rather than an hour. Not yet verified is not a failure: DNS takes time to propagate, and the records stay printed so you can confirm what you published.
Flags:

reliant forge env

Manage deploy environments: bring stacks up/down, deploy releases, and inspect
Subcommands:

reliant forge env build

Build an environment’s artifacts; —push publishes them, —release records an immutable release Build the artifacts an environment declares. Iterates the workloads deploy/kcl/<env>/ declares and dispatches on each one’s build.type — go, docker, shell, remote — exactly as the compile-only forge build does. What this command adds is everything that needs an environment to mean anything: —push publish each image to the reference ITS OWN workload declares (its image field in deploy/kcl/workloads.k) — the same reference forge env deploy reads, so what is pushed is what is deployed. Two workloads may name two different registries; both are pushed. Takes no value and carries no registry. —release vX record an IMMUTABLE RELEASE: capture every artifact’s digest into a release ledger (.forge/releases/vX.json, or the control plane when the env declares one). IMPLIES —push — a release pins registry-addressable digests, and a local tag cannot be promoted anywhere. A release is build-once → promote: forge env deploy <env> vX pins the SAME digests in every environment, with no per-env rebuild. The images are env-agnostic; the env argument supplies the artifact SET to build and the registries to push to, so pick any env that declares the full set. CUTTING WITHOUT REBUILDING. In a pipeline where the build and the release are separate jobs, pass —no-build with —release: the digests an earlier --push recorded in .forge/state are harvested and the release is recorded without rebuilding anything. Rebuilding to cut would be exactly the rebuild the release model exists to avoid. forge env build prod —push # job 1: build and push forge env build prod —release v1.4.0 —no-build # job 2: record the release forge env deploy prod v1.4.0 # bind prod to it Re-cutting the same version over the same artifacts is a no-op; over DIFFERENT artifacts it is refused. The cut FAILS if anything the env declares is missing from the ledger — a release with a hole in it promotes like a complete one and ships an environment that is missing a piece. —release owns the image tag (it IS the release version), so a conflicting —tag is refused: a release build pushes under the release version only, so a cut that fails part-way can never leave a shared, mutable tag pointing at bytes no release contains. Examples: forge env build dev # build the env’s artifacts forge env build prod —push # build + push to declared registries forge env build prod —release v1.4.0 # build + push + record the release forge env build prod —release v1.4.0 —plan # preflight the cut, build nothing forge env build prod —target api —push # scope to one workload
Flags:

reliant forge env config

Print the resolved configuration deploy/kcl/<env>/ hands each workload Print the environment variables deploy/kcl/<env>/ resolves for every workload — the same values cli forge env up passes to each process. This is the readback for “what is this environment actually configured with”: which database, which broker, which upstream. Ports that were resolved at launch are reported as launched, so the values match the running stack rather than a fresh render’s guess. It prints whatever the environment declares. cli forge has no opinion about which variables a project uses, so pick the one you want: cli forge env config dev cli forge env config dev —workload api cli forge env config dev —json | jq -r ‘.workloads[].env.DATABASE_URL // empty’

connect to whatever this project calls its database

psql ”$(cli forge env config dev —json | jq -r ‘.workloads[].env.DATABASE_URL // empty’ | head -1)”
Flags:

reliant forge env delete

Tear down a HOSTED environment on the control plane (destructive) Delete a hosted environment: its deployments, Flux objects, custom resources and namespaces on the control plane. forge env delete preview prompts: type the env name to confirm forge env delete preview —yes no prompt (required with no TTY or CI=1) DATA RETENTION: a ManagedDatabase’s data is RETAINED but orphaned. A later deploy of the same name creates a NEW environment and does NOT reattach that data; recovering it is a support operation. Only hosted environments: for a local stack use forge env down, which never deletes anything on a control plane and which this command never uses.
Flags:

reliant forge env deploy

Make an environment run a release: record it, apply it, and wait for health Make <environment> run a release. TWO FORMS, and the difference is whether you name a version. forge env deploy prod # SHIP THIS CHECKOUT: build, push, cut a

release, plan, confirm, deploy, wait

forge env deploy prod v1.4.0 # deploy a release that already exists forge env deploy prod —from staging # exactly what staging runs NO VERSION DOES ALL THE DEPLOYMENT BITS. In order: build the env’s artifacts at the current checkout (images, static artifacts) exactly as forge env build does, push them, cut a release named <YYYYMMDD>.<HHMMSS>-<tree12> for this tree, record the env’s declared shape, compute the plan, ask you to confirm it, then promote, apply and wait. A release whose provenance tree already matches this checkout is REUSED rather than cut again, so a retried deploy never cuts twice. A SCOPED DEPLOY NEEDS A RELEASE. —frontends-only and —target ship part of the env, so they are refused with NO version: a no-version deploy cuts a release over every artifact, and cutting one that ships only some of them would record a release that does not describe what is running. Name the version (forge env deploy prod v1.7.1 --frontends-only) or deploy everything (forge env deploy prod). —dry-run and —explain are unaffected: they cut nothing, so a scoped preview is an honest question. NAMING A VERSION BUILDS NOTHING. It deploys a release that was already cut — a redeploy, a move to an older one, or a spec-change deploy (the KCL moved and the release did not: name the version the env already runs). A version nobody cut is an error whose fix is forge env deploy <env>, the form that builds it. NOTHING IS WRITTEN UNTIL YOU CONFIRM. A promotion IS the deploy — the converger picks it up within minutes — so the plan is printed and approved BEFORE the write, not after it. On a terminal you are prompted (default no). In CI pass —yes, which means “I read the plan”. With no terminal and no —yes the command refuses (exit 5) having built, pushed and cut but written NO promotion, so approving it afterwards needs no rebuild. —plan-only stops after the plan and is the first stage of a two-stage pipeline. —yes AND —approve ARE NOT THE SAME APPROVAL. —yes means “I read the plan” and approves whatever forge computes at the moment that command runs. —approve <digest> approves the plan you actually READ: if Live moved in between — another deploy landed, a new bundle was applied, drift appeared — the digest no longer matches and the deploy is refused (exit 3) instead of shipping a change set nobody saw. Use —yes for a one-shot deploy; use —approve whenever the plan and the approval are separate steps. THE TWO-STAGE PIPELINE, in full: PLAN=(forge env deploy prod --plan-only --json) # builds, cuts, writes NO promotion VERSION=(jq -r .target.release <<<”PLAN") # the release stage one cut DIGEST=( jq -r .deploy_plan.digest <<<“PLAN") # the plan to bind the approval to forge env deploy prod "VERSION” —approve “$DIGEST” next_step in stage one’s document is that final command already assembled, including --acknowledge-destructive <codes> when the plan carries stop-class findings — those codes cannot be written in advance, because a code is not known until the plan is computed. Paste it rather than rebuilding it. A nil deploy_plan means no plan could be computed (a never-built env, or a control plane that predates bundles) — NOT “no changes”. There is then no digest to approve, so next_step falls back to the —yes form and says why. THE VERB IS record + apply + wait, AND THAT IS NOT OPTIONAL. Recording a binding ships nothing, so a step that only recorded one reported success before any byte had moved and the release’s actual failure surfaced minutes later with nothing connecting the two. So the health gate is ON by default; —no-wait is how you opt out, and it says what to run instead. A HOSTED DEPLOY CAN BE QUEUED, NOT REFUSED. When what the env runs needs something only a person can provide — billing for its hosted workloads, managed database or static sites — the control plane still ACCEPTS the deploy: the release and its bundle are recorded, the promotion is written, and the env shows “waiting on billing”. It goes live by itself the moment billing is set up; nothing is re-run. forge prints one block — what it waits on, why, and the URL to act at — and exits 7. —wait instead blocks (up to —timeout) until it is live. A newer deploy to the env replaces a queued one. WHO APPLIES IT IS DECLARED, NOT CHOSEN. An env whose KCL declares forge.ControlPlane is converged by that control plane — forge records the promotion and waits on the rollout the server computes. Every other env is SELF-MANAGED: the same command renders the new binding and applies it from this machine, and that apply’s per-resource rollout wait IS the health gate. Both reach “the release is live or this command is red”; which machinery got there is an implementation detail of where the env runs. forge env build <env> --release <version> builds the env-agnostic images ONCE, captures their content-addressed digests, and cuts a release. Naming that version here advances it BY REFERENCE: one entry — env, release, and the per-image digests frozen at this moment — appended to the env’s append-only promotion ledger. No image is rebuilt, so the exact bytes cut as <version> are what every env running that release ships, byte-identical. This eliminates the per-env rebuild that re-cross-compiles (and can drift arch/tag) for every environment. WHERE THE LEDGER LIVES is declared by the environment, not chosen by a flag: an env whose KCL declares forge.ControlPlane records promotions on that control plane; every other env records them in .forge/promotions/<env>.jsonl. EVERY DEPLOY IS A NEW ENTRY, NOT AN EDIT. Re-deploying the release an env already runs appends nothing and still applies and waits; a CI retry is safe. THERE IS NO ROLLBACK. Recovery is ROLL FORWARD: cut a release with the fix and deploy it. Binding an env to an OLDER release is still possible — it is an ordinary deploy — but it cannot undo the newer release: that release’s migrations stay applied and the data it wrote stays written, so the older code runs against a schema it was never tested on. The plan labels such a move direction: BEHIND and says so; read it before you write it. SEE THE CHANGE BEFORE IT IS WRITTEN. —plan computes the ENTIRE change set and writes nothing: the release the env runs now versus the one it would move to, every image classified as unchanged / changed / added / removed (with both digests where they differ), the git commits between the two releases, and — the fact most worth reading twice — the DIRECTION. A deploy to an older release is reported as BEHIND rather than left for you to infer from version numbers. The plan and the real deploy are computed by the SAME function, so the preview cannot disagree with the write. EVERY RELEASE DEPLOY IS A COMPARE-AND-SET. The write asserts that the env is still on the promotion the plan read (current.promotion_id in —json), and is REFUSED if someone else moved it since — so a hotfix that lands while a pipeline waits for approval turns the pipeline red instead of being overwritten. No flag is needed. —expect-current <id> replaces the planned value with one captured earlier (e.g. when the approval was requested); --expect-current unbound (or —expect-unbound) asserts the env has never been promoted. Re-deploying the release the env already runs is a no-op whatever the expectation says, so a retried success is never a conflict. Exit codes (release deploys): 0 deployed and healthy, or already on this release (no-op), or —plan / —plan-only, or a confirmation answered “no” (nothing was written) 1 failed: invalid input, unreadable ledger, release not found, DEGRADED 2 undetermined — we could not look (the control plane was unreachable) 3 promotion_conflict / source_moved — the env (or —from’s source) moved since the plan was read. Stop and look; retrying would overwrite it 4 rollout_in_flight / environment_pinned — declined, nothing lost. Wait and retry, or pass —supersede (recorded) to replace an unfinished rollout 5 plan_unconfirmed — the plan was printed and nobody approved it: no terminal to prompt on and no —yes. NOTHING was promoted. Add —yes 6 superseded — the env was promoted past the promotion being waited on 7 queued — the deploy was ACCEPTED and RECORDED and the control plane is holding it on a person (today: billing for the hosted workloads or database it runs). Nothing failed and nothing is to be re-run: it goes live on its own once they act. The output names what it waits on and the action URL (—json: .queued). —wait blocks until it is live instead 8 the wait’s budget expired while the rollout was still progressing 5 USED TO MEAN THE TIMEOUT, which is now 8. The confirmation gate took 5 because it is the one outcome that must be impossible to misread as success: a pipeline that upgrades without adding —yes exits 5, and even a handler that still reads 5 as “the wait timed out” fails the job rather than passing it. Pre-1.0, so this is a clean renumber with no alias. —json carries the same outcome: applied, confirmed (whether the gate let the write happen), and a refusal object naming what was expected and what is actually there. Examples: forge env build prod —release v1.4.0 # build once, cut the release forge env deploy staging v1.4.0 —plan # what WOULD change (writes nothing) forge env deploy staging v1.4.0 —plan —json # the same, machine-readable forge env deploy staging v1.4.0 # record, apply, wait forge env deploy prod —from staging # the bytes that passed staging forge env deploy prod v1.3.0 —plan | grep BEHIND # catch a backwards move forge env deploy prod v1.4.0 —gate e2e.json # freeze evidence onto the entry ID=(forgeenvdeployprodv1.4.0−−plan−−json∣jq−r′.current.promotionid//"unbound"′)forgeenvdeployprodv1.4.0−−expect−current"(forge env deploy prod v1.4.0 --plan --json | jq -r '.current.promotion_id // "unbound"') forge env deploy prod v1.4.0 --expect-current "ID” # after approval: exit 3 if prod moved ───────────────────────────────────────────────────────────────────────────── THE APPLY (both paths, and the whole of a no-version deploy) Deploy each service to the target declared on its Service.deploy block. Supported deploy targets (declared in deploy/kcl/<env>/main.k):
  • forge.K8sCluster — Kubernetes deployment via render → kubectl apply → wait-rollouts. Forge auto-creates a k3d cluster for dev.
  • forge.Compose — docker compose pull + up -d.
forge.HostDeploy and forge.BuildOnly are skipped by deploy — those are owned by forge run / forge env up and forge build respectively. Safety (declarative context): the kubectl context is read SOLELY from the env’s KCL — forge.K8sCluster.cluster IS the kubectl context name (e.g. “gke_<project>_<region>_prod”; defaults to k3d-<project> for dev). Every kubectl call in the apply/wait/prune/secrets path runs —context <declared> per command, so the deploy applies to EXACTLY the cluster the env declares — independent of whatever context is currently active. There is NO CLI override and NO fall-back to the current context: the binding lives in the env file, full stop, so you can’t deploy the wrong env to the wrong cluster. forge fails fast (even under —dry-run) if the declared cluster has no matching kubectl context — the only remedy is to fix your kubeconfig or the KCL forge.K8sCluster.cluster. Use —explain to print the declared context, whether it exists in your kubeconfig, and the verdict without applying. Machine-readable output: —json emits ONE JSON document covering the whole invocation, with the same exit code text mode produces. It reports the MODE actually performed (explain / dry_run / apply) so a consumer never has to infer whether bytes moved; the guard verdict, the target cluster + namespace (every declared context, for a multi-cluster env); whether the preflight ran and its findings as structured entries; per-image digest-vs-tag pinning, so a deploy shipping a MUTABLE reference is visible rather than implied; the resource identities applied (kind/name — a diffable list, not a YAML dump); and the per-resource rollout outcome as three distinct states: ready, failed, and timed_out / not_waited. A timeout is neither a success nor a failure — it is the absence of an answer — and the document keeps all three apart. The human output moves to stderr so stdout carries exactly one document. Works with —explain and —dry-run, which is how a UI previews a deploy before asking anyone to confirm it. Deployability preflight: before the first apply (remote/cloud clusters), forge verifies against the LIVE target that every Secret KEY the rendered manifests reference is provisioned and every container image: resolves in its registry. A missing key (CreateContainerConfigError) or image (ImagePullBackOff) is reported up front — all at once — and the deploy refuses to apply, instead of surfacing one pod crash at a time mid-rollout. The preflight is skipped for local dev clusters and runs under —dry-run as a pure read-only check. Bypass with —skip-preflight. Use —target <app> (repeatable) to deploy ONLY the named application(s) instead of the whole env bundle. It filters by app NAME — service, operator, or frontend: the K8sCluster apply keeps the targeted app’s workload manifests plus all shared resources (Namespace, the shared ConfigMap/Secret, RBAC), and the External/Compose dispatch + rollout-wait are scoped to the named apps. A typo’d target errors with the list of available app names. Targeting an operator (e.g. workspace-controller) applies just that operator’s Deployment + cluster RBAC. k8s-only deploy: naming only backend apps via —target is itself the “k8s without touching the frontend” path — a Firebase frontend isn’t in the —target set, so its build+deploy step never runs. To ship the WHOLE backend bundle while skipping the frontend (without enumerating every service), pass —skip-frontend: the k8s apply runs as normal and the Frontend (e.g. Firebase) build+deploy dispatch is skipped. Examples: forge env deploy dev # Deploy to dev (local k3d) forge env deploy staging —tag v1.2 # Deploy to staging with specific tag forge env deploy prod —dry-run # Preview prod manifests (guard runs) forge env deploy prod —explain # Show the declared-cluster guard verdict forge env deploy dev —namespace custom-ns # Override namespace forge env deploy dev —target admin-server # Deploy only the admin-server app forge env deploy prod —target workspace-controller # Deploy only that operator forge env deploy prod —skip-frontend # Deploy backend k8s, skip Firebase
Flags:

reliant forge env devstack

Parallel-dev-stack host helpers (worktree key + port allocation) Host-side helpers for forge’s parallel-dev-stack primitives (ADR 0003). A launcher (Taskfile target, bootstrap script) that starts a host process BEFORE ‘forge env up’ renders the KCL needs the SAME host port the render will allocate. ‘forge env devstack port’ resolves it through the same lock-guarded block registry (.forge/blocks.json) the forge.allocate_port KCL builtin uses, so the launcher and the render can never drift. On the PRIMARY checkout the worktree key is "" so every port is returned unchanged (block 0) — the default dev loop is byte-identical to today. A linked git worktree gets its own stable 100-port block. Examples: forge env devstack port 3091 # the reliant-api host port for this worktree forge env devstack key # the worktree key ("" on the primary checkout)
Subcommands:

reliant forge env devstack key

Print the current worktree key ("" on the primary checkout)

reliant forge env devstack list

List every holder of a port block, labelled by kind (—stacks-only for the machine-readable worktree roster) Print EVERY entry in the lock-guarded block registry (.forge/blocks.json), sorted by block index and labelled with what holds it. This is the diagnostic view, and showing everything is the point. The ceiling (dev_stack.max_stacks) counts BLOCKS, so a plain port-block key consumes the cluster’s pre-mapped host-port range exactly as a worktree does. This command used to print only worktree stacks, which meant a reader who hit “8-block ceiling” and ran it saw ONE line accounting for eight blocks — the command recommended by the error message contradicted the error message. Three kinds of holder are labelled, and the label says whether prune can ever reclaim it: dev stack the key IS a worktree name. Reclaimable once that worktree is gone. derived port-block the key was COMPOSED from a worktree, e.g. “prod-wt-x” from ‘fp.allocate_port(3000, “prod-” + option(“worktree”))’. Reclaimable once that worktree is gone; the origin worktree is named in the label. standalone tied to no worktree (e.g. “prod”, allocated from the primary checkout). NEVER reclaimable — nothing on disk can make it dead, and reclaiming it would move a live stack’s port. —stacks-only restores the old output: just the DEV-STACK keys, one per line, no labels. That is the machine-readable roster a per-stack config generator consumes, and it must stay strictly stacks: a generator that enumerated the raw registry and treated every key as a worktree emitted a dev NATS account for a prod web port into a tracked config file. Those keys are the EXACT values option(“worktree”) renders to in KCL, so a generator’s per-key derivation (NATS user/password, DB name, …) can be made byte-identical to the KCL’s. The DEFAULT stack (the primary checkout, key "") is shown in the full listing when it holds an entry, and is never included in —stacks-only — a generator always emits the default’s config itself. Inside KCL, prefer the fp.dev_stacks() builtin over shelling out to this command: it returns the same roster during the render (and EMPTY when forge generate / forge ci render without one). Write the generated file with fp.write_file(path, content), not KCL’s file.write: file.write fires on every evaluation, so ci, lint, doctor and env render would rewrite it from whatever roster they saw; fp.write_file writes only on forge env up and an applying forge env deploy of a local env. A roster file that SHARED infrastructure mounts (one NATS for every stack) takes fp.write_file(path, content, shared=True): it lands in the primary checkout, so every stack’s render updates the one copy the shared server reads.
Flags:

reliant forge env devstack port

Resolve the worktree-allocated host port for a base port Print base + block(worktree)*100 — the host port forge.allocate_port(base, option(“worktree”)) renders to for the CURRENT worktree. The block is read from (or allocated into) .forge/blocks.json under the same file lock the KCL builtin uses, so the printed port is identical to what ‘forge env up’/‘forge env deploy’ renders for this worktree. On the primary checkout the key is "" so <base> is returned unchanged. Used by the dev launcher to start the host ‘reliant’ process listening on the exact port the in-cluster workspace-controller will dial.

reliant forge env devstack prune

Reclaim dev-stack port blocks for worktrees that no longer exist on disk Reclaim entries in the block registry (.forge/blocks.json) for git worktrees that have been removed from disk. Nothing is ever removed from this registry automatically — a deleted worktree’s block stays held forever unless something reclaims it. Blocks are DENSE and the reachable range is finite (see dev_stack.max_stacks in forge.yaml, default 8), so leaked entries from old worktrees are exactly how a project runs out of dev stacks. What gets reclaimed: any entry TIED TO A WORKTREE that no longer appears in ‘git worktree list —porcelain’. Two kinds of entry are tied to one:
  • A DEV-STACK key (one per git worktree, e.g. “wt-feature-x”) — the key is the worktree’s name.
  • A DERIVED port-block key — one COMPOSED from a worktree, e.g. ‘fp.allocate_port(3000, “prod-” + option(“worktree”))’ allocating “prod-wt-feature-x”. The origin worktree is recorded when the block is allocated, so reclaiming never has to guess it from the key’s name.
What is NEVER reclaimed, no matter how “dead” it looks:
  • The default key "" (block 0) — the primary checkout’s implicit block.
  • A STANDALONE port-block key — e.g. “prod”, which prod’s reliant-web dev-server port allocates under on the primary checkout, where there is no worktree to derive from. It is tied to no worktree at all, so it can never legitimately look dead; reclaiming one would silently move a live, running stack’s port. This is the single most important correctness property of this command, and it is why the distinction is drawn from what forge RECORDED at allocation rather than from how composed the key looks: “prod” and “prod-wt-x” are indistinguishable by shape.
  • An entry allocated by a forge older than the origin field, until the next render from its own worktree records where it came from. Leaving such a block held is the deliberately safe failure: a held block wastes a slot, while a wrongly-moved port breaks a k3d host mapping and the dev IdP’s baked-in issuer.
If enumerating live worktrees fails for any reason (git missing, this isn’t a git checkout, the git command errors), prune reclaims NOTHING and reports the failure — deleting a block that might actually still be live would move a running stack’s ports. Freeing a block lets the NEXT new worktree take it (blocks are filled densely, lowest free index first), so pruning is what keeps the block range from being exhausted by worktrees nobody remembers to clean up. By default this only PRINTS what would be reclaimed and changes nothing — pass —apply to actually rewrite the registry. This mutates machine-local state that other running dev stacks depend on (a concurrent ‘forge env up’ is briefly locked out while the rewrite happens), so making it opt-in to apply is the safer default for a command most people will run interactively to see what’s accumulated. Examples: forge env devstack prune # show what would be reclaimed forge env devstack prune —apply # actually reclaim it
Flags:

reliant forge env devstack release

Reclaim one named port block that you know is no longer in use Release the block held by exactly one named key. This is the manual counterpart to ‘prune’. Prune reclaims blocks forge can PROVE are dead — a dev stack or a derived port-block key whose worktree is gone — and will never touch an entry it has no evidence for. Release is for those remaining entries: typically blocks allocated before forge recorded which worktree a key came from, whose worktree was deleted before it could ever be recorded. ‘forge env devstack prune’ lists them. forge cannot make this call itself. A key composed from a worktree (“prod-wt-x”) and a standalone key (“prod”) are indistinguishable by name once the worktree is gone, and the two possible mistakes are not equally bad: leaving a block held wastes one slot of dev_stack.max_stacks, while releasing a live one moves that stack’s ports — invalidating its k3d host-port mapping and the dev IdP’s baked-in ‘iss’ claim and redirect URIs, which surfaces much later as an unrelated-looking failure. So only release a key when you know which worktree it belonged to and that the worktree is gone. If a listed key matches a worktree that still exists, leave it: the next render from that worktree records its origin, after which prune handles it automatically. Releasing frees the block for the next new key (blocks fill densely, lowest free index first). By default this only PRINTS what it would do — pass —apply to rewrite. Examples: forge env devstack release prod-cp-obs # show what would happen forge env devstack release prod-cp-obs —apply # actually release it
Flags:

reliant forge env diff

Render this checkout and diff each environment against what is deployed Render the environments declared in this checkout and compare each one against what is actually deployed. READ-ONLY: no cluster is contacted, no image is built, nothing is pushed. WHAT IS COMPARED. Shapes, not manifests — the diff is a set comparison over one hash per rendered object, so it needs no YAML. You get: objects added, removed and changed (with image changes split out from config changes), workloads added and removed, runtime and cluster moves, secrets newly needed, and domain changes. THE LIVE SIDE is the last recorded BUNDLE’s shape — what was actually deployed. An environment with no bundle yet falls back to its declared shape, and one with neither is reported as “no recorded config” rather than as “everything is new”. The report says which, because a diff against a declared shape is weaker evidence than one against an applied bundle. THREE GUARDS, each with a PER-ENVIRONMENT status. A failure never silently reads as “no changes”: ok the render succeeded, wrote nothing, and the tree held still impure the render wrote files; the paths are listed and the result is discarded stale the tree changed while rendering (you were editing); retried once error the render failed; forge’s message is included Charts are NOT templated by default: that needs helm and the network, and a chart’s objects belong to the platform dependency rather than this project. --charts opts in. Examples: cli forge env diff prod # one environment cli forge env diff —all # every declared environment cli forge env diff —all —json # the daemon’s form
Flags:

reliant forge env down

Stop Forge host processes for an environment (or —all: across projects) Stop a running forge env up stack. forge env down dev stop THIS project’s dev stack forge env down —all stop every forge stack on this machine, all projects Only processes forge itself started — the ones carrying its ownership markers for the project and environment being stopped — are ever signalled. A process forge did not start is never touched by either form. Neither form ever stops the process running it, or any ancestor of it. The ownership markers are inherited by everything a forge-started process runs — an agent server’s shells included — so forge env down typed inside one would otherwise select the very server hosting it. Before signalling anything, forge walks its own parent chain and leaves each ancestor (with the tree under it) running, saying so: skipped pid 1234 (reliant serve —port 3090): it is an ancestor of this command — stopping it would end the session running you Everything else is stopped as usual, and the command still succeeds. The per-environment form also leaves that environment’s host infrastructure up, because the server it backs is still running. To stop such a stack, run the command from a shell outside it. Docker Compose containers, Kubernetes workloads/clusters, and Docker Desktop are not stopped by this command. The per-environment form also stops declared host infrastructure servers while preserving their data. This command never touches a HOSTED environment (one the control plane runs): suspend/resume it with forge env stop / forge env start, tear it down with forge env delete. Use —all when a stack outlived its project directory: without a forge.yaml there is no project to scope to, and the per-environment form cannot reach it. forge env ps lists what is running first. The per-environment form also marks the stack’s LOCAL SESSION — the presence row forge env up records so forge env status can show what is running where — as stopped. That is presence only, and it is best-effort: a record that cannot be updated never fails the teardown, and any record nothing refreshes is discarded after 24h regardless.
Flags:

reliant forge env list

List the environments declared in deploy/kcl/

reliant forge env new

Scaffold a new deploy environment from an existing one Scaffold deploy/kcl/<name>/ for a new environment by deriving it from one of the project’s existing envs (instead of hand-copying a sibling). The boilerplate — the full_stack / ClusterTarget wiring, sibling-repo build commands, frontends, in-cluster infra — is copied verbatim from the template env. The DANGEROUS per-env knobs (cluster context, namespace, registry, platform, a frontend’s bucket, supabase URL / JWT issuer) are replaced with REPLACE_ME_* placeholders carrying inline ‘check:’ guidance, so a knob you forget to set is a visible author-time error rather than a value silently inherited from the wrong environment. A hosted env’s forge.ControlPlane endpoint is not copied either: the new env falls back to the declaration’s default, Reliant cloud, with a comment naming the template’s value. Set it only to target another control plane. The template env is auto-selected (a cloud-shaped sibling is preferred) or chosen explicitly with —from. After filling the placeholders, run ‘forge env new <name> —check’ (or just re-run with —check) to confirm no placeholder remains and the env KCL-compiles. Each workload’s binding — where it runs — is copied from the template env, one line per workload (_on_cluster(wl.api)). There is no env-level runtime: to run a workload somewhere else, edit its line, or rebind it as the env is created with —bind <name>=<binder>. A frontend binds the same way (its line is _on_bucket(_web_frontend)): —bind api=hosted run api on the forge control plane (adds control_plane = forge.ControlPlane {} when the env has none; the platform admits it under its Restricted profile) —bind api=cluster run api on the env’s cluster —bind web=hosted serve the web frontend from the control plane’s static hosting (it owns the bucket and the CDN) —bind web=bucket publish the web frontend to your own bucket Examples: forge env new preview # derive from an auto-picked cloud sibling forge env new cloud —from prod —bind api=hosted —bind migrate=hosted —bind web=hosted forge env new preview —from staging # derive explicitly from staging forge env new preview —check # verify no REPLACE_ME_* remains + it compiles
Flags:

reliant forge env options

List the render options an environment’s KCL declares List the -D name=value render options that deploy/kcl/<env>/ declares. An env declares an option by READING it — the call site is the declaration, so there is nothing to keep in sync: _host_runner = option(“host_runner”, type=“str”, default=“air”, help=“Host launch runner: air (default) or go-run”) forge env up dev -D host_runner=go-run forge discovers these by parsing the env’s KCL with its kcl.mod dependencies resolved. It reports the name, and whatever type / default / help the declaration passed — those are optional, but an option declared bare shows up here with nothing to explain it, which is worth fixing for whoever reads it next. Options forge derives and binds itself (env, namespace, image_tag, image_digests, worktree, branch) are not listed: they are not yours to set. Examples: forge env options dev forge env options dev —json
Flags:

reliant forge env ps

List every forge stack running on this machine, across all projects List every stack forge env up has running on this machine, across every project — not just the one in the working directory. Each row is one (project, env) stack, with the number of live processes and the project directory it was started from. A stack whose directory has since been DELETED is still listed and still stoppable: it is discovered from the ownership markers its processes carry, not from anything on disk. Stop one: cd <project> && forge env down <env> Stop all: forge env down —all Examples: forge env ps forge env ps —json
Flags:

reliant forge env render

Print the Kubernetes objects deploy/kcl/<env>/ renders, with the cluster each lands on Print the manifests cli forge env deploy would apply, as a ----separated YAML stream, without contacting a cluster. Every document is preceded by a # cluster: comment naming the cluster(s) it lands on. An environment renders one stream but may deploy it to several clusters, and the routing is decided per document by the same function the deploy performs it with (internal/cluster.ScopeManifestsToGroup): a document carrying the first-class forge.dev/cluster label goes to that cluster; otherwise one labelled app.kubernetes.io/name goes to the cluster its owning workload declares; otherwise it is replicated to EVERY cluster the environment deploys to. A replicated document is printed ONCE with every cluster named, so the object count matches the render’s own — pass —cluster to see exactly the stream one cluster receives. Declared platform dependencies (forge.HelmChart — cert-manager, Envoy Gateway, Flux, …) are rendered too, through the SAME code path deploy applies them with: helm template with the declared values, forge’s pinned CRD bundle, and the chart’s own CRDs. Each chart document is labelled # source: helm chart <name> and attributed to the chart’s declared cluster. Templating a chart pulls it from its repository (helm caches it after the first pull), so it needs helm on PATH and registry access; —no-charts skips them and says so in the summary. The render is READ-ONLY as far as forge is concerned: no kubectl context is resolved, no cluster is created, no image is built or pushed, and none of the deploy-time refusals (stale build state, declared-context guard) apply. It is NOT guaranteed pure, because KCL evaluates file.write: a project whose deploy KCL generates a file writes it on every render, forge’s included. Rather than promise otherwise, cli forge watches the tree and reports on stderr every file that changed while rendering; —fail-on-write turns that report into a non-zero exit for callers that need the guarantee. Exits non-zero with the KCL error when the environment does not render, so it is usable as a CI gate. Examples: cli forge env render dev # every object, cluster-annotated cli forge env render dev —list # one line per object (kind/name/cluster) cli forge env render dev —cluster k3d-cp-daemon # only what that cluster receives cli forge env render prod —kind Deployment,Job # only those kinds cli forge env render prod —target workspace-proxy # only that app’s objects cli forge env render prod | kubectl diff -f - # diff the render against a live cluster cli forge env render dev —fail-on-write >/dev/null # assert the render touched nothing
Flags:

reliant forge env secrets

Project the env’s secret_provider into the cluster Work with the secrets an environment’s bundle secret_provider implies. A secret is declared once as a reference (EnvVar.secret_ref) and its value comes from the env’s bundle secret_provider. For a DotenvSecrets provider, forge can render the declared cluster secret_refs into k8s Secret objects and apply them. ExternalSecrets envs are a no-op (their values are provisioned out-of-band).
Subcommands:

reliant forge env secrets sync

Render + apply the k8s Secrets for an env’s dotenv secret_provider (local clusters only) Render the k8s Secret objects implied by an environment’s bundle secret_provider and apply them to the current kubectl context. Only DotenvSecrets providers produce output — forge reads the gitignored dotenv (keyed by env-var name), builds a Secret per declared cluster secret_ref, and applies it. ExternalSecrets / no provider are a no-op. The dotenv renders PLAINTEXT, so this refuses any non-local cluster. Use it in a CI test lane (k3d/kind) to provision cluster secrets the forge-native way instead of a bespoke create-secret script: forge env secrets sync dev kcl run deploy/kcl/dev/main.k -D image_tag=ci | kubectl apply -f -
Flags:

reliant forge env shape

Print what an environment DECLARES — kind, workloads, secrets, domains, and one hash per object Print the projection of deploy/kcl/<env>/‘s render: what the environment is, rather than the manifests it produces. cli forge env render prints the objects themselves — megabytes of YAML for a real environment. This prints the SHAPE: the environment’s kind (persistent, self-managed or local, derived from what it binds), each workload with its runtime and cluster, each declared secret by NAME with its provider, the domains it binds, the clusters it deploys to, and one hash per rendered object. It is small enough to store per environment and compare as a set, which is what makes “what changed” answerable without re-rendering anything. This is the SAME projection cli forge env build records on the control plane, so what you read here is what a Live view shows and what a deploy plan is computed against. IT CARRIES NO SECRET VALUE, ever. Declared secrets appear as names and providers. A rendered kind: Secret has every value replaced by the hash of that value before the object is hashed, so a changed secret is still visible while the value itself is not recorded. READ-ONLY, AND CHECKED. No cluster is contacted, no image is built, nothing is pushed. forge cannot promise the render is side-effect-free, because KCL evaluates file.write — so this scans the project before and after, and a render that wrote anything FAILS, naming the paths. A declaration derived from an impure render is one nobody can reproduce. Examples: cli forge env shape prod # the human summary cli forge env shape prod —json # {project, env, kind, shape, provenance}
Flags:

reliant forge env smoke

Probe every declared ingress route after deploy (TLS + routing + CORS) Verify the ingress graph forge deployed actually serves traffic. forge models the whole ingress graph (Gateways, HTTPRoutes, GRPCRoutes, Frontends) but deploy never checks it. Real bugs shipped silently and were only caught in a browser: a gateway with a stuck cert (TLS handshake dropped -> ERR_CONNECTION_CLOSED), a route pointing at a backend that 404s the path, and an API route missing CORS for the frontend origin. smoke renders the env’s KCL (same path forge env deploy uses), resolves each Gateway’s live external IP from its status, and probes every route through that IP via a curl —resolve-style dial (host:443 -> gatewayIP), setting the TLS ServerName + Host header to the route host so it works before DNS cutover. Each route is classified: PASS backend answered any structured HTTP response (200/401/403/ 415, a Connect error envelope, a non-default 404) — TLS + routing reached a backend. WARN likely misroute a plain text/plain “404 page not found” (the Go default mux) — the host reached a backend that doesn’t serve that path. FAIL tls-transport TLS handshake error / reset / no response — cert stuck or gateway not programmed. FAIL cors-missing an API route the frontend calls answered but carried no Access-Control-Allow-Origin. Exits non-zero if any route FAILs, so it can gate a deploy or CI run. Examples: forge env smoke preprod # probe every preprod route forge env smoke prod —json # machine-readable output for CI forge env smoke staging —tag v1.2.3 # (tag reserved; render is tag-agnostic)
Flags:

reliant forge env start

Resume a HOSTED environment’s workloads on the control plane (not local processes) Resume every deployment of a hosted environment (or one with —target). forge env start prod resume the whole environment forge env start prod —target api resume one workload forge env start prod —wait poll until the platform reports it ready The control plane refuses a resume the organization is not entitled to (for example a billing condition); nothing is started then, and the server’s reason is printed with the fix. This acts on the CONTROL PLANE. Locally running a stack is forge env up. An env that is not hosted is refused.
Flags:

reliant forge env status

The one read view of an environment: bound release, rollout, health, verify, gates, ledger Report everything about an environment in one read. forge env status every environment, and how far behind each is forge env status prod prod right now: runtime AND release forge env status prod —wait block until the rollout settles forge env status prod —history prod’s promotion ledger, newest first WHAT ONE ENV’S REPORT CARRIES:
  • the BOUND RELEASE — the version this env’s promotion ledger declares, and when it was promoted (promote time, NOT deploy time; that gap is what the verify half exists to measure);
  • VERIFY — the digests the env is actually RUNNING against the ones the binding froze, per declared image, in five states: match / drift / missing / untagged / unreachable. A hosted env is read through its control plane’s observer, because forge cannot read a hosted cluster;
  • the ROLLOUT PHASE per workload, as the control plane computes it;
  • RUNTIME HEALTH — every host service and frontend with its URL, log file and whether a listener is accepting right now, plus the compose infra, the app’s /healthz + /readyz, pprof and the telemetry backends;
  • GATES — the evidence recorded for the current promotion;
  • LEDGER FRESHNESS — whether this checkout’s copy of the promotion log is the newest one;
  • RECORDS — the PROVENANCE of the bound release (tree, commit, branch, dirty) and how the binding was made; the CONVERGENCE of it (what the reconciler did, onto which bundle, when it was observed); and the LOCAL SESSIONS running it.
CONVERGENCE IS AN OBSERVATION, NOT FORGE’S REPORT. forge never applies to a cluster — a reconciler converges each environment to its promoted bundle — so this is read from the environment’s control plane and labelled as what the control plane observed. An environment with no control plane has no reconciler and shows one plain line saying so. If a control plane has not reported yet, the report says that rather than inventing a state. RECORDS DESCRIBE, THEY DO NOT JUDGE. An unreadable records store never changes the exit code — the cluster comparison above already has an opinion about whether the bytes landed. An environment with no records shows the empty state plainly, which is an answer, never an error. LOCAL SESSIONS are PRESENCE ONLY: one row per worktree per machine, recorded best-effort by forge env up, and never a promotion, a deploy target or an input to policy or billing. A record nothing refreshes is discarded after 24h, so a crashed stack simply goes quiet — shown as “quiet”, not as failed, because forge cannot tell a crash from a closed laptop. Only LOCAL environments have sessions; for any other kind the report says so rather than showing an empty list, since “no sessions” and “sessions do not apply here” are different facts. THE TWO HALVES ARE NOT SYMMETRIC ABOUT FAILURE, on purpose. Runtime health REPORTS: “the app is down” is a state this command must be able to print, so it never changes the exit code. The release half makes a CLAIM — the env is or is not running what it declares — so its verdict is the exit code. --wait blocks until the rollout of a promotion finishes, and reports whether it STAYED up past the stability window. It is pinned to ONE promotion, never to “whatever the env declares now”, so a hotfix promoted mid-wait reports SUPERSEDED rather than succeeding on bytes it was never asked about. --wait --timeout 0 reads the phase ONCE and never blocks. --history pages the promotion ledger with a keyset cursor: pass the previous page’s next_before as —before, and stop on an EMPTY next_before rather than on a short page. EXIT CODES — a pipeline branches on these directly: 0 everything declared is running (or nothing is declared) 1 we looked and it is WRONG — an image drifted or is missing; with —wait, a workload is DEGRADED 2 we could not DETERMINE — a cluster or control plane was unreachable, a credential was refused, the thing is unobservable, or this checkout’s promotion ledger is stale 6 —wait only: SUPERSEDED — a newer promotion replaced the one waited on 7 QUEUED — the bound release is accepted and recorded, and the control plane holds it on a person (billing). The output names what it waits on and the action URL; it goes live by itself once they act. —wait waits through it (still queued at the deadline is 7 again) 8 —wait only: TIMED OUT while still pending / progressing / stabilizing. The rollout was progressing, so retry the wait; do not re-promote 1 and 2 are separate because CI must tell a bad release from a broken control plane; a gate that reports both with one code gets switched off the first week it is wrong about one of them. 6, 7 and 8 are deliberately not 1: “the release was overtaken”, “a person has to act” and “we never saw this finish” are not “the release is bad”. A STALE FILE LEDGER IS EXIT 2. .forge/promotions/<env>.jsonl is committed to git, so a checkout that has not pulled compares the cluster against an OLDER promotion — a fine deploy reads as DRIFT, and a deploy that never happened can read as MATCH. Neither is evidence, so the verdict is “could not determine” with the fix. AHEAD (a release recorded here, not yet merged) is noted, not failed. —json emits ONE document with the same verdict and IDENTICAL exit codes; ok is false exactly when the process exits non-zero. Examples: forge env status # the whole topology, offline forge env status prod # did prod receive its release? forge env status prod —wait # gate a release on the rollout forge env status prod —wait —timeout 0 —json # where is it NOW? one read forge env status prod —history —limit 1 —json | jq -r ‘.promotions[0].id’ forge env status prod —json | jq -r ‘.images[] | select(.state == “drift”)’
Flags:

reliant forge env stop

Suspend a HOSTED environment’s workloads on the control plane (not local processes) Suspend every deployment of a hosted environment (or one with —target). forge env stop prod suspend the whole environment forge env stop prod —target api suspend one workload forge env stop prod —wait poll until the platform reports it suspended Suspending is never refused, keeps the environment and its data, and stops compute billing for what it suspends. Bring it back with forge env start. This acts on the CONTROL PLANE. It does not touch processes on this machine: that is forge env down. An env that is not hosted is refused.
Flags:

reliant forge env up

Bring the whole dev loop up on this machine: build + deploy + host + frontend Bring the whole dev loop up for an environment, on this machine. Reads deploy/kcl/<env>/ to figure out which services run in-cluster vs on the host and which frontends to start. LOCAL ONLY. This verb builds, applies and runs the env HERE. An env with anything bound to a non-local runtime — a forge.OnHosted workload, a hosted database, a frontend that ships to a bucket, a cluster that is not a local one (k3d / kind / docker-desktop / minikube / colima / orbstack) — is refused with a pointer to forge env deploy <env>, before any work is done. Phases:
  1. build: docker build + push every cluster image; go build each build-only variant
  2. deploy: kubectl apply cluster manifests; wait rollouts and one-shot Jobs
  3. host: start every host-mode service (go-run / air / binary / delve)
  4. frontend: start every declared frontend in its path
Reaching cluster services from the host is the Gateway API ingress path; run forge cluster urls to list the routes. Use —no-build / —no-deploy to skip phases when iterating. Use —target <name> to scope the whole run to one service — a CI lane that only wants that service’s cluster apply, or a dev loop iterating on one host-mode service, both scope the same way. Lifecycle (what happens after host services + frontends start):
  • With a TTY (interactive shell): forge holds the foreground and tears the whole stack down on Ctrl-C.
  • Without a TTY (agent / CI / piped): forge brings everything up, prints the summary (URLs + per-service log paths), and RETURNS, leaving the processes running — the same end-state as —background. This is what keeps forge env up <env> from hanging an agent.
  • —watch forces the hold-and-teardown lifecycle even without a TTY (for a human piping the output through a tool).
  • —background always detaches and returns immediately, regardless of the TTY. If both —watch and —background are passed, —background wins (detach + return).
Either way the long-running children are tracked under ~/.cache/forge/up/<project-id>/; stop a detached / non-TTY stack with forge env down <env>. That is safe to run from anywhere, including from inside the stack: it never stops the process running it or any of its ancestors (see forge env down --help). ONE stack per (project, env). If this project already has a stack running for this env — tracked, detached, or orphaned by a crashed run — it is STOPPED before the new one starts. It is not adopted: this invocation may carry different config, a different allocated port, or reinstalled deps, so the old process is not the process you asked for. Only processes carrying forge’s own ownership markers for THIS project and env are ever signalled; a port held by anything else is an error, never a kill. If the running stack hosts this very command (you are in a shell it spawned), nothing is stopped and the run is refused: replacing it would end the session running you. Tokens after -- are forwarded to each frontend’s dev server (npm run dev -- <flags>), so a Vite/Next dev server can be told to bind a specific host or port. This is what an agent-driven preview flow uses: forge env up dev -- --host 0.0.0.0 starts the scaffolded frontend bound to 0.0.0.0 so a workspace proxy can reach it. On first boot against a dev environment the app boots alive: the fresh database is auto-seeded with deterministic, FK-coherent demo data derived from the applied schema — only when the DB is reachable and every seedable table is empty. Pass --no-seed to skip it, or inspect with forge db seed status. Examples: forge env up dev forge env up dev —no-build forge env up dev —target admin-server -D host_runner=go-run forge env up dev —watch # hold + Ctrl-C teardown even when piped forge env up dev —background forge env up dev — —host 0.0.0.0 # forward flags to the dev servers forge env down dev Render options (-D): An env’s KCL can declare options for the things you want to vary per run — which runner a host service launches under, whether to point at a remote dependency, anything else the env models. It declares one by reading it: _host_runner = option(“host_runner”, type=“str”, default=“air”, help=“Host launch runner: air (default) or go-run”) and you set it with: forge env up dev -D host_runner=go-run forge does not interpret these. It checks the NAME against what the env declares (so a typo fails instead of silently doing nothing), relays the value verbatim, and the KCL decides what it means. List what an env declares with forge env options <env>. Options forge derives itself (env, namespace, image_tag, image_digests, worktree, branch) are not yours to set and are rejected. -D is accepted on env up only — a cluster apply must stay reproducible from the repo alone. LOCAL SESSIONS (what this reports about itself) Once the stack is up, forge records a PRESENCE row for it — one per worktree per machine, naming the environment, the branch and whether the tree is dirty — so forge env status and the Live view can show what is running where. It is presence ONLY: never a promotion, never a release, never a deploy target, and nothing reads it for policy or billing. It is BEST-EFFORT and never blocks this command. If the record cannot be written — no control plane, no network, no credential — forge says so once and the stack runs normally. A supervising run refreshes the record while it holds the foreground, and marks it stopped on Ctrl-C. --background reports once at start and has no process left to refresh it, so the record goes quiet until forge env down marks it stopped. A record nothing refreshes is discarded after 24h, which is also what happens when a stack crashes — forge does not try to tell those apart. Only LOCAL environments report. An environment whose workloads run on the platform or on a cluster has no local presence to describe, and forge says which when it declines.
Flags:

reliant forge gate

Record and read check evidence against a promotion Attach check results to a promotion, and read them back. A gate is one check’s result — lint, test, smoke, rollout, a manual sign-off — recorded against the promotion it is evidence about. Gates are EVIDENCE, NOT ENFORCEMENT: forge records what it is told and who told it, and refuses nothing on a gate’s status. What a recorded gate proves is that this credential claimed this result at this time. Two attachment points, because they answer different questions: forge env deploy … —gate lint.json what had passed BEFORE the environment moved (frozen into the promotion entry) forge gate record prod —from wait.json what was learned AFTER (appended to an append-only child record) “Was this known before the button was pressed?” is the first question asked about a bad release, and merging the two would lose it. Recording needs deploy:write, which is deliberately weaker than promote: a CI test job should be able to report its result without being able to move production.
Subcommands:

reliant forge gate list

Read every check result attached to a promotion Print the evidence trail of a promotion. Promote-time gates come first — what had already passed when the environment was bound — then the gates recorded afterwards, oldest first. That order carries the distinction a reader of a bad release needs first: what was known BEFORE the environment moved, versus what was learned after. Defaults to the environment’s current promotion.
Flags:

reliant forge gate record

Record one check’s result against a promotion Append one check result to a promotion. The gate comes from a document, or is stated inline: forge gate record prod —from wait.json forge gate record prod —from smoke.json forge gate record prod —name qa-signoff —status passed —summary “checked by sean” —from reads a gate-shaped JSON document (what —gate-json writes) OR any forge —json document, deriving the check’s name, verdict and summary from it. So every forge verb that can judge something is recordable with no glue script: forge env status prod —wait —json > wait.json && forge gate record prod —from wait.json forge env smoke prod —json > smoke.json && forge gate record prod —from smoke.json forge lint —gate-json lint.json && forge gate record prod —from lint.json By default the gate attaches to the environment’s CURRENT promotion. Name a specific one with —promotion, or assert which release you believe is current with —release: if the environment has moved on, that exits 3 rather than recording evidence against the wrong promotion. APPEND-ONLY AND IDEMPOTENT. A re-run appends a new row; a repeat of the same (promotion, check, run id) returns the existing one. The run id includes the CI attempt number, so a re-run of one step records afresh rather than colliding with the first attempt. RECORDING A FAILED GATE EXITS 0 — the recording succeeded. The check’s own step already failed the job.
Flags:

reliant forge generate

Generate code from proto files Generate code from proto files based on project configuration or directory conventions. When forge.yaml exists, generation is driven by the config:
  • buf generate for Go stubs (protoc-gen-go + protoc-gen-connect-go)
  • protoc-gen-forge for entity protos in proto/db/
  • buf generate for TypeScript stubs for Next.js frontends
  • Service stubs and mocks for new services
  • pkg/app/bootstrap.go with explicit service bootstrapping
  • sqlc generate if sqlc.yaml exists
  • go mod tidy in gen/
Without forge.yaml, falls back to directory convention scanning: proto/ - Root proto directory (for buf generate) proto/services/ - Service definitions (stubs + mocks) proto/api/ - API messages proto/db/ - Database models (protoc-gen-forge) Examples: forge generate # Generate all code forge generate —watch # Watch mode for development forge generate —force # Discard hand-edits to Tier-1 files and regenerate forge generate —check # Run generate into a tmpdir; exit 1 if it would change the tree (CI guard) forge generate —explain # Print per-file provenance log after generate forge generate —verbose # Print one line per gate-off skipped step Additional maintainer/debug flags exist (pipeline narrowing, drift forensics, parallel-lane and migration escape hatches); run ‘forge generate —help-dev’ to list them.
Flags:

reliant forge kcl

Evaluate this project’s KCL directly Work with this project’s KCL files directly. A forge project’s KCL resolves import forge from the forge binary, not from a kcl.mod dependency or a copy on disk, so a plain kcl run on a project file CANNOT evaluate it. That is by design: the binary is the module version. These subcommands are how anything outside forge reads a value out of project KCL.
Subcommands:

reliant forge kcl eval

Evaluate one KCL file of this project and print a selected field Evaluate a single .k file in this project and print a field out of it. This is THE way a script or a test reads a value from project KCL. A plain kcl run cannot do it: since the forge KCL module is supplied by the forge binary, import forge resolves only inside a forge evaluation. Nothing needs kcl on PATH, and nothing needs to know where forge keeps its module. The file is evaluated with its OWN kcl.mod package root as the working directory, so a relative import (import lib.foo) resolves exactly as it does for a shell script that cd’d there first. It does not matter which directory you run this from: the answer is the same from the project root and from a subdirectory. -S takes a dotted path, kcl-style. A numeric segment indexes a list (pools.0.name). Repeat -S to get an object keyed by selector. Unlike kcl run -S, a selected LIST comes back as one list rather than as one YAML document per element, and a path that does not exist is an ERROR naming what the document does have — not an empty result that a caller reads as a value. —format raw prints a scalar’s own text: no quotes, no trailing newline. It is what makes $(...) capture the value exactly, replacing the | tr -d "\n'\"" pipelines that a YAML scalar’s conditional quoting forces (and that silently corrupt a value legitimately containing a quote). It refuses a list or an object rather than printing JSON a caller would interpolate without noticing. -D binds a top-level option(), as it does for any KCL evaluation. The evaluation is READ-ONLY as far as forge is concerned: no port block is claimed, no port store is written, no cluster is contacted, and no image is built. It is not guaranteed PURE, for the same reason env render is not: KCL evaluates file.write itself, so a file that generates a file writes it. To evaluate an ENVIRONMENT into its Kubernetes objects, use cli forge env render <env> instead. Examples: cli forge kcl eval deploy/kcl/lib/kata_pool.k -S sbd.image_family —format raw cli forge kcl eval deploy/kcl/lib/kata_pool.k -S sbd.disk_size_gb —format raw cli forge kcl eval deploy/kcl/lib/daemon_placement.k -S daemon_placement | jq -r ‘.prod.context’ cli forge kcl eval deploy/kcl/lib/platform_local.k -S cloudnative_pg.name -S cloudnative_pg.namespace cli forge kcl eval deploy/kcl/lib/kata_pool.k # the whole document
Flags:

reliant forge ledger

Move and inspect deploy ledgers Work with an entire deploy ledger — the releases a project has cut and the promotions that bound its environments. An environment’s ledger is chosen by its DECLARATION, in one place and with no flag: an env whose KCL declares forge.ControlPlane records on that control plane; every other env records in this machine’s ledger under $FORGE_LEDGER_HOME (default ~/.forge/ledger), keyed by project so every worktree shares one history. import is how history reaches whichever store an env selected. forge once kept promotions in .forge/promotions/<env>.jsonl inside the checkout; those files are no longer read, and a command refuses while the checkout holds records the selected ledger has never imported.
Subcommands:

reliant forge ledger export

Write the selected ledger to a directory, to VERIFY an import Write a ledger out in file-ledger format, so an import can be checked against the files it came from. THIS IS A VERIFICATION AID, NOT A BACKUP. It exists for one sequence: cli forge ledger import —from-git —rev origin/main —apply cli forge ledger export —dir /tmp/verify diff -r /tmp/verify/promotions .forge/promotions If those agree, the import lost nothing — which is what you need to know before the committed ledger is deleted from the repository. It is NOT a way to keep a second copy of deploy history. A copy nothing writes to is stale as soon as the next promotion lands, and a stale ledger that looks authoritative is the failure the ledger was moved out of the checkout to end. There is no git export in either direction, and no default directory — a default is a thing people start to rely on. The output is the ledger’s own canonical JSON, so a diff is meaningful rather than a re-rendering. Four differences against a retired git ledger are expected and are printed with every run. Examples: cli forge ledger export —dir /tmp/verify cli forge ledger export —dir /tmp/verify —env prod # the ledger prod selected
Flags:

reliant forge ledger import

Import a ledger from a checkout’s committed history, or from another ledger directory Read a ledger from somewhere else and record it in the one each environment selected. A DRY RUN unless you pass --apply. TWO SOURCES: —from-git [—rev <ref>] the retired in-checkout ledger (.forge/releases/*.json and .forge/promotions/<env>.jsonl) as COMMITTED at a rev. Default rev: origin/main. —from-file-ledger <dir> a machine ledger directory, for an env whose KCL newly declares a control plane. IT READS A REV, NEVER THE WORKING TREE. The retired ledger was committed, so the repository is the authority. A .forge/promotions deleted but not committed still has its history, and a half-written local edit is not history at all. HISTORY IS NEVER RE-JUDGED. Every record is admitted exactly as written, including the uncomfortable parts — a release cut from a dirty tree is imported saying so. Promotion ids and timestamps are preserved verbatim: the id is how a re-run recognises what it already recorded, and the timestamp is the order an environment actually moved in. IDEMPOTENT. Running --apply twice records once. A run interrupted half-way is re-run, not repaired. IT REFUSES AN ENVIRONMENT THAT ALREADY HOLDS PROMOTIONS OF ITS OWN. An append-only log’s current binding is its last line, so interleaving imported history with promotions made since would leave the environment reading whatever sorted last. The remedy is to import first. Examples: cli forge ledger import —from-git # dry run against origin/main cli forge ledger import —from-git —rev origin/main —apply cli forge ledger import —from-file-ledger ~/.forge/ledger/myproj-ab12cd34 —apply
Flags:

reliant forge ledger show

Print this machine’s ledger records — promotions, applies and local sessions Read the records in this machine’s ledger: each environment’s promotions and applies, and the local stacks running here. WHY THIS EXISTS. An environment that declares forge.ControlPlane has hosted rows, and a UI reads them directly. An environment with no control plane does not: its history and its running stacks exist only in one machine’s ledger under $FORGE_LEDGER_HOME. So this is how that half becomes visible — the reliant daemon runs it and the UI renders the result. It is therefore a DAEMON-DEPENDENT view, and the only one. A surface showing it must distinguish “no sessions” from “cannot see sessions”: they are different facts, and collapsing them tells a user their dev stack is down when really their daemon is. Local sessions are PRESENCE ONLY. A session is an observation — never a promotion, never a deploy target — and nothing reads it for policy or billing. Examples: cli forge ledger show # every environment cli forge ledger show dev —json # one environment, for the daemon
Flags:

reliant forge ledger where

Print which backend holds an environment’s ledger, and the declaration that chose it Say where an environment’s promotions and releases are recorded, and why. Selection is DECLARATIVE and has no flag: an environment whose KCL declares forge.ControlPlane records on that control plane — whatever its kind, including local — and every other environment records in this machine’s ledger under $FORGE_LEDGER_HOME (default ~/.forge/ledger), keyed by project so that every worktree of a project shares one history. This renders the environment and reports the declaration it FOUND. It does not infer the answer from which store happens to hold records: a project that has moved an environment to a control plane still has its old machine-ledger files on disk, and “where are the records” is a different question from “where do records go”. Examples: cli forge ledger where prod cli forge ledger where dev —json
Flags:

reliant forge lint

Run linters on the project Run various linters on the Forge project. This command will:
  • Run standard Go linters (golangci-lint)
  • Run proto linters (buf lint)
  • Lint and TYPECHECK every frontend the project declares (forge resolves the set from forge.yaml — no shell loop over frontends/*/ required). The typecheck runs the project’s own “typecheck” npm script when it has one, and otherwise runs the frontend’s TypeScript compiler with —noEmit. Severity is lint.frontend.typecheck (error | warn | off) in forge.yaml.
  • Optionally run targeted rule sets (—contract, —db, —migration-safety, —conventions, —tests)
By default ‘forge lint’ AUTO-FIXES deterministic-safe issues first, then gates on the residue: it canonicalizes Go formatting (goimports/gofmt import grouping + whitespace) across generated AND owned/hand-edited files, applies golangci-lint’s safe autofixes, runs each frontend’s own prettier over the src/ files you own (never generated or forge-refreshed ones), and runs eslint —fix on frontends — so mechanical formatting never surfaces as a gating error. Pass —no-fix to gate only and mutate nothing (CI / read-only checks); —json is always detect-only. Examples: forge lint # Auto-fix deterministic-safe issues, then gate forge lint —no-fix # Gate only; mutate nothing (CI / read-only) forge lint —skip-frontends # Backend-only gate; no Node toolchain needed forge lint —contract # Run contract interface enforcement linter forge lint —db # Run DB entity lint rules forge lint —migration-safety # Run SQL migration safety checks forge lint —conventions # Run forge convention rules on proto files forge lint —generated-drift # Fail if a forge-generated (“DO NOT EDIT”)

file was hand-edited

forge lint —tests # Run test-convention rules across backend

handlers and frontend hooks (warnings only)

forge lint —config-deps # Flag scalar Deps fields — a scalar is

configuration, not a collaborator; prints the

exact proto block + AppConfig line to write

forge lint —column-markers # Flag COMMENT ON COLUMN/CONSTRAINT text

carrying an unrecognized forge:* marker

forge lint —fixture-drift # Execute every literal fixture statement in

a scaffolded handlers_crud_test.go against

the CURRENT schema (shadow postgres,

rolled back) and report postgres’s own

error per statement it rejects

forge lint —time-bucketing # Flag a two-argument date_trunc over a

TIMESTAMPTZ column — it truncates in the

SESSION timezone, so every bucketed total

moves with the deploy host. Pin the zone:

date_trunc(‘day’, col, ‘UTC’)

forge lint —proto-markers # Flag a .proto comment carrying an

unrecognized forge:* marker (a misspelled

one does nothing and warns nowhere)

forge lint —create-nullability # Fail when a field’s optional label

disagrees between an entity message and

its Create<Entity>Request (the flattened

request silently drops write presence)

forge lint —computed-fields # FAIL on a forge:computed field that no

non-generated Go file assigns — nothing

populates it, so the column default ships

forge lint —read-only-fields # FAIL on a forge:read-only column that

nothing populates — no write path, no

meaningful DEFAULT, so every row ships as

the zero with no other symptom at all.

Both WARN instead (“pending: implement X”)

while the service still holds forge’s own

unwired rpc stubs

forge lint —guarded-fields # Flag a scaffolded edit page whose

update_mask still writes a column a

custom rpc guards (forge:guards) — the

form bypasses the rpc’s own rules, and

scaffold-once means regenerating cannot

fix it

forge lint —static-export # For a Next.js frontend that must be a static

export (output: static, or bound to a static

runtime): FAIL on what the export refuses

([id] without generateStaticParams, server

actions, cookies(), next/image, no export)

and WARN on what it drops (middleware,

rewrites, POST handlers, no trailingSlash

on a bucket). CI’s next build is the

authority; this says it first, file:line

forge lint —proto-options # Flag a (forge.v1.*) annotation naming an

option field forge’s descriptors do not

define — it compiles, and forge reads it

nowhere

forge lint —vendored-protos # Fail when proto/forge/v1/forge.proto has

drifted from this forge binary’s copy

(forge’s upgrade path does not track it)

forge lint —config-reach # Flag config fields no binary or frontend

loads (per-binary configs can strand a

whole message)

forge lint —optional-deps-guard # Flag unguarded derefs of a

// forge:optional-dep field (nil at runtime)

forge lint —frontend-stores # Flag Zustand stores holding server data that

belongs in a generated React Query hook

forge lint —json # Machine-readable findings for sub-agents / CI

(schema in lint_json.go; exit code matches

text mode; combines with the targeted flags

above, but not with —fix / —suggest-*)

forge lint —quiet # Only what FAILED, then the verdict as the

last line — safe to pipe through head/tail.

Combines with any targeted flag above:

forge lint —read-only-fields —quiet

forge lint —scope internal/handlers/jobs —scope proto/services/jobs

Only findings in files under those paths.

golangci-lint/contract run on the scope’s

packages; whole-project linters (frontend

lint, component-drift, config-reach) are

skipped and named in the verdict, so run an

unscoped ‘forge lint’ before merging

The three advisory rules above are warnings only — they never fail the build. Additional maintainer/debug flags exist (forge-repo internals, wiring audits, suggest-* helpers); run ‘forge lint —help-dev’ to list them.
Flags:

reliant forge login

Authenticate to the control plane(s) this project declares Obtain a credential for a hosted control plane and store it in the shared credentials file (~/.config/forge/credentials.json, 0600), keyed by the endpoint. Login is about WHO you are, not WHERE you deploy, so it takes no environment. With no flag it logs into EVERY distinct control plane this project’s envs declare in KCL: control_plane = forge.ControlPlane {} # Reliant cloud (https://admin.reliantapi.com) control_plane = forge.ControlPlane { # any other control plane endpoint = “http://127.0.0.1:8090” } (two envs declaring the same endpoint share one login). When nothing declares one — outside a project, or before any env does — it logs into Reliant cloud. —endpoint names one directly. There is no “current” server: every forge command finds its credential by the endpoint its own env declares, so which env a command acts on is always the env it names — never a login’s side effect. INTERACTIVE (a human): the OAuth authorization-code flow with PKCE. forge opens your browser at <endpoint>/oauth/authorize, you sign in and approve, and the browser returns a one-time code to a temporary listener on a loopback port (chosen by the OS, so two logins can run at once). forge redeems it at <endpoint>/oauth/token for an access token (rlat_…, 90 days). NON-INTERACTIVE (CI): pass —token (with —endpoint when the project declares more than one control plane), or set the environment variable the environment declares (FORGE_CONTROL_PLANE_TOKEN by default) and skip login entirely. A pipeline has no browser, which is what org tokens are for. SIGNED IN TO RELIANT? You do not need this command. reliant forge … and every shell a Reliant agent runs set $FORGE_CREDENTIAL_HELPER, and forge asks that helper for a short-lived token minted from your Reliant session for exactly the control plane an env declares. forge login is for standalone forge (no Reliant) and for overriding the session with a different identity. CREDENTIAL PRECEDENCE, when any forge command talks to the control plane:
  1. —token explicit, beats everything
  2. $<token_env> the env var the environment declares — CI
  3. the credentials file entry for that env’s endpoint — what this command stores
  4. $FORGE_CREDENTIAL_HELPER — a host application’s session (Reliant’s); also used when the stored login (3) has expired
Flags:

reliant forge logout

Forget the stored credential for this project’s control plane(s)
Flags:

reliant forge package

Manage internal packages Manage internal packages with Go interface contracts. Internal packages live under internal/<name>/ and define their boundary through a Go interface in contract.go. Unlike proto API services, internal package contracts use native Go interfaces — supporting channels, complex types, factories, and other constructs that proto cannot express. Subcommands: forge package new <name> Create a new internal package
Subcommands:

reliant forge package new

Create a new internal package with contract interface Create a new internal package under internal/<name>/ with:
  • contract.go — Go interface that IS the package contract
  • service.go — Implementation with unexported concrete type
After creation, define your interface methods in contract.go, then run ‘forge generate’ to produce mock_gen.go and middleware_gen.go. package shape: service|adapter (default service) service Standard internal/<name>/ with Service/Deps/New — wired into the composition, callable by handlers. The default, and the right answer for a use-case orchestrator too: an orchestrator is a service whose Deps are other services’ interfaces. adapter The same shape plus ’// forge:outbound-io’, which asserts the package calls OUT to a third-party system and serves nothing inbound. Lint keeps RPC handlers out of it and the observe heuristic treats it as doing I/O. See: forge skill load adapter Example: forge package new cache forge package new notifications forge package new events —kind eventbus forge package new stripe-adapter —type adapter
Flags:

reliant forge project

Create, evolve, and inspect the project as a whole
Subcommands:

reliant forge project annotations

Dump forge’s authoritative entity-authoring annotation spec Dump forge’s authoritative annotation spec. Emits the vocabulary forge itself understands, sourced from forge’s own definitions so it cannot drift from behavior: markers the // forge:* comment markers, in proto AND Go source, plus the forge:* COMMENT ON COLUMN/CONSTRAINT markers field_types the proto->column mapping an entity birth applies validate_rules the projected protovalidate subset and its db/zod/wire effects service_options the (forge.v1.service) options method_options the (forge.v1.method) options Use —kind to limit the dump to one annotation level, and —json for a machine-readable dump that tools can query instead of re-deriving the spec. —kind column is the forge:* markers declared as a postgres catalog COMMENT in a migration rather than a proto/Go comment (forge:immutable on a column, forge:ref on a foreign-key constraint); —kind table is the same mechanism one level up, on the table itself (forge:append-only). —kind go is the wiring/observability /contract vocabulary forge reads out of .go files (forge:optional-dep, forge:constructor, forge:no-observe, forge:exclude-contract, …) — the markers you meet in scaffolded code. Examples: forge project annotations —json forge project annotations —kind entity —json forge project annotations —kind column forge project annotations —kind go
Flags:

reliant forge project audit

Print a comprehensive project state snapshot Print a comprehensive snapshot of forge project state. Audit reports forge version pin, project shape, lint roll-ups, codegen state, proto vs migration alignment, scaffold markers, and dep health. Use —json for machine-readable output (sub-agents). EXIT CODE. Audit exits non-zero when a category reports an ERROR (✗), and zero when the worst finding is a warning (⚠). Warnings are reported and never gate: a freshly-scaffolded project legitimately carries several, so failing on them would make forge’s own output fail forge’s own gate. Categories that can error are ones you armed. unscoped_auth is the clearest case: it warns about authenticated RPCs that never resolve the caller, and becomes an error only for RPCs over a table whose migration declares a forge:owner column — your sentence, in your schema, is what turns the advice into a gate. Examples: forge project audit # human-readable; exits 1 on any ✗ forge project audit —json # machine-readable (same exit code)
Flags:

reliant forge project capabilities

List everything forge can do — every command, analyzer, and marker, in one call List everything forge can do, in one call. Emits forge’s whole surface, enumerated from forge itself: commands every verb in the command tree, with its one-line summary analyzers every ‘forge lint’ analyzer, including the maintainer flags hidden from —help markers every // forge:* comment marker the proto scanner reads Read this BEFORE hand-writing something forge already scaffolds. Two sibling dumps cover the rest of the vocabulary: forge project annotations the entity-authoring spec (the proto->column mapping, projected buf.validate rules, proto options) forge skill list the skill catalog; ‘forge skill load <name>’ prints the copy THIS binary ships Examples: forge project capabilities forge project capabilities —json
Flags:

reliant forge project checkouts

List origin/main plus every git worktree — the Preview picker’s source List the checkouts of this project: origin/<main> plus every git worktree, each with its branch, HEAD, dirty flag, dev-stack key, and how far it is from main. WHAT IT IS FOR. Preview renders an arbitrary checkout and diffs it against Live, so something has to offer the choice. The reliant daemon runs this and the picker shows the result — and because the daemon accepts only a path this command returned, the list is also the allowlist that keeps the UI from pointing forge at an arbitrary directory. “main” MEANS origin/<main>, NOT your local main branch. That is what a protected environment is judged against, and a local main that has not been fetched is a different tree. The remote entry carries no path, because there is nothing on disk to render until someone checks it out. GIT ONLY, AND FAST. No tree hash by default: hashing one checkout costs about half a second, so hashing twenty would make the picker unusable to answer a question nobody has asked yet. --tree hashes exactly one — the checkout you are in — which is the only one whose cache key is about to be needed. ahead/behind are OMITTED rather than zero when the comparison cannot be made (no origin, or main never fetched). “In step with main” and “I could not tell” are different answers. Examples: cli forge project checkouts cli forge project checkouts —json # the form the daemon’s hook returns cli forge project checkouts —json —tree # plus the tree hash of this checkout
Flags:

reliant forge project delete

Delete (retire) a service from the project — the inverse of forge scaffold service Delete a component from an existing Forge project. Subcommands: forge project delete service <name> Retire a service: remove the handlers/<name>/ scaffold and leave a types-only tombstone in pkg/app/services.go so the proto types / Connect client keep generating for callers while the handler is no longer served.
Subcommands:

reliant forge project delete service

Retire a service (inverse of forge scaffold service) Retire a service from this project. What it does:
  • deletes the handlers/<name>/ scaffold directory
  • leaves a types-only tombstone comment in pkg/app/services.go (so the proto types, Connect client, and frontend hooks keep generating for callers — the service is no longer SERVED, but its contract survives)
Pass —no-keep-types to omit the tombstone comment entirely; the service then reverts to “unlisted” and forge will re-scaffold its handler on the next generate if it’s still declared in proto. This is destructive (it removes a directory). It prompts for confirmation unless —yes; —dry-run prints the plan and changes nothing. Example: forge project delete service reporting
Flags:

reliant forge project disown

Permanently transfer a forge-generated file to user ownership (one-way) Transfer one or more forge-owned (Tier-1) generated files to user ownership. After disowning, forge NEVER touches the file again: no regeneration, no overwrite (not even with —force), no drift errors. The file is yours, like any starter scaffold. This is a ONE-WAY door — there is no “un-disown” that keeps your edits. —reason is required. Disowning means the generated code couldn’t express what you needed; the reason is design feedback, recorded per path in .forge/disowned.json (surfaced by forge project audit --json). Re-adoption (returning the file to forge ownership) is by deletion: rm <path> && forge generate The emitter re-emits the pristine render and the entry returns to Tier-1. Your disowned content is discarded — copy anything you want to keep into a user-owned extension point first. Prefer NOT disowning when you can: most customizations have a designated user-owned home (pkg/app/setup.go / app_extras.go, …) that survives every regenerate. Example: forge project disown pkg/app/wire_gen.go —reason “custom connection-pool wiring forge can’t express”
Flags:

reliant forge project features

Print the resolved feature graph (on/off, why, dependencies) Print every forge feature for this project: whether it is enabled, WHY (what in the repo made it on or off), and the features / shape preconditions it depends on. Nothing here is configured: every feature is derived from what exists in the repository. The dependency graph is validated at config-load time — a feature enabled with a dependency off is a load error. This command shows the resolved, coherent set: codegen, orm, migrations, frontend, deploy, ingress, and the rest, with their edges.

reliant forge project graph

Emit a JSON dependency graph of the project’s declared resources Emit a single JSON document describing every resource the project declares (services, packages, frontends, binaries, gateways, routes) and the explicit dependency edges between them. Sources are existing forge parsers: forge.yaml, the KCL render for the selected env, contract.go’s Deps struct, and gen/forge_descriptor.json. Partial-data conditions (missing forge.yaml, KCL fails to render) are reported via the top-level “warnings” array rather than aborting the command — consumers should always read warnings before trusting the graph as complete. Output goes to stdout; warnings and errors to stderr. Examples: forge project graph # dev env forge project graph —env=staging # staging KCL render
Flags:

reliant forge project introspect

Inspect what the assembled binary will expose at runtime Inspect what the assembled binary will expose at runtime. Subcommands answer “what would the binary do” questions without requiring it to be running. Useful for catching wiring mistakes early. Subcommands: handlers Print every RPC path the binary will register.
Subcommands:

reliant forge project introspect handlers

Print every RPC path the binary will register Print every RPC path the binary will register. Walks the project’s proto service definitions and prints one line per RPC in the canonical Connect form: /<package>.<Service>/<Method>. Output is sorted by service then method for stable diffs. Examples: forge project introspect handlers forge project introspect handlers —format json forge project introspect handlers —proto-dir proto/services
Flags:

reliant forge project libraries

Print forge/pkg’s API — name a package to get its full signatures Print forge’s public runtime libraries, and the full API of any you name. With NO arguments this is an index: forge ships TWO runtime libraries and it prints both — forge/pkg for Go and @reliantlabs/forge-web-runtime for the frontend — one line per package, from each package’s own doc comment. NAME A PACKAGE and it stops being an index and answers the question: forge project libraries crud every signature crud exports forge project libraries crud orm svcerr three packages, one call forge project libraries orm.Context one type, with its methods forge project libraries all everything (large — prefer a list) That prints every func with its parameters, every struct with its fields, every interface and type with its methods, parsed out of the source this project actually resolves. Doc prose is omitted, so the block is API and nothing else. Use this INSTEAD of ‘go doc <pkg>’, which cannot answer the same question: ‘go doc’ renders a struct or interface as ‘struct{ ... }’ and lists no methods at all, so ‘go doc …/crud’ never mentions crud.Repo.UpdateMasked. ‘go doc -all <pkg>’ is complete but roughly ten times larger, most of it prose. Neither needs the source directory, and neither does reading files. A selector naming a package or symbol that does not exist is an error listing what does, so a briefing script whose list has gone stale fails loudly rather than quietly shipping an incomplete API. Examples: forge project libraries forge project libraries —json forge project libraries svcerr crud tdd testkit orm.Context
Flags:

reliant forge project map

Print project tree with ownership annotations Print project tree with ownership annotations. Each path is annotated as user-owned, forge-space (regenerated), scaffold (FORGE_SCAFFOLD markers), or drifted (forge-space file with hand-edits). Use —depth to truncate, —filter to focus on a subtree, —json for machine-readable output. Examples: forge project map # full tree forge project map —depth 2 # shallow forge project map —filter handlers # subtree forge project map —json # machine-readable
Flags:

reliant forge project migrate

Project-level migration tooling (import, convert) Project-level migration tooling. This command group operates on migration files in the working tree. It is distinct from forge db migrate, which drives the golang-migrate runner against a live database. Examples: forge project migrate import —from goose —src-dir ../old-project/migrations forge project migrate tdd —dry-run
Subcommands:

reliant forge project migrate import

Import migrations from another format (e.g. goose) into golang-migrate shape Import SQL migrations from another tool’s format into forge’s forward-only golang-migrate shape (one .up.sql per migration). Currently supports: —from goose One-file goose migrations with — +goose Up / — +goose Down For each *.sql file in —src-dir, the importer:
  1. Keeps the — +goose Up section and DROPS the — +goose Down section. Forge rolls forward only and never runs down SQL, so the importer writes no .down.sql; each dropped Down section is listed so you can see what was discarded.
  2. Drops — +goose StatementBegin / — +goose StatementEnd markers.
  3. Carries — +goose NO TRANSACTION over to a golang-migrate x-no-tx-wrap header.
  4. Renumbers starting from the next-available index in —dest-dir, so pack-installed migrations (00001-0000N) keep their slots.
Files with no goose markers are skipped. Examples: forge project migrate import —from goose —src-dir ../old-project/migrations forge project migrate import —from goose —src-dir ./legacy —dry-run forge project migrate import —from goose —src-dir ./legacy —force
Flags:

reliant forge project migrate tdd

Rewrite hand-rolled handler tests to use tdd.RunRPCCases Codemod hand-rolled tests := []struct{name, call}``{...} Connect-RPC test scaffolds into per-RPC TestXxx_Generated functions that delegate to forge/pkg/tdd.RunRPCCases. Walks every *_test.go file under handlers/<svc>/ in the project root (or —path) and transforms files that match the recognised hand-rolled shape. Files that don’t match are skipped with a clear reason and never partially rewritten. Examples: forge project migrate tdd # Apply codemod under handlers/ forge project migrate tdd —dry-run # Show summary without writing forge project migrate tdd —path some/dir # Walk a specific subtree
Flags:

reliant forge project new

Create a new Forge project (service / CLI / library) Create a new project with the Forge framework structure. By default no service is scaffolded: the binary is a deployment unit that mounts services — it is not a domain entity. Add your first service after scaffolding with ‘forge scaffold service <entity>’ (name it after a domain entity like item/order/user, not the binary), or opt into an initial service at creation time with —service <entity>. Pick a project kind with —kind: —kind service (default) Connect-RPC service: handlers, middleware, deploy manifests, observability wiring, frontend support. —kind cli Cobra-based CLI binary: cmd/<name>/main.go + cmd/<name>/version.go, no server scaffolding, no proto/services, no deploy/. —kind library Pure Go module: pkg/<name>/ skeleton, no cmd/, no CI workflows by default. Use —disable to turn off features at creation time: forge project new my-project —mod … —disable ci,deploy forge project new my-project —mod … —disable orm —disable migrations Valid feature names: orm, codegen, migrations, ci, build, deploy, contracts, docs, frontend, observability, hot_reload. Example: forge project new my-project —mod github.com/example/my-project forge project new my-project —mod github.com/example/my-project —service gateway forge project new my-project —mod github.com/example/my-project —frontend web forge project new mycli —mod github.com/example/mycli —kind cli forge project new mylib —mod github.com/example/mylib —kind library forge project new —in-place —mod github.com/example/my-project forge project new —in-place —name my-project —mod github.com/example/my-project With —in-place and no name, the project is named after the DIRECTORY. That name becomes cmd/<name>/, the binary, the image and the deploy manifests, so in a worktree or a branch checkout — where the directory is named after the branch rather than the product — pass —name. —in-place never overwrites a file that already exists: forge keeps yours, skips its own version, and lists what it kept (—force replaces them). An existing .gitignore is merged — forge appends only the entries it lacks, in a ”# --- forge ---” block. A directory already inside a git repository (its root or any subdirectory) is left alone: no git init, no commit. A fresh directory outside any repository gets ‘git init’ + an initial commit. —link-forge bridges the new project to the forge checkout THIS binary was built from: a gitignored, machine-local go.work ‘use’, plus the npm twin for @reliantlabs/forge-web-runtime (.forge-link/). The project then compiles the library from that checkout instead of a published version — what a forge contributor wants, and something nobody else should get by accident, so it is never written unless asked for. An unreleased forge build cannot pin itself in go.mod, so a service scaffold from one is refused without it. Once bridged, generate, lint and doctor warn whenever this binary and the checkout stop being the same source; drop the bridge with ‘go work edit -dropuse=<checkout>’.
Flags:

reliant forge project rescaffold

Re-create scaffold-once files you deleted, as forge would scaffold them today Re-create one or more scaffold-once (“yours”) files. forge writes a scaffold-once file exactly once. Deleting it is an act of ownership that .forge/scaffolded.json records, so forge generate leaves it deleted — in this checkout and every clone. To get forge’s version back, name it here: forge project rescaffold .github/workflows/ci.yml For each path, rescaffold drops its birth record, writes the file the way forge scaffolds it for this project today, and records the birth again: the file is yours once more, and deleting it again sticks. Every file forge scaffolds can be re-created this way — CI workflows (through the same mapper forge generate uses, so only workflows this project has), .pre-commit-config.yaml, the devcontainer, the command tree, internal/app, the frontend pages, and the rest. A path that still exists is refused: rescaffold never overwrites your bytes. To replace a file you have edited with the current template, use forge project upgrade --force <path>, which shows the diff first. Examples: forge project rescaffold .github/workflows/e2e.yml .pre-commit-config.yaml forge project rescaffold internal/handlers/item/handlers_crud_test.go

reliant forge project scaffolded

List the scaffold-once files forge has written, and which are absent List every scaffold-once (“yours”) file forge has written in this project, from the birth ledger at .forge/scaffolded.json. forge writes a scaffold-once file exactly once and then it is yours. Deleting one is an act of ownership the ledger records, so forge generate leaves it deleted — here and in every clone. forge generate reports that set only when it CHANGES; this command answers it any time. present the file is on disk — your bytes, forge leaves them alone ABSENT you deleted it, and forge is respecting that To have forge write an absent one again, fresh against the project as it stands today: cli forge project rescaffold <path> Examples: cli forge project scaffolded cli forge project scaffolded —absent
Flags:

reliant forge project shapes

List every API shape — RPCs, messages, enums, tables, handlers, hooks — with file:line List every API shape in the project with its source location. Derived LIVE from .proto, db/migrations and source on every call — never from gen/forge_descriptor.json, which is a cache and can be stale. Answers “where is X and what shape is it” in ONE call instead of a sequence of greps. Start recon here. A measured fan-out spent 28 minutes across 123 calls re-deriving this by hand, mostly paging one large proto by symbol. —grep is a REGEX, so ask about every entity you own in ONE call. A later run adopted this command and still invoked it once per entity — 11 turns of —grep Property then —grep Customer — which is the old grep loop wearing a new command’s name. Examples: forge project shapes # everything forge project shapes —grep Estimate # one entity across all layers forge project shapes —grep ‘Invoice|Payment’ # SEVERAL entities in one call forge project shapes —kind rpc,handler # what is declared vs implemented forge project shapes —kind deploy-target # what a workload’s deploy= accepts
Flags:

reliant forge project upgrade

Update frozen project files from latest Forge templates Detect template drift on frozen files (files written at ‘forge project new’ time but not updated by ‘forge generate’) and apply updates from newer Forge templates. Files that haven’t been modified by the user are updated automatically. User-modified files show a diff and are skipped unless —force is used. —force is per-path. Naming paths after —force restricts the overwrite to exactly those files, so you can adopt one new template without stomping every other file you have customized. Bare —force keeps its whole-project meaning. A file you have deliberately taken over is better claimed than defended: ‘forge project disown <path> —reason …’ records the ownership transfer, and upgrade then skips the file permanently — even under —force. Scaffold-once files (each frontend’s shared mechanism modules under src/lib and src/hooks, plus the .github starters) get their own section and are NEVER written implicitly. They are yours from birth; upgrade only reports what their templates gained, how far behind you are in lines, and how many lines in your copy no template accounts for. Adopt one by naming it: ‘forge project upgrade —force <path>’. Bare —force does not reach them. After a successful upgrade the project’s forge_version field in forge.yaml is bumped to the current binary version (or to —to when provided). Migrations whose declared version range AND detection script both match this project are surfaced first, so the LLM running upgrade can follow them step-by-step. —check SUMMARIZES; —check <path> DETAILS. The report names each file that differs and sizes the difference in lines — it does NOT print diffs inline, because on a real project that is thousands of lines nobody reads. Files are grouped and ranked by what adopting them costs: the ones with no local edits come first, since for those a single —force is the whole job. Name a path after —check to see that one file’s full diff, or pass —all to list every file the groups summarized. Examples: forge project upgrade # Upgrade to latest, run all needed migrations forge project upgrade —to 1.5.0 # Upgrade to a specific target version forge project upgrade —dry-run # Show what would change (alias for —check) forge project upgrade —check # Dry-run summary: what differs, and by how much forge project upgrade —check buf.yaml # That one file’s full diff forge project upgrade —check —all # Every reported file (still no inline diffs) forge project upgrade —force # Apply all updates, even for user-modified files forge project upgrade —force buf.yaml # Overwrite ONLY buf.yaml; leave other edits alone
Subcommands: Flags:

reliant forge project upgrade apply

Record a migration as applied (writes .forge/migrations.json) Mark a migration as applied. This does NOT execute the migration — loading the skill and running its steps is the user’s (or an agent’s) job. ‘apply’ just records the outcome so later ‘forge project upgrade list’ runs hide migrations that have already been worked through. The migration ID is the release version it belongs to, which is also its directory name under skills/forge/migrations/ (e.g. “v0.5.0”).

reliant forge project upgrade list

List pending forge migrations for this project List forge migration skills for releases this project has not crossed yet and whose detection script matches the project’s current shape. Migrations are LLM-readable playbooks under skills/forge/migrations/, one per release that introduced a breaking change. This command does NOT apply them — it surfaces the worklist so the user (or an agent) can load each skill via ‘forge skill load’ and execute the steps. Use ‘forge project upgrade apply <id>’ to record a migration as applied once you’ve finished its steps.
Flags:

reliant forge registry

Log in to and read refs from the registries an env’s workloads declare Work with the image registries an environment’s workloads DECLARE — each workload’s image in deploy/kcl/workloads.k carries its own registry, and no environment declares one. The registry is never passed to forge: these commands read it from the workload declarations, exactly as forge env build <env> --push and forge env deploy <env> do. An env whose workloads name two registries is handled by both commands without forge needing a concept for it.
Subcommands:

reliant forge registry login

docker login to every registry the env’s workloads declare Log docker in to each registry host named by the images the env’s workloads declare, so that forge env build <env> --push and any signing / SBOM / scanning step that follows can reach them. The registry HOSTS come from the workload declarations and nowhere else — there is no host argument and no flag that takes one. An env whose workloads push to two registries logs in to both, in one command. THE PLATFORM REGISTRY NEEDS NO CREDENTIAL FROM YOU. For the host named by an env’s forge.ControlPlane registry_host (Reliant’s registry by default), forge presents the SAME control-plane credential it reaches the control plane with — --token, then the env’s declared token_env, then what forge login stored, then the credential helper $FORGE_CREDENTIAL_HELPER (a host application’s session — Reliant sets it). One token, so there is nothing to mint and nothing to rotate: forge registry login prod forge env build <env> --push and forge env deploy <env> do this for themselves before their first push, so a hosted pipeline usually needs no login step at all. FOR ANYBODY ELSE’S REGISTRY the credential is the only thing you pass, and it is not a registry pointer: echo “GITHUBTOKEN"∣forgeregistryloginprod−−username"GITHUB_TOKEN" | forge registry login prod --username "GITHUB_ACTOR” —password-stdin or name the environment variable holding it, the way forge.ControlPlane names its token_env — so a CI config states a variable NAME (non-sensitive, belongs in git) rather than piping a secret through a shell: forge registry login prod —username “$GITHUB_ACTOR” —password-env GITHUB_TOKEN One credential is used for every foreign host. That is correct for the overwhelmingly common case (one org, one registry, one token) and honest about the rest: for two foreign registries needing two credentials, run the command twice, or log the second one in with plain docker login — forge holds no credential store and inventing one here would be a secrets manager, not a build tool. An env that pushes to the platform registry AND a foreign one logs in to both in one run: ours from the control-plane credential, theirs from the flags. A k3d-local registry (localhost / *.localhost) takes no credentials, so it is skipped.
Flags:

reliant forge registry ref

Print the digest-pinned refs forge env build <env> --push pushed Print <image>@<digest> for each image the last forge env build <env> --push pushed — the immutable references a signing, SBOM, provenance or vulnerability scan step should act on. They are read from .forge/state/ (what the build recorded), so each ref names the registry its own workload declared. By default every built image is printed, one per line, prefixed with the workload that declared it. —image narrows to one. —github-output also appends ref=, image= and digest= to the file GitHub Actions names in $GITHUB_OUTPUT. With several images it writes the <workload>_ prefixed form as well, so a later step can address a specific one.
Flags:

reliant forge release

Inspect and verify release ledgers Work with the release ledgers forge env build <env> --release <version> writes. A release ledger (.forge/releases/<version>.json) names every artifact a release ships — container images, npm packages, Go modules, published files — with the coordinate and hash each one was cut with.
Subcommands:

reliant forge release verify

Prove every artifact a release names actually exists and matches Check that every artifact named in a release ledger really exists in its public registry, and that its bytes match what the ledger recorded. WHY THIS EXISTS. A ledger that NAMES an artifact is a claim, not a fact. forge v0.1.12 tagged its web runtime at 0.3.1, recorded the integrity hash of the tarball on the build machine, and never published it. Nothing compared the two, so the gap surfaced days later as a scaffolded project failing to install. This command is that comparison. WHAT IS CHECKED, PER KIND: oci the registry serves a manifest at the recorded digest. A digest is content-addressed, so existence IS the byte check. npm the registry has that exact version AND its dist.integrity equals the recorded hash. A mismatch means different bytes shipped under a version number that is now permanently taken. gomod the public checksum database has that version AND its h1: module hash equals the recorded one. file reported UNVERIFIABLE — nothing yet records where a file artifact is published, so there is no URL to fetch. NO CREDENTIALS. Every read is an anonymous request to a public registry. That is deliberate: if proving a release were to require a login, only the operator of that login could prove it, and the check would stop being independently verifiable by the person who most needs it. THREE OUTCOMES, NOT TWO: VERIFIED the artifact exists and matches. FAILED proven wrong — absent, or present with different bytes. UNVERIFIABLE a structural gap makes the check impossible (a file artifact with no publish URL, a private Go module, an OCI artifact whose ledger names no registry). Says nothing about validity. UNREACHABLE the check could not complete — a timeout, DNS failure, or a registry demanding credentials. Transient; retry may verify. EXIT CODES: 0 nothing failed 1 at least one artifact FAILED, or —strict was set and something was UNVERIFIABLE 2 a check could not COMPLETE (UNREACHABLE) and nothing outright failed Exit 2 is separate from 1 on purpose. A network blip is not evidence against a release, and a gate that reports a missing artifact and a flaky DNS lookup with the same code is a gate that gets switched off the first week it is wrong. Examples: forge release verify v1.4.0 # check every artifact forge release verify v1.4.0 —strict # also fail on anything unverifiable forge release verify v1.4.0 —timeout 1m # slow or distant registry forge release verify v1.4.0 —json # machine-readable, same exit codes —json emits the same verdicts as a document, with the ledger’s git provenance alongside them. Read the git.dirty field: a release cut from a tree with uncommitted changes ships bytes that correspond to no reviewable commit, which no per-artifact check can detect. The four statuses stay four values — “unverifiable” is not “verified”.
Flags:

reliant forge release where

Show which environments are currently bound to a release Show which environments are currently bound to a release. “Bound” is the ledger’s answer: the release is the environment’s CURRENT promotion. It does not prove the bytes arrived — forge env status <env> does. With —env, the release is looked up on that env’s control plane (hosted) or in this project’s files. Without it, every environment this checkout declares is consulted through its own ledger. Exit codes: 0 the release is bound somewhere, 1 it exists but no environment is bound to it (or it was never cut), 2 a ledger could not be read. Examples: forge release where v1.4.0 forge release where v1.4.0 —env prod —json
Flags:

reliant forge scaffold

Scaffold code: bare, everything the protos imply; with a noun, exactly one thing Scaffold code into a Forge project. Arity picks the mode. With NO arguments it scaffolds everything the protos imply, in one visible, phased run. Author the wire truth first — messages (marking entities with a leading // forge:entity comment) and custom RPCs — then run it: Phase 1: entity births. Every marked message with no applied table gets its missing CRUD quintet injected into the service proto (one-time) and an owned create-table migration. A marked message whose table already exists is INERT (evolution is a new migration); envelope shapes (Request/Response names, pagination fields) are refused loudly — the marker never overrides the guard. Phase 2: projection. Runs the generate pipeline in-process when phase 1 wrote anything or the descriptor is stale against the raw protos; skipped loudly otherwise. Projection emits the pb-through handler stub for every new custom RPC (a method on the handler *Service returning Unimplemented) and the CRUD wiring for every entity-backed one. With a NOUN it scaffolds exactly that one thing, no marker required: forge scaffold entity <name> —from-proto <svc> one DB entity, from its authored proto message forge scaffold service <name> a new Go service forge scaffold worker <name> a background worker forge scaffold operator <name> [—group G] [—version V] a Kubernetes operator forge scaffold crd <Name> a CRD + reconciler on an operator forge scaffold binary <name> a non-server long-running binary forge scaffold frontend <name> a Next.js frontend forge scaffold scenario <name> a frontend mock scenario forge scaffold webhook <name> —service S a webhook endpoint on a service forge scaffold package <name> an internal package (alias for forge package new) forge scaffold adapter <name> an outbound adapter (HTTP/queue/storage gateway) forge scaffold library <name> a library-shaped package (no contract.go; pre-excluded) forge scaffold handler-file <svc> <name> an additional RPC-group file in handlers/<svc>/ forge scaffold rpc <svc> <Name> a custom RPC: pb-through *Service stub when the RPC is in the proto; signed stub + proto snippet otherwise Everything written is scaffold-once and yours from birth — re-running with nothing missing is a clean no-op. Births are one-time: after birth, no command ever writes or modifies a migration from proto state. Examples: forge scaffold forge scaffold —service tasks forge scaffold —dry-run forge scaffold entity product —from-proto tasks forge scaffold service orders forge scaffold rpc tasks ArchiveTask
Subcommands: Flags:

reliant forge scaffold adapter

Scaffold an outbound adapter (HTTP client, queue producer, storage gateway) Scaffold an outbound adapter into an existing Forge project. An adapter is an outbound boundary translator — it owns the implementation of a third-party API, queue, storage gateway, or similar downstream behind a narrow Go interface. Callers depend on the adapter’s Service interface, never on the concrete type, so business logic stays free of vendor specifics and the adapter stays unit-testable with a mocked downstream. This scaffolds internal/<name>/ with: contract.go // forge:outbound-io Service interface adapter.go Deps + service struct + New(Deps) (Service, error) adapter_test.go httptest-stub round-trip tests cache.go empty stub — fill in TTL + per-host rate budget, or delete the file if the adapter is fully local This is equivalent to ‘forge scaffold package —type adapter <name>’; that form stays wired for existing scripts. Use whichever is shorter for you. Skill: forge skill load adapter Example: forge scaffold adapter stripe forge scaffold adapter pricing_feed
Flags:

reliant forge scaffold binary

Scaffold a non-server long-running binary Scaffold a non-server long-running binary into an existing Forge project. A binary is a process with its own Deployment shape. Use this when:
  • You need a reverse proxy / gateway in front of pods.
  • You want an off-service NATS consumer that isn’t an in-process worker.
  • You need a sidecar with its own deploy lifecycle.
For in-process background work, use ‘forge scaffold worker’ instead — workers share the canonical server’s lifecycle and Deps. This creates: cmd/<name>.go Cobra subcommand (registered against the shared root) internal/<name>/contract.go Deps, Service, New(deps) (*Runner, error) internal/<name>/<name>.go Runner.Run(ctx) lifecycle body internal/<name>/<name>_test.go Lifecycle + validateDeps tests And an entry under ‘binaries:’ in forge.yaml so deploy emits a Deployment for the binary. See the binaries skill (forge skill load binaries) for when to choose a binary vs worker vs service. Example: forge scaffold binary workspace-proxy

reliant forge scaffold crd

Scaffold a Custom Resource Definition + reconciler on an operator Scaffold a Kubernetes Custom Resource Definition and its reconciler onto an existing operator. Generates:
  • api/<version>/<name>_types.go CRD spec + status types
  • operators/<operator>/<name>_controller.go thin reconciler shim
  • operators/<operator>/<name>_controller_test.go fake-client unit test
The reconciler shim embeds forge/pkg/controller.Reconciler[T] which provides fetch / NotFound / finalizer / dispatch lifecycle automatically. You implement ReconcileSpec (and FinalizeSpec, when finalization needs cleanup) for the domain logic. Shapes: state-machine Spec.State drives the loop through observable phases (default). config Declarative-only — Spec describes a configuration to project. composite Spec owns sub-resources whose lifetime is coupled to the parent. Example: forge scaffold crd Workspace forge scaffold crd Database —shape config forge scaffold crd Cluster —shape composite —operator manager
Flags:

reliant forge scaffold entity

Birth a database entity from its already-authored proto message: the owned forward migration + the CRUD wire contract Birth a database entity from the proto. The proto is where an entity is declared. Author the message, mark it with a leading // forge:entity comment, and run bare forge scaffold — that births every marked message at once (migration + the missing CRUD quintet) and generates. This command is the same birth, narrowed to one named message. The migration is yours at birth and is NEVER re-derived: evolution is a new migration plus a proto edit, on independent clocks. Running forge generate projects the APPLIED schema into entity structs, the ORM, CRUD wiring, and frontend pages. —from-proto <svc>[.<Message>] Derive the create-table migration from the already-authored proto message (read from gen/forge_descriptor.json). With no <Message> and no <name>, sweeps every message sitting in entity position of a full CRUD quintet that has no applied table yet, PLUS every message carrying a leading // forge:entity marker (those also get their missing CRUD quintet injected, one-time). Positional message names birth an explicit list. —dry-run prints the plan and writes nothing. Example: forge scaffold # birth every marked message forge scaffold entity order —from-proto tasks forge scaffold entity —from-proto tasks.Order forge scaffold entity —from-proto tasks # batch: quintets with no table forge scaffold entity —from-proto tasks —dry-run
Flags:

reliant forge scaffold frontend

Scaffold a new frontend Scaffold a new frontend into an existing forge project. By default this creates a Next.js web frontend with Connect RPC client setup. Use —kind mobile to scaffold a React Native app using Expo. Use —kind vite-spa to scaffold a Vite + React + tanstack-router SPA. For Next.js frontends (—kind web, the default), —output selects the production build/runtime shape, persisted as frontends[].output. “static” (the default) makes “npm run build” a static export into out/ — what the hosted runtime (forge.OnHosted) and every bucket/CDN serves. The generated CRUD pages are static routes (/<entity>/view?id=…, /<entity>/edit?id=…), so a project with entities exports cleanly. “standalone” emits a self-contained Node server that the generated Dockerfile runs — opt in when the frontend needs a server at request time (server actions, middleware, cookies()). Use “server” for full Next.js dev+prod (next start). “next dev” is the same in every mode. —routes limits which entities get generated CRUD pages. By default forge scaffolds a list/detail/create/edit route set for EVERY entity in the project, which is right for a project’s first frontend and wrong for every one after it — a purpose-built frontend starts by deleting most of what was just written. Naming routes makes the set an allowlist, so entities added later do not silently appear in this frontend. The value is persisted as frontends[].routes and honored by every subsequent forge generate run. —routes none generates no CRUD pages at all (a marketing site, or a frontend whose screens are all hand-written). —base-path mounts the frontend under a URL prefix (e.g. /admin behind a reverse proxy that blends several apps on one host). It is rendered into next.config.ts (basePath + assetPrefix; forge reads it back from there) and the generated src/lib/basepath_gen.ts helper. The single runtime override is NEXT_PUBLIC_BASE_PATH. —auth-mode names the sign-in flow this frontend uses. “native” is the default and the only mode forge scaffolds, and it is a first-party form: the browser POSTs an email and a password to your own app (POST /auth/login) and gets back an HttpOnly session cookie. The browser never contacts the identity provider — no /authorize redirect, no PKCE in the bundle, and no token any script can read. Your server runs the whole OIDC flow against the issuer, in internal/app/login_broker.go, which forge scaffolds once and then leaves to you. That is what makes a first-party form portable here: the provider-specific part is one server-side file written against forge/pkg/devidp, not a flow spread through the frontend. Bringing the environment up registers the new frontend with the dev IdP; nothing else to run. Example: forge scaffold frontend web forge scaffold frontend dashboard —port 3001 forge scaffold frontend mobile —kind mobile forge scaffold frontend admin —kind vite-spa forge scaffold frontend dashboard —output standalone # a Node server instead of a static export forge scaffold frontend admin —base-path /admin forge scaffold frontend web —auth-mode native forge scaffold frontend ops —routes users,usage-events
Flags:

reliant forge scaffold handler-file

Scaffold an additional RPC-group file in an existing handler directory Scaffold a new .go file under handlers/<svc>/ to hold a subset of the service’s RPC implementations. Useful when a single handlers.go has grown unwieldy and you want to split RPCs into per-feature files (handlers_billing.go, handlers_admin.go, …). The new file is a one-line stub: just the package declaration plus a comment noting the convention. mock_gen.go discovers methods across every non-test .go file in the package, so no extra registration is required — copy the method bodies you want to split out from handlers.go (or your generated stub file) into the new file and you are done. Run ‘forge generate’ after splitting to refresh mock_gen.go. Example: forge scaffold handler-file billing payment_methods forge scaffold handler-file admin user_admin

reliant forge scaffold library

Scaffold a library-shaped Go package (no contract.go) and pre-register the contracts.exclude entry Scaffold a library-shaped Go package into an existing Forge project. A library package is utility code — a thin wrapper around a third-party API, a helper module, anything where the Service/Deps/New(Deps) Service contract pattern is overkill. It deliberately skips contract.go and is pre-registered in forge.yaml’s contracts.exclude so the contract- required linter won’t fire on it. This is distinct from ‘forge scaffold package <name>’, which scaffolds a contract.go and wires the package into bootstrap. Use ‘scaffold library’ when the code is genuinely library-shaped; use ‘scaffold package’ when it belongs in the Deps graph. Default path is internal/<name>/. Override with —path to put the package under pkg/<name>/ (or anywhere else). Example: forge scaffold library httputil forge scaffold library crypto —path pkg/crypto forge scaffold library legacy —no-exclude # skip the forge.yaml edit forge scaffold library httputil —force # overwrite an existing dir
Flags:

reliant forge scaffold operator

Scaffold a new Kubernetes operator Scaffold a new Kubernetes operator (manager binary) into an existing Forge project. By default this scaffolds only the operator package + manager wiring; CRDs are added with ‘forge scaffold crd <Name>’ which produces a thin shim that delegates to forge/pkg/controller.Reconciler[T]. Pass —with-placeholder-crd to keep the legacy combined types.go + controller.go scaffold (kept for backward compatibility while users migrate to the forge scaffold crd workflow). When —with-placeholder-crd is set, —api-package and —crd-type tune the legacy scaffold’s CRD package and type name. Example: forge scaffold operator manager forge scaffold operator manager —group myapp.io —version v1alpha1 forge scaffold operator workspace —with-placeholder-crd forge scaffold operator workspace-controller —with-placeholder-crd —api-package workspace —crd-type Workspace —group reliant.dev —version v1alpha1
Flags:

reliant forge scaffold package

Scaffold a new internal package (alias for ‘forge package new’) Scaffold a new internal package under internal/<name>/. The —type flag picks the scaffold shape: service (default) classic Service/Deps/New(Deps) Service. Wired into the composition; callable by handlers. Also the right shape for a use-case orchestrator — that is a service whose Deps are other services’ interfaces. adapter Outbound boundary translator (HTTP client, queue producer, storage gateway). No business logic; thin translation to a third-party system. Marker: ’// forge:outbound-io’. Skill: forge skill load adapter Example: forge scaffold package cache forge scaffold package events —kind eventbus forge scaffold package stripe-adapter —type adapter
Flags:

reliant forge scaffold rpc

Scaffold a custom RPC on the service’s handler package Scaffold a custom RPC onto an existing service. Every RPC is a method on the handler package’s *Service working with the generated pb (wire) types — the “pb-through” shape. Either way the stub lands in its own internal/handlers/<svc>/rpc_<name>.go — one file per RPC, so two people implementing two RPCs of the same service never touch the same file. When the RPC already exists in the service proto (run ‘forge generate’ after editing the proto), this runs the generate pipeline so the pb-through handler stub lands there — a method on *Service returning Unimplemented until you fill it in. (An entity-backed CRUD-shaped RPC is wired as a CRUD shim in handlers_crud.go instead.) When the RPC is NOT in the proto yet, a handler stub with the correct Connect signature is written and the proto snippet is printed for you to paste (—stream picks the streaming shape: server, client, bidi; omit for unary). The proto edit is left to you because proto files have hand-curated section markers and ordering that an automated injector would regress. Examples: forge scaffold rpc tasks ListTasksByOwner forge scaffold rpc events TailEvents —stream server forge scaffold rpc chat Chat —stream bidi
Flags:

reliant forge scaffold scenario

Scaffold a new frontend mock scenario Scaffold a new mock scenario for the frontend. Scenarios are typed Connect-RPC handler overlays that let agents and humans teleport into specific server-state shapes by navigating to ?scenario=<name> in the URL. Anything not overridden by the scenario falls through to the base fixture transport. The command writes src/mocks/scenarios/<name>.ts and regenerates the registry so the new file is picked up automatically. Edit the new file to add typed handlers — the contract is src/mocks/scenario-types_gen.ts, and forge skill load frontend covers mock mode end to end. Examples: forge scaffold scenario github-connected forge scaffold scenario github-revoked —from github-connected forge scaffold scenario admin-dashboard —frontend admin-web
Flags:

reliant forge scaffold service

Scaffold one or more Go services Scaffold one or more Go services into an existing Forge project. This creates each service’s directory structure, proto file, Dockerfile, hot-reload config, and updates the project configuration. Pass several names to scaffold them as ONE batch. The generate pipeline is the dominant cost of this command and it derives everything it emits from the tree, so a batch writes every service’s sources and then projects them once — four services in one call cost one pipeline run instead of four. There is no per-service port: every service in a binary mounts onto the SAME Connect mux, and the process listens once on the AppConfig ‘port’ field (env var PORT, default 8080). Change it per environment in deploy/kcl/<env>/config.k, or change the default in proto/config/v1/config.proto. Flags: —resume Re-run a partial scaffold. Skips every output file that already exists on disk. Safe to invoke repeatedly. —force Re-stamp the scaffold even when files exist. Overwrites service.go, the test files, and the proto stub. Use after manually editing a scaffolded file and wanting to start over. —resume and —force are mutually exclusive, and apply to every name in the batch. Example: forge scaffold service users forge scaffold service customers sales jobs billing # one pipeline run forge scaffold service users —resume # recover from a partial failure forge scaffold service users —force # re-stamp every output file
Flags:

reliant forge scaffold webhook

Scaffold a webhook endpoint on an existing service Scaffold a webhook ingestion endpoint onto an existing Go service. This scaffolds a webhook handler with signature verification and idempotency, along with a test file. The handler is added to the service’s handler directory. Example: forge scaffold webhook stripe —service payments forge scaffold webhook github —service notifications
Flags:

reliant forge scaffold worker

Scaffold a new background worker Scaffold a new background worker into an existing Forge project. A worker is a long-running process that doesn’t serve HTTP but participates in the single-binary lifecycle. It has Start(ctx)/Stop(ctx) methods, health reporting, and the same Deps injection as services. The —no-generate flag suppresses the post-scaffold forge generate run. The scaffold itself (workers/<name>/*.go + forge.yaml services append) is the only step the verb promises; the pipeline run is a convenience that becomes hostile under parallel-agent work (see kalshi-trader migration round friction forge-add-worker-runs-full-pipeline). Pass —no-generate when staging scaffold-only changes in a multi-lane round and follow up with an explicit forge generate at a coordination point. Example: forge scaffold worker email_sender forge scaffold worker order_processor forge scaffold worker cleanup —kind cron —schedule ”*/5 * * * *” forge scaffold worker engine_shadow —kind cron —schedule “0 3 * * *” —no-generate
Flags:

reliant forge secret

Manage an environment’s secret store (local file or hosted control plane) Manage the secret store an environment’s secret_provider declares: forge.FileSecrets the gitignored YAML store (a local env) — a flat map of env-var NAME to value. forge.HostedSecrets the env’s control plane (control_plane). set / unset / list go through its API. A hosted env’s values are never read back by these commands; a LOCAL env’s (control_plane with no hosted tier) are pulled into memory by forge env up and nothing else. Every command names its environment with a REQUIRED —env flag. There is no default and no positional form: the env is the one thing a secret command must never guess. A secret is declared ONCE in KCL as a reference (EnvVar.secret_ref); its value lives here and never enters git or KCL render output. A value only reaches a service that DECLARES it, so putting something here that no service references does nothing — config belongs in deploy/kcl/<env>/config.k. forge secret set —env dev STRIPE_SECRET_KEY # value on stdin; add, or replace (= rotate) forge secret unset —env dev STRIPE_SECRET_KEY forge secret list —env dev # names + presence, never values forge secret ensure —env dev # FileSecrets: create the file + report missing forge secret migrate —env dev # FileSecrets: convert a legacy .env file
Subcommands:

reliant forge secret ensure

Create the FileSecrets store and report missing values Create the environment’s FileSecrets store (0600) if absent and list every declared secret that has no value yet. Secrets the provider declares in FileSecrets.generate (pure random key material: an encryption key, a session secret) are minted when absent, never overwritten, and reported by name only. Rotating one is a manual ‘forge secret set’. Hosted and external providers never generate. Exits non-zero when a declared secret is missing a value, so it works as a setup gate in a task/Makefile before ‘forge env up’.
Flags:

reliant forge secret list

List declared secrets and whether each has a value List every secret the environment’s KCL declares, and whether the store holds a value for it. Values are NEVER printed. Works for every secret_provider: file presence read from the YAML store; inert (undeclared) keys listed. hosted names and current versions from the control plane’s store. external the declarations only — presence is “unknown”: forge cannot see a Secret provisioned out of band. rendered the declared Secrets’ keys, and whether each resolves from its declared source. none the declarations only; nothing can supply a value. —json emits the same facts as a machine-readable document, and holds the same promise: the report has no field capable of carrying a value.
Flags:

reliant forge secret migrate

Convert a legacy .env secrets file into the YAML store Convert a legacy dotenv into the FileSecrets YAML store, then delete the original. Run with —dry-run first to see exactly which keys move.
Flags:

reliant forge secret set

Add or replace one secret value (read from stdin) Set a single secret in the environment’s secret store. Setting a key that already has a value REPLACES it — for a hosted store that is a new version, which is how a secret is rotated (there is no separate rotate command). The VALUE is read from stdin, never from argv — an argv value would land in shell history and in the process table. Pipe it, or type it and press Ctrl-D: printf ‘%s’ “$TOKEN” | forge secret set —env dev STRIPE_SECRET_KEY forge secret set —env dev TLS_KEY —from-file ./key.pem A trailing newline is trimmed. Multi-line values (a PEM key, a JSON blob) round-trip unchanged.
Flags:

reliant forge secret unset

Remove one secret from the store
Flags:

reliant forge skill

Manage Forge skills — conventions and playbooks for LLM agents
Subcommands:

reliant forge skill list

List available skills (forge-shipped, project, and user-global) List available skills (forge-shipped, project, and user-global). The default view is GROUPED and fits on one screen: the start-here skills first, then one row per skill group with its sub-skills collapsed into a ‘+N’ count. Every collapsed sub-skill is still loadable by its exact path (‘forge skill load db/seeding’). Pass —all for the exhaustive flat table. ‘forge skill search <keyword>’ is usually faster than reading either view — it scores paths, descriptions and BODIES, so it finds skills whose title never mentions your term. One-time migration playbooks (relevance: migration — the skills under migrations/) are hidden by default; they only matter while crossing a specific forge version transition. Pass —include-migrations to list them, or use ‘forge project upgrade list’ to see the migrations that actually apply to this project’s pinned forge_version.
Flags:

reliant forge skill load

Print a skill’s content to stdout (resolves user > project > forge)

Search skills across all scopes by keyword (path/name=3, desc/body=1)
Flags:

reliant forge skill write

Write every bundled skill to a directory Bulk-export forge’s skills to a target directory. Layouts: —style forge (default) Forge’s native layout: <dir>/<skill>/SKILL.md. —style claude Claude Code-compatible layout: <dir>/<skill>/SKILL.md with YAML frontmatter guaranteed (synthesized if missing). Drop —out at <repo>/.claude/skills/ so an LLM running Claude Code picks them up. —style md Flat: <dir>/<skill>.md, no per-skill subdirectory. The skill content is the same body returned by forge skill load <name>. Note: inside a forge project you don’t need this for .claude/skills/ — ‘forge generate’ regenerates the forge-shipped skills there on every run (Tier-1 tracked), keeping them in sync with the forge binary version. ‘skill write’ remains for exporting skills to arbitrary locations.
Flags:

reliant forge start

Print the greenfield brief: empty directory to authored protos, in one call Print the greenfield brief. Everything needed to go from an empty directory to authored protos, and nothing else: the end-to-end sequence, the ‘project new’ flags that matter, how an entity is marked and what scaffold births from it, the rule that forge injects CRUD rpcs ONLY, the proto->column vocabulary, and the FK diamond declaration. This command PRINTS. It creates nothing and changes nothing. Depth is named rather than inlined — ‘forge skill load <name>’ prints the copy this binary ships. Three sibling dumps cover the rest of the surface: forge project capabilities every verb, analyzer and marker forge project annotations the full entity-authoring spec forge skill list the skill catalog

reliant forge storage

Inspect and bound local caches; preserve persistent application data
Subcommands: Flags:

reliant forge storage check

Refuse a build below the physical host free-space reserve

reliant forge storage configure-nodes

Preview kubelet GC migration for existing registered nodes; —apply restarts them
Flags:

reliant forge storage daemon

Run maintenance periodically, reloading policy for every pass
Flags:

reliant forge storage gc

Preview cleanup; —apply deletes only eligible cache and registry versions
Flags:

reliant forge storage install

Install daily maintenance (launchd, systemd timer, or Windows Task Scheduler) using a stable copy of this executable

reliant forge storage policy

Print the effective storage policy

reliant forge storage register

Register local cluster contexts, declared repositories or release pins
Flags:

reliant forge storage status

Report physical host capacity, Docker usage and node cleanup settings

reliant forge storage worktrees

Preview idle, clean, pushed worktrees of a repo; —apply removes them
Flags:

reliant forge tools

Manage developer tooling forge depends on (proto plugins, etc.) Manage developer tooling that forge expects on PATH but does not ship. Subcommands: install Install the codegen tools forge runs (protoc-gen-go, protoc-gen-connect-go, goimports) via ‘go install’, at the versions this project’s go.mod resolves, and check that every frontend declares its TypeScript plugin. Forge scaffolds buf.gen.yaml with ‘local:’ plugins by default so that ‘forge generate’ works without any BSR (buf.build) authentication. Those local plugins must be on PATH; this command installs them.
Subcommands:

reliant forge tools install

Install codegen tools (protoc-gen-go, protoc-gen-connect-go, goimports) Install the codegen tools forge needs on PATH for the default local:-plugin buf.gen.yaml workflow, plus goimports, which formats every Go file forge generates. Each tool is installed at the version this project’s go.mod resolves for it (google.golang.org/protobuf, connectrpc.com/connect, golang.org/x/tools) — the version the committed generated code was produced with — falling back to @latest when go.mod does not contain the module. —version overrides that for every tool. By default, tools already present on PATH are skipped. Use —force to re-install them. The TypeScript plugin, @bufbuild/protoc-gen-es, is not installed here. It is an ordinary devDependency of each frontend, so the frontend’s own ‘npm ci’ installs it at the version its lockfile pins. This command only checks that every frontend whose buf.gen.yaml runs it declares it, and fails with the edit to make when one does not. It never runs npm and never writes a frontend’s package.json or package-lock.json — with or without —force. Examples: forge tools install forge tools install —version v1.34.2 forge tools install —force
Flags:

reliant forge version

Print the forge version and build identity

reliant open

Open a project in Reliant Opens a project in the Reliant cloud platform. This is the primary command for starting a Reliant session:
  1. Detects the project from the given path (defaults to current directory)
  2. Ensures you are authenticated (runs login flow if needed)
  3. Registers the project with the cloud API
  4. Starts the tools daemon for local tool execution
  5. Opens the Reliant web UI in your browser
This is the “reliant .” command.
Flags:

reliant preview-url

Print the shareable preview URL for a local port Deterministically construct the workspace preview URL for a listening port. It reads the RELIANT_PREVIEW_URL_TEMPLATE template the workspace-controller injects into the daemon container and substitutes the given port — no round-trip to the control plane. On a local (non-managed) daemon, where no template is present, it falls back to the loopback URL for the port. The port is checked against the daemon’s listening sockets; if it is not up yet the URL is still printed (a warning goes to stderr) unless —require-listening is set. The URL is openable by the workspace OWNER under the authenticated default; sharing it beyond the owner requires the port to be made public. With —json, emits {built, run_command, port, url, listening, access_level} — the deliverable shape a handoff node can post.
Flags:

reliant project

Manage Reliant projects Create and list Reliant projects. A project is the unit of ownership a workflow run executes against. Every ‘reliant workflow run’ needs a project — use ‘reliant project create’ to mint one (or let ‘reliant workflow run —project-path’ resolve it for you).
Subcommands:

reliant project create

Create a project and print its ID Creates a project via the Reliant cloud API and prints the new project ID. If —name is omitted it defaults to the base name of —path. Creation is idempotent by path: if a project already exists at —path, its existing ID is printed instead of erroring, so this doubles as an “ensure project exists” one-liner. Targets —server (else RELIANT_SERVER_URL, else the default), authenticated by RELIANT_TOKEN or the login ‘reliant auth login’ stored for that server.
Flags:

reliant project list

List your projects Lists the projects owned by the authenticated user. Targets —server (else RELIANT_SERVER_URL, else the default), authenticated by RELIANT_TOKEN or the login ‘reliant auth login’ stored for that server.
Flags:

reliant server

Run cloud server components Run Reliant cloud server components. Each subcommand starts a specific server role for split-deployment mode.
Subcommands:

reliant server api

Run the stateless HTTP + gRPC API server Starts the Reliant API server for cloud deployments. This is a stateless server that handles HTTP REST and gRPC/ConnectRPC requests, connecting to external Temporal, Postgres, and NATS services. Designed to run as N replicas behind a load balancer.
Flags:

reliant server gateway

Run the daemon connection gateway Starts the daemon gateway that manages bidirectional gRPC streams to tools-daemon processes. Routes tool execution requests from API servers and Temporal workers to the correct daemon via NATS. Designed to run as few stateful replicas.
Flags:

reliant server worker

Run the Temporal workflow worker Starts a Temporal worker that processes workflow executions. Connects to an external Temporal server and executes workflow activities (LLM inference, tool execution routing, etc.). Designed to run as N replicas for horizontal scaling.
Flags:

reliant trigger

Manage schedule triggers Manage triggers — standing instructions that start runs without a human typing. A schedule trigger runs a workflow in a project on a cron or interval schedule, seeded with a fixed prompt. Its runs are unattended: nobody will answer a question or approve a request, so the agent is told to decide and proceed.
Subcommands:

reliant trigger create

Create a schedule trigger
Flags:

reliant trigger delete

Delete a trigger and its schedule Delete a trigger and its schedule. The trigger’s firing history survives, with its trigger id cleared — the runs it started are real and still exist.

reliant trigger disable

Pause a trigger’s schedule Pause a trigger’s schedule. Disabling pauses the Temporal schedule rather than deleting it, so the definition and firing history survive and re-enabling needs no re-derivation.

reliant trigger enable

Resume a trigger’s schedule Resume a trigger’s schedule. Disabling pauses the Temporal schedule rather than deleting it, so the definition and firing history survive and re-enabling needs no re-derivation.

reliant trigger events

List a trigger’s firings, newest first
Flags:

reliant trigger fire

Run a trigger now Run a trigger now, out of band with its schedule. A manual fire skips the enabled and overlap checks: asking for it is the decision those policies exist to make for the unattended case. The firing is asynchronous — use trigger events to see what it did.

reliant trigger get

Show one trigger

reliant trigger list

List your triggers
Flags:

reliant trigger update

Replace a trigger’s definition Replace a trigger’s definition. This is a full replacement, not a patch: every field is written as given, so flags you omit are written as their zero value. The exception is enabled, which is left as it is unless you pass —disabled or —disabled=false.
Flags:

reliant version

Print version information
Flags:

reliant workflow

Manage and validate workflows Commands for validating, listing, and running Reliant workflows.
Subcommands:

reliant workflow answer

Answer a pending question on a workflow execution Answers the pending question for a workflow execution via QuestionService.ResolveQuestion — no hand-built JSON required. The response is assembled into the exact ask_user shape the workflow expects ({"answers":[{question, selected, freetext}]}), including multi-sub-question asks. Selecting options: —select “<label>” Pick an option by its exact label. Repeatable — one per sub-question, in declaration order. When a label is unambiguous it is matched to whichever sub-question offers it, so order-independence works for distinct option sets. —text “<freetext>” Attach free-text feedback (applies to the first sub-question, or the only one). —interactive Prompt for each sub-question’s options on the terminal and pick interactively. This is the default when no —select/—text is given. —question <qid> Target a specific question id (defaults to the pending one). Must match the currently pending question. Examples: reliant workflow answer <id> —select “Continue” reliant workflow answer <id> —select “Vanilla” —select “Sprinkles” reliant workflow answer <id> —text “please also add rate limiting” reliant workflow answer <id> # interactive picker
Flags:

reliant workflow follow

Follow a workflow execution and stream NDJSON lifecycle events Follows a workflow execution, printing one JSON event per line to stdout: node and workflow state transitions (old_state -> new_state) with timestamps. All diagnostics go to stderr, so stdout is pipeline-safe. The execution ID is the chat/execution ID returned by ‘reliant workflow run’. A ‘question’ or ‘approval’ event is emitted the moment a gate opens — and, because the follower reconciles currently-open gates every poll, one is also emitted if you attach (or —tail) while a gate is already open. Pass —exit-on-gate to stop at the next gate with exit code 3. Exit codes: 0 the root workflow completed successfully 1 the root workflow failed, was cancelled, or expired (or follow errored) 2 —timeout elapsed before the workflow reached a terminal state 3 —exit-on-gate: a question/approval gate opened Hooks run matching events through ‘sh -c <cmd>’ with the event JSON on stdin and RELIANT_EVENT_* environment variables (RELIANT_EVENT, RELIANT_EVENT_EXECUTION_ID, RELIANT_EVENT_NODE_ID, RELIANT_EVENT_STATE, …). A failing hook is logged and never stops the follow.
Flags:

reliant workflow list

List available workflows Lists workflows in the current project and builtin workflows.
Flags:

reliant workflow pause

Pause a running workflow execution (keeps it resumable) Pauses the workflow for a chat via ChatService.PauseChat. Pause PRESERVES the resume checkpoint: the workflow stops at a safe point and can be continued later with ‘workflow resume’. Contrast ‘workflow terminate’, which is terminal and drops the checkpoint.

reliant workflow questions

List the open question(s) awaiting input on a workflow execution Fetches the pending question for a workflow execution via QuestionService.GetPendingQuestion and prints each sub-question’s prompt and option labels. A single ask_user question may bundle multiple sub-questions; all are shown. Prints “No open questions.” and exits 0 when nothing is pending. Answer with ‘reliant workflow answer <execution-id>’.
Flags:

reliant workflow resume

Resume a paused or expired workflow execution Resumes a paused (or expired) workflow for a chat via ChatService.ResumeChat, continuing from its preserved checkpoint. If the underlying Temporal workflow was lost, the server reports that a new message is needed to recover.

reliant workflow run

Run a workflow Triggers a workflow execution by creating a chat bound to the workflow, via the Reliant ChatService.StartChat Connect RPC — the exact path the web app takes. A run IS a chat: sending the first user message kicks the root workflow. The target server is —server (else RELIANT_SERVER_URL, else the default); the bearer is RELIANT_TOKEN, else the login ‘reliant auth login’ stored for that server. Either way it is an rlat_ access token. A run executes against a project. Supply it by ID (—project-id) or by path (—project-path); with —project-path the project is resolved by its path — reused if one already exists there, or created on the fly — so no manual project setup is needed. —project-id takes precedence when both are given. Bare workflow names that match a builtin are normalized to builtin://<name> (like the web app); other names resolve as drafts / project workflows server-side. The first user message is taken from —message, then inputs.message, then inputs.prompt, then a generic kick message. All —input values land on the workflow_params plane (message/prompt excluded). With —follow, streams NDJSON lifecycle events until the workflow reaches a terminal state (see ‘reliant workflow follow —help’ for the event format, exit codes, and —hook).
Flags:

reliant workflow scenario

Run and manage workflow scenarios Commands for running and listing workflow scenario tests.
Subcommands:

reliant workflow scenario list

List available scenarios for workflows Lists scenarios discovered from co-located *_scenarios.yaml files or from scenarios/<workflow-name>/ directories. Examples: reliant workflow scenario list # list all project scenarios reliant workflow scenario list my-workflow.yaml # list scenarios for one workflow reliant workflow scenario list —include-builtins # include builtin workflow scenarios
Flags:

reliant workflow scenario run

Run scenario tests against workflows Runs scenario tests against workflow definitions on the real workflow runtime (DynamicWorkflow in an in-memory Temporal environment; only activities are mocked). Scenarios are discovered from co-located *_scenarios.yaml files or from scenarios/<workflow-name>/ directories. If a specific workflow file is given, runs scenarios for that workflow only. Otherwise, discovers all workflows in the workflow directory and runs their associated scenarios. Examples: reliant workflow scenario run # run all project scenarios reliant workflow scenario run my-workflow.yaml # run scenarios for one workflow reliant workflow scenario run —include-builtins # include builtin workflow scenarios reliant workflow scenario run —filter happy_path # run only matching scenarios reliant workflow scenario run —json # JSON output for CI Exit code 0 if all scenarios pass, 1 if any fail.
Flags:

reliant workflow status

Show a one-shot snapshot of a workflow execution Prints a point-in-time snapshot of a workflow execution over Connect RPCs (no streaming, no DB): overall status AND outcome, the per-node execution tree with status/timing, recorded step executions, and the count of open questions and approvals awaiting input. Status and Outcome are two different facts and both are printed: Status the LIFECYCLE — did the Temporal execution finish, and how Outcome the run’s own VERDICT — did the work pass They are not the same. A run that fails verification routes to its workflow’s failure terminal node and finishes cleanly: Status COMPLETED, Outcome FAILURE. Reading Status alone reports that run as a success. Outcome is blank when the workflow declares no pass/fail terminal — blank means “it never said”, not failure. A node thread parked on a gate keeps the stored status RUNNING, so the tree marks the thread holding the open question as GATED and names the step that raised it. Only that thread is marked — its siblings keep reading RUNNING, because in a fanned-out run they really are still executing. Exit codes: 0 the run succeeded, or is still running 1 the command failed (could not reach the server, no such execution, …) 2 the command ran fine and the run did NOT succeed — Outcome FAILURE, or a failed/cancelled/expired lifecycle Use ‘workflow ps’ for every live run at once, with time-in-state and suspected stalls; use ‘workflow watch’ to block on live boundaries instead.
Flags:

reliant workflow terminate

Terminate a running workflow execution Terminates the workflow for a chat via ChatService.TerminateChat. Terminate is terminal and DROPS the resume checkpoint — the workflow cannot be resumed afterward (start a new run instead). Use ‘workflow pause’ to stop while preserving the ability to resume.

reliant workflow validate

Validate workflow YAML files Validates workflow YAML files against the Reliant workflow schema. Runs static analysis including structural validation, CEL expression checking, input/output type verification, and cross-workflow contract validation. If a specific file path is given, validates that file only. Otherwise, validates all *.yaml files in the workflow directory. Exit code 0 if all workflows are valid, 1 if any errors are found.
Flags:

reliant workflow validate-tree

Validate a workflow tree with preset-aware cross-workflow checks Validates a workflow and all recursively reachable child workflows using the same static analysis that runs server-side at StartChat time. Wires a PresetLoader alongside the WorkflowLoader so preset-param mismatches (for example a preset setting params that aren’t declared inputs on the target workflow) are surfaced offline. The reference may be either a filesystem path to a workflow YAML file, or a builtin reference like “builtin://get-it-right”. Exit code 0 if no errors, 1 if any errors are found.
Flags:

reliant workflow wait-for-gate

Block until the workflow needs you — the next open question/approval Blocks until the workflow reaches the next OPEN gate — a question or approval awaiting input — then prints that gate (id, node, prompts + option labels) and exits 3. This is the “run until it needs me” supervision primitive: unlike ‘watch’/‘follow’ it does not stream every boundary, it stays quiet until there is something for you to act on. If a gate is ALREADY open when you call it, it returns immediately (the same per-poll reconciler that ‘watch’/‘follow’ use surfaces an already-open gate). A historical question/approval that has since been ANSWERED does not count — only a currently-pending gate ends the wait. Typical loop: while reliant workflow wait-for-gate <id> —json > gate.json; do reliant workflow answer <id> —select ”$(pick_answer gate.json)” done Exit codes: 3 a question/approval gate is open (its details are printed) 0 the workflow completed AND passed before any gate opened 1 the workflow failed, was cancelled, expired, or ended without passing (outcome: failure) before any gate opened — —json reports which 2 —timeout elapsed before a gate opened Drives the same event engine as ‘watch’/‘follow’ and honors —timeout, —interval and —tail.
Flags:

reliant workflow watch

Watch a workflow execution and print meaningful boundaries as they happen Streams a workflow execution and prints only the boundaries a supervisor cares about, in human-readable form: ▶ node started ✓ node completed ✗ node failed (exit N) ▶ workflow started ✓ workflow completed ✗ workflow failed ❓ question raised (id + prompt + option labels) ▶ question answered (id + time spent in the gate) ⏸ approval required (id + title) ▶ approval approved/denied (id + time spent in the gate) A run step that exits non-zero prints ✗ with its exit code, not ✓: the activity completed, the COMMAND failed, and those are not the same event. A run that ends at a terminal node declaring outcome: failure prints ”✗ workflow ended WITHOUT PASSING” rather than ✓, and exits 1. Reaching the end of the graph is not the same as the work having passed. watch BLOCKS on the live update feed until the next boundary or a terminal state — it does not print a snapshot and exit (use ‘workflow status’ for a one-shot snapshot). It consumes the same durable update feed as ‘workflow follow’; the two are deliberate siblings: watch human/agent supervision — readable boundary lines (this command) follow machine pipelines — one NDJSON event per line on stdout Gates are reconciled every poll, so a question/approval that is already open when you attach (or that you —tail past) is still printed — you cannot sit at a gate with no signal. Pass —exit-on-gate to stop at the next gate (exit 3). Both drive the identical event engine and honor the same —hook, —timeout, —interval and —tail flags. ‘question’ and ‘approval’ are additional hookable events here and on follow, so an agent can auto-answer: reliant workflow watch <id> —hook ‘on=question cmd=./answer.sh’ The hook receives the event JSON (including the question payload) on stdin and RELIANT_EVENT_* env vars, exactly as follow’s hooks do. Exit codes: 0 the root workflow completed AND passed 1 the root workflow failed, was cancelled, expired, or ended at a terminal node declaring outcome: failure 2 —timeout elapsed before a terminal state 3 —exit-on-gate: a question/approval gate opened
Flags: