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.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
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.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).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.
reliant auth token list
List API tokens (metadata only, never secrets)reliant auth token revoke
Revoke an API token by name or IDreliant 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.reliant daemon logs
Tail daemon logs Streams daemon log output. Defaults to the last 50 lines with live follow.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.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:- Daemon credentials file (created by ‘reliant daemon register’)
- If logged in but not registered, auto-registers and creates credentials
- 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.
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.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.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.reliant db migrate
Manage database migrationsreliant 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.
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.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.
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 onforge 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
<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).
reliant forge ci
CI helper commands — verify, scan, and validate in CI pipelinesreliant 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”)’
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:- 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.
- Freshness: runs forge generate and verifies no files changed — catches stale generated code after an input (proto/forge.yaml) change.
- 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 ago 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”
-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.
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.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 withforge login, or set the declared token env var.
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.
reliant forge cloud status
Show the endpoint and credential source for an environmentreliant 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, viaforge 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
reliant forge cloud token create
Mint an org automation token; its secret is printed ONCEreliant forge cloud token list
List the org’s automation tokens (never their secrets)reliant forge cloud token revoke
Revoke an org automation token, immediatelyreliant 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
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.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.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 withowner, 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.
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?) useforge cluster status.
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 —jsonreliant 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 streamingreliant 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-runreliant forge cluster reset
Delete then recreate the clusterreliant 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) runforge cluster info.
Examples:
forge cluster status
forge cluster status —json # machine-readable for scripts/dashboards
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 asforge 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
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/dashboardsreliant forge component
Manage UI components from the component library List, search, and install UI components from Forge’s built-in component library.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.reliant forge component list
List all available componentsreliant forge component search
Search components by keywordreliant 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)
<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.
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 “DATABASE_URL” —format jsonreliant 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
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
reliant forge db migrate status
Show migration statusreliant forge db migrate up
Apply pending migrationsreliant forge db migrate version
Show the current migration versionreliant 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
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”
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 laterup 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
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
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
reliant forge db seed apply
Materialize seed data into the dev database (dev-only)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.
reliant forge db seed status
Show per-table seeded-row counts vs the seed modelreliant 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)
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 stopreliant forge debug args
Show function arguments in the current scopereliant 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”reliant forge debug breakpoints
List all breakpointsreliant forge debug clear
Clear a breakpoint by IDreliant forge debug continue
Resume execution until the next breakpointreliant 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)”reliant forge debug goroutines
List goroutinesreliant forge debug locals
Show local variables in the current scopereliant forge debug stack
Show the current call stackreliant 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/serverreliant forge debug step
Step over the current linereliant forge debug step-in
Step into the current function callreliant forge debug step-out
Step out of the current functionreliant 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.
<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)
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
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 adomains 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).
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.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.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.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 — useforge domain unbind instead.
reliant forge domain show
Show one domain’s state, binding and required DNSreliant 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, useforge domain rm.
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.reliant forge env
Manage deploy environments: bring stacks up/down, deploy releases, and inspectreliant 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
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)”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 useforge env down, which never
deletes anything on a control plane and which this command never uses.
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 asforge 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=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.
<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
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)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.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.
- 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.
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 itreliant 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 needshelm 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
reliant forge env down
Stop Forge host processes for an environment (or —all: across projects) Stop a runningforge 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.
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
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
reliant forge env ps
List every forge stack running on this machine, across all projects List every stackforge 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
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
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).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 -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}
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)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 isforge env up.
An env that is not hosted is refused.
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.
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”)’
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 withforge 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.
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:
- build: docker build + push every cluster image; go build each build-only variant
- deploy: kubectl apply cluster manifests; wait rollouts and one-shot Jobs
- host: start every host-mode service (go-run / air / binary / delve)
- frontend: start every declared frontend in its path
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).
<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.
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.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.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.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/
reliant forge kcl
Evaluate this project’s KCL directly Work with this project’s KCL files directly. A forge project’s KCL resolvesimport 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.
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 plainkcl 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
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 declaresforge.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.
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 selectedreliant 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
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 declaresforge.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
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 declaresforge.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
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)
file was hand-edited
forge lint —tests # Run test-convention rules across backendhandlers and frontend hooks (warnings only)
forge lint —config-deps # Flag scalar Deps fields — a scalar isconfiguration, not a collaborator; prints the
exact proto block + AppConfig line to write
forge lint —column-markers # Flag COMMENT ON COLUMN/CONSTRAINT textcarrying an unrecognized forge:* marker
forge lint —fixture-drift # Execute every literal fixture statement ina 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 aTIMESTAMPTZ 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 anunrecognized forge:* marker (a misspelled
one does nothing and warns nowhere)
forge lint —create-nullability # Fail when a field’s optional labeldisagrees 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 nonon-generated Go file assigns — nothing
populates it, so the column default ships
forge lint —read-only-fields # FAIL on a forge:read-only column thatnothing 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 whoseupdate_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 staticexport (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 anoption 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 hasdrifted from this forge binary’s copy
(forge’s upgrade path does not track it)
forge lint —config-reach # Flag config fields no binary or frontendloads (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 thatbelongs 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 thelast 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/jobsOnly 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.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:
- —token explicit, beats everything
- $
<token_env>the env var the environment declares — CI - the credentials file entry for that env’s endpoint — what this command stores
- $FORGE_CREDENTIAL_HELPER — a host application’s session (Reliant’s); also used when the stored login (3) has expired
reliant forge logout
Forget the stored credential for this project’s control plane(s)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
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
<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
reliant forge project
Create, evolve, and inspect the project as a wholereliant 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 goreliant 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)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
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
reliant forge project delete
Delete (retire) a service from the project — the inverse offorge 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.
reliant forge project delete service
Retire a service (inverse offorge 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)
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 byforge 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”
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 renderreliant 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.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
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
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-readablereliant 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 fromforge 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
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:- 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.
- Drops — +goose StatementBegin / — +goose StatementEnd markers.
- Carries — +goose NO TRANSACTION over to a golang-migrate x-no-tx-wrap header.
- Renumbers starting from the next-available index in —dest-dir, so pack-installed migrations (00001-0000N) keep their slots.
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
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>’.
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, soforge 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, soforge 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
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= acceptsreliant 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
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.
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’simage 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.
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 thatforge 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 “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.
reliant forge registry ref
Print the digest-pinned refsforge 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.
reliant forge release
Inspect and verify release ledgers Work with the release ledgersforge 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.
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”.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
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
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
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.
<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
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
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
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
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
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
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
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
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
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 notificationsreliant 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-scaffoldforge 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
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 byforge 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
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’.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.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.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.reliant forge secret unset
Remove one secret from the storereliant forge skill
Manage Forge skills — conventions and playbooks for LLM agentsreliant 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.
reliant forge skill load
Print a skill’s content to stdout (resolves user > project > forge)reliant forge skill search
Search skills across all scopes by keyword (path/name=3, desc/body=1)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.
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
Flags:
reliant forge storage check
Refuse a build below the physical host free-space reservereliant forge storage configure-nodes
Preview kubelet GC migration for existing registered nodes; —apply restarts themreliant forge storage daemon
Run maintenance periodically, reloading policy for every passreliant forge storage gc
Preview cleanup; —apply deletes only eligible cache and registry versionsreliant forge storage install
Install daily maintenance (launchd, systemd timer, or Windows Task Scheduler) using a stable copy of this executablereliant forge storage policy
Print the effective storage policyreliant forge storage register
Register local cluster contexts, declared repositories or release pinsreliant forge storage status
Report physical host capacity, Docker usage and node cleanup settingsreliant forge storage worktrees
Preview idle, clean, pushed worktrees of a repo; —apply removes themreliant 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.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 —forcereliant forge version
Print the forge version and build identityreliant open
Open a project in Reliant Opens a project in the Reliant cloud platform. This is the primary command for starting a Reliant session:- Detects the project from the given path (defaults to current directory)
- Ensures you are authenticated (runs login flow if needed)
- Registers the project with the cloud API
- Starts the tools daemon for local tool execution
- Opens the Reliant web UI in your browser
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.
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).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.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.reliant server
Run cloud server components Run Reliant cloud server components. Each subcommand starts a specific server role for split-deployment mode.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.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.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.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.reliant trigger create
Create a schedule triggerreliant 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 firstreliant 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 — usetrigger events to see what it did.
reliant trigger get
Show one triggerreliant trigger list
List your triggersreliant 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.reliant version
Print version informationreliant workflow
Manage and validate workflows Commands for validating, listing, and running Reliant workflows.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
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.
reliant workflow list
List available workflows Lists workflows in the current project and builtin workflows.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>’.
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).
reliant workflow scenario
Run and manage workflow scenarios Commands for running and listing workflow scenario tests.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
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.
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.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.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.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.
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