> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reliantlabs.io/llms.txt
> Use this file to discover all available pages before exploring further.

# CLI Reference

> Complete reference for all Reliant CLI commands, flags, and options

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

## Quick Reference

| Command | Description |
| - | - |
| [`reliant auth`](#reliant-auth) | Manage authentication |
| [`reliant auth login`](#reliant-auth-login) | Log in to a Reliant server |
| [`reliant auth logout`](#reliant-auth-logout) | Forget the login for the resolved server |
| [`reliant auth serve`](#reliant-auth-serve) | Start a local OAuth helper server |
| [`reliant auth status`](#reliant-auth-status) | Show the login for the resolved server |
| [`reliant auth token`](#reliant-auth-token) | Manage API tokens (rlat\_ access tokens acting as you) |
| [`reliant auth token create`](#reliant-auth-token-create) | Create a new API token |
| [`reliant auth token list`](#reliant-auth-token-list) | List API tokens (metadata only, never secrets) |
| [`reliant auth token revoke`](#reliant-auth-token-revoke) | Revoke an API token by name or ID |
| [`reliant daemon`](#reliant-daemon) | Manage the local tools daemon |
| [`reliant daemon logs`](#reliant-daemon-logs) | Tail daemon logs |
| [`reliant daemon ls`](#reliant-daemon-ls) | List every daemon instance on this machine |
| [`reliant daemon register`](#reliant-daemon-register) | Register this machine as a daemon |
| [`reliant daemon start`](#reliant-daemon-start) | Start the tools daemon |
| [`reliant daemon status`](#reliant-daemon-status) | Check daemon status |
| [`reliant daemon stop`](#reliant-daemon-stop) | Stop the tools daemon |
| [`reliant db`](#reliant-db) | Database schema commands |
| [`reliant db migrate`](#reliant-db-migrate) | Manage database migrations |
| [`reliant db migrate status`](#reliant-db-migrate-status) | Print which embedded migrations the database is missing |
| [`reliant db migrate up`](#reliant-db-migrate-up) | Apply every pending migration |
| [`reliant forge`](#reliant-forge) | Connect RPC development framework for LLM-optimized applications |
| [`reliant forge api`](#reliant-forge-api) | Inspect and exercise Connect RPC endpoints over plain HTTP+JSON |
| [`reliant forge api curl`](#reliant-forge-api-curl) | Print a copy-pasteable curl command for a Connect RPC method |
| [`reliant forge build`](#reliant-forge-build) | Compile the project's binaries and frontends locally (never publishes) |
| [`reliant forge ci`](#reliant-forge-ci) | CI helper commands — verify, scan, and validate in CI pipelines |
| [`reliant forge ci migration-safety`](#reliant-forge-ci-migration-safety) | Run SQL migration safety checks based on forge.yaml config |
| [`reliant forge ci run`](#reliant-forge-ci-run) | Show one CI run's timeline — cut, checks, promotes, rollouts — and whether it passed |
| [`reliant forge ci summarize`](#reliant-forge-ci-summarize) | Render forge --json documents as a Markdown job summary (for \$GITHUB\_STEP\_SUMMARY) |
| [`reliant forge ci validate-kcl`](#reliant-forge-ci-validate-kcl) | Validate that every environment renders manifests kubectl will accept |
| [`reliant forge ci verify-generated`](#reliant-forge-ci-verify-generated) | Verify generated code is pristine and up to date |
| [`reliant forge ci verify-test-run`](#reliant-forge-ci-verify-test-run) | Verify a `go test -json` run actually ran its tests (reads output; runs nothing) |
| [`reliant forge ci vuln-scan`](#reliant-forge-ci-vuln-scan) | Run vulnerability scanners based on forge.yaml config |
| [`reliant forge cloud`](#reliant-forge-cloud) | Talk to the hosted control plane an environment declares |
| [`reliant forge cloud releases`](#reliant-forge-cloud-releases) | List releases from the hosted control plane |
| [`reliant forge cloud status`](#reliant-forge-cloud-status) | Show the endpoint and credential source for an environment |
| [`reliant forge cloud token`](#reliant-forge-cloud-token) | Mint, list and revoke ORG automation tokens (the CI deploy credential) |
| [`reliant forge cloud token create`](#reliant-forge-cloud-token-create) | Mint an org automation token; its secret is printed ONCE |
| [`reliant forge cloud token list`](#reliant-forge-cloud-token-list) | List the org's automation tokens (never their secrets) |
| [`reliant forge cloud token revoke`](#reliant-forge-cloud-token-revoke) | Revoke an org automation token, immediately |
| [`reliant forge cluster`](#reliant-forge-cluster) | Manage the local k3d cluster and inspect dev state |
| [`reliant forge cluster connect`](#reliant-forge-cluster-connect) | Register a Kubernetes cluster the control plane may deploy into |
| [`reliant forge cluster disconnect`](#reliant-forge-cluster-disconnect) | Deregister a connected cluster and remove what forge created in it |
| [`reliant forge cluster down`](#reliant-forge-cluster-down) | Delete the k3d cluster |
| [`reliant forge cluster info`](#reliant-forge-cluster-info) | Print static dev-loop config (cluster name, expected context, declared ports) |
| [`reliant forge cluster instances`](#reliant-forge-cluster-instances) | List every forge dev namespace on every reachable cluster |
| [`reliant forge cluster logs`](#reliant-forge-cluster-logs) | Stream kubectl logs for one or all services in the dev namespace |
| [`reliant forge cluster reload`](#reliant-forge-cluster-reload) | Re-render deploy/kcl/dev + kubectl apply + wait rollout |
| [`reliant forge cluster reset`](#reliant-forge-cluster-reset) | Delete then recreate the cluster |
| [`reliant forge cluster status`](#reliant-forge-cluster-status) | Print dynamic dev-loop state (cluster up/down, pods, ingress URLs) |
| [`reliant forge cluster up`](#reliant-forge-cluster-up) | Create the k3d cluster from deploy/k3d.yaml |
| [`reliant forge cluster urls`](#reliant-forge-cluster-urls) | Print the ingress URL table for the dev env |
| [`reliant forge component`](#reliant-forge-component) | Manage UI components from the component library |
| [`reliant forge component install`](#reliant-forge-component-install) | Install components into your project |
| [`reliant forge component list`](#reliant-forge-component-list) | List all available components |
| [`reliant forge component search`](#reliant-forge-component-search) | Search components by keyword |
| [`reliant forge db`](#reliant-forge-db) | Database and migration commands |
| [`reliant forge db introspect`](#reliant-forge-db-introspect) | Inspect the migrated database schema |
| [`reliant forge db migrate`](#reliant-forge-db-migrate) | Run migration lifecycle commands with golang-migrate |
| [`reliant forge db migrate force`](#reliant-forge-db-migrate-force) | Clear a dirty migration state by recording a version without running SQL |
| [`reliant forge db migrate status`](#reliant-forge-db-migrate-status) | Show migration status |
| [`reliant forge db migrate up`](#reliant-forge-db-migrate-up) | Apply pending migrations |
| [`reliant forge db migrate version`](#reliant-forge-db-migrate-version) | Show the current migration version |
| [`reliant forge db migration`](#reliant-forge-db-migration) | Create and re-version forward-only SQL migration files |
| [`reliant forge db migration new`](#reliant-forge-db-migration-new) | Create a new forward-only migration with a fresh UTC timestamp version |
| [`reliant forge db migration rebase`](#reliant-forge-db-migration-rebase) | Re-version migrations to fresh timestamps that sort after everything merged |
| [`reliant forge db reset`](#reliant-forge-db-reset) | DROP the dev database, recreate it, migrate to head, and seed (dev-only) |
| [`reliant forge db seed`](#reliant-forge-db-seed) | Materialize deterministic development seed data at runtime |
| [`reliant forge db seed apply`](#reliant-forge-db-seed-apply) | Materialize seed data into the dev database (dev-only) |
| [`reliant forge db seed reset`](#reliant-forge-db-seed-reset) | Delete seeded rows (child-first) and re-seed (dev-only) |
| [`reliant forge db seed status`](#reliant-forge-db-seed-status) | Show per-table seeded-row counts vs the seed model |
| [`reliant forge db squash`](#reliant-forge-db-squash) | Collapse N migrations into one canonical baseline (.up.sql) |
| [`reliant forge debug`](#reliant-forge-debug) | Debug a running service with Delve |
| [`reliant forge debug args`](#reliant-forge-debug-args) | Show function arguments in the current scope |
| [`reliant forge debug break`](#reliant-forge-debug-break) | Set a breakpoint |
| [`reliant forge debug breakpoints`](#reliant-forge-debug-breakpoints) | List all breakpoints |
| [`reliant forge debug clear`](#reliant-forge-debug-clear) | Clear a breakpoint by ID |
| [`reliant forge debug continue`](#reliant-forge-debug-continue) | Resume execution until the next breakpoint |
| [`reliant forge debug eval`](#reliant-forge-debug-eval) | Evaluate an expression in the current scope |
| [`reliant forge debug goroutines`](#reliant-forge-debug-goroutines) | List goroutines |
| [`reliant forge debug locals`](#reliant-forge-debug-locals) | Show local variables in the current scope |
| [`reliant forge debug stack`](#reliant-forge-debug-stack) | Show the current call stack |
| [`reliant forge debug start`](#reliant-forge-debug-start) | Start a debug session for a service |
| [`reliant forge debug step`](#reliant-forge-debug-step) | Step over the current line |
| [`reliant forge debug step-in`](#reliant-forge-debug-step-in) | Step into the current function call |
| [`reliant forge debug step-out`](#reliant-forge-debug-step-out) | Step out of the current function |
| [`reliant forge debug stop`](#reliant-forge-debug-stop) | Stop the debug session (detaches from attached processes; kills only what forge launched) |
| [`reliant forge doctor`](#reliant-forge-doctor) | Check that the PROJECT is well-formed (deployability, payload caps, tooling, cluster capability) |
| [`reliant forge doctor parity`](#reliant-forge-doctor-parity) | Diff a service's host-mode vs cluster-mode env+config |
| [`reliant forge domain`](#reliant-forge-domain) | Manage custom domains and what they serve |
| [`reliant forge domain add`](#reliant-forge-domain-add) | Claim a hostname and print the DNS records to publish |
| [`reliant forge domain bind`](#reliant-forge-domain-bind) | Point a domain at one environment's workload or frontend |
| [`reliant forge domain ls`](#reliant-forge-domain-ls) | List your organization's domains and what they serve |
| [`reliant forge domain rm`](#reliant-forge-domain-rm) | Remove a domain and stop serving it |
| [`reliant forge domain show`](#reliant-forge-domain-show) | Show one domain's state, binding and required DNS |
| [`reliant forge domain unbind`](#reliant-forge-domain-unbind) | Stop a domain serving, and keep the domain |
| [`reliant forge domain verify`](#reliant-forge-domain-verify) | Check a domain's DNS now, instead of waiting for the poller |
| [`reliant forge env`](#reliant-forge-env) | Manage deploy environments: bring stacks up/down, deploy releases, and inspect |
| [`reliant forge env build`](#reliant-forge-env-build) | Build an environment's artifacts; --push publishes them, --release records an immutable release |
| [`reliant forge env config`](#reliant-forge-env-config) | Print the resolved configuration deploy/kcl/`<env>`/ hands each workload |
| [`reliant forge env delete`](#reliant-forge-env-delete) | Tear down a HOSTED environment on the control plane (destructive) |
| [`reliant forge env deploy`](#reliant-forge-env-deploy) | Make an environment run a release: record it, apply it, and wait for health |
| [`reliant forge env devstack`](#reliant-forge-env-devstack) | Parallel-dev-stack host helpers (worktree key + port allocation) |
| [`reliant forge env devstack key`](#reliant-forge-env-devstack-key) | Print the current worktree key ("" on the primary checkout) |
| [`reliant forge env devstack list`](#reliant-forge-env-devstack-list) | List every holder of a port block, labelled by kind (--stacks-only for the machine-readable worktree roster) |
| [`reliant forge env devstack port`](#reliant-forge-env-devstack-port) | Resolve the worktree-allocated host port for a base port |
| [`reliant forge env devstack prune`](#reliant-forge-env-devstack-prune) | Reclaim dev-stack port blocks for worktrees that no longer exist on disk |
| [`reliant forge env devstack release`](#reliant-forge-env-devstack-release) | Reclaim one named port block that you know is no longer in use |
| [`reliant forge env diff`](#reliant-forge-env-diff) | Render this checkout and diff each environment against what is deployed |
| [`reliant forge env down`](#reliant-forge-env-down) | Stop Forge host processes for an environment (or --all: across projects) |
| [`reliant forge env list`](#reliant-forge-env-list) | List the environments declared in deploy/kcl/ |
| [`reliant forge env new`](#reliant-forge-env-new) | Scaffold a new deploy environment from an existing one |
| [`reliant forge env options`](#reliant-forge-env-options) | List the render options an environment's KCL declares |
| [`reliant forge env ps`](#reliant-forge-env-ps) | List every forge stack running on this machine, across all projects |
| [`reliant forge env render`](#reliant-forge-env-render) | Print the Kubernetes objects deploy/kcl/`<env>`/ renders, with the cluster each lands on |
| [`reliant forge env secrets`](#reliant-forge-env-secrets) | Project the env's secret\_provider into the cluster |
| [`reliant forge env secrets sync`](#reliant-forge-env-secrets-sync) | Render + apply the k8s Secrets for an env's dotenv secret\_provider (local clusters only) |
| [`reliant forge env shape`](#reliant-forge-env-shape) | Print what an environment DECLARES — kind, workloads, secrets, domains, and one hash per object |
| [`reliant forge env smoke`](#reliant-forge-env-smoke) | Probe every declared ingress route after deploy (TLS + routing + CORS) |
| [`reliant forge env start`](#reliant-forge-env-start) | Resume a HOSTED environment's workloads on the control plane (not local processes) |
| [`reliant forge env status`](#reliant-forge-env-status) | The one read view of an environment: bound release, rollout, health, verify, gates, ledger |
| [`reliant forge env stop`](#reliant-forge-env-stop) | Suspend a HOSTED environment's workloads on the control plane (not local processes) |
| [`reliant forge env up`](#reliant-forge-env-up) | Bring the whole dev loop up on this machine: build + deploy + host + frontend |
| [`reliant forge gate`](#reliant-forge-gate) | Record and read check evidence against a promotion |
| [`reliant forge gate list`](#reliant-forge-gate-list) | Read every check result attached to a promotion |
| [`reliant forge gate record`](#reliant-forge-gate-record) | Record one check's result against a promotion |
| [`reliant forge generate`](#reliant-forge-generate) | Generate code from proto files |
| [`reliant forge kcl`](#reliant-forge-kcl) | Evaluate this project's KCL directly |
| [`reliant forge kcl eval`](#reliant-forge-kcl-eval) | Evaluate one KCL file of this project and print a selected field |
| [`reliant forge ledger`](#reliant-forge-ledger) | Move and inspect deploy ledgers |
| [`reliant forge ledger export`](#reliant-forge-ledger-export) | Write the selected ledger to a directory, to VERIFY an import |
| [`reliant forge ledger import`](#reliant-forge-ledger-import) | Import a ledger from a checkout's committed history, or from another ledger directory |
| [`reliant forge ledger show`](#reliant-forge-ledger-show) | Print this machine's ledger records — promotions, applies and local sessions |
| [`reliant forge ledger where`](#reliant-forge-ledger-where) | Print which backend holds an environment's ledger, and the declaration that chose it |
| [`reliant forge lint`](#reliant-forge-lint) | Run linters on the project |
| [`reliant forge login`](#reliant-forge-login) | Authenticate to the control plane(s) this project declares |
| [`reliant forge logout`](#reliant-forge-logout) | Forget the stored credential for this project's control plane(s) |
| [`reliant forge package`](#reliant-forge-package) | Manage internal packages |
| [`reliant forge package new`](#reliant-forge-package-new) | Create a new internal package with contract interface |
| [`reliant forge project`](#reliant-forge-project) | Create, evolve, and inspect the project as a whole |
| [`reliant forge project annotations`](#reliant-forge-project-annotations) | Dump forge's authoritative entity-authoring annotation spec |
| [`reliant forge project audit`](#reliant-forge-project-audit) | Print a comprehensive project state snapshot |
| [`reliant forge project capabilities`](#reliant-forge-project-capabilities) | List everything forge can do — every command, analyzer, and marker, in one call |
| [`reliant forge project checkouts`](#reliant-forge-project-checkouts) | List origin/main plus every git worktree — the Preview picker's source |
| [`reliant forge project delete`](#reliant-forge-project-delete) | Delete (retire) a service from the project — the inverse of `forge scaffold service` |
| [`reliant forge project delete service`](#reliant-forge-project-delete-service) | Retire a service (inverse of `forge scaffold service`) |
| [`reliant forge project disown`](#reliant-forge-project-disown) | Permanently transfer a forge-generated file to user ownership (one-way) |
| [`reliant forge project features`](#reliant-forge-project-features) | Print the resolved feature graph (on/off, why, dependencies) |
| [`reliant forge project graph`](#reliant-forge-project-graph) | Emit a JSON dependency graph of the project's declared resources |
| [`reliant forge project introspect`](#reliant-forge-project-introspect) | Inspect what the assembled binary will expose at runtime |
| [`reliant forge project introspect handlers`](#reliant-forge-project-introspect-handlers) | Print every RPC path the binary will register |
| [`reliant forge project libraries`](#reliant-forge-project-libraries) | Print forge/pkg's API — name a package to get its full signatures |
| [`reliant forge project map`](#reliant-forge-project-map) | Print project tree with ownership annotations |
| [`reliant forge project migrate`](#reliant-forge-project-migrate) | Project-level migration tooling (import, convert) |
| [`reliant forge project migrate import`](#reliant-forge-project-migrate-import) | Import migrations from another format (e.g. goose) into golang-migrate shape |
| [`reliant forge project migrate tdd`](#reliant-forge-project-migrate-tdd) | Rewrite hand-rolled handler tests to use tdd.RunRPCCases |
| [`reliant forge project new`](#reliant-forge-project-new) | Create a new Forge project (service / CLI / library) |
| [`reliant forge project rescaffold`](#reliant-forge-project-rescaffold) | Re-create scaffold-once files you deleted, as forge would scaffold them today |
| [`reliant forge project scaffolded`](#reliant-forge-project-scaffolded) | List the scaffold-once files forge has written, and which are absent |
| [`reliant forge project shapes`](#reliant-forge-project-shapes) | List every API shape — RPCs, messages, enums, tables, handlers, hooks — with file:line |
| [`reliant forge project upgrade`](#reliant-forge-project-upgrade) | Update frozen project files from latest Forge templates |
| [`reliant forge project upgrade apply`](#reliant-forge-project-upgrade-apply) | Record a migration as applied (writes .forge/migrations.json) |
| [`reliant forge project upgrade list`](#reliant-forge-project-upgrade-list) | List pending forge migrations for this project |
| [`reliant forge registry`](#reliant-forge-registry) | Log in to and read refs from the registries an env's workloads declare |
| [`reliant forge registry login`](#reliant-forge-registry-login) | docker login to every registry the env's workloads declare |
| [`reliant forge registry ref`](#reliant-forge-registry-ref) | Print the digest-pinned refs `forge env build <env> --push` pushed |
| [`reliant forge release`](#reliant-forge-release) | Inspect and verify release ledgers |
| [`reliant forge release verify`](#reliant-forge-release-verify) | Prove every artifact a release names actually exists and matches |
| [`reliant forge release where`](#reliant-forge-release-where) | Show which environments are currently bound to a release |
| [`reliant forge scaffold`](#reliant-forge-scaffold) | Scaffold code: bare, everything the protos imply; with a noun, exactly one thing |
| [`reliant forge scaffold adapter`](#reliant-forge-scaffold-adapter) | Scaffold an outbound adapter (HTTP client, queue producer, storage gateway) |
| [`reliant forge scaffold binary`](#reliant-forge-scaffold-binary) | Scaffold a non-server long-running binary |
| [`reliant forge scaffold crd`](#reliant-forge-scaffold-crd) | Scaffold a Custom Resource Definition + reconciler on an operator |
| [`reliant forge scaffold entity`](#reliant-forge-scaffold-entity) | Birth a database entity from its already-authored proto message: the owned forward migration + the CRUD wire contract |
| [`reliant forge scaffold frontend`](#reliant-forge-scaffold-frontend) | Scaffold a new frontend |
| [`reliant forge scaffold handler-file`](#reliant-forge-scaffold-handler-file) | Scaffold an additional RPC-group file in an existing handler directory |
| [`reliant forge scaffold library`](#reliant-forge-scaffold-library) | Scaffold a library-shaped Go package (no contract.go) and pre-register the contracts.exclude entry |
| [`reliant forge scaffold operator`](#reliant-forge-scaffold-operator) | Scaffold a new Kubernetes operator |
| [`reliant forge scaffold package`](#reliant-forge-scaffold-package) | Scaffold a new internal package (alias for 'forge package new') |
| [`reliant forge scaffold rpc`](#reliant-forge-scaffold-rpc) | Scaffold a custom RPC on the service's handler package |
| [`reliant forge scaffold scenario`](#reliant-forge-scaffold-scenario) | Scaffold a new frontend mock scenario |
| [`reliant forge scaffold service`](#reliant-forge-scaffold-service) | Scaffold one or more Go services |
| [`reliant forge scaffold webhook`](#reliant-forge-scaffold-webhook) | Scaffold a webhook endpoint on an existing service |
| [`reliant forge scaffold worker`](#reliant-forge-scaffold-worker) | Scaffold a new background worker |
| [`reliant forge secret`](#reliant-forge-secret) | Manage an environment's secret store (local file or hosted control plane) |
| [`reliant forge secret ensure`](#reliant-forge-secret-ensure) | Create the FileSecrets store and report missing values |
| [`reliant forge secret list`](#reliant-forge-secret-list) | List declared secrets and whether each has a value |
| [`reliant forge secret migrate`](#reliant-forge-secret-migrate) | Convert a legacy .env secrets file into the YAML store |
| [`reliant forge secret set`](#reliant-forge-secret-set) | Add or replace one secret value (read from stdin) |
| [`reliant forge secret unset`](#reliant-forge-secret-unset) | Remove one secret from the store |
| [`reliant forge skill`](#reliant-forge-skill) | Manage Forge skills — conventions and playbooks for LLM agents |
| [`reliant forge skill list`](#reliant-forge-skill-list) | List available skills (forge-shipped, project, and user-global) |
| [`reliant forge skill load`](#reliant-forge-skill-load) | Print a skill's content to stdout (resolves user > project > forge) |
| [`reliant forge skill search`](#reliant-forge-skill-search) | Search skills across all scopes by keyword (path/name=3, desc/body=1) |
| [`reliant forge skill write`](#reliant-forge-skill-write) | Write every bundled skill to a directory |
| [`reliant forge start`](#reliant-forge-start) | Print the greenfield brief: empty directory to authored protos, in one call |
| [`reliant forge storage`](#reliant-forge-storage) | Inspect and bound local caches; preserve persistent application data |
| [`reliant forge storage check`](#reliant-forge-storage-check) | Refuse a build below the physical host free-space reserve |
| [`reliant forge storage configure-nodes`](#reliant-forge-storage-configure-nodes) | Preview kubelet GC migration for existing registered nodes; --apply restarts them |
| [`reliant forge storage daemon`](#reliant-forge-storage-daemon) | Run maintenance periodically, reloading policy for every pass |
| [`reliant forge storage gc`](#reliant-forge-storage-gc) | Preview cleanup; --apply deletes only eligible cache and registry versions |
| [`reliant forge storage install`](#reliant-forge-storage-install) | Install daily maintenance (launchd, systemd timer, or Windows Task Scheduler) using a stable copy of this executable |
| [`reliant forge storage policy`](#reliant-forge-storage-policy) | Print the effective storage policy |
| [`reliant forge storage register`](#reliant-forge-storage-register) | Register local cluster contexts, declared repositories or release pins |
| [`reliant forge storage status`](#reliant-forge-storage-status) | Report physical host capacity, Docker usage and node cleanup settings |
| [`reliant forge storage worktrees`](#reliant-forge-storage-worktrees) | Preview idle, clean, pushed worktrees of a repo; --apply removes them |
| [`reliant forge tools`](#reliant-forge-tools) | Manage developer tooling forge depends on (proto plugins, etc.) |
| [`reliant forge tools install`](#reliant-forge-tools-install) | Install codegen tools (protoc-gen-go, protoc-gen-connect-go, goimports) |
| [`reliant forge version`](#reliant-forge-version) | Print the forge version and build identity |
| [`reliant open`](#reliant-open) | Open a project in Reliant |
| [`reliant preview-url`](#reliant-preview-url) | Print the shareable preview URL for a local port |
| [`reliant project`](#reliant-project) | Manage Reliant projects |
| [`reliant project create`](#reliant-project-create) | Create a project and print its ID |
| [`reliant project list`](#reliant-project-list) | List your projects |
| [`reliant server`](#reliant-server) | Run cloud server components |
| [`reliant server api`](#reliant-server-api) | Run the stateless HTTP + gRPC API server |
| [`reliant server gateway`](#reliant-server-gateway) | Run the daemon connection gateway |
| [`reliant server worker`](#reliant-server-worker) | Run the Temporal workflow worker |
| [`reliant trigger`](#reliant-trigger) | Manage schedule triggers |
| [`reliant trigger create`](#reliant-trigger-create) | Create a schedule trigger |
| [`reliant trigger delete`](#reliant-trigger-delete) | Delete a trigger and its schedule |
| [`reliant trigger disable`](#reliant-trigger-disable) | Pause a trigger's schedule |
| [`reliant trigger enable`](#reliant-trigger-enable) | Resume a trigger's schedule |
| [`reliant trigger events`](#reliant-trigger-events) | List a trigger's firings, newest first |
| [`reliant trigger fire`](#reliant-trigger-fire) | Run a trigger now |
| [`reliant trigger get`](#reliant-trigger-get) | Show one trigger |
| [`reliant trigger list`](#reliant-trigger-list) | List your triggers |
| [`reliant trigger update`](#reliant-trigger-update) | Replace a trigger's definition |
| [`reliant version`](#reliant-version) | Print version information |
| [`reliant workflow`](#reliant-workflow) | Manage and validate workflows |
| [`reliant workflow answer`](#reliant-workflow-answer) | Answer a pending question on a workflow execution |
| [`reliant workflow follow`](#reliant-workflow-follow) | Follow a workflow execution and stream NDJSON lifecycle events |
| [`reliant workflow list`](#reliant-workflow-list) | List available workflows |
| [`reliant workflow pause`](#reliant-workflow-pause) | Pause a running workflow execution (keeps it resumable) |
| [`reliant workflow questions`](#reliant-workflow-questions) | List the open question(s) awaiting input on a workflow execution |
| [`reliant workflow resume`](#reliant-workflow-resume) | Resume a paused or expired workflow execution |
| [`reliant workflow run`](#reliant-workflow-run) | Run a workflow |
| [`reliant workflow scenario`](#reliant-workflow-scenario) | Run and manage workflow scenarios |
| [`reliant workflow scenario list`](#reliant-workflow-scenario-list) | List available scenarios for workflows |
| [`reliant workflow scenario run`](#reliant-workflow-scenario-run) | Run scenario tests against workflows |
| [`reliant workflow status`](#reliant-workflow-status) | Show a one-shot snapshot of a workflow execution |
| [`reliant workflow terminate`](#reliant-workflow-terminate) | Terminate a running workflow execution |
| [`reliant workflow validate`](#reliant-workflow-validate) | Validate workflow YAML files |
| [`reliant workflow validate-tree`](#reliant-workflow-validate-tree) | Validate a workflow tree with preset-aware cross-workflow checks |
| [`reliant workflow wait-for-gate`](#reliant-workflow-wait-for-gate) | Block until the workflow needs you — the next open question/approval |
| [`reliant workflow watch`](#reliant-workflow-watch) | Watch a workflow execution and print meaningful boundaries as they happen |

## Global Flags

These flags are available on all commands.

| Flag | Type | Default | Description |
| - | - | - | - |
| `--gateway` | `string` | `https://gateway.reliantapi.com` | Daemon gateway URL (defaults to the gateway subdomain of the resolved server) |
| `--server` | `string` | `https://api.reliantapi.com` | Reliant API server URL (also RELIANT\_SERVER\_URL) |
| `--verbose`, `-v` | `bool` | - | Enable verbose output |

***

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

**Subcommands:**

| Command | Description |
| - | - |
| [`login`](#reliant-auth-login) | Log in to a Reliant server |
| [`logout`](#reliant-auth-logout) | Forget the login for the resolved server |
| [`serve`](#reliant-auth-serve) | Start a local OAuth helper server |
| [`status`](#reliant-auth-status) | Show the login for the resolved server |
| [`token`](#reliant-auth-token) | Manage API tokens (rlat\_ access tokens acting as you) |

***

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

***

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

***

### 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 serve [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--port` | `int` | `19284` | Port to listen on |

***

### 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 status [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output in JSON format |

***

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

**Subcommands:**

| Command | Description |
| - | - |
| [`create`](#reliant-auth-token-create) | Create a new API token |
| [`list`](#reliant-auth-token-list) | List API tokens (metadata only, never secrets) |
| [`revoke`](#reliant-auth-token-revoke) | Revoke an API token by name or ID |

***

#### 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 create [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--name` | `string` | - | Label for the token, e.g. ci-deploy (required) |

***

#### reliant auth token list

List API tokens (metadata only, never secrets)

```
reliant auth token list [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output in JSON format |

***

#### reliant auth token revoke

Revoke an API token by name or ID

```
reliant auth token revoke <name-or-id>
```

***

## reliant daemon

Manage the local tools daemon

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

```
reliant daemon
```

**Subcommands:**

| Command | Description |
| - | - |
| [`logs`](#reliant-daemon-logs) | Tail daemon logs |
| [`ls`](#reliant-daemon-ls) | List every daemon instance on this machine |
| [`register`](#reliant-daemon-register) | Register this machine as a daemon |
| [`start`](#reliant-daemon-start) | Start the tools daemon |
| [`status`](#reliant-daemon-status) | Check daemon status |
| [`stop`](#reliant-daemon-stop) | Stop the tools daemon |

***

### reliant daemon logs

Tail daemon logs

Streams daemon log output. Defaults to the last 50 lines with live follow.

```
reliant daemon logs [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--account` | `string` | - | Account (Supabase subject) this daemon runs as; selects among several accounts on one server |
| `--data-dir` | `string` | - | Data directory containing logs (default: this instance's directory under \~/.reliant/instances) |
| `--follow`, `-f` | `bool` | `true` | Follow log output |
| `--lines`, `-n` | `int` | `50` | Number of lines to show |
| `--workspace` | `string` | - | Workspace this daemon serves (defaults to the git worktree root of the current directory) |

***

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

***

### 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 register [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--account` | `string` | - | Account (Supabase subject) to register; separate accounts on one server keep separate credentials |

***

### reliant daemon start

Start the tools daemon

Starts the tools daemon in the foreground (default) or background.

The daemon connects to the Reliant cloud platform and provides local tool
execution capabilities.

Credential resolution order:

1. Daemon credentials file (created by 'reliant daemon register')
2. If logged in but not registered, auto-registers and creates credentials
3. If not logged in, prompts for login and then auto-registers — unless
   \--non-interactive (or RELIANT\_DAEMON\_NON\_INTERACTIVE) is set, in which
   case the daemon never opens a browser or runs the login flow itself. It
   instead stays resident and idle, publishing "awaiting\_credentials" in its
   runtime state, and polls for a credentials file to appear on disk (see
   'reliant daemon status' and internal/toolexec/daemonstate). This is the
   mode Electron spawns in: its own login page owns interactive sign-in, and
   the daemon must never pop a second one.

```
reliant daemon start [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--account` | `string` | - | Account (Supabase subject) this daemon runs as; selects among several accounts on one server |
| `--background` | `bool` | - | Run daemon in background (detached) |
| `--data-dir` | `string` | - | Data directory (default: this instance's directory under \~/.reliant/instances) |
| `--grpc-url` | `string` | - | gRPC server URL to connect to |
| `--listen-port` | `int` | `9190` | Port to listen on in server mode |
| `--name` | `string` | - | Human-friendly daemon name (default: `<instance-id>`@`<hostname>`) |
| `--non-interactive` | `bool` | - | Never open a browser or run interactive login; idle and wait for credentials to appear on disk instead |
| `--port` | `string` | `9190` | Daemon listen port |
| `--server-mode` | `bool` | - | Listen for incoming gateway connections instead of dialing out |
| `--tls-cert` | `string` | - | TLS certificate file path |
| `--tls-key` | `string` | - | TLS key file path |
| `--tls-mode` | `string` | - | TLS mode (tls, insecure\_tls\_skip\_verify, h2c, or disabled) |
| `--token` | `bool` | - | Read a PAT from stdin and use it as the daemon credential |
| `--workspace` | `string` | - | Workspace this daemon serves (defaults to the git worktree root of the current directory) |

***

### 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 status [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--account` | `string` | - | Account (Supabase subject) this daemon runs as; selects among several accounts on one server |
| `--data-dir` | `string` | - | Data directory (default: this instance's directory under \~/.reliant/instances) |
| `--workspace` | `string` | - | Workspace this daemon serves (defaults to the git worktree root of the current directory) |

***

### 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 daemon stop [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--account` | `string` | - | Account (Supabase subject) this daemon runs as; selects among several accounts on one server |
| `--data-dir` | `string` | - | Data directory (default: this instance's directory under \~/.reliant/instances) |
| `--force` | `bool` | - | Force kill (SIGKILL) instead of graceful shutdown |
| `--workspace` | `string` | - | Workspace this daemon serves (defaults to the git worktree root of the current directory) |

***

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

**Subcommands:**

| Command | Description |
| - | - |
| [`migrate`](#reliant-db-migrate) | Manage database migrations |

***

### reliant db migrate

Manage database migrations

```
reliant db migrate
```

**Subcommands:**

| Command | Description |
| - | - |
| [`status`](#reliant-db-migrate-status) | Print which embedded migrations the database is missing |
| [`up`](#reliant-db-migrate-up) | Apply every pending migration |

***

#### reliant db migrate status

Print which embedded migrations the database is missing

Report how many of this binary's embedded migrations the database has
applied, and list any it has not.

Opens the database WITHOUT migrating and WITHOUT waiting, so it stays usable
to diagnose the very deadlock the wait produces.

```
reliant db migrate status
```

***

#### 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 db migrate up
```

***

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

```
reliant forge
```

**Subcommands:**

| Command | Description |
| - | - |
| [`api`](#reliant-forge-api) | Inspect and exercise Connect RPC endpoints over plain HTTP+JSON |
| [`build`](#reliant-forge-build) | Compile the project's binaries and frontends locally (never publishes) |
| [`ci`](#reliant-forge-ci) | CI helper commands — verify, scan, and validate in CI pipelines |
| [`cloud`](#reliant-forge-cloud) | Talk to the hosted control plane an environment declares |
| [`cluster`](#reliant-forge-cluster) | Manage the local k3d cluster and inspect dev state |
| [`component`](#reliant-forge-component) | Manage UI components from the component library |
| [`db`](#reliant-forge-db) | Database and migration commands |
| [`debug`](#reliant-forge-debug) | Debug a running service with Delve |
| [`doctor`](#reliant-forge-doctor) | Check that the PROJECT is well-formed (deployability, payload caps, tooling, cluster capability) |
| [`domain`](#reliant-forge-domain) | Manage custom domains and what they serve |
| [`env`](#reliant-forge-env) | Manage deploy environments: bring stacks up/down, deploy releases, and inspect |
| [`gate`](#reliant-forge-gate) | Record and read check evidence against a promotion |
| [`generate`](#reliant-forge-generate) | Generate code from proto files |
| [`kcl`](#reliant-forge-kcl) | Evaluate this project's KCL directly |
| [`ledger`](#reliant-forge-ledger) | Move and inspect deploy ledgers |
| [`lint`](#reliant-forge-lint) | Run linters on the project |
| [`login`](#reliant-forge-login) | Authenticate to the control plane(s) this project declares |
| [`logout`](#reliant-forge-logout) | Forget the stored credential for this project's control plane(s) |
| [`package`](#reliant-forge-package) | Manage internal packages |
| [`project`](#reliant-forge-project) | Create, evolve, and inspect the project as a whole |
| [`registry`](#reliant-forge-registry) | Log in to and read refs from the registries an env's workloads declare |
| [`release`](#reliant-forge-release) | Inspect and verify release ledgers |
| [`scaffold`](#reliant-forge-scaffold) | Scaffold code: bare, everything the protos imply; with a noun, exactly one thing |
| [`secret`](#reliant-forge-secret) | Manage an environment's secret store (local file or hosted control plane) |
| [`skill`](#reliant-forge-skill) | Manage Forge skills — conventions and playbooks for LLM agents |
| [`start`](#reliant-forge-start) | Print the greenfield brief: empty directory to authored protos, in one call |
| [`storage`](#reliant-forge-storage) | Inspect and bound local caches; preserve persistent application data |
| [`tools`](#reliant-forge-tools) | Manage developer tooling forge depends on (proto plugins, etc.) |
| [`version`](#reliant-forge-version) | Print the forge version and build identity |

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--project-dir`, `-C` | `string` | - | resolve the project from this directory instead of the current one |

***

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

**Subcommands:**

| Command | Description |
| - | - |
| [`curl`](#reliant-forge-api-curl) | Print a copy-pasteable curl command for a Connect RPC method |

***

#### 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 api curl <service.method> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--auth-token` | `string` | - | Bearer token to inline (default: a \$TOKEN placeholder on RPCs that require auth) |
| `--body` | `string` | - | Request body JSON (default: zero-value skeleton from proto fields) |
| `--host` | `string` | `localhost` | Host name to embed in the URL |
| `--port` | `int` | `0` | Port the app listens on (default 8080; set per-env in deploy/kcl) |

***

### reliant forge build

Compile the project's binaries and frontends locally (never publishes)

Compile the project's services and frontends. A LOCAL CHECK.

This command never publishes anything. Pushing images and recording a
release are environment acts — a push destination is declared per workload
in an env's render, and a release's artifact set is discovered from it — so
they live on `forge env build <env>`:

forge env build prod --push             # build + push to declared registries
forge env build prod --release v1.4.0   # build + push + record the release

which is also why THIS command's environment argument is optional: it scopes
docker builds and tag resolution, and nothing here needs an env to compile.

This command is a PURE EXECUTOR of the per-service, per-env build
declaration in KCL. With an environment argument it iterates the
services the rendered env declares and dispatches on each service's
build.type:

* go     → go build the declared cmd (CGO\_ENABLED=0, stripped) — no
  hardcoded ./cmd; the target package comes from KCL
* docker → docker build the service's image (dockerfile/platform/...)
* shell  → run the verbatim build command

It also builds Next.js frontends (npm run build) and, with --docker,
the shared project image. Output binaries land in the output dir.

Examples:
forge build                                # Compile everything
forge build staging                        # Scope docker builds/tag resolution to deploy/kcl/staging/
forge build -t web                         # Build only the "web" frontend
forge build -o bin                         # Output binaries to bin/
forge build --docker                       # Also build Docker images (locally; never pushed)
forge build --debug                        # Build with debug symbols for Delve

When a declared reference is a k3d-local localhost:`<port>`, the image is
also tagged registry.localhost:`<port>`/`<path>` (LOCAL alias only — the host
can't DNS-resolve registry.localhost, so it isn't pushed; the containerd
mirror config inside k3d resolves that reference at pull time).

```
reliant forge build [environment] [flags]
```

**Flags:**

| Flag | Type | Default | Description | | |
| - | - | - | - | - | - |
| `--debug` | `bool` | - | Build with debug symbols for Delve | | |
| `--docker` | `bool` | - | Build Docker images for all services | | |
| `--gate-json` | `string` | - | Also write this build's result to `FILE` as a gate document, for `forge gate record` or `forge env deploy --gate`. A FILE, not a stdout mode: the build log and the exit code are unchanged. | | |
| `--no-generate` | `bool` | - | Skip the pre-build code-generation check. By default forge build runs forge generate when gen/ is missing or proto sources are newer than the generated tree. | | |
| `--option`, `-D` | `stringArray` | `[]` | Set a render option the env's KCL declares, as name=value (repeatable). Relayed to KCL verbatim — forge does not interpret the value. Requires the environment argument. List an env's options with `forge env options <env>`. | | |
| `--output`, `-o` | `string` | `bin` | Output directory for binaries | | |
| `--parallel` | `bool` | `true` | Build services in parallel | | |
| `--plan` | `bool` | - | Resolve the exact build set this invocation would build (same KCL discovery, same --target narrowing) and PREFLIGHT every step without running it: each go-build package exists and is a main package, each Dockerfile and frontend build script exists, each ShellBuild cwd exists, and with --release the ledger would cover everything the env declares. Builds, pushes, generates and writes nothing; exits non-zero on anything the real build would fail on. Pass it the release cut's exact arguments to gate a PR on the cut. | | |
| `--tag` | `string` | - | Override the image tag of every image this build writes (default: the tag a workload's image pins, else the env's image\_tag, else git describe --tags --always --dirty). Refused when it differs from the tag a selected workload's image pins — the deploy pulls the pin. Recorded in .forge/state so forge env deploy uses the same value. | | |
| `--target`, `-t` | `string` | `all` | Build target (all | external | a specific service/frontend name). `external` builds only the KCL services declaring build\_cmd; requires the environment argument. |
| `--target-arch` | `string` | - | Override target GOARCH for cross-compilation (default: forge.yaml deploy.target\_arch, then amd64 for docker builds) | | |

***

### reliant forge ci

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

```
reliant forge ci
```

**Subcommands:**

| Command | Description |
| - | - |
| [`migration-safety`](#reliant-forge-ci-migration-safety) | Run SQL migration safety checks based on forge.yaml config |
| [`run`](#reliant-forge-ci-run) | Show one CI run's timeline — cut, checks, promotes, rollouts — and whether it passed |
| [`summarize`](#reliant-forge-ci-summarize) | Render forge --json documents as a Markdown job summary (for \$GITHUB\_STEP\_SUMMARY) |
| [`validate-kcl`](#reliant-forge-ci-validate-kcl) | Validate that every environment renders manifests kubectl will accept |
| [`verify-generated`](#reliant-forge-ci-verify-generated) | Verify generated code is pristine and up to date |
| [`verify-test-run`](#reliant-forge-ci-verify-test-run) | Verify a `go test -json` run actually ran its tests (reads output; runs nothing) |
| [`vuln-scan`](#reliant-forge-ci-vuln-scan) | Run vulnerability scanners based on forge.yaml config |

***

#### reliant forge ci migration-safety

Run SQL migration safety checks based on forge.yaml config

Checks SQL migrations for patterns that pass on empty databases but fail or lock populated databases.

```
reliant forge ci migration-safety
```

***

#### 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 run <run-id> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | - | Env whose declared control plane to ask (default: the project's only one) |
| `--json` | `bool` | - | Emit the timeline as JSON (same exit codes as text mode) |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |

***

#### 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 summarize <doc.json>...
```

***

#### 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 validate-kcl
```

***

#### reliant forge ci verify-generated

Verify generated code is pristine and up to date

Three checks, all local to the checkout:

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

```
reliant forge ci verify-generated
```

***

#### reliant forge ci verify-test-run

Verify a `go test -json` run actually ran its tests (reads output; runs nothing)

Reads a `go test -json` stream and reports the packages that skipped so much
that their pass proves nothing.

This command RUNS NO TESTS. It reads the record of the run your project
already did, so it costs one extra flag and no extra time:

go test -json ./... | tee test.json | forge ci verify-test-run
go test -json ./... > test.json; forge ci verify-test-run --from test.json

`go test -json` swallows the human-readable output, hence the `tee` — keep the
raw stream for a human and hand a copy to forge.

TWO RULES, AND WHY NOT MORE. Skips are legitimate: `-short` exists, framework
limitations get documented skips, and one reference package keeps a genuine
unconditional skip even when fully configured. A gate that fires on every skip
is a gate that gets switched off. So:

zero-evidence   every test in the package skipped — its "ok" is a statement
about nothing. No sample-size floor; unambiguous at any size.
mass-skip       the package skipped more than --max-skip-ratio of its tests
and has at least --min-tests of them.

Healthy packages are never listed. When a package's heavy skipping is genuinely
expected — an integration-only package on a machine with no docker — declare it
once in forge.yaml, with a reason:

ci:
test\_skips:
allow:

* package: internal/dockerintegration
  reason: "every test here needs a live docker daemon"

The reason is required and is read by humans, not by forge: an exemption nobody
had to justify is one nobody will revisit. A declaration that stops suppressing
anything is reported as no longer needed rather than left to rot.

WHICH RUN TO POINT IT AT. The one whose green you are treating as coverage.
A deliberately-reduced run (`-short`, a single package, a -run filter) is not a
claim about the whole suite, so gating it teaches people to ignore the gate;
forge cannot see the flags a stream was produced with and will not guess.

THREE STATES. Input that carries no `go test -json` events, or a stream that
ends mid-run, is UNDETERMINED — forge could not obtain the facts. That is not a
pass and it exits non-zero: this command never reports a clean run it did not
read. Failures in the stream also fail the command, because
`go test -json ./... | forge ci verify-test-run` in a shell without
`set -o pipefail` reports only the LAST command's status — a checker that
ignored them would launder a red suite green.

```
reliant forge ci verify-test-run [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--from` | `string` | - | Read the `go test -json` stream from this file instead of stdin |
| `--gate-json` | `string` | - | Also write this run's result to `FILE` as a gate document, for `forge gate record` or `forge env deploy --gate`. A FILE, not a stdout mode: the report and the exit code are unchanged. |
| `--max-skip-ratio` | `float64` | `0.5` | Share of a package's tests that may skip before it is reported |
| `--min-tests` | `int` | `5` | Sample-size floor for the mass-skip rule (a package that skipped EVERY test is reported regardless) |
| `--warn-only` | `bool` | - | Report skip findings without failing (adoption ramp; UNDETERMINED and test failures still fail) |

***

#### 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 ci vuln-scan [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--all` | `bool` | - | Run all scanners enabled in forge.yaml (default) |
| `--go` | `bool` | - | Run govulncheck only |
| `--npm` | `bool` | - | Run npm audit only |

***

### reliant forge cloud

Talk to the hosted control plane an environment declares

Commands that reach the hosted control plane declared by an environment's
forge.ControlPlane block.

The endpoint is per-environment and comes from KCL, so which server a
command hits follows from its env argument — not from any stored
"current context".

Authenticate with `forge login`, or set the declared token env var.

```
reliant forge cloud
```

**Subcommands:**

| Command | Description |
| - | - |
| [`releases`](#reliant-forge-cloud-releases) | List releases from the hosted control plane |
| [`status`](#reliant-forge-cloud-status) | Show the endpoint and credential source for an environment |
| [`token`](#reliant-forge-cloud-token) | Mint, list and revoke ORG automation tokens (the CI deploy credential) |

***

#### 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 releases <env> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Emit the raw response as JSON |
| `--limit` | `int` | `20` | Maximum releases to return |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |

***

#### reliant forge cloud status

Show the endpoint and credential source for an environment

```
reliant forge cloud status <env> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |

***

#### reliant forge cloud token

Mint, list and revoke ORG automation tokens (the CI deploy credential)

Manage the organization's automation tokens on the hosted control plane.

An org token has no acting user, so it outlives any one person — which is what
a CI pipeline's FORGE\_CONTROL\_PLANE\_TOKEN must be. Minting needs a human session
(an org admin, via `forge login`); the control plane refuses a machine credential.

Typical setup for the scaffolded release workflow:

forge cloud token create --env prod --name github-actions \
\--scopes deploy:read,deploy:write --json | jq -r .secret \
\| gh secret set FORGE\_CONTROL\_PLANE\_TOKEN

```
reliant forge cloud token
```

**Subcommands:**

| Command | Description |
| - | - |
| [`create`](#reliant-forge-cloud-token-create) | Mint an org automation token; its secret is printed ONCE |
| [`list`](#reliant-forge-cloud-token-list) | List the org's automation tokens (never their secrets) |
| [`revoke`](#reliant-forge-cloud-token-revoke) | Revoke an org automation token, immediately |

***

#### reliant forge cloud token create

Mint an org automation token; its secret is printed ONCE

```
reliant forge cloud token create --env <env> --name <name> --scopes <s1,s2> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | - | Env whose declared control plane to use (required) |
| `--expires-in` | `duration` | `0s` | Expire after this long (default: never) |
| `--json` | `bool` | - | Emit machine-readable JSON |
| `--name` | `string` | - | What the token is for, e.g. github-actions (required) |
| `--scopes` | `string` | - | Comma-separated scopes, e.g. deploy:read,deploy:write (required) |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |

***

#### reliant forge cloud token list

List the org's automation tokens (never their secrets)

```
reliant forge cloud token list --env <env> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | - | Env whose declared control plane to use (required) |
| `--include-revoked` | `bool` | - | Also list revoked tokens |
| `--json` | `bool` | - | Emit machine-readable JSON |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |

***

#### reliant forge cloud token revoke

Revoke an org automation token, immediately

```
reliant forge cloud token revoke <token-id> --env <env> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | - | Env whose declared control plane to use (required) |
| `--json` | `bool` | - | Emit machine-readable JSON |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |

***

### reliant forge cluster

Manage the local k3d cluster and inspect dev state

Manage the local k3d development cluster and inspect dev-loop state.

forge cluster owns the universal mechanics every k8s-targeting forge
project needs: k3d cluster lifecycle, ingress URLs, status, logs.
Project-specific orchestration (sibling-repo deploys, helm chart
bootstraps, webhook listeners) lives in your scripts/ and Taskfile.yml —
composed with the forge cluster primitives.

The cluster config is read from deploy/k3d.yaml (override via --config).
Lifecycle subcommands pin the kubectl context to k3d-`<cluster-name>` as a
guardrail against accidental prod-context leaks.

Examples:
forge cluster up             # create k3d cluster from deploy/k3d.yaml
forge cluster reload         # re-render KCL + kubectl apply + wait rollout
forge cluster urls           # print the ingress URL table for the env
forge cluster status         # cluster + pods + ingress URLs
forge cluster logs --service api  # kubectl logs -f for a service
forge cluster instances      # list every forge dev namespace on the host

forge cluster connect prod-us --context gke\_acme\_us-central1\_prod --env prod
forge cluster disconnect prod-us --env prod   # a cluster YOU operate, as a deploy target

```
reliant forge cluster
```

**Subcommands:**

| Command | Description |
| - | - |
| [`connect`](#reliant-forge-cluster-connect) | Register a Kubernetes cluster the control plane may deploy into |
| [`disconnect`](#reliant-forge-cluster-disconnect) | Deregister a connected cluster and remove what forge created in it |
| [`down`](#reliant-forge-cluster-down) | Delete the k3d cluster |
| [`info`](#reliant-forge-cluster-info) | Print static dev-loop config (cluster name, expected context, declared ports) |
| [`instances`](#reliant-forge-cluster-instances) | List every forge dev namespace on every reachable cluster |
| [`logs`](#reliant-forge-cluster-logs) | Stream kubectl logs for one or all services in the dev namespace |
| [`reload`](#reliant-forge-cluster-reload) | Re-render deploy/kcl/dev + kubectl apply + wait rollout |
| [`reset`](#reliant-forge-cluster-reset) | Delete then recreate the cluster |
| [`status`](#reliant-forge-cluster-status) | Print dynamic dev-loop state (cluster up/down, pods, ingress URLs) |
| [`up`](#reliant-forge-cluster-up) | Create the k3d cluster from deploy/k3d.yaml |
| [`urls`](#reliant-forge-cluster-urls) | Print the ingress URL table for the dev env |

***

#### 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 connect <name> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--auth` | `string` | `auto` | How the hub authenticates: auto, gcp or token |
| `--context` | `string` | - | kubectl context addressing the cluster (required) |
| `--dry-run` | `bool` | - | Print what would be sent and applied, and contact nothing |
| `--env` | `string` | - | Environment whose control plane to talk to (required) |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |

***

#### 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 disconnect <name> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--context` | `string` | - | kubectl context to delete forge's objects through |
| `--env` | `string` | - | Environment whose control plane to talk to (required) |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |

***

#### reliant forge cluster down

Delete the k3d cluster

Delete the k3d cluster named by --config's metadata.name, and FIRST every
cluster nested on its docker network (declared with `owner`, so it has no
config file of its own and cannot outlive its owner's network).

Given an environment, delete every k3d cluster its KCL declares instead —
including secondaries declared with `owner` and no config file, which
\--config cannot name. Secondaries are deleted before their owner: k3d cannot
remove a docker network a secondary is still attached to.

This deletes whole clusters, and with them every namespace on them — including
other environments' and other worktrees' stacks sharing the cluster.

```
reliant forge cluster down [environment] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--config` | `string` | `deploy/k3d.yaml` | k3d config file (ignored when an environment is given) |

***

#### reliant forge cluster info

Print static dev-loop config (cluster name, expected context, declared ports)

Print the static dev-loop config declared in forge.yaml + deploy/k3d.yaml.

Static means "what the project says it expects" — cluster name, expected
kubectl context, registry URL, declared service/frontend ports. It does
NOT contact the cluster or check pod state.

For dynamic state (is the cluster up? are pods running? what are the
live ingress URLs?) use `forge cluster status`.

```
reliant forge cluster info [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--config` | `string` | `deploy/k3d.yaml` | k3d config file |

***

#### reliant forge cluster instances

List every forge dev namespace on every reachable cluster

List every forge-managed dev namespace on the host.

Inspects each k3d cluster's kubeconfig context and reports the
namespaces labelled app.kubernetes.io/managed-by=forge. Useful for the
multi-worktree workflow: many worktrees, each with its own namespace,
all sharing one cluster (or one per worktree).

Examples:
forge cluster instances
forge cluster instances --json

```
reliant forge cluster instances [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Emit machine-readable JSON |

***

#### reliant forge cluster logs

Stream kubectl logs for one or all services in the dev namespace

Stream kubectl logs.

Without --service, streams logs from every pod managed by forge in the
dev namespace (the same label forge uses to discover deployments).
With --service, scopes to one service deployment.

Examples:
forge cluster logs                       # tail every forge-managed pod
forge cluster logs --service api         # tail one service
forge cluster logs --service api --tail 200
forge cluster logs --no-follow           # one-shot, no streaming

```
reliant forge cluster logs [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--config` | `string` | `deploy/k3d.yaml` | k3d config file |
| `--follow` | `bool` | `true` | Stream new logs (kubectl logs -f) |
| `--service` | `string` | - | Scope to one service deployment |
| `--tail` | `int` | `100` | Lines of recent log file to display (-1 = all) |

***

#### reliant forge cluster reload

Re-render deploy/kcl/dev + kubectl apply + wait rollout

Re-render the dev KCL manifests, apply, and wait for rollout.

This is the inner loop during local development: after editing code or
KCL, run this to push the change into the cluster without rebuilding the
docker image (the same code path forge env deploy dev uses, but skips the
cluster bootstrap).

Examples:
forge cluster reload
forge cluster reload --image-tag dev-2026-06-01
forge cluster reload --dry-run

```
reliant forge cluster reload [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--config` | `string` | `deploy/k3d.yaml` | k3d config file |
| `--dry-run` | `bool` | - | Print manifests without applying |
| `--image-tag` | `string` | - | Image tag (default: git short SHA) |
| `--namespace` | `string` | - | Override namespace from environment config |

***

#### reliant forge cluster reset

Delete then recreate the cluster

```
reliant forge cluster reset [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--config` | `string` | `deploy/k3d.yaml` | k3d config file |
| `--wait` | `bool` | `true` | Wait until cluster nodes are ready after recreate |

***

#### reliant forge cluster status

Print dynamic dev-loop state (cluster up/down, pods, ingress URLs)

Print the dynamic state of the local dev environment.

Dynamic means "what's actually happening right now" — does the k3d
cluster exist, what's the current kubectl context, what pods are in
the dev namespace, what ingress URLs are exposed by the dev env's
KCL gateways, what sibling dev namespaces exist on this cluster.

For static config (declared cluster name, expected context, declared
service/frontend ports) run `forge cluster info`.

Examples:
forge cluster status
forge cluster status --json    # machine-readable for scripts/dashboards

```
reliant forge cluster status [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--config` | `string` | `deploy/k3d.yaml` | k3d config file |
| `--json` | `bool` | - | Emit machine-readable JSON |

***

#### reliant forge cluster up

Create the k3d cluster from deploy/k3d.yaml

Create the k3d cluster from deploy/k3d.yaml.

If the cluster already exists, this is a no-op success. With --wait,
blocks until the cluster's nodes report ready.

Given an environment, the clusters come from that environment's KCL instead
of a k3d config file: every forge.Cluster the env's bundle declares is ensured exactly
as `forge env up <env>` ensures it — the declared pod/Service CIDRs, API port,
owner network and registry-inherit included, none of which a k3d YAML carries.
Use it whenever the env declares its clusters: a cluster created from the bare
config file lacks those fields, and the env's own deploy then refuses it.

Examples:
forge cluster up
forge cluster up --wait
forge cluster up --config deploy/k3d.custom.yaml
forge cluster up dev-k8s --wait

```
reliant forge cluster up [environment] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--config` | `string` | `deploy/k3d.yaml` | k3d config file (ignored when an environment is given) |
| `--wait` | `bool` | - | Wait until cluster nodes are ready |

***

#### reliant forge cluster urls

Print the ingress URL table for the dev env

Print the ingress URL table for the dev env.

Reads deploy/kcl/dev/ via the same KCL render the deploy pipeline
uses, then prints one URL per HTTPRoute/GRPCRoute grouped by gateway
and listener.

When features.ingress is disabled this prints a short notice and
exits 0. When the dev env has no gateways declared yet it prints a
pointer to deploy/kcl/dev/ingress.k.

Examples:
forge cluster urls
forge cluster urls --json    # machine-readable for scripts/dashboards

```
reliant forge cluster urls [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Emit machine-readable JSON |

***

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

**Subcommands:**

| Command | Description |
| - | - |
| [`install`](#reliant-forge-component-install) | Install components into your project |
| [`list`](#reliant-forge-component-list) | List all available components |
| [`search`](#reliant-forge-component-search) | Search components by keyword |

***

#### 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 install <component-names...> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dir`, `-d` | `string` | - | target directory (default: auto-detect nearest frontend) |

***

#### reliant forge component list

List all available components

```
reliant forge component list [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--category`, `-c` | `string` | - | filter by category (layouts, charts, diagrams, deck, ui) |

***

#### reliant forge component search

Search components by keyword

```
reliant forge component search <query>
```

***

### reliant forge db

Database and migration commands

Manage database migrations.

Forge uses a migration-first database model:

* Checked-in SQL migrations in db/migrations/ are the source of truth
* golang-migrate is the canonical migration runner
* Entity types are projections of the applied schema (forge generate)

When something is wedged:

A migration failed part-way and the state is marked dirty
No further migration will run until the flag clears, and 'migrate up'
refuses too. Repair the schema by hand, then:
forge db migrate force `<version>`   # record it as applied, runs no SQL
forge db migrate up
Or, on a scratch dev database, skip the repair entirely:
forge db reset                     # DROP, recreate, migrate, seed (dev-only)

A migration cannot apply because existing rows violate it
Adding a constraint to a column seeding filled with placeholders wedges
both repairs against each other: 'seed reset' refuses because the schema
is behind, 'migrate up' refuses because of the rows. Discard the state:
forge db reset                     # needs no dirty-state reasoning (dev-only)

The dev database is full of bad or stale rows
Do not drop the database by hand:
forge db seed reset                # delete seeded rows and re-seed (dev-only)
forge db reset                     # or rebuild the whole database (dev-only)

Seeded rows are rejected by their own schema
'forge db seed apply' names the constraint it could not place, and why.
Load the db/seeding skill for the constraint shapes forge can seed.

Project files (db/migrations, db/seeds/vocab.yaml, db/seeds/custom/) are read
from the project root: the one -C names, or the one the current directory is
in. A relative --dir resolves against that root too.

```
reliant forge db
```

**Subcommands:**

| Command | Description |
| - | - |
| [`introspect`](#reliant-forge-db-introspect) | Inspect the migrated database schema |
| [`migrate`](#reliant-forge-db-migrate) | Run migration lifecycle commands with golang-migrate |
| [`migration`](#reliant-forge-db-migration) | Create and re-version forward-only SQL migration files |
| [`reset`](#reliant-forge-db-reset) | DROP the dev database, recreate it, migrate to head, and seed (dev-only) |
| [`seed`](#reliant-forge-db-seed) | Materialize deterministic development seed data at runtime |
| [`squash`](#reliant-forge-db-squash) | Collapse N migrations into one canonical baseline (.up.sql) |

***

#### 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" --table users
  forge db introspect --dsn "$DATABASE\_URL" --format json

```
reliant forge db introspect [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dsn` | `string` | - | PostgreSQL connection string (required) |
| `--format` | `string` | `text` | Output format: text, json |
| `--table` | `string` | - | Filter to a specific table |

***

#### reliant forge db migrate

Run migration lifecycle commands with golang-migrate

Apply, inspect, and repair migration state using golang-migrate.

There is no "down": forge rolls forward only. A bad migration is repaired by a
new forward migration written against the state the database is actually in.

Migrations are stored in db/migrations/ by default.
Install golang-migrate from [https://github.com/golang-migrate/migrate/tree/master/cmd/migrate](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
```

**Subcommands:**

| Command | Description |
| - | - |
| [`force`](#reliant-forge-db-migrate-force) | Clear a dirty migration state by recording a version without running SQL |
| [`status`](#reliant-forge-db-migrate-status) | Show migration status |
| [`up`](#reliant-forge-db-migrate-up) | Apply pending migrations |
| [`version`](#reliant-forge-db-migrate-version) | Show the current migration version |

***

#### 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 force [version] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dir` | `string` | - | Migrations directory (default: forge.yaml database.migrations\_dir, else db/migrations; a relative path resolves against the project root, not the CWD) |
| `--dsn` | `string` | - | Database connection string (falls back to \$DATABASE\_URL) |

***

#### reliant forge db migrate status

Show migration status

```
reliant forge db migrate status [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dir` | `string` | - | Migrations directory (default: forge.yaml database.migrations\_dir, else db/migrations; a relative path resolves against the project root, not the CWD) |
| `--dsn` | `string` | - | Database connection string (falls back to \$DATABASE\_URL) |

***

#### reliant forge db migrate up

Apply pending migrations

```
reliant forge db migrate up [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dir` | `string` | - | Migrations directory (default: forge.yaml database.migrations\_dir, else db/migrations; a relative path resolves against the project root, not the CWD) |
| `--dsn` | `string` | - | Database connection string (falls back to \$DATABASE\_URL) |

***

#### reliant forge db migrate version

Show the current migration version

```
reliant forge db migrate version [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dir` | `string` | - | Migrations directory (default: forge.yaml database.migrations\_dir, else db/migrations; a relative path resolves against the project root, not the CWD) |
| `--dsn` | `string` | - | Database connection string (falls back to \$DATABASE\_URL) |

***

#### reliant forge db migration

Create and re-version forward-only SQL migration files

Create a new forward-only SQL migration in db/migrations/, or re-version one.

Versions are 14-digit UTC timestamps (`<YYYYMMDDHHMMSS>`\_`<name>`.up.sql), never
sequential numbers. A sequential allocator picks max+1 by reading the
directory, which is collision-free on one checkout and collision-prone across
branches: every branch cut from the same commit reads the same highest number
and picks the same next one. A timestamp cannot collide without two branches
allocating in the same second. Existing sequential files are never renamed —
a 14-digit timestamp sorts after any 5-digit number, so adoption costs nothing.

Commands:
new     allocate a fresh timestamp and scaffold the .up.sql file
rebase  re-version an existing file whose version the schema has passed

The .up.sql file includes rich schema context so LLMs can immediately write
the migration SQL. Context includes:

* Current schema (parsed from existing migrations, or from DB with --dsn)
* Previous migration content
* Migration history

Examples:
forge db migration new add\_users\_table
forge db migration new add\_preferences --dsn "\$DATABASE\_URL"
forge db migration new "backfill account status" --dir db/migrations
forge db migration rebase db/migrations/20260101120000\_add\_users.up.sql
forge db migration rebase --all-pending

```
reliant forge db migration
```

**Subcommands:**

| Command | Description |
| - | - |
| [`new`](#reliant-forge-db-migration-new) | Create a new forward-only migration with a fresh UTC timestamp version |
| [`rebase`](#reliant-forge-db-migration-rebase) | Re-version migrations to fresh timestamps that sort after everything merged |

***

#### 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 new [name] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dir` | `string` | - | Migrations directory (default: forge.yaml database.migrations\_dir, else db/migrations; a relative path resolves against the project root, not the CWD) |
| `--dsn` | `string` | - | Database connection string for live schema introspection |

***

#### reliant forge db migration rebase

Re-version migrations to fresh timestamps that sort after everything merged

Re-version one or more migrations to fresh UTC timestamps.

WHEN YOU NEED THIS. A migration whose version is below the schema's current
version can never run: golang-migrate applies only what is above its recorded
high-water mark, so the file is not pending, it is invisible. That happens one
way — branches A and B are cut, A allocates the earlier timestamp, and B
merges and deploys first. Main now holds A's SQL at a version the database has
already passed, and every later `up` reports the schema current. The first
symptom is a query for a column that does not exist.

The file has not run anywhere, so re-versioning it is safe and is the fix.
Both the migrator's refusal and `forge lint`'s version rules point here.

Each file keeps its name stem and gains a fresh timestamp that sorts after
every version in the directory AND after the highest version on the default
branch, allocated by the same allocator as `forge db migration new`. Tracked
files are moved with `git mv`. A batch keeps its relative order.

WHAT IT REFUSES. A migration already on the default branch keeps its version,
always. Some database has recorded it as applied under its current filename,
and renaming it would leave that database with a recorded version whose file
no longer exists — worse than the problem, and unrecoverable without editing
schema\_migrations by hand. Write a new forward migration instead.

Examples:
forge db migration rebase db/migrations/20260101120000\_add\_users.up.sql
forge db migration rebase --all-pending

```
reliant forge db migration rebase [file...] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--all-pending` | `bool` | - | Re-version every migration added on this branch (above the default branch's merge-base) |
| `--dir` | `string` | - | Migrations directory (default: forge.yaml database.migrations\_dir, else db/migrations; a relative path resolves against the project root, not the CWD) |

***

#### reliant forge db reset

DROP the dev database, recreate it, migrate to head, and seed (dev-only)

DROP the dev database, recreate it empty, apply every migration, and seed.
One verb for "this scratch database is wedged — just rebuild it".

This is the way out of a state nothing else can exit. When a migration fails
part-way the database is marked dirty, and the two repairs block each other:
'forge db seed reset' refuses because the schema is behind, while
'forge db migrate up' refuses because the rows only seed reset can delete
violate the new constraint. Adding a foreign key to a column that seeding
filled with placeholders is enough to produce it.

reset needs none of that reasoning because it DISCARDS the state rather than
repairing it. There is no dirty flag to clear and no row to fix: the database
is gone and rebuilt from db/migrations.

It is destructive and dev-only, so it is gated three ways:

* the environment must be confirmed development (from deploy/kcl/`<env>`/config.k);
  there is no override flag
* the connection string must be the one that environment declares — a DSN
  forge cannot reconcile with `<env>` is refused, not assumed. Unlike
  'forge db seed apply', an arbitrary loopback --dsn is NOT accepted: this
  command DROPs the database, and other projects' databases live on
  loopback too
* the resolved host and database name are printed and must be confirmed;
  pass --yes for non-interactive use

Prefer 'forge db seed reset' when only the ROWS are bad: it keeps your schema
and migration state, so there is nothing to re-migrate afterwards.

Examples:
forge db reset                    # confirm interactively
forge db reset --yes              # non-interactive (CI, scripts)
forge db reset --dsn "\$DATABASE\_URL" --yes

```
reliant forge db reset [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dir` | `string` | - | Migrations directory (default: forge.yaml database.migrations\_dir, else db/migrations; a relative path resolves against the project root, not the CWD) |
| `--dsn` | `string` | - | Database connection string (falls back to \$DATABASE\_URL, then what the env declares) |
| `--env` | `string` | `dev` | Target environment (must be dev; there is no override) |
| `--yes` | `bool` | - | Skip the confirmation prompt (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
```

**Subcommands:**

| Command | Description |
| - | - |
| [`apply`](#reliant-forge-db-seed-apply) | Materialize seed data into the dev database (dev-only) |
| [`reset`](#reliant-forge-db-seed-reset) | Delete seeded rows (child-first) and re-seed (dev-only) |
| [`status`](#reliant-forge-db-seed-status) | Show per-table seeded-row counts vs the seed model |

***

#### reliant forge db seed apply

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

```
reliant forge db seed apply [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--allow-remote-dsn` | `bool` | - | Let --dsn name a NON-loopback server that is not the env's declared database (the env must still be dev) |
| `--dir` | `string` | - | Migrations directory (default: forge.yaml database.migrations\_dir, else db/migrations; a relative path resolves against the project root, not the CWD) |
| `--dsn` | `string` | - | Database to seed: any loopback database (localhost, 127.0.0.1, ::1, unix socket), or the env's own (default: what the env declares, or \$DATABASE\_URL when it is that database) |
| `--env` | `string` | `dev` | Target environment (must be dev; there is no override) |

***

#### 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 reset [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--allow-remote-dsn` | `bool` | - | Let --dsn name a NON-loopback server that is not the env's declared database (the env must still be dev) |
| `--dir` | `string` | - | Migrations directory (default: forge.yaml database.migrations\_dir, else db/migrations; a relative path resolves against the project root, not the CWD) |
| `--dsn` | `string` | - | Database to seed: any loopback database (localhost, 127.0.0.1, ::1, unix socket), or the env's own (default: what the env declares, or \$DATABASE\_URL when it is that database) |
| `--env` | `string` | `dev` | Target environment (must be dev; there is no override) |

***

#### reliant forge db seed status

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

```
reliant forge db seed status [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dir` | `string` | - | Migrations directory (default: forge.yaml database.migrations\_dir, else db/migrations; a relative path resolves against the project root, not the CWD) |
| `--dsn` | `string` | - | Database connection string (falls back to \$DATABASE\_URL) |

***

#### reliant forge db squash

Collapse N migrations into one canonical baseline (.up.sql)

Squash applies every migration in --from-dir against an ephemeral Postgres
container, dumps the resulting schema + seed data with pg\_dump, and writes
a single forward-only baseline migration.

Output:
`<out-dir>`/`<baseline>`.up.sql    (CREATE statements + INSERTs for non-schema\_migrations rows)

No .down.sql is written: forge rolls forward only.

This is the canonical "N migrations → one baseline" workflow used when
pulling a long-lived schema into a new project, or when collapsing
historical migrations into a checkpoint. Paths resolve against the project
root (-C, or the project the current directory is in).

Requires:

* docker (for the ephemeral postgres)
* migrate (golang-migrate CLI) on PATH
* pg\_dump on PATH (matching the postgres image major version)

Examples:
forge db squash
forge db squash --from-dir db/migrations --to 00001\_baseline
forge db squash --to 20260506\_baseline --out-dir db/baselines/

```
reliant forge db squash [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--db-name` | `string` | `forge_squash` | Database name created inside the ephemeral container |
| `--db-pass` | `string` | `forge_squash` | Database password for the ephemeral container |
| `--db-user` | `string` | `postgres` | Database user (default container superuser) |
| `--from-dir` | `string` | - | Source directory holding the migrations to squash (default: the project's migrations directory; a relative path resolves against the project root) |
| `--image` | `string` | `postgres:16-alpine` | Postgres docker image used for the ephemeral container |
| `--out-dir` | `string` | - | Output directory for the baseline files (default: same as --from-dir; a relative path resolves against the project root) |
| `--to` | `string` | `00001_baseline` | Baseline filename stem (writes `<stem>`.up.sql) |

***

### reliant forge debug

Debug a running service with Delve

Debug Go services with Delve.

Session state is persisted to .forge/debug-session.json so subsequent
commands (break, continue, eval, ...) reconnect to the same debugger.

Examples:
forge debug start api-gateway
forge debug break handler.go:42
forge debug continue
forge debug eval "req.UserID"
forge debug stop

```
reliant forge debug
```

**Subcommands:**

| Command | Description |
| - | - |
| [`args`](#reliant-forge-debug-args) | Show function arguments in the current scope |
| [`break`](#reliant-forge-debug-break) | Set a breakpoint |
| [`breakpoints`](#reliant-forge-debug-breakpoints) | List all breakpoints |
| [`clear`](#reliant-forge-debug-clear) | Clear a breakpoint by ID |
| [`continue`](#reliant-forge-debug-continue) | Resume execution until the next breakpoint |
| [`eval`](#reliant-forge-debug-eval) | Evaluate an expression in the current scope |
| [`goroutines`](#reliant-forge-debug-goroutines) | List goroutines |
| [`locals`](#reliant-forge-debug-locals) | Show local variables in the current scope |
| [`stack`](#reliant-forge-debug-stack) | Show the current call stack |
| [`start`](#reliant-forge-debug-start) | Start a debug session for a service |
| [`step`](#reliant-forge-debug-step) | Step over the current line |
| [`step-in`](#reliant-forge-debug-step-in) | Step into the current function call |
| [`step-out`](#reliant-forge-debug-step-out) | Step out of the current function |
| [`stop`](#reliant-forge-debug-stop) | Stop the debug session (detaches from attached processes; kills only what forge launched) |

***

#### reliant forge debug args

Show function arguments in the current scope

```
reliant forge debug args [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output as JSON |

***

#### reliant forge debug break

Set a breakpoint

Set a breakpoint at a file:line location or on a function by name.

The positional argument is auto-detected: a "file.go:42" spec sets a
source-line breakpoint, while anything else (e.g. "main.handleRequest",
"runtime.gopark", "(\*Server).Serve") is resolved as a function breakpoint
via Delve's location parser. The --func flag forces function resolution.

Examples:
forge debug break handler.go:42
forge debug break main.handleRequest
forge debug break runtime.gopark
forge debug break '(\*Server).Serve'
forge debug break --func main.handleRequest
forge debug break handler.go:42 --cond "id > 5"

```
reliant forge debug break <file:line | function> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--cond` | `string` | - | Conditional expression for the breakpoint |
| `--func` | `string` | - | Set breakpoint on a function by name |
| `--json` | `bool` | - | Output as JSON |

***

#### reliant forge debug breakpoints

List all breakpoints

```
reliant forge debug breakpoints [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output as JSON |

***

#### reliant forge debug clear

Clear a breakpoint by ID

```
reliant forge debug clear <id> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output as JSON |

***

#### reliant forge debug continue

Resume execution until the next breakpoint

```
reliant forge debug continue [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output as JSON |

***

#### reliant forge debug eval

Evaluate an expression in the current scope

Evaluate a Go expression in the debugger's current scope.

Examples:
forge debug eval "req.UserID"
forge debug eval "len(items)"

```
reliant forge debug eval <expression> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output as JSON |

***

#### reliant forge debug goroutines

List goroutines

```
reliant forge debug goroutines [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output as JSON |

***

#### reliant forge debug locals

Show local variables in the current scope

```
reliant forge debug locals [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output as JSON |

***

#### reliant forge debug stack

Show the current call stack

```
reliant forge debug stack [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--depth` | `int` | `50` | Maximum stack depth |
| `--json` | `bool` | - | Output as JSON |

***

#### reliant forge debug start

Start a debug session for a service

Start a debug session for a Go service.

The binary is built with debug flags (-gcflags=all=-N -l) and launched under Delve.

If the argument contains "/" or ".", it is treated as a direct path.
Otherwise it is looked up by name in forge.yaml.

Examples:
forge debug start api-gateway
forge debug start --attach 12345
forge debug start --port 2345 api-gateway
forge debug start ./cmd/server

```
reliant forge debug start <service> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--attach` | `int` | `0` | Attach to an existing process by PID |
| `--docker` | `bool` | - | Start debug session in Docker container |
| `--json` | `bool` | - | Output as JSON |
| `--port` | `int` | `0` | Debugger listen port (0 = auto) |

***

#### reliant forge debug step

Step over the current line

```
reliant forge debug step [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output as JSON |

***

#### reliant forge debug step-in

Step into the current function call

```
reliant forge debug step-in [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output as JSON |

***

#### reliant forge debug step-out

Step out of the current function

```
reliant forge debug step-out [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output as JSON |

***

#### reliant forge debug stop

Stop the debug session (detaches from attached processes; kills only what forge launched)

Stop the debug session.

For sessions forge launched (forge debug start `<service>` / --docker), stop
kills the debugged process and reaps the dlv server.

For an ATTACH session (forge debug --attach `<pid>`), stop DETACHES the
debugger and leaves the target process running. forge never kills a process
it did not launch.

```
reliant forge debug stop
```

***

### reliant forge doctor

Check that the PROJECT is well-formed (deployability, payload caps, tooling, cluster capability)

Check that this project is healthy and well-formed.

Everything doctor checks is answerable from the project's own artefacts,
across every declared environment, with nothing running:

* deployability of the rendered manifests — probes, resource requests
  and limits, credentials sourced from Secrets, ServiceAccounts bound to
  pods, a way to apply pending migrations,
* Connect payload caps,
* disowned generated files and dead forge.yaml keys,
* the host tools forge shells out to (and their minimum versions),
* cluster CAPABILITY: does the cluster carry the GatewayClass /
  ClusterIssuer this project's manifests require.

Doctor takes no --env, because it never asks about one.

For "is the stack for environment X up and reachable" — the app's
/healthz, pprof, the compose infra, the telemetry backends, Delve — use:

forge env status `<env>`                 # host services + frontends + runtime checks
forge env status `<env>` --signal traces # one signal only

That command resolves the ports the stack actually bound (rather than
guessing a default), and reports the holder pid, whether the process is
forge-owned, whether its build is stale against HEAD, and whether two
processes are serving one service.

Examples:
forge doctor                  # Check the project
forge doctor --json           # Machine-readable output
forge doctor --verbose        # Show evidence for passing checks
forge doctor --signal deploy  # Deployability gate only (the CI arm)

```
reliant forge doctor [flags]
```

**Subcommands:**

| Command | Description |
| - | - |
| [`parity`](#reliant-forge-doctor-parity) | Diff a service's host-mode vs cluster-mode env+config |

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output results as JSON |
| `--signal` | `string` | - | Run only the deployability gate (deploy). Environment-runtime signals live on 'forge env status `<env>` --signal'. |
| `--timeout` | `duration` | `30s` | Overall timeout for all checks |
| `--verbose`, `-v` | `bool` | - | Show evidence for all checks (not just failures) |

***

#### 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 doctor parity <service> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | `dev` | Environment to compare (dev, staging, prod) |
| `--json` | `bool` | - | Emit a machine-readable JSON document to stdout |

***

### 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](http://www.hounders.club) --env prod --redirect-to hounders.club
forge domain unbind hounders.club --env prod
forge domain rm hounders.club --env prod

Domains are NOT declared in KCL. A hostname you bring needs an action at
your registrar, ownership verification and a certificate — asynchronous and
human-gated, none of which a deploy converges — and it binds to ONE
environment while an env file renders to many. A hosted spec that carries a
`domains` field is refused at render. (forge.OnCluster keeps
`Port.domains`: there you own the ingress.)

\--env names WHICH CONTROL PLANE to talk to, from that env's
forge.ControlPlane declaration — a domain itself has no environment. Your
organization comes from the credential and is never sent, so there is
nothing to widen. The credential is --token, then the declared env var, then
the credentials file entry for that endpoint (`forge login`), then the
credential helper \$FORGE\_CREDENTIAL\_HELPER (a host application's session —
Reliant sets it, so a user signed in to Reliant needs no `forge login`).

```
reliant forge domain
```

**Subcommands:**

| Command | Description |
| - | - |
| [`add`](#reliant-forge-domain-add) | Claim a hostname and print the DNS records to publish |
| [`bind`](#reliant-forge-domain-bind) | Point a domain at one environment's workload or frontend |
| [`ls`](#reliant-forge-domain-ls) | List your organization's domains and what they serve |
| [`rm`](#reliant-forge-domain-rm) | Remove a domain and stop serving it |
| [`show`](#reliant-forge-domain-show) | Show one domain's state, binding and required DNS |
| [`unbind`](#reliant-forge-domain-unbind) | Stop a domain serving, and keep the domain |
| [`verify`](#reliant-forge-domain-verify) | Check a domain's DNS now, instead of waiting for the poller |

***

#### 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 add <hostname> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | - | Environment whose control plane to talk to (required; deploy/kcl/`<env>`/) |
| `--json` | `bool` | - | Emit the created domain as JSON |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |

***

#### 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](http://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 bind <hostname> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | - | Environment whose control plane to talk to (required; deploy/kcl/`<env>`/) |
| `--redirect-to` | `string` | - | Serve a 308 to this hostname instead of a target |
| `--target` | `string` | - | Workload or frontend name in --env to serve |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |

***

#### 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 ls [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | - | Environment whose control plane to talk to (required; deploy/kcl/`<env>`/) |
| `--json` | `bool` | - | Emit the raw response as JSON |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |

***

#### reliant forge domain rm

Remove a domain and stop serving it

Remove a domain, its binding, and stop serving it.

This FREES THE HOSTNAME for another organization to claim, which is the only
self-service escape from a conflict: the org holding it lets go.

To stop serving a domain but KEEP it — and keep its verification, so
re-binding later costs no DNS work and no new certificate — use
`forge domain unbind` instead.

```
reliant forge domain rm <hostname> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | - | Environment whose control plane to talk to (required; deploy/kcl/`<env>`/) |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |

***

#### reliant forge domain show

Show one domain's state, binding and required DNS

```
reliant forge domain show <hostname> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | - | Environment whose control plane to talk to (required; deploy/kcl/`<env>`/) |
| `--json` | `bool` | - | Emit the domain as JSON |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |

***

#### reliant forge domain unbind

Stop a domain serving, and keep the domain

Stop the domain serving, and KEEP the domain.

Its verification survives, so re-binding later costs no DNS work and no new
certificate. To give the hostname up entirely, use `forge domain rm`.

```
reliant forge domain unbind <hostname> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | - | Environment whose control plane to talk to (required; deploy/kcl/`<env>`/) |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |

***

#### 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 domain verify <hostname> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | - | Environment whose control plane to talk to (required; deploy/kcl/`<env>`/) |
| `--json` | `bool` | - | Emit the domain as JSON |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |

***

### reliant forge env

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

```
reliant forge env
```

**Subcommands:**

| Command | Description |
| - | - |
| [`build`](#reliant-forge-env-build) | Build an environment's artifacts; --push publishes them, --release records an immutable release |
| [`config`](#reliant-forge-env-config) | Print the resolved configuration deploy/kcl/`<env>`/ hands each workload |
| [`delete`](#reliant-forge-env-delete) | Tear down a HOSTED environment on the control plane (destructive) |
| [`deploy`](#reliant-forge-env-deploy) | Make an environment run a release: record it, apply it, and wait for health |
| [`devstack`](#reliant-forge-env-devstack) | Parallel-dev-stack host helpers (worktree key + port allocation) |
| [`diff`](#reliant-forge-env-diff) | Render this checkout and diff each environment against what is deployed |
| [`down`](#reliant-forge-env-down) | Stop Forge host processes for an environment (or --all: across projects) |
| [`list`](#reliant-forge-env-list) | List the environments declared in deploy/kcl/ |
| [`new`](#reliant-forge-env-new) | Scaffold a new deploy environment from an existing one |
| [`options`](#reliant-forge-env-options) | List the render options an environment's KCL declares |
| [`ps`](#reliant-forge-env-ps) | List every forge stack running on this machine, across all projects |
| [`render`](#reliant-forge-env-render) | Print the Kubernetes objects deploy/kcl/`<env>`/ renders, with the cluster each lands on |
| [`secrets`](#reliant-forge-env-secrets) | Project the env's secret\_provider into the cluster |
| [`shape`](#reliant-forge-env-shape) | Print what an environment DECLARES — kind, workloads, secrets, domains, and one hash per object |
| [`smoke`](#reliant-forge-env-smoke) | Probe every declared ingress route after deploy (TLS + routing + CORS) |
| [`start`](#reliant-forge-env-start) | Resume a HOSTED environment's workloads on the control plane (not local processes) |
| [`status`](#reliant-forge-env-status) | The one read view of an environment: bound release, rollout, health, verify, gates, ledger |
| [`stop`](#reliant-forge-env-stop) | Suspend a HOSTED environment's workloads on the control plane (not local processes) |
| [`up`](#reliant-forge-env-up) | Bring the whole dev loop up on this machine: build + deploy + host + frontend |

***

#### reliant forge env build

Build an environment's artifacts; --push publishes them, --release records an immutable release

Build the artifacts an environment declares.

Iterates the workloads deploy/kcl/`<env>`/ declares and dispatches on each
one's build.type — go, docker, shell, remote — exactly as the compile-only
`forge build` does. What this command adds is everything that needs
an environment to mean anything:

\--push          publish each image to the reference ITS OWN workload
declares (its image field in deploy/kcl/workloads.k) — the
same reference `forge env deploy` reads, so what is
pushed is what is deployed. Two workloads may name two
different registries; both are pushed. Takes no value and
carries no registry.

\--release vX    record an IMMUTABLE RELEASE: capture every artifact's digest
into a release ledger (.forge/releases/vX.json, or the
control plane when the env declares one).
IMPLIES --push — a release pins registry-addressable
digests, and a local tag cannot be promoted anywhere.

A release is build-once → promote: `forge env deploy <env> vX` pins the
SAME digests in every environment, with no per-env rebuild. The images are
env-agnostic; the env argument supplies the artifact SET to build and the
registries to push to, so pick any env that declares the full set.

CUTTING WITHOUT REBUILDING. In a pipeline where the build and the release are
separate jobs, pass --no-build with --release: the digests an earlier
`--push` recorded in .forge/state are harvested and the release is
recorded without rebuilding anything. Rebuilding to cut would be exactly the
rebuild the release model exists to avoid.

forge env build prod --push                  # job 1: build and push
forge env build prod --release v1.4.0 --no-build   # job 2: record the release
forge env deploy prod v1.4.0                 # bind prod to it

Re-cutting the same version over the same artifacts is a no-op; over
DIFFERENT artifacts it is refused. The cut FAILS if anything the env declares
is missing from the ledger — a release with a hole in it promotes like a
complete one and ships an environment that is missing a piece.

\--release owns the image tag (it IS the release version), so a conflicting
\--tag is refused: a release build pushes under the release version only, so a
cut that fails part-way can never leave a shared, mutable tag pointing at
bytes no release contains.

Examples:
forge env build dev                          # build the env's artifacts
forge env build prod --push                  # build + push to declared registries
forge env build prod --release v1.4.0        # build + push + record the release
forge env build prod --release v1.4.0 --plan # preflight the cut, build nothing
forge env build prod --target api --push     # scope to one workload

```
reliant forge env build <environment> [flags]
```

**Flags:**

| Flag | Type | Default | Description | | |
| - | - | - | - | - | - |
| `--bundle-envs` | `string` | - | Write a bundle for each of these envs (comma-separated) from this one build, instead of only for the env argument. A release is env-agnostic and a bundle is not, so this is how one cut produces the manifests for every env it will ship to — each pinned to the same release and the same source. Each env is rendered; each bundle is pushed beside its images for a control-plane env, or written to this machine's ledger otherwise. | | |
| `--debug` | `bool` | - | Build with debug symbols for Delve | | |
| `--docker` | `bool` | - | Build Docker images for all services (implied by --push and --release) | | |
| `--gate-json` | `string` | - | Also write this build's result to `FILE` as a gate document, for `forge gate record` or `forge env deploy --gate`. A FILE, not a stdout mode: the build log and the exit code are unchanged. | | |
| `--no-build` | `bool` | - | With --release: record the release over the digests an earlier `--push` already captured in .forge/state, without rebuilding. This is the cut-only half of a pipeline whose build and release are separate jobs. | | |
| `--no-generate` | `bool` | - | Skip the pre-build code-generation check. By default this runs forge generate when gen/ is missing or proto sources are newer than the generated tree. | | |
| `--no-run` | `bool` | - | Record no run, even inside CI | | |
| `--option`, `-D` | `stringArray` | `[]` | Set a render option the env's KCL declares, as name=value (repeatable). Relayed to KCL verbatim — forge does not interpret the value. List an env's options with `forge env options <env>`. | | |
| `--output`, `-o` | `string` | `bin` | Output directory for binaries | | |
| `--parallel` | `bool` | `true` | Build services in parallel | | |
| `--plan` | `bool` | - | Resolve the exact build set this invocation would build (same KCL discovery, same --target narrowing) and PREFLIGHT every step without running it: each go-build package exists and is a main package, each Dockerfile and frontend build script exists, each ShellBuild cwd exists, and with --release the ledger would cover everything the env declares. Builds, pushes, generates and writes nothing; exits non-zero on anything the real build would fail on. Pass it the release's exact arguments to gate a PR on the cut. | | |
| `--push` | `bool` | - | Push docker images after build (implies --docker), each to the reference its own workload declares (its image field in deploy/kcl/workloads.k). Takes no value and carries no registry | | |
| `--release` | `string` | - | Record an immutable release with this version label (e.g. v1.4.0). IMPLIES --push. The release's artifact SET (project images plus per-env external build\_cmd images) is discovered from deploy/kcl/`<env>`/main.k; the built images stay env-agnostic, so promote the release to every env with `forge env deploy <env> <version>`. Captures each artifact's digest into a release ledger, which every later deploy pins. | | |
| `--run-id` | `string` | - | CI run this write belongs to (default: detected from the CI environment, e.g. github:`<repo>`/`<run>`/`<attempt>`) | | |
| `--run-url` | `string` | - | Link to the CI run, for a human reading the ledger (default: detected from the CI environment) | | |
| `--tag` | `string` | - | Override the image tag of every image this build writes (default: the tag a workload's image pins, else the env's image\_tag, else git describe --tags --always --dirty). Refused when it differs from the tag a selected workload's image pins, or when it conflicts with --release. Recorded in .forge/state so `forge env deploy` uses the same value. | | |
| `--target`, `-t` | `string` | `all` | Build target (all | external | a specific service/frontend name). `external` builds only the KCL services declaring build\_cmd. |
| `--target-arch` | `string` | - | Override target GOARCH for cross-compilation (default: forge.yaml deploy.target\_arch, then amd64 for docker builds) | | |

***

#### 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 config <env> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Emit machine-readable JSON |
| `--workload` | `string` | - | Only this workload (service, job, frontend, operator or cronjob) |

***

#### reliant forge env delete

Tear down a HOSTED environment on the control plane (destructive)

Delete a hosted environment: its deployments, Flux objects, custom resources
and namespaces on the control plane.

forge env delete preview             prompts: type the env name to confirm
forge env delete preview --yes       no prompt (required with no TTY or CI=1)

DATA RETENTION: a ManagedDatabase's data is RETAINED but orphaned. A later
deploy of the same name creates a NEW environment and does NOT reattach that
data; recovering it is a support operation.

Only hosted environments: for a local stack use `forge env down`, which never
deletes anything on a control plane and which this command never uses.

```
reliant forge env delete <environment> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |
| `--yes` | `bool` | - | Confirm the deletion without the typed-name prompt (required when non-interactive) |

***

#### reliant forge env deploy

Make an environment run a release: record it, apply it, and wait for health

Make `<environment>` run a release. TWO FORMS, and the difference is whether you
name a version.

forge env deploy prod               # SHIP THIS CHECKOUT: build, push, cut a

# release, plan, confirm, deploy, wait

forge env deploy prod v1.4.0        # deploy a release that already exists
forge env deploy prod --from staging  # exactly what staging runs

NO VERSION DOES ALL THE DEPLOYMENT BITS. In order: build the env's artifacts at
the current checkout (images, static artifacts) exactly as `forge env build`
does, push them, cut a release named `<YYYYMMDD>.<HHMMSS>-<tree12>` for this
tree, record the env's declared shape, compute the plan, ask you to confirm it,
then promote, apply and wait. A release whose provenance tree already matches
this checkout is REUSED rather than cut again, so a retried deploy never cuts
twice.

A SCOPED DEPLOY NEEDS A RELEASE. --frontends-only and --target ship part of the
env, so they are refused with NO version: a no-version deploy cuts a release
over every artifact, and cutting one that ships only some of them would record
a release that does not describe what is running. Name the version
(`forge env deploy prod v1.7.1 --frontends-only`) or deploy everything
(`forge env deploy prod`). --dry-run and --explain are unaffected: they cut
nothing, so a scoped preview is an honest question.

NAMING A VERSION BUILDS NOTHING. It deploys a release that was already cut — a
redeploy, a move to an older one, or a spec-change deploy (the KCL moved and the
release did not: name the version the env already runs). A version nobody cut is
an error whose fix is `forge env deploy <env>`, the form that builds it.

NOTHING IS WRITTEN UNTIL YOU CONFIRM. A promotion IS the deploy — the converger
picks it up within minutes — so the plan is printed and approved BEFORE the
write, not after it. On a terminal you are prompted (default no). In CI pass
\--yes, which means "I read the plan". With no terminal and no --yes the command
refuses (exit 5) having built, pushed and cut but written NO promotion, so
approving it afterwards needs no rebuild. --plan-only stops after the plan and
is the first stage of a two-stage pipeline.

\--yes AND --approve ARE NOT THE SAME APPROVAL. --yes means "I read the plan"
and approves whatever forge computes at the moment that command runs.
\--approve `<digest>` approves the plan you actually READ: if Live moved in
between — another deploy landed, a new bundle was applied, drift appeared —
the digest no longer matches and the deploy is refused (exit 3) instead of
shipping a change set nobody saw. Use --yes for a one-shot deploy; use
\--approve whenever the plan and the approval are separate steps.

THE TWO-STAGE PIPELINE, in full:

PLAN=$(forge env deploy prod --plan-only --json)    # builds, cuts, writes NO promotion
  VERSION=$(jq -r .target.release    \<\<\<"$PLAN")      # the release stage one cut
  DIGEST=$( jq -r .deploy\_plan.digest \<\<\<"$PLAN")     # the plan to bind the approval to
  forge env deploy prod "$VERSION" --approve "\$DIGEST"

`next_step` in stage one's document is that final command already assembled,
including `--acknowledge-destructive <codes>` when the plan carries stop-class
findings — those codes cannot be written in advance, because a code is not
known until the plan is computed. Paste it rather than rebuilding it.

A nil `deploy_plan` means no plan could be computed (a never-built env, or a
control plane that predates bundles) — NOT "no changes". There is then no
digest to approve, so `next_step` falls back to the --yes form and says why.

THE VERB IS record + apply + wait, AND THAT IS NOT OPTIONAL. Recording a
binding ships nothing, so a step that only recorded one reported success before
any byte had moved and the release's actual failure surfaced minutes later with
nothing connecting the two. So the health gate is ON by default; --no-wait is
how you opt out, and it says what to run instead.

A HOSTED DEPLOY CAN BE QUEUED, NOT REFUSED. When what the env runs needs
something only a person can provide — billing for its hosted workloads,
managed database or static sites — the control plane still
ACCEPTS the deploy: the release and its bundle are recorded, the promotion is
written, and the env shows "waiting on billing". It goes live by itself the
moment billing is set up; nothing is re-run. forge prints one block — what it
waits on, why, and the URL to act at — and exits 7. --wait instead blocks
(up to --timeout) until it is live. A newer deploy to the env replaces a
queued one.

WHO APPLIES IT IS DECLARED, NOT CHOSEN. An env whose KCL declares
forge.ControlPlane is converged by that control plane — forge records the
promotion and waits on the rollout the server computes. Every other env is
SELF-MANAGED: the same command renders the new binding and applies it from this
machine, and that apply's per-resource rollout wait IS the health gate. Both
reach "the release is live or this command is red"; which machinery got there
is an implementation detail of where the env runs.

`forge env build <env> --release <version>` builds the env-agnostic images
ONCE, captures their content-addressed digests, and cuts a release. Naming that
version here advances it BY REFERENCE: one entry — env, release, and the
per-image digests frozen at this moment — appended to the env's append-only
promotion ledger. No image is rebuilt, so the exact bytes cut as `<version>` are
what every env running that release ships, byte-identical. This eliminates the
per-env rebuild that re-cross-compiles (and can drift arch/tag) for every
environment.

WHERE THE LEDGER LIVES is declared by the environment, not chosen by a flag:
an env whose KCL declares forge.ControlPlane records promotions on that control
plane; every other env records them in .forge/promotions/`<env>`.jsonl.

EVERY DEPLOY IS A NEW ENTRY, NOT AN EDIT. Re-deploying the release an env
already runs appends nothing and still applies and waits; a CI retry is safe.

THERE IS NO ROLLBACK. Recovery is ROLL FORWARD: cut a release with the fix and
deploy it. Binding an env to an OLDER release is still possible — it is an
ordinary deploy — but it cannot undo the newer release: that release's
migrations stay applied and the data it wrote stays written, so the older code
runs against a schema it was never tested on. The plan labels such a move
`direction: BEHIND` and says so; read it before you write it.

SEE THE CHANGE BEFORE IT IS WRITTEN. --plan computes the ENTIRE change set and
writes nothing: the release the env runs now versus the one it would move to,
every image classified as unchanged / changed / added / removed (with both
digests where they differ), the git commits between the two releases, and —
the fact most worth reading twice — the DIRECTION. A deploy to an older
release is reported as BEHIND rather than left for you to infer from version
numbers. The plan and the real deploy are computed by the SAME function, so the
preview cannot disagree with the write.

EVERY RELEASE DEPLOY IS A COMPARE-AND-SET. The write asserts that the env is
still on the promotion the plan read (`current.promotion_id` in --json),
and is REFUSED if someone else moved it since — so a hotfix that lands while a
pipeline waits for approval turns the pipeline red instead of being
overwritten. No flag is needed. --expect-current `<id>` replaces the planned
value with one captured earlier (e.g. when the approval was requested);
`--expect-current unbound` (or --expect-unbound) asserts the env has never
been promoted. Re-deploying the release the env already runs is a no-op
whatever the expectation says, so a retried success is never a conflict.

Exit codes (release deploys):
0  deployed and healthy, or already on this release (no-op), or --plan /
\--plan-only, or a confirmation answered "no" (nothing was written)
1  failed: invalid input, unreadable ledger, release not found, DEGRADED
2  undetermined — we could not look (the control plane was unreachable)
3  promotion\_conflict / source\_moved — the env (or --from's source) moved
since the plan was read. Stop and look; retrying would overwrite it
4  rollout\_in\_flight / environment\_pinned — declined, nothing lost. Wait
and retry, or pass --supersede (recorded) to replace an unfinished rollout
5  plan\_unconfirmed — the plan was printed and nobody approved it: no
terminal to prompt on and no --yes. NOTHING was promoted. Add --yes
6  superseded — the env was promoted past the promotion being waited on
7  queued — the deploy was ACCEPTED and RECORDED and the control plane is
holding it on a person (today: billing for the hosted workloads or
database it runs). Nothing failed and nothing is to be re-run: it goes
live on its own once they act. The output names what it waits on and
the action URL (--json: .queued). --wait blocks until it is live instead
8  the wait's budget expired while the rollout was still progressing

5 USED TO MEAN THE TIMEOUT, which is now 8. The confirmation gate took 5
because it is the one outcome that must be impossible to misread as success:
a pipeline that upgrades without adding --yes exits 5, and even a handler
that still reads 5 as "the wait timed out" fails the job rather than passing
it. Pre-1.0, so this is a clean renumber with no alias.

\--json carries the same outcome: `applied`, `confirmed` (whether the gate let
the write happen), and a `refusal` object naming what was expected and what is
actually there.

Examples:
forge env build prod --release v1.4.0            # build once, cut the release
forge env deploy staging v1.4.0 --plan           # what WOULD change (writes nothing)
forge env deploy staging v1.4.0 --plan --json    # the same, machine-readable
forge env deploy staging v1.4.0                  # record, apply, wait
forge env deploy prod --from staging             # the bytes that passed staging
forge env deploy prod v1.3.0 --plan | grep BEHIND  # catch a backwards move
forge env deploy prod v1.4.0 --gate e2e.json     # freeze evidence onto the entry
ID=$(forge env deploy prod v1.4.0 --plan --json | jq -r '.current.promotion_id // "unbound"')
  forge env deploy prod v1.4.0 --expect-current "$ID"  # after approval: exit 3 if prod moved

─────────────────────────────────────────────────────────────────────────────
THE APPLY (both paths, and the whole of a no-version deploy)

Deploy each service to the target declared on its Service.deploy block.

Supported deploy targets (declared in deploy/kcl/`<env>`/main.k):

* forge.K8sCluster — Kubernetes deployment via render → kubectl apply
  → wait-rollouts. Forge auto-creates a k3d cluster for dev.
* forge.Compose    — docker compose pull + up -d.

forge.HostDeploy and forge.BuildOnly are skipped by deploy — those are
owned by forge run / forge env up and forge build respectively.

Safety (declarative context): the kubectl context is read SOLELY from the
env's KCL — forge.K8sCluster.cluster IS the kubectl context name (e.g.
"gke\_`<project>`\_`<region>`\_prod"; defaults to k3d-`<project>` for dev). Every
kubectl call in the apply/wait/prune/secrets path runs
\--context `<declared>` per command, so the deploy applies to EXACTLY the
cluster the env declares — independent of whatever context is currently
active. There is NO CLI override and NO fall-back to the current context:
the binding lives in the env file, full stop, so you can't deploy the
wrong env to the wrong cluster. forge fails fast (even under --dry-run) if
the declared cluster has no matching kubectl context — the only remedy is
to fix your kubeconfig or the KCL forge.K8sCluster.cluster.

Use --explain to print the declared context, whether it exists in your
kubeconfig, and the verdict without applying.

Machine-readable output: --json emits ONE JSON document covering the whole
invocation, with the same exit code text mode produces. It reports the MODE
actually performed (explain / dry\_run / apply) so a consumer never
has to infer whether bytes moved; the guard verdict, the target cluster +
namespace (every declared context, for a multi-cluster env); whether the
preflight ran and its findings as structured entries; per-image digest-vs-tag
pinning, so a deploy shipping a MUTABLE reference is visible rather than
implied; the resource identities applied (kind/name — a diffable list, not a
YAML dump); and the per-resource rollout outcome as three distinct states:
ready, failed, and timed\_out / not\_waited. A timeout is neither a success nor a
failure — it is the absence of an answer — and the document keeps all three
apart. The human output moves to stderr so stdout carries exactly one document.
Works with --explain and --dry-run, which is how a UI previews a deploy before
asking anyone to confirm it.

Deployability preflight: before the first apply (remote/cloud clusters),
forge verifies against the LIVE target that every Secret KEY the rendered
manifests reference is provisioned and every container image: resolves in
its registry. A missing key (CreateContainerConfigError) or image
(ImagePullBackOff) is reported up front — all at once — and the deploy
refuses to apply, instead of surfacing one pod crash at a time mid-rollout.
The preflight is skipped for local dev clusters and runs under --dry-run as
a pure read-only check. Bypass with --skip-preflight.

Use --target `<app>` (repeatable) to deploy ONLY the named application(s)
instead of the whole env bundle. It filters by app NAME — service,
operator, or frontend: the K8sCluster apply keeps the targeted app's
workload manifests plus all shared resources (Namespace, the shared
ConfigMap/Secret, RBAC), and the External/Compose dispatch + rollout-wait
are scoped to the named apps. A typo'd target errors with the list of
available app names. Targeting an operator (e.g. workspace-controller)
applies just that operator's Deployment + cluster RBAC.

k8s-only deploy: naming only backend apps via --target is itself the
"k8s without touching the frontend" path — a Firebase frontend isn't in
the --target set, so its build+deploy step never runs. To ship the WHOLE
backend bundle while skipping the frontend (without enumerating every
service), pass --skip-frontend: the k8s apply runs as normal and the
Frontend (e.g. Firebase) build+deploy dispatch is skipped.

Examples:
forge env deploy dev                          # Deploy to dev (local k3d)
forge env deploy staging --tag v1.2           # Deploy to staging with specific tag
forge env deploy prod --dry-run               # Preview prod manifests (guard runs)
forge env deploy prod --explain               # Show the declared-cluster guard verdict
forge env deploy dev --namespace custom-ns    # Override namespace
forge env deploy dev --target admin-server    # Deploy only the admin-server app
forge env deploy prod --target workspace-controller # Deploy only that operator
forge env deploy prod --skip-frontend         # Deploy backend k8s, skip Firebase

```
reliant forge env deploy <environment> [version] [flags]
```

**Flags:**

| Flag | Type | Default | Description | | | |
| - | - | - | - | - | - | - |
| `--acknowledge-destructive` | `stringSlice` | `[]` | Accept the named stop-class finding codes (comma-separated), e.g. stateful\_deletion. REQUIRED for every destructive change the plan reports, and --yes does not cover them: --yes is the flag that ends up hard-coded in CI, and one that covered destructive changes would silently pre-approve every future one. The codes are not knowable in advance — run --plan-only to see them | | | |
| `--actor` | `string` | - | Name the automation recording this (e.g. ci); default is the local user | | | |
| `--approve` | `string` | - | Proceed only if the plan is EXACTLY this digest (from an earlier --plan-only). The stronger form of --yes, for a two-stage pipeline where a human reviews stage one's plan: a plan that changed between the stages is refused (exit 3, plan\_stale) rather than re-approved blind. The digest also travels on the write, and the server recomputes the plan under the environment's row lock before admitting it | | | |
| `--dry-run` | `bool` | - | Print manifests without applying (env-cluster guard still runs) | | | |
| `--expect-current` | `string` | - | Promotion id the env must still be on (default: the one the plan read); `unbound` = --expect-unbound. Exit 3 if it moved | | | |
| `--expect-unbound` | `bool` | - | Refuse (exit 3) unless the env has never been promoted | | | |
| `--explain` | `bool` | - | Print the declared-cluster guard decision (declared/current/verdict) and exit | | | |
| `--fail-fast` | `bool` | - | Exit 1 on the first DEGRADED observation instead of waiting out --timeout | | | |
| `--from` | `string` | - | Deploy exactly what this environment is running (same control plane only); the version may be omitted | | | |
| `--from-promotion` | `string` | - | With --from: the source promotion captured earlier; refused (exit 3, source\_moved) if the source moved | | | |
| `--frontends-only` | `bool` | - | Deploy ONLY the env's shippable frontend(s) — build + ship to Firebase Hosting or a static-site bucket, skipping the entire k8s apply (Services, Operators, CronJobs, gateways). The inverse of --skip-frontend; the native 'ship just the frontend' path that doesn't touch kubectl. Mutually exclusive with --skip-frontend and --target. | | | |
| `--gate` | `stringArray` | `[]` | Pre-deploy evidence frozen onto the entry: a gate JSON file, or name=…,status=passed | failed | skipped | error\[,url=…] (repeatable) |
| `--json` | `bool` | - | Emit machine-readable JSON describing the whole invocation — mode (explain/dry\_run/apply), the declared-cluster guard verdict, the target cluster + namespace, the preflight findings, per-image digest-vs-tag pinning, the resource identities applied, and the per-resource rollout outcome (ready / failed / timed\_out / not\_waited). Works with --explain and --dry-run, which is how a UI previews a deploy. Same exit codes as text mode; the human output moves to stderr so stdout carries exactly one JSON document. | | | |
| `--namespace` | `string` | - | Override namespace from environment config | | | |
| `--no-digest` | `bool` | - | Deploy by the mutable :tag even when the build state captured an immutable image digest. By default forge pins the manifest to `<image>`@sha256:... so a re-tagged/cached layer can't ship; this escape hatch restores tag-based references. | | | |
| `--no-run` | `bool` | - | Record no run, even inside CI | | | |
| `--no-wait` | `bool` | - | Record and apply, but do NOT wait for health. Gate on it later with `forge env status <env> --wait` | | | |
| `--note` | `string` | - | Why — recorded on the ledger entry (most valuable on a deploy that moves the env BEHIND) | | | |
| `--option`, `-D` | `stringArray` | `[]` | Set a render option the env's KCL declares, as name=value (repeatable). Relayed to KCL verbatim — forge does not interpret the value. List an env's options with `forge env options <env>`. | | | |
| `--plan` | `bool` | - | Compute and print the full change set WITHOUT writing the binding or applying anything | | | |
| `--plan-only` | `bool` | - | Build, push and cut as usual, print the plan, and STOP without writing the promotion (exit 0). The first stage of a two-stage pipeline | | | |
| `--prune` | `bool` | - | Delete forge-managed Deployments in the namespace that the current KCL render no longer produces (opt-in) | | | |
| `--rollout` | `string` | `wait` | What to do after the manifests land: 'wait' (wait for every Deployment/Job and FAIL if any does not become ready — the default), 'warn' (wait and report, but exit 0), or 'skip' (apply and return immediately). | | | |
| `--rollout-fail-fast` | `bool` | - | Stop at the FIRST resource that fails instead of waiting for the rest. Default reports every failure, which is usually what you want when diagnosing a bad deploy. | | | |
| `--rollout-order` | `stringArray` | `[]` | Wait for these applications FIRST, in this order, before the rest (repeatable). A wait ordering, not an apply ordering — Kubernetes converges concurrently — so it controls what a phased deploy reports first: put the migration or the API server here and its failure surfaces before its dependents time out. | | | |
| `--rollout-timeout` | `duration` | `0s` | Per-resource readiness budget (e.g. 90s, 10m). Applies to EACH Deployment and one-shot Job, not the set. Default 5m. | | | |
| `--run-id` | `string` | - | CI run this write belongs to (default: detected from the CI environment, e.g. github:`<repo>`/`<run>`/`<attempt>`) | | | |
| `--run-url` | `string` | - | Link to the CI run, for a human reading the ledger (default: detected from the CI environment) | | | |
| `--skip-frontend` | `bool` | - | Run the k8s apply but skip the Frontend (e.g. Firebase) build+deploy dispatch. The k8s-only path for the whole backend bundle without enumerating every --target. | | | |
| `--skip-preflight` | `bool` | - | Skip the deploy preflight (verify referenced Secret keys + container images exist on the live target BEFORE applying). Default-on for remote/cloud clusters; bypass at your own risk. | | | |
| `--supersede` | `bool` | - | Deploy even though the current promotion is still rolling out (recorded on the new entry); without it that is exit 4 | | | |
| `--tag` | `string` | - | Override the image tag (priority: --tag > .forge/state/build-`<env>`.json > git describe --tags --always --dirty) | | | |
| `--target` | `stringArray` | `[]` | Deploy ONLY the named application(s) (service/operator/frontend name; repeatable). Scopes K8sCluster apply to the app's workload + shared resources, and External/Compose dispatch to the named apps. Empty = deploy the whole env bundle (default). | | | |
| `--target-arch` | `string` | - | Override target GOARCH for cross-compilation (default: forge.yaml deploy.target\_arch, then amd64) | | | |
| `--timeout` | `duration` | `0s` | Whole health-gate budget (default 15m hosted, 5m per resource self-managed) | | | |
| `--wait` | `bool` | - | Block until the deploy is LIVE even when the control plane queues it on a person (billing): wait for them to act, up to --timeout. Without it a queued deploy prints what it waits on and the action URL, and exits 7 | | | |
| `--yes` | `bool` | - | Proceed without the interactive confirmation: "I read the plan". The paved CI path. The plan is still computed and printed | | | |

***

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

**Subcommands:**

| Command | Description |
| - | - |
| [`key`](#reliant-forge-env-devstack-key) | Print the current worktree key ("" on the primary checkout) |
| [`list`](#reliant-forge-env-devstack-list) | List every holder of a port block, labelled by kind (--stacks-only for the machine-readable worktree roster) |
| [`port`](#reliant-forge-env-devstack-port) | Resolve the worktree-allocated host port for a base port |
| [`prune`](#reliant-forge-env-devstack-prune) | Reclaim dev-stack port blocks for worktrees that no longer exist on disk |
| [`release`](#reliant-forge-env-devstack-release) | Reclaim one named port block that you know is no longer in use |

***

#### reliant forge env devstack key

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

```
reliant forge env devstack key
```

***

#### 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 list [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--stacks-only` | `bool` | - | Print only the dev-stack (worktree) keys, one per line, unlabelled — the machine-readable roster for a per-stack config generator |

***

#### 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 port <base>
```

***

#### reliant forge env devstack prune

Reclaim dev-stack port blocks for worktrees that no longer exist on disk

Reclaim entries in the block registry (.forge/blocks.json) for git
worktrees that have been removed from disk.

Nothing is ever removed from this registry automatically — a deleted
worktree's block stays held forever unless something reclaims it. Blocks are
DENSE and the reachable range is finite (see dev\_stack.max\_stacks in
forge.yaml, default 8), so leaked entries from old worktrees are exactly how
a project runs out of dev stacks.

What gets reclaimed: any entry TIED TO A WORKTREE that no longer appears in
'git worktree list --porcelain'. Two kinds of entry are tied to one:

* A DEV-STACK key (one per git worktree, e.g. "wt-feature-x") — the key is
  the worktree's name.
* A DERIVED port-block key — one COMPOSED from a worktree, e.g.
  'fp.allocate\_port(3000, "prod-" + option("worktree"))' allocating
  "prod-wt-feature-x". The origin worktree is recorded when the block is
  allocated, so reclaiming never has to guess it from the key's name.

What is NEVER reclaimed, no matter how "dead" it looks:

* The default key "" (block 0) — the primary checkout's implicit block.
* A STANDALONE port-block key — e.g. "prod", which prod's reliant-web
  dev-server port allocates under on the primary checkout, where there is
  no worktree to derive from. It is tied to no worktree at all, so it can
  never legitimately look dead; reclaiming one would silently move a live,
  running stack's port. This is the single most important correctness
  property of this command, and it is why the distinction is drawn from
  what forge RECORDED at allocation rather than from how composed the key
  looks: "prod" and "prod-wt-x" are indistinguishable by shape.
* An entry allocated by a forge older than the origin field, until the next
  render from its own worktree records where it came from. Leaving such a
  block held is the deliberately safe failure: a held block wastes a slot,
  while a wrongly-moved port breaks a k3d host mapping and the dev IdP's
  baked-in issuer.

If enumerating live worktrees fails for any reason (git missing, this isn't
a git checkout, the git command errors), prune reclaims NOTHING and reports
the failure — deleting a block that might actually still be live would move
a running stack's ports.

Freeing a block lets the NEXT new worktree take it (blocks are filled
densely, lowest free index first), so pruning is what keeps the block range
from being exhausted by worktrees nobody remembers to clean up.

By default this only PRINTS what would be reclaimed and changes nothing —
pass --apply to actually rewrite the registry. This mutates machine-local
state that other running dev stacks depend on (a concurrent 'forge env up'
is briefly locked out while the rewrite happens), so making it opt-in to
apply is the safer default for a command most people will run interactively
to see what's accumulated.

Examples:
forge env devstack prune            # show what would be reclaimed
forge env devstack prune --apply    # actually reclaim it

```
reliant forge env devstack prune [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--apply` | `bool` | - | Actually rewrite the registry (default is dry-run: print what would be reclaimed) |

***

#### reliant forge env devstack release

Reclaim one named port block that you know is no longer in use

Release the block held by exactly one named key.

This is the manual counterpart to 'prune'. Prune reclaims blocks forge can
PROVE are dead — a dev stack or a derived port-block key whose worktree is gone
— and will never touch an entry it has no evidence for. Release is for those
remaining entries: typically blocks allocated before forge recorded which
worktree a key came from, whose worktree was deleted before it could ever be
recorded. 'forge env devstack prune' lists them.

forge cannot make this call itself. A key composed from a worktree
("prod-wt-x") and a standalone key ("prod") are indistinguishable by name once
the worktree is gone, and the two possible mistakes are not equally bad:
leaving a block held wastes one slot of dev\_stack.max\_stacks, while releasing a
live one moves that stack's ports — invalidating its k3d host-port mapping and
the dev IdP's baked-in 'iss' claim and redirect URIs, which surfaces much later
as an unrelated-looking failure.

So only release a key when you know which worktree it belonged to and that the
worktree is gone. If a listed key matches a worktree that still exists, leave
it: the next render from that worktree records its origin, after which prune
handles it automatically.

Releasing frees the block for the next new key (blocks fill densely, lowest
free index first).

By default this only PRINTS what it would do — pass --apply to rewrite.

Examples:
forge env devstack release prod-cp-obs           # show what would happen
forge env devstack release prod-cp-obs --apply   # actually release it

```
reliant forge env devstack release <key> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--apply` | `bool` | - | Actually rewrite the registry (default is dry-run) |

***

#### reliant forge env diff

Render this checkout and diff each environment against what is deployed

Render the environments declared in this checkout and compare each one against
what is actually deployed. READ-ONLY: no cluster is contacted, no image is
built, nothing is pushed.

WHAT IS COMPARED. Shapes, not manifests — the diff is a set comparison over
one hash per rendered object, so it needs no YAML. You get: objects added,
removed and changed (with image changes split out from config changes),
workloads added and removed, runtime and cluster moves, secrets newly needed,
and domain changes.

THE LIVE SIDE is the last recorded BUNDLE's shape — what was actually
deployed. An environment with no bundle yet falls back to its declared shape,
and one with neither is reported as "no recorded config" rather than as
"everything is new". The report says which, because a diff against a declared
shape is weaker evidence than one against an applied bundle.

THREE GUARDS, each with a PER-ENVIRONMENT status. A failure never silently
reads as "no changes":

ok           the render succeeded, wrote nothing, and the tree held still
impure       the render wrote files; the paths are listed and the result is
discarded
stale        the tree changed while rendering (you were editing); retried once
error        the render failed; forge's message is included

Charts are NOT templated by default: that needs `helm` and the network, and a
chart's objects belong to the platform dependency rather than this project.
`--charts` opts in.

Examples:
cli forge env diff prod                 # one environment
cli forge env diff --all                # every declared environment
cli forge env diff --all --json         # the daemon's form

```
reliant forge env diff <environment>|--all [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--against` | `string` | `live` | What to compare against. Only "live" (what is currently deployed) is implemented; a bundle reference is refused rather than silently compared against live |
| `--all` | `bool` | - | Diff every environment declared in this checkout |
| `--charts` | `bool` | - | Also template Helm charts (needs helm and the network; off by default) |
| `--json` | `bool` | - | Print the §8.2 document as JSON (the form the daemon's hook returns) |

***

#### reliant forge env down

Stop Forge host processes for an environment (or --all: across projects)

Stop a running `forge env up` stack.

forge env down dev     stop THIS project's dev stack
forge env down --all   stop every forge stack on this machine, all projects

Only processes forge itself started — the ones carrying its ownership
markers for the project and environment being stopped — are ever signalled.
A process forge did not start is never touched by either form.

Neither form ever stops the process running it, or any ancestor of it. The
ownership markers are inherited by everything a forge-started process runs —
an agent server's shells included — so `forge env down` typed inside one would
otherwise select the very server hosting it. Before signalling anything,
forge walks its own parent chain and leaves each ancestor (with the tree
under it) running, saying so:

skipped pid 1234 (reliant serve --port 3090): it is an ancestor of this
command — stopping it would end the session running you

Everything else is stopped as usual, and the command still succeeds. The
per-environment form also leaves that environment's host infrastructure up,
because the server it backs is still running. To stop such a stack, run the
command from a shell outside it.
Docker Compose containers, Kubernetes workloads/clusters, and Docker Desktop
are not stopped by this command. The per-environment form also stops declared
host infrastructure servers while preserving their data.

This command never touches a HOSTED environment (one the control plane runs):
suspend/resume it with `forge env stop` / `forge env start`, tear it down with
`forge env delete`.

Use --all when a stack outlived its project directory: without a forge.yaml
there is no project to scope to, and the per-environment form cannot reach
it. `forge env ps` lists what is running first.

The per-environment form also marks the stack's LOCAL SESSION — the presence
row `forge env up` records so `forge env status` can show what is running
where — as stopped. That is presence only, and it is best-effort: a record
that cannot be updated never fails the teardown, and any record nothing
refreshes is discarded after 24h regardless.

```
reliant forge env down [environment] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--all` | `bool` | - | Stop every forge stack running on this machine, across all projects (the only way to reach a stack whose project directory is gone) |

***

#### reliant forge env list

List the environments declared in deploy/kcl/

```
reliant forge env list
```

***

#### 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 new <name> [flags]
```

**Flags:**

| Flag | Type | Default | Description | | |
| - | - | - | - | - | - |
| `--bind` | `stringArray` | `[]` | Rebind a workload or frontend in the new env: `<workload>`=hosted | cluster, `<frontend>`=hosted | bucket (repeatable) |
| `--check` | `bool` | - | Don't scaffold; verify the existing env has no REPLACE\_ME\_\* placeholders left and KCL-compiles (CI gate) | | |
| `--force` | `bool` | - | Overwrite deploy/kcl/`<name>`/ if it already exists | | |
| `--from` | `string` | - | Existing env to derive the new env from (default: auto-pick a cloud-shaped sibling) | | |

***

#### 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 options <env> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Emit machine-readable JSON (name/type/default/help/required) |

***

#### reliant forge env ps

List every forge stack running on this machine, across all projects

List every stack `forge env up` has running on this
machine, across every project — not just the one in the working directory.

Each row is one (project, env) stack, with the number of live processes and
the project directory it was started from. A stack whose directory has since
been DELETED is still listed and still stoppable: it is discovered from the
ownership markers its processes carry, not from anything on disk.

Stop one:   cd `<project>` && forge env down `<env>`
Stop all:   forge env down --all

Examples:
forge env ps
forge env ps --json

```
reliant forge env ps [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Emit machine-readable JSON (project/project\_id/env/processes/dir\_exists) |

***

#### 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 render <environment> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--cluster` | `string` | - | Print ONLY the objects that land on this cluster (the kubectl context named by forge.K8sCluster.cluster) |
| `--fail-on-write` | `bool` | - | Exit non-zero if any file in the project changed while rendering (the render is not guaranteed side-effect-free — see the command description) |
| `--kind` | `stringSlice` | `[]` | Print only these kinds (comma-separated or repeated; case-insensitive) |
| `--list` | `bool` | - | Print one line per object (cluster, kind, namespace, name) instead of the YAML stream |
| `--name` | `string` | - | Print only objects with this metadata.name |
| `--namespace` | `string` | - | Override the namespace the environment declares |
| `--no-charts` | `bool` | - | Skip templating declared forge.HelmChart platform deps (no helm, no network); the summary names what was left out |
| `--no-digest` | `bool` | - | Render image references as the mutable :tag even when a build state captured an immutable digest (matches forge env deploy --no-digest) |
| `--no-write-check` | `bool` | - | Skip the before/after scan that detects files the render wrote |
| `--tag` | `string` | - | Image tag to render with (default: the same source forge env deploy reads — .forge/state/build-`<env>`.json, then git describe) |
| `--target` | `stringArray` | `[]` | Print only the named application's objects (the app.kubernetes.io/name group forge env deploy --target selects; repeatable) |

***

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

**Subcommands:**

| Command | Description |
| - | - |
| [`sync`](#reliant-forge-env-secrets-sync) | Render + apply the k8s Secrets for an env's dotenv secret\_provider (local clusters only) |

***

#### 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 secrets sync <environment> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dry-run` | `bool` | - | Print the Secret manifests instead of applying them |

***

#### 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 shape <environment> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Print `{project, env, kind, shape, provenance}` as JSON (the form the reliant daemon's forge.env\_shape hook returns) |

***

#### 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 smoke <environment> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--context` | `string` | - | kubectl context to read Gateway status from (overrides the env's declared K8sCluster.cluster). Read-only override: smoke only queries status, so it never risks a wrong-cluster write the way deploy would. |
| `--json` | `bool` | - | Emit machine-readable JSON instead of the table |
| `--namespace` | `string` | - | namespace the Gateways live in (overrides the env's declared K8sCluster.namespace) |
| `--tag` | `string` | - | Image tag to associate with this smoke run (informational; render is tag-agnostic) |
| `--timeout` | `int` | `10` | Per-probe timeout in seconds |

***

#### reliant forge env start

Resume a HOSTED environment's workloads on the control plane (not local processes)

Resume every deployment of a hosted environment (or one with --target).

forge env start prod                 resume the whole environment
forge env start prod --target api    resume one workload
forge env start prod --wait          poll until the platform reports it ready

The control plane refuses a resume the organization is not entitled to (for
example a billing condition); nothing is started then, and the server's reason
is printed with the fix.

This acts on the CONTROL PLANE. Locally running a stack is `forge env up`.
An env that is not hosted is refused.

```
reliant forge env start <environment> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--poll-interval` | `duration` | `3s` | How often --wait re-reads status |
| `--target` | `string` | - | Act on ONE workload (by name) instead of the whole environment |
| `--timeout` | `duration` | `5m0s` | How long --wait polls before giving up |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |
| `--wait` | `bool` | - | Poll until every affected deployment is observed in the requested state |

***

#### reliant forge env status

The one read view of an environment: bound release, rollout, health, verify, gates, ledger

Report everything about an environment in one read.

forge env status                 every environment, and how far behind each is
forge env status prod            prod right now: runtime AND release
forge env status prod --wait     block until the rollout settles
forge env status prod --history  prod's promotion ledger, newest first

WHAT ONE ENV'S REPORT CARRIES:

* the BOUND RELEASE — the version this env's promotion ledger declares, and
  when it was promoted (promote time, NOT deploy time; that gap is what the
  verify half exists to measure);
* VERIFY — the digests the env is actually RUNNING against the ones the
  binding froze, per declared image, in five states: match / drift /
  missing / untagged / unreachable. A hosted env is read through its
  control plane's observer, because forge cannot read a hosted cluster;
* the ROLLOUT PHASE per workload, as the control plane computes it;
* RUNTIME HEALTH — every host service and frontend with its URL, log file
  and whether a listener is accepting right now, plus the compose infra,
  the app's /healthz + /readyz, pprof and the telemetry backends;
* GATES — the evidence recorded for the current promotion;
* LEDGER FRESHNESS — whether this checkout's copy of the promotion log is
  the newest one;
* RECORDS — the PROVENANCE of the bound release (tree, commit, branch,
  dirty) and how the binding was made; the CONVERGENCE of it (what the
  reconciler did, onto which bundle, when it was observed); and the LOCAL
  SESSIONS running it.

CONVERGENCE IS AN OBSERVATION, NOT FORGE'S REPORT. forge never applies to a
cluster — a reconciler converges each environment to its promoted bundle — so
this is read from the environment's control plane and labelled as what the
control plane observed. An environment with no control plane has no reconciler
and shows one plain line saying so. If a control plane has not reported yet,
the report says that rather than inventing a state.

RECORDS DESCRIBE, THEY DO NOT JUDGE. An unreadable records store never
changes the exit code — the cluster comparison above already has an opinion
about whether the bytes landed. An environment with no records shows the empty
state plainly, which is an answer, never an error.

LOCAL SESSIONS are PRESENCE ONLY: one row per worktree per machine, recorded
best-effort by `forge env up`, and never a promotion, a deploy target or an
input to policy or billing. A record nothing refreshes is discarded after 24h,
so a crashed stack simply goes quiet — shown as "quiet", not as failed, because
forge cannot tell a crash from a closed laptop. Only LOCAL environments have
sessions; for any other kind the report says so rather than showing an empty
list, since "no sessions" and "sessions do not apply here" are different facts.

THE TWO HALVES ARE NOT SYMMETRIC ABOUT FAILURE, on purpose. Runtime health
REPORTS: "the app is down" is a state this command must be able to print, so
it never changes the exit code. The release half makes a CLAIM — the env is or
is not running what it declares — so its verdict is the exit code.

`--wait` blocks until the rollout of a promotion finishes, and reports whether
it STAYED up past the stability window. It is pinned to ONE promotion, never
to "whatever the env declares now", so a hotfix promoted mid-wait reports
SUPERSEDED rather than succeeding on bytes it was never asked about.
`--wait --timeout 0` reads the phase ONCE and never blocks.

`--history` pages the promotion ledger with a keyset cursor: pass the previous
page's `next_before` as --before, and stop on an EMPTY next\_before rather than
on a short page.

EXIT CODES — a pipeline branches on these directly:

0  everything declared is running (or nothing is declared)
1  we looked and it is WRONG — an image drifted or is missing; with --wait,
a workload is DEGRADED
2  we could not DETERMINE — a cluster or control plane was unreachable, a
credential was refused, the thing is unobservable, or this checkout's
promotion ledger is stale
6  --wait only: SUPERSEDED — a newer promotion replaced the one waited on
7  QUEUED — the bound release is accepted and recorded, and the control plane
holds it on a person (billing). The output names what it waits on and the
action URL; it goes live by itself once they act. --wait waits through it
(still queued at the deadline is 7 again)
8  --wait only: TIMED OUT while still pending / progressing / stabilizing.
The rollout was progressing, so retry the wait; do not re-promote

1 and 2 are separate because CI must tell a bad release from a broken control
plane; a gate that reports both with one code gets switched off the first week
it is wrong about one of them. 6, 7 and 8 are deliberately not 1: "the release
was overtaken", "a person has to act" and "we never saw this finish" are not
"the release is bad".

A STALE FILE LEDGER IS EXIT 2. .forge/promotions/`<env>`.jsonl is committed to
git, so a checkout that has not pulled compares the cluster against an OLDER
promotion — a fine deploy reads as DRIFT, and a deploy that never happened can
read as MATCH. Neither is evidence, so the verdict is "could not determine"
with the fix. AHEAD (a release recorded here, not yet merged) is noted, not
failed.

\--json emits ONE document with the same verdict and IDENTICAL exit codes;
`ok` is false exactly when the process exits non-zero.

Examples:
forge env status                                  # the whole topology, offline
forge env status prod                             # did prod receive its release?
forge env status prod --wait                      # gate a release on the rollout
forge env status prod --wait --timeout 0 --json   # where is it NOW? one read
forge env status prod --history --limit 1 --json | jq -r '.promotions\[0].id'
forge env status prod --json | jq -r '.images\[] | select(.state == "drift")'

```
reliant forge env status [environment...] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--before` | `string` | - | --history only: return entries older than this promotion id (the previous page's next\_before) |
| `--fail-fast` | `bool` | - | --wait only: exit 1 on the FIRST degraded observation instead of waiting out --timeout |
| `--history` | `bool` | - | Page the environment's promotion ledger, newest first |
| `--include-unpinned` | `bool` | - | Let a degraded UNPINNED workload (a database, a third-party image) count too |
| `--interval` | `duration` | `5s` | --wait only: poll cadence |
| `--json` | `bool` | - | Emit ONE machine-readable document (same exit codes as text mode) |
| `--limit` | `int` | `20` | --history only: entries per page (1–500) |
| `--promotion` | `string` | - | Promotion id to read (default: the env's current promotion). A CI retry passes the id the deploy returned |
| `--release` | `string` | - | Refuse unless the promotion read binds this release version (exit 6 if it moved on). With --history, only promotions of this version |
| `--signal` | `string` | - | Run only one runtime signal: app, metrics, traces, logs, profiles (default: all) |
| `--stable-for` | `duration` | `0s` | --wait only: extra hold AFTER the phase reaches succeeded (default 0: the server's own stability window already applies) |
| `--timeout` | `duration` | `0s` | Maximum time to spend reading the cluster (default 1m0s). With --wait this is the whole wait budget instead (default 15m0s); `--wait --timeout 0` reads the phase ONCE and never blocks |
| `--verbose`, `-v` | `bool` | - | Show evidence for all runtime checks (not just failures) |
| `--verify` | `bool` | - | All-environments view only: also read each environment's cluster and reconcile it against the ledger (slow, needs credentials) |
| `--wait` | `bool` | - | Block until the rollout settles, and prove it stayed up (exit 0/1/2/5/6) |
| `--watch-json` | `bool` | - | --wait only: emit NDJSON, one line per phase change, as the rollout progresses |

***

#### reliant forge env stop

Suspend a HOSTED environment's workloads on the control plane (not local processes)

Suspend every deployment of a hosted environment (or one with --target).

forge env stop prod                  suspend the whole environment
forge env stop prod --target api     suspend one workload
forge env stop prod --wait           poll until the platform reports it suspended

Suspending is never refused, keeps the environment and its data, and stops
compute billing for what it suspends. Bring it back with `forge env start`.

This acts on the CONTROL PLANE. It does not touch processes on this machine:
that is `forge env down`. An env that is not hosted is refused.

```
reliant forge env stop <environment> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--poll-interval` | `duration` | `3s` | How often --wait re-reads status |
| `--target` | `string` | - | Act on ONE workload (by name) instead of the whole environment |
| `--timeout` | `duration` | `5m0s` | How long --wait polls before giving up |
| `--token` | `string` | - | Credential to use, ahead of the env var and the credentials file |
| `--wait` | `bool` | - | Poll until every affected deployment is observed in the requested state |

***

#### reliant forge env up

Bring the whole dev loop up on this machine: build + deploy + host + frontend

Bring the whole dev loop up for an environment, on this machine.

Reads deploy/kcl/`<env>`/ to figure out which services run in-cluster vs
on the host and which frontends to start.

LOCAL ONLY. This verb builds, applies and runs the env HERE. An env with
anything bound to a non-local runtime — a forge.OnHosted workload, a
hosted database, a frontend that ships to a bucket, a cluster that is not
a local one (k3d / kind / docker-desktop / minikube / colima / orbstack) —
is refused with a pointer to `forge env deploy <env>`, before any
work is done.

Phases:

1. build:    docker build + push every cluster image; go build
   each build-only variant
2. deploy:   kubectl apply cluster manifests; wait rollouts and
   one-shot Jobs
3. host:     start every host-mode service (go-run / air / binary
   / delve)
4. frontend: start every declared frontend in its path

Reaching cluster services from the host is the Gateway API ingress
path; run `forge cluster urls` to list the routes.

Use --no-build / --no-deploy to skip phases when iterating. Use --target
`<name>` to scope the whole run to one service — a CI lane that only wants
that service's cluster apply, or a dev loop iterating on one host-mode
service, both scope the same way.

Lifecycle (what happens after host services + frontends start):

* With a TTY (interactive shell): forge holds the foreground and
  tears the whole stack down on Ctrl-C.
* Without a TTY (agent / CI / piped): forge brings everything up,
  prints the summary (URLs + per-service log paths), and RETURNS,
  leaving the processes running — the same end-state as --background.
  This is what keeps `forge env up <env>` from hanging an agent.
* \--watch forces the hold-and-teardown lifecycle even without a TTY
  (for a human piping the output through a tool).
* \--background always detaches and returns immediately, regardless of
  the TTY. If both --watch and --background are passed, --background
  wins (detach + return).

Either way the long-running children are tracked under
\~/.cache/forge/up/`<project-id>`/; stop a detached / non-TTY stack with
`forge env down <env>`. That is safe to run from anywhere, including
from inside the stack: it never stops the process running it or any of its
ancestors (see `forge env down --help`).

ONE stack per (project, env). If this project already has a stack running
for this env — tracked, detached, or orphaned by a crashed run — it is
STOPPED before the new one starts. It is not adopted: this invocation may
carry different config, a different allocated port, or reinstalled deps, so
the old process is not the process you asked for. Only processes carrying
forge's own ownership markers for THIS project and env are ever signalled;
a port held by anything else is an error, never a kill. If the running stack
hosts this very command (you are in a shell it spawned), nothing is stopped
and the run is refused: replacing it would end the session running you.

Tokens after `--` are forwarded to each frontend's dev server
(`npm run dev -- <flags>`), so a Vite/Next dev server can be told
to bind a specific host or port. This is what an agent-driven preview flow
uses: `forge env up dev -- --host 0.0.0.0` starts the scaffolded
frontend bound to 0.0.0.0 so a workspace proxy can reach it.

On first boot against a dev environment the app boots alive: the fresh
database is auto-seeded with deterministic, FK-coherent demo data derived
from the applied schema — only when the DB is reachable and every seedable
table is empty. Pass `--no-seed` to skip it, or inspect with
`forge db seed status`.

Examples:
forge env up dev
forge env up dev --no-build
forge env up dev --target admin-server -D host\_runner=go-run
forge env up dev --watch        # hold + Ctrl-C teardown even when piped
forge env up dev --background
forge env up dev -- --host 0.0.0.0   # forward flags to the dev servers
forge env down dev

Render options (-D):
An env's KCL can declare options for the things you want to vary per run —
which runner a host service launches under, whether to point at a remote
dependency, anything else the env models. It declares one by reading it:

\_host\_runner = option("host\_runner", type="str", default="air",
help="Host launch runner: air (default) or go-run")

and you set it with:

forge env up dev -D host\_runner=go-run

forge does not interpret these. It checks the NAME against what the env
declares (so a typo fails instead of silently doing nothing), relays the
value verbatim, and the KCL decides what it means. List what an env
declares with `forge env options <env>`.

Options forge derives itself (env, namespace, image\_tag, image\_digests,
worktree, branch) are not yours to set and are rejected. -D is accepted on
`env up` only — a cluster apply must stay reproducible from the repo alone.

LOCAL SESSIONS (what this reports about itself)

Once the stack is up, forge records a PRESENCE row for it — one per
worktree per machine, naming the environment, the branch and whether the
tree is dirty — so `forge env status` and the Live view can show what is
running where. It is presence ONLY: never a promotion, never a release,
never a deploy target, and nothing reads it for policy or billing.

It is BEST-EFFORT and never blocks this command. If the record cannot be
written — no control plane, no network, no credential — forge says so once
and the stack runs normally. A supervising run refreshes the record while
it holds the foreground, and marks it stopped on Ctrl-C.

`--background` reports once at start and has no process left to refresh
it, so the record goes quiet until `forge env down` marks it stopped. A
record nothing refreshes is discarded after 24h, which is also what
happens when a stack crashes — forge does not try to tell those apart.

Only LOCAL environments report. An environment whose workloads run on the
platform or on a cluster has no local presence to describe, and forge says
which when it declines.

```
reliant forge env up <environment> [-- <dev-server flags>] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--background` | `bool` | - | Detach long-running phases and return immediately (stop with `forge env down <env>`). Beats --watch and the TTY default. |
| `--host-ready-timeout` | `duration` | `2m0s` | Maximum wait for host services to bind their ports, including compilation; exited runners fail immediately |
| `--no-build` | `bool` | - | Skip the build phase (use already-built images / binaries) |
| `--no-deploy` | `bool` | - | Skip the cluster apply phase (host services and frontends still launch) |
| `--no-generate` | `bool` | - | Skip the pre-build code-generation check. By default `forge env up` runs `forge generate` when gen/ is missing or proto sources are newer than the generated tree. |
| `--no-install` | `bool` | - | Skip the pre-dev-serve frontend dependency install. By default `forge env up` installs a frontend's deps when node\_modules is missing or older than its lockfile/manifest. |
| `--no-seed` | `bool` | - | Skip the first-boot dev auto-seed. By default `forge env up` seeds a dev database once when it is reachable and all seedable tables are empty. |
| `--option`, `-D` | `stringArray` | `[]` | Set a render option the env's KCL declares, as name=value (repeatable). Relayed to KCL verbatim — forge does not interpret the value. List an env's options with `forge env options <env>`. |
| `--target` | `stringArray` | `[]` | Scope the whole run — build, deploy, host and frontend phases — to specific services/operators/frontends by name (repeatable). Targeting only host/frontend apps builds no images. An unknown name is an error listing the env's app names. Default: everything. |
| `--watch` | `bool` | - | Force the hold-and-teardown lifecycle (block until Ctrl-C, then cascade-stop) even without a TTY. Default without --watch/--background: hold when stdin is a TTY, otherwise return after start (non-TTY agent/CI path). |

***

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

**Subcommands:**

| Command | Description |
| - | - |
| [`list`](#reliant-forge-gate-list) | Read every check result attached to a promotion |
| [`record`](#reliant-forge-gate-record) | Record one check's result against a promotion |

***

#### 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 list <environment> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Emit one JSON document instead of the table |
| `--promotion` | `string` | - | Promotion to read (default: 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 gate record <environment> [flags]
```

**Flags:**

| Flag | Type | Default | Description | | | |
| - | - | - | - | - | - | - |
| `--from` | `string` | - | Read the gate from a JSON document: a gate document (--gate-json output), or any forge --json document | | | |
| `--json` | `bool` | - | Emit one JSON document instead of the table | | | |
| `--name` | `string` | - | Check name: lint, test, smoke, rollout, qa-signoff. Required unless --from names it | | | |
| `--no-run` | `bool` | - | Record no run, even inside CI | | | |
| `--promotion` | `string` | - | Promotion to attach the gate to (default: the environment's current promotion) | | | |
| `--release` | `string` | - | Assert the environment's current promotion is this release; exits 3 if it has moved on | | | |
| `--run-id` | `string` | - | CI run this write belongs to (default: detected from the CI environment, e.g. github:`<repo>`/`<run>`/`<attempt>`) | | | |
| `--run-url` | `string` | - | Link to the CI run, for a human reading the ledger (default: detected from the CI environment) | | | |
| `--status` | `string` | - | Verdict: passed | failed | skipped | error. Required unless --from carries one |
| `--summary` | `string` | - | One line: "412 passed, 0 failed, 3 skipped" | | | |
| `--url` | `string` | - | Where the full report lives (a CI run, an artifact) | | | |

***

### reliant forge generate

Generate code from proto files

Generate code from proto files based on project configuration or directory conventions.

When forge.yaml exists, generation is driven by the config:

* buf generate for Go stubs (protoc-gen-go + protoc-gen-connect-go)
* protoc-gen-forge for entity protos in proto/db/
* buf generate for TypeScript stubs for Next.js frontends
* Service stubs and mocks for new services
* pkg/app/bootstrap.go with explicit service bootstrapping
* sqlc generate if sqlc.yaml exists
* go mod tidy in gen/

Without forge.yaml, falls back to directory convention scanning:
proto/           - Root proto directory (for buf generate)
proto/services/  - Service definitions (stubs + mocks)
proto/api/       - API messages
proto/db/        - Database models (protoc-gen-forge)

Examples:
forge generate            # Generate all code
forge generate --watch    # Watch mode for development
forge generate --force    # Discard hand-edits to Tier-1 files and regenerate
forge generate --check    # Run generate into a tmpdir; exit 1 if it would change the tree (CI guard)
forge generate --explain  # Print per-file provenance log after generate
forge generate --verbose  # Print one line per gate-off skipped step

Additional maintainer/debug flags exist (pipeline narrowing, drift
forensics, parallel-lane and migration escape hatches); run
'forge generate --help-dev' to list them.

```
reliant forge generate [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--check` | `bool` | - | Run generate into a tmpdir and diff against the current tree; exit 1 on drift (for CI guards) |
| `--dry-run` | `bool` | - | Print the pipeline plan (\[RUN]/\[SKIP] per step + gate reason) and exit without running any step. |
| `--explain` | `bool` | - | Print a per-file provenance log after generate |
| `--force`, `-f` | `bool` | - | Discard hand-edits to Tier-1 files and regenerate from current templates |
| `--heal` | `bool` | - | Overwrite on-disk content that matches a PRIOR forge render (an older vintage) with the current template. OFF by default: such content is byte-indistinguishable from a deliberate edit, so forge leaves it untouched and tells you how to proceed rather than silently reverting your work. Pass --heal to advance every such file to the current templates. |
| `--help-dev` | `bool` | - | List maintainer/debug flags hidden from --help |
| `--verbose`, `-v` | `bool` | - | Print one line per gate-off skipped step ('⏩ skipped: \<step name> (`<reason>`)'). Default is silent skip. |
| `--watch`, `-w` | `bool` | - | Watch for changes and regenerate |

***

### reliant forge kcl

Evaluate this project's KCL directly

Work with this project's KCL files directly.

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

```
reliant forge kcl
```

**Subcommands:**

| Command | Description |
| - | - |
| [`eval`](#reliant-forge-kcl-eval) | Evaluate one KCL file of this project and print a selected field |

***

#### reliant forge kcl eval

Evaluate one KCL file of this project and print a selected field

Evaluate a single .k file in this project and print a field out of it.

This is THE way a script or a test reads a value from project KCL. A plain
`kcl run` cannot do it: since the forge KCL module is supplied by the forge
binary, `import forge` resolves only inside a forge evaluation. Nothing needs
`kcl` on PATH, and nothing needs to know where forge keeps its module.

The file is evaluated with its OWN kcl.mod package root as the working
directory, so a relative import (`import lib.foo`) resolves exactly as it does
for a shell script that cd'd there first. It does not matter which directory
you run this from: the answer is the same from the project root and from a
subdirectory.

-S takes a dotted path, kcl-style. A numeric segment indexes a list
(`pools.0.name`). Repeat -S to get an object keyed by selector. Unlike
`kcl run -S`, a selected LIST comes back as one list rather than as one YAML
document per element, and a path that does not exist is an ERROR naming what
the document does have — not an empty result that a caller reads as a value.

\--format raw prints a scalar's own text: no quotes, no trailing newline. It is
what makes `$(...)` capture the value exactly, replacing the
`| tr -d "\n'\""` pipelines that a YAML scalar's conditional quoting forces
(and that silently corrupt a value legitimately containing a quote). It
refuses a list or an object rather than printing JSON a caller would
interpolate without noticing.

-D binds a top-level `option()`, as it does for any KCL evaluation.

The evaluation is READ-ONLY as far as forge is concerned: no port block is
claimed, no port store is written, no cluster is contacted, and no image is
built. It is not guaranteed PURE, for the same reason `env render` is not:
KCL evaluates `file.write` itself, so a file that generates a file writes it.

To evaluate an ENVIRONMENT into its Kubernetes objects, use
`cli forge env render <env>` instead.

Examples:
cli forge kcl eval deploy/kcl/lib/kata\_pool.k -S sbd.image\_family --format raw
cli forge kcl eval deploy/kcl/lib/kata\_pool.k -S sbd.disk\_size\_gb --format raw
cli forge kcl eval deploy/kcl/lib/daemon\_placement.k -S daemon\_placement | jq -r '.prod.context'
cli forge kcl eval deploy/kcl/lib/platform\_local.k -S cloudnative\_pg.name -S cloudnative\_pg.namespace
cli forge kcl eval deploy/kcl/lib/kata\_pool.k                       # the whole document

```
reliant forge kcl eval <file> [-S <path>]... [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--format` | `string` | `json` | Output format: \[json yaml raw] (raw prints a scalar's own text, unquoted and with no trailing newline) |
| `--option`, `-D` | `stringArray` | `[]` | Bind a top-level KCL option, key=value (repeatable) |
| `--select`, `-S` | `stringArray` | `[]` | Print only this dotted field path (repeatable; repeated paths yield an object keyed by path) |

***

### reliant forge ledger

Move and inspect deploy ledgers

Work with an entire deploy ledger — the releases a project has cut and the
promotions that bound its environments.

An environment's ledger is chosen by its DECLARATION, in one place and with no
flag: an env whose KCL declares `forge.ControlPlane` records on that control
plane; every other env records in this machine's ledger under
\$FORGE\_LEDGER\_HOME (default \~/.forge/ledger), keyed by project so every
worktree shares one history.

`import` is how history reaches whichever store an env selected. forge once
kept promotions in `.forge/promotions/<env>.jsonl` inside the checkout; those
files are no longer read, and a command refuses while the checkout holds
records the selected ledger has never imported.

```
reliant forge ledger
```

**Subcommands:**

| Command | Description |
| - | - |
| [`export`](#reliant-forge-ledger-export) | Write the selected ledger to a directory, to VERIFY an import |
| [`import`](#reliant-forge-ledger-import) | Import a ledger from a checkout's committed history, or from another ledger directory |
| [`show`](#reliant-forge-ledger-show) | Print this machine's ledger records — promotions, applies and local sessions |
| [`where`](#reliant-forge-ledger-where) | Print which backend holds an environment's ledger, and the declaration that chose it |

***

#### reliant forge ledger export

Write the selected ledger to a directory, to VERIFY an import

Write a ledger out in file-ledger format, so an import can be checked against
the files it came from.

THIS IS A VERIFICATION AID, NOT A BACKUP. It exists for one sequence:

cli forge ledger import --from-git --rev origin/main --apply
cli forge ledger export --dir /tmp/verify
diff -r /tmp/verify/promotions .forge/promotions

If those agree, the import lost nothing — which is what you need to know
before the committed ledger is deleted from the repository.

It is NOT a way to keep a second copy of deploy history. A copy nothing writes
to is stale as soon as the next promotion lands, and a stale ledger that looks
authoritative is the failure the ledger was moved out of the checkout to end.
There is no git export in either direction, and no default directory — a
default is a thing people start to rely on.

The output is the ledger's own canonical JSON, so a diff is meaningful rather
than a re-rendering. Four differences against a retired git ledger are
expected and are printed with every run.

Examples:
cli forge ledger export --dir /tmp/verify
cli forge ledger export --dir /tmp/verify --env prod   # the ledger prod selected

```
reliant forge ledger export --dir <directory> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dir` | `string` | - | The directory to write to (required; use a throwaway path) |
| `--env` | `string` | - | Export the ledger this environment selected (default: this machine's ledger for the project) |

***

#### reliant forge ledger import

Import a ledger from a checkout's committed history, or from another ledger directory

Read a ledger from somewhere else and record it in the one each environment
selected. A DRY RUN unless you pass `--apply`.

TWO SOURCES:

\--from-git \[--rev `<ref>`]        the retired in-checkout ledger
(.forge/releases/\*.json and
.forge/promotions/`<env>`.jsonl) as COMMITTED at
a rev. Default rev: origin/main.
\--from-file-ledger `<dir>`       a machine ledger directory, for an env whose
KCL newly declares a control plane.

IT READS A REV, NEVER THE WORKING TREE. The retired ledger was committed, so
the repository is the authority. A `.forge/promotions` deleted but not
committed still has its history, and a half-written local edit is not history
at all.

HISTORY IS NEVER RE-JUDGED. Every record is admitted exactly as written,
including the uncomfortable parts — a release cut from a dirty tree is imported
saying so. Promotion ids and timestamps are preserved verbatim: the id is how a
re-run recognises what it already recorded, and the timestamp is the order an
environment actually moved in.

IDEMPOTENT. Running `--apply` twice records once. A run interrupted half-way
is re-run, not repaired.

IT REFUSES AN ENVIRONMENT THAT ALREADY HOLDS PROMOTIONS OF ITS OWN. An
append-only log's current binding is its last line, so interleaving imported
history with promotions made since would leave the environment reading whatever
sorted last. The remedy is to import first.

Examples:
cli forge ledger import --from-git                        # dry run against origin/main
cli forge ledger import --from-git --rev origin/main --apply
cli forge ledger import --from-file-ledger \~/.forge/ledger/myproj-ab12cd34 --apply

```
reliant forge ledger import [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--apply` | `bool` | - | Record the import. Without it, print the plan and change nothing |
| `--from-file-ledger` | `string` | - | Read a machine ledger directory (\$FORGE\_LEDGER\_HOME/`<project-id>`/) instead |
| `--from-git` | `bool` | - | Read the retired in-checkout ledger (.forge/releases, .forge/promotions) as committed at --rev |
| `--rev` | `string` | `origin/main` | The git rev to read the checkout's ledger at (--from-git only) |

***

#### reliant forge ledger show

Print this machine's ledger records — promotions, applies and local sessions

Read the records in this machine's ledger: each environment's promotions and
applies, and the local stacks running here.

WHY THIS EXISTS. An environment that declares `forge.ControlPlane` has hosted
rows, and a UI reads them directly. An environment with no control plane does
not: its history and its running stacks exist only in one machine's ledger
under \$FORGE\_LEDGER\_HOME. So this is how that half becomes visible — the
reliant daemon runs it and the UI renders the result.

It is therefore a DAEMON-DEPENDENT view, and the only one. A surface showing
it must distinguish "no sessions" from "cannot see sessions": they are
different facts, and collapsing them tells a user their dev stack is down when
really their daemon is.

Local sessions are PRESENCE ONLY. A session is an observation — never a
promotion, never a deploy target — and nothing reads it for policy or billing.

Examples:
cli forge ledger show                  # every environment
cli forge ledger show dev --json       # one environment, for the daemon

```
reliant forge ledger show [environment] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Print the records as JSON (the form the reliant daemon's hook returns) |

***

#### reliant forge ledger where

Print which backend holds an environment's ledger, and the declaration that chose it

Say where an environment's promotions and releases are recorded, and why.

Selection is DECLARATIVE and has no flag: an environment whose KCL declares
`forge.ControlPlane` records on that control plane — whatever its kind,
including local — and every other environment records in this machine's
ledger under \$FORGE\_LEDGER\_HOME (default \~/.forge/ledger), keyed by project so
that every worktree of a project shares one history.

This renders the environment and reports the declaration it FOUND. It does not
infer the answer from which store happens to hold records: a project that has
moved an environment to a control plane still has its old machine-ledger files
on disk, and "where are the records" is a different question from "where do
records go".

Examples:
cli forge ledger where prod
cli forge ledger where dev --json

```
reliant forge ledger where <environment> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Print `{env, backend, location, because, declaration, project}` as 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)

By default 'forge lint' AUTO-FIXES deterministic-safe issues first, then
gates on the residue: it canonicalizes Go formatting (goimports/gofmt import
grouping + whitespace) across generated AND owned/hand-edited files, applies
golangci-lint's safe autofixes, runs each frontend's own prettier over the
src/ files you own (never generated or forge-refreshed ones), and runs
eslint --fix on frontends — so mechanical formatting never surfaces as a
gating error. Pass --no-fix to gate only and mutate nothing (CI / read-only
checks); --json is always detect-only.

Examples:
forge lint                     # Auto-fix deterministic-safe issues, then gate
forge lint --no-fix            # Gate only; mutate nothing (CI / read-only)
forge lint --skip-frontends    # Backend-only gate; no Node toolchain needed
forge lint --contract          # Run contract interface enforcement linter
forge lint --db                # Run DB entity lint rules
forge lint --migration-safety  # Run SQL migration safety checks
forge lint --conventions       # Run forge convention rules on proto files
forge lint --generated-drift   # Fail if a forge-generated ("DO NOT EDIT")

# file was hand-edited

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

# handlers and frontend hooks (warnings only)

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

# configuration, not a collaborator; prints the

# exact proto block + AppConfig line to write

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

# carrying an unrecognized forge:\* marker

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

# a scaffolded handlers\_crud\_test.go against

# the CURRENT schema (shadow postgres,

# rolled back) and report postgres's own

# error per statement it rejects

forge lint --time-bucketing    # Flag a two-argument date\_trunc over a

# TIMESTAMPTZ column — it truncates in the

# SESSION timezone, so every bucketed total

# moves with the deploy host. Pin the zone:

# date\_trunc('day', col, 'UTC')

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

# unrecognized forge:\* marker (a misspelled

# one does nothing and warns nowhere)

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

# disagrees between an entity message and

# its Create`<Entity>`Request (the flattened

# request silently drops write presence)

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

# non-generated Go file assigns — nothing

# populates it, so the column default ships

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

# nothing populates — no write path, no

# meaningful DEFAULT, so every row ships as

# the zero with no other symptom at all.

# Both WARN instead ("pending: implement X")

# while the service still holds forge's own

# unwired rpc stubs

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

# update\_mask still writes a column a

# custom rpc guards (forge:guards) — the

# form bypasses the rpc's own rules, and

# scaffold-once means regenerating cannot

# fix it

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

# export (output: static, or bound to a static

# runtime): FAIL on what the export refuses

# (\[id] without generateStaticParams, server

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

# and WARN on what it drops (middleware,

# rewrites, POST handlers, no trailingSlash

# on a bucket). CI's next build is the

# authority; this says it first, file:line

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

# option field forge's descriptors do not

# define — it compiles, and forge reads it

# nowhere

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

# drifted from this forge binary's copy

# (forge's upgrade path does not track it)

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

# loads (per-binary configs can strand a

# whole message)

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

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

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

# belongs in a generated React Query hook

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

# (schema in lint\_json.go; exit code matches

# text mode; combines with the targeted flags

# above, but not with --fix / --suggest-\*)

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

# last line — safe to pipe through head/tail.

# Combines with any targeted flag above:

# forge lint --read-only-fields --quiet

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

# Only findings in files under those paths.

# golangci-lint/contract run on the scope's

# packages; whole-project linters (frontend

# lint, component-drift, config-reach) are

# skipped and named in the verdict, so run an

# unscoped 'forge lint' before merging

The three advisory rules above are warnings only — they never fail the build.

Additional maintainer/debug flags exist (forge-repo internals, wiring
audits, suggest-\* helpers); run 'forge lint --help-dev' to list them.

```
reliant forge lint [paths...] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--column-markers` | `bool` | - | Flag COMMENT ON COLUMN/CONSTRAINT text containing forge: that matches no known column marker (warnings only) |
| `--computed-fields` | `bool` | - | Flag a forge:computed field that no non-generated Go file assigns — nothing populates it, so the insert takes the column default. FAILS the build, except while the service still holds forge-scaffolded unwired rpc stubs (then a warning naming them) |
| `--config-deps` | `bool` | - | Flag scalar Deps fields — scalars are configuration; declare a `<Component>`Config block in proto/config and take it as a typed field (warnings only) |
| `--config-reach` | `bool` | - | Flag config fields that no binary and no frontend loads — with per-binary configs, an unbound config message generates but is never loaded (warnings only) |
| `--contract` | `bool` | - | Run contract interface enforcement linter |
| `--conventions` | `bool` | - | Run forge convention rules on proto files |
| `--create-nullability` | `bool` | - | Fail when a field's optional label disagrees between an entity message and its Create`<Entity>`Request — the flattened request drops write presence silently |
| `--fix` | `bool` | - | Deprecated: auto-fix of deterministic-safe issues is now the default; this flag is a no-op kept for back-compat (use --no-fix to opt out) |
| `--fixture-drift` | `bool` | - | Execute every literal fixture statement in a scaffolded handlers\_crud\_test.go against the current schema — migrations applied to a shadow postgres, each statement run in a rolled-back transaction — and report postgres's own error for each one it rejects: a GENERATED column, a UNIQUE collision, a dangling foreign key, a CHECK a later migration added (warnings only) |
| `--frontend-stores` | `bool` | - | Flag Zustand stores that import generated Connect clients (warnings only) |
| `--gate-json` | `string` | - | Also write this run's result to `FILE` as a gate document, for `forge gate record` or `forge env deploy --gate`. A FILE, not a stdout mode: the findings and the exit code are unchanged. |
| `--generated-drift` | `bool` | - | Fail when a forge-generated ("Code generated by forge. DO NOT EDIT.") file was edited after forge wrote it |
| `--guarded-fields` | `bool` | - | Flag a scaffolded edit page whose update\_mask still names a column declared `forge:guards` — saving the form writes it raw and bypasses the rpc that owns it, and pages are scaffold-once so `forge generate` cannot repair them (warnings only) |
| `--help-dev` | `bool` | - | List maintainer/debug flags hidden from --help |
| `--json` | `bool` | - | Output findings as JSON (see lint\_json.go header for the schema; exit code matches text mode) |
| `--migration-safety` | `bool` | - | Run SQL migration safety checks |
| `--no-fix` | `bool` | - | Skip the deterministic-safe auto-fix pre-pass (Go formatting, golangci autofixes, frontend prettier, eslint --fix); gate only and mutate nothing (CI / read-only) |
| `--optional-deps-guard` | `bool` | - | Flag unguarded derefs of // forge:optional-dep Deps fields (warnings only; suppress with // forge:optional-checked on the deref line) |
| `--proto-markers` | `bool` | - | Flag .proto comments containing forge: that match no known proto marker — a misspelled marker is inert and warns nowhere (warnings only) |
| `--proto-options` | `bool` | - | Flag (forge.v1.\*) annotation fields this forge binary's descriptors do not define — a retired or misspelled option field compiles under buf and is read by nothing (warnings only) |
| `--quiet`, `-q` | `bool` | - | Print only what failed: a one-line summary per failing linter, its error findings, and the verdict as the LAST line (a clean run is one line). Auto-fix still applies; combines with the targeted flags and --scope |
| `--read-only-fields` | `bool` | - | Flag a forge:read-only field whose column nothing populates — no non-generated Go file assigns it, no meaningful DEFAULT, not GENERATED — so every row ships as the type's zero with no error anywhere. FAILS the build, except while the service still holds forge-scaffolded unwired rpc stubs (then a warning naming them) |
| `--scope` | `stringSlice` | `[]` | Report only findings in files under `PATH` (repeatable or comma-separated, project-relative). Go linters run on PATH's packages only; file-anchored linters report only findings under PATH; whole-project linters (frontend lint, component-drift, config-reach) cannot be scoped, are skipped, and are named in the verdict. Auto-fix touches only files under PATH |
| `--skip-frontends` | `bool` | - | Skip the whole frontend lane (eslint/stylelint + TypeScript typecheck + static-export) for a backend-only gate that needs no Node toolchain |
| `--static-export` | `bool` | - | For each Next.js frontend that must build to a static export (forge.yaml output: static, or bound to forge.OnHosted / OnBucket / OnFirebase in any env), report with file:line what the export cannot serve. FAILS on what next build refuses (a dynamic route without generateStaticParams, server actions, next/headers, next/image without images.unoptimized, a build that is not an export); WARNS on what it silently drops (middleware, non-GET route handlers, ungated rewrites/redirects/headers, a bucket-bound export without trailingSlash) |
| `--strict` | `bool` | - | Escalate advisory findings to errors so they fail the build / CI: RPCs missing a (forge.v1.method) auth-posture annotation, and any lane that could NOT run (frontend typecheck or eslint with deps not installed; typed-config guardrail when golangci-lint never reported) |
| `--tests` | `bool` | - | Run test-convention rules (handler-tests-use-tdd + frontend-hook-tests; warnings only) |
| `--time-bucketing` | `bool` | - | Flag a two-argument date\_trunc over a TIMESTAMPTZ column — postgres truncates it in the SESSION timezone, which the driver sets from the client host, so every bucketed total is attributed to the wrong day by an amount that changes with the deploy host. Pin the zone: date\_trunc('day', col, 'UTC') (warnings only) |
| `--vendored-protos` | `bool` | - | Fail when a vendored proto (proto/forge/v1/forge.proto) differs from the copy embedded in this forge binary — forge's upgrade path does not track these copies, so drift is otherwise invisible |

***

### 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](https://admin.reliantapi.com))
control\_plane = forge.ControlPlane \{     # any other control plane
endpoint = "[http://127.0.0.1:8090](http://127.0.0.1:8090)"
}

(two envs declaring the same endpoint share one login). When nothing declares
one — outside a project, or before any env does — it logs into Reliant cloud.
\--endpoint names one directly. There is no "current" server: every forge
command finds its credential by the endpoint its own env declares, so which
env a command acts on is always the env it names — never a login's side
effect.

INTERACTIVE (a human): the OAuth authorization-code flow with PKCE. forge
opens your browser at `<endpoint>`/oauth/authorize, you sign in and approve,
and the browser returns a one-time code to a temporary listener on a
loopback port (chosen by the OS, so two logins can run at once). forge
redeems it at `<endpoint>`/oauth/token for an access token (rlat\_…, 90 days).

NON-INTERACTIVE (CI): pass --token (with --endpoint when the project declares
more than one control plane), or set the environment variable the environment
declares (FORGE\_CONTROL\_PLANE\_TOKEN by default) and skip login entirely. A
pipeline has no browser, which is what org tokens are for.

SIGNED IN TO RELIANT? You do not need this command. `reliant forge …` and every
shell a Reliant agent runs set \$FORGE\_CREDENTIAL\_HELPER, and forge asks that
helper for a short-lived token minted from your Reliant session for exactly the
control plane an env declares. forge login is for standalone forge (no Reliant)
and for overriding the session with a different identity.

CREDENTIAL PRECEDENCE, when any forge command talks to the control plane:

1. \--token           explicit, beats everything
2. \$`<token_env>`      the env var the environment declares — CI
3. the credentials file entry for that env's endpoint — what this command stores
4. \$FORGE\_CREDENTIAL\_HELPER — a host application's session (Reliant's);
   also used when the stored login (3) has expired

```
reliant forge login [--endpoint URL] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--endpoint` | `string` | - | Log into this control plane URL only, instead of every one the project declares (default when none is declared: Reliant cloud) |
| `--no-verify` | `bool` | - | With --token: store it without checking it against the endpoint |
| `--token` | `string` | - | Store this token instead of running the browser flow |

***

### reliant forge logout

Forget the stored credential for this project's control plane(s)

```
reliant forge logout [--endpoint URL] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--endpoint` | `string` | - | Forget this control plane URL only, instead of every one the project declares |

***

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

**Subcommands:**

| Command | Description |
| - | - |
| [`new`](#reliant-forge-package-new) | Create a new internal package with contract interface |

***

#### reliant forge package new

Create a new internal package with contract interface

Create a new internal package under internal/`<name>`/ with:

* contract.go  — Go interface that IS the package contract
* service.go   — Implementation with unexported concrete type

After creation, define your interface methods in contract.go, then run
'forge generate' to produce mock\_gen.go and middleware\_gen.go.

package shape: service|adapter (default service)

service     Standard internal/`<name>`/ with Service/Deps/New — wired into
the composition, callable by handlers. The default, and the
right answer for a use-case orchestrator too: an orchestrator
is a service whose Deps are other services' interfaces.
adapter     The same shape plus '// forge:outbound-io', which asserts the
package calls OUT to a third-party system and serves nothing
inbound. Lint keeps RPC handlers out of it and the observe
heuristic treats it as doing I/O.
See: forge skill load adapter

Example:
forge package new cache
forge package new notifications
forge package new events --kind eventbus
forge package new stripe-adapter --type adapter

```
reliant forge package new <name> [flags]
```

**Flags:**

| Flag | Type | Default | Description | |
| - | - | - | - | - |
| `--kind` | `string` | - | package kind template (e.g. eventbus, client) | |
| `--type` | `string` | `service` | package shape: service | adapter (see --help for details) |

***

### reliant forge project

Create, evolve, and inspect the project as a whole

```
reliant forge project
```

**Subcommands:**

| Command | Description |
| - | - |
| [`annotations`](#reliant-forge-project-annotations) | Dump forge's authoritative entity-authoring annotation spec |
| [`audit`](#reliant-forge-project-audit) | Print a comprehensive project state snapshot |
| [`capabilities`](#reliant-forge-project-capabilities) | List everything forge can do — every command, analyzer, and marker, in one call |
| [`checkouts`](#reliant-forge-project-checkouts) | List origin/main plus every git worktree — the Preview picker's source |
| [`delete`](#reliant-forge-project-delete) | Delete (retire) a service from the project — the inverse of `forge scaffold service` |
| [`disown`](#reliant-forge-project-disown) | Permanently transfer a forge-generated file to user ownership (one-way) |
| [`features`](#reliant-forge-project-features) | Print the resolved feature graph (on/off, why, dependencies) |
| [`graph`](#reliant-forge-project-graph) | Emit a JSON dependency graph of the project's declared resources |
| [`introspect`](#reliant-forge-project-introspect) | Inspect what the assembled binary will expose at runtime |
| [`libraries`](#reliant-forge-project-libraries) | Print forge/pkg's API — name a package to get its full signatures |
| [`map`](#reliant-forge-project-map) | Print project tree with ownership annotations |
| [`migrate`](#reliant-forge-project-migrate) | Project-level migration tooling (import, convert) |
| [`new`](#reliant-forge-project-new) | Create a new Forge project (service / CLI / library) |
| [`rescaffold`](#reliant-forge-project-rescaffold) | Re-create scaffold-once files you deleted, as forge would scaffold them today |
| [`scaffolded`](#reliant-forge-project-scaffolded) | List the scaffold-once files forge has written, and which are absent |
| [`shapes`](#reliant-forge-project-shapes) | List every API shape — RPCs, messages, enums, tables, handlers, hooks — with file:line |
| [`upgrade`](#reliant-forge-project-upgrade) | Update frozen project files from latest Forge templates |

***

#### reliant forge project annotations

Dump forge's authoritative entity-authoring annotation spec

Dump forge's authoritative annotation spec.

Emits the vocabulary forge itself understands, sourced from forge's own
definitions so it cannot drift from behavior:

markers          the // forge:\* comment markers, in proto AND Go source,
plus the forge:\* COMMENT ON COLUMN/CONSTRAINT markers
field\_types      the proto->column mapping an entity birth applies
validate\_rules   the projected protovalidate subset and its db/zod/wire effects
service\_options  the (forge.v1.service) options
method\_options   the (forge.v1.method) options

Use --kind to limit the dump to one annotation level, and --json for a
machine-readable dump that tools can query instead of re-deriving the spec.
\--kind column is the forge:\* markers declared as a postgres catalog COMMENT
in a migration rather than a proto/Go comment (forge:immutable on a column,
forge:ref on a foreign-key constraint); --kind table is the same mechanism one
level up, on the table itself (forge:append-only). --kind go is the wiring/observability
/contract vocabulary forge reads out of .go files (forge:optional-dep,
forge:constructor, forge:no-observe, forge:exclude-contract, ...) — the
markers you meet in scaffolded code.

Examples:
forge project annotations --json
forge project annotations --kind entity --json
forge project annotations --kind column
forge project annotations --kind go

```
reliant forge project annotations [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | emit the spec as JSON |
| `--kind` | `string` | - | limit to one annotation kind: entity, field, column, table, service, method, or go (default: all) |

***

#### reliant forge project audit

Print a comprehensive project state snapshot

Print a comprehensive snapshot of forge project state.

Audit reports forge version pin, project shape, lint roll-ups, codegen
state, proto vs migration alignment, scaffold markers, and dep health.
Use --json for machine-readable output (sub-agents).

EXIT CODE. Audit exits non-zero when a category reports an ERROR (✗), and
zero when the worst finding is a warning (⚠). Warnings are reported and
never gate: a freshly-scaffolded project legitimately carries several, so
failing on them would make forge's own output fail forge's own gate.

Categories that can error are ones you armed. unscoped\_auth is the
clearest case: it warns about authenticated RPCs that never resolve the
caller, and becomes an error only for RPCs over a table whose migration
declares a forge:owner column — your sentence, in your schema, is what
turns the advice into a gate.

Examples:
forge project audit            # human-readable; exits 1 on any ✗
forge project audit --json     # machine-readable (same exit code)

```
reliant forge project audit [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output as JSON |

***

#### 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 capabilities [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | emit the inventory as 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 checkouts [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Print the checkouts as JSON |
| `--tree` | `bool` | - | Also compute the tree hash of the checkout you are in (adds \~0.5s) |

***

#### reliant forge project delete

Delete (retire) a service from the project — the inverse of `forge scaffold service`

Delete a component from an existing Forge project.

Subcommands:
forge project delete service `<name>`   Retire a service: remove the handlers/`<name>`/
scaffold and leave a types-only tombstone in
pkg/app/services.go so the proto types / Connect
client keep generating for callers while the
handler is no longer served.

```
reliant forge project delete
```

**Subcommands:**

| Command | Description |
| - | - |
| [`service`](#reliant-forge-project-delete-service) | Retire a service (inverse of `forge scaffold service`) |

***

#### reliant forge project delete service

Retire a service (inverse of `forge scaffold service`)

Retire a service from this project.

What it does:

* deletes the handlers/`<name>`/ scaffold directory
* leaves a types-only tombstone comment in pkg/app/services.go (so the
  proto types, Connect client, and frontend hooks keep generating for
  callers — the service is no longer SERVED, but its contract survives)

Pass --no-keep-types to omit the tombstone comment entirely; the service
then reverts to "unlisted" and forge will re-scaffold its handler on the
next generate if it's still declared in proto.

This is destructive (it removes a directory). It prompts for confirmation
unless --yes; --dry-run prints the plan and changes nothing.

Example:
forge project delete service reporting

```
reliant forge project delete service <name> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dry-run` | `bool` | - | Print what would change without touching any files |
| `--keep-types` | `bool` | `true` | Leave a types-only tombstone in pkg/app/services.go so proto types + Connect client keep generating (default; --keep-types=false to fully unlist) |
| `--yes` | `bool` | - | Skip the confirmation prompt |

***

#### reliant forge project disown

Permanently transfer a forge-generated file to user ownership (one-way)

Transfer one or more forge-owned (Tier-1) generated files to user ownership.

After disowning, forge NEVER touches the file again: no regeneration, no
overwrite (not even with --force), no drift errors. The file is yours, like any
starter scaffold. This is a ONE-WAY door — there is no "un-disown" that keeps
your edits.

\--reason is required. Disowning means the generated code couldn't express what
you needed; the reason is design feedback, recorded per path in
.forge/disowned.json (surfaced by `forge project audit --json`).

Re-adoption (returning the file to forge ownership) is by deletion:

rm `<path>` && forge generate

The emitter re-emits the pristine render and the entry returns to Tier-1. Your
disowned content is discarded — copy anything you want to keep into a
user-owned extension point first.

Prefer NOT disowning when you can: most customizations have a designated
user-owned home (pkg/app/setup.go / app\_extras.go,
…) that survives every regenerate.

Example:
forge project disown pkg/app/wire\_gen.go --reason "custom connection-pool wiring forge can't express"

```
reliant forge project disown <path>... --reason <text> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dry-run` | `bool` | - | Print what would change without writing .forge/disowned.json |
| `--reason` | `string` | - | WHY forge's generated code couldn't express what you needed (required; recorded in .forge/disowned.json) |

***

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

***

#### reliant forge project graph

Emit a JSON dependency graph of the project's declared resources

Emit a single JSON document describing every resource the
project declares (services, packages, frontends, binaries, gateways,
routes) and the explicit dependency edges between them.

Sources are existing forge parsers: forge.yaml, the KCL render for
the selected env, contract.go's Deps struct, and gen/forge\_descriptor.json.
Partial-data conditions (missing forge.yaml, KCL fails to render) are
reported via the top-level "warnings" array rather than aborting the
command — consumers should always read warnings before trusting the
graph as complete.

Output goes to stdout; warnings and errors to stderr.

Examples:
forge project graph                 # dev env
forge project graph --env=staging   # staging KCL render

```
reliant forge project graph [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | `dev` | KCL environment to render |

***

#### reliant forge project introspect

Inspect what the assembled binary will expose at runtime

Inspect what the assembled binary will expose at runtime.

Subcommands answer "what would the binary do" questions without
requiring it to be running. Useful for catching wiring mistakes early.

Subcommands:
handlers   Print every RPC path the binary will register.

```
reliant forge project introspect
```

**Subcommands:**

| Command | Description |
| - | - |
| [`handlers`](#reliant-forge-project-introspect-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 introspect handlers [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--format` | `string` | `text` | Output format: text or json |
| `--proto-dir` | `string` | `proto/services` | Directory containing service proto files |

***

#### 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 libraries [package...] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | emit the inventory as JSON |
| `--signatures` | `string` | - | same selectors as the positional form, comma-separated: `pkg`, `pkg.Symbol`, or `all` |

***

#### reliant forge project map

Print project tree with ownership annotations

Print project tree with ownership annotations.

Each path is annotated as user-owned, forge-space (regenerated), scaffold
(FORGE\_SCAFFOLD markers), or drifted (forge-space file with hand-edits).
Use --depth to truncate, --filter to focus on a subtree, --json for
machine-readable output.

Examples:
forge project map                    # full tree
forge project map --depth 2          # shallow
forge project map --filter handlers  # subtree
forge project map --json             # machine-readable

```
reliant forge project map [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--depth` | `int` | `0` | Max depth (0 = unlimited) |
| `--filter` | `string` | - | Subtree path filter (e.g. handlers/) |
| `--json` | `bool` | - | Output as JSON |

***

#### reliant forge project migrate

Project-level migration tooling (import, convert)

Project-level migration tooling.

This command group operates on migration files in the working tree. It is
distinct from `forge db migrate`, which drives the golang-migrate runner
against a live database.

Examples:
forge project migrate import --from goose --src-dir ../old-project/migrations
forge project migrate tdd --dry-run

```
reliant forge project migrate
```

**Subcommands:**

| Command | Description |
| - | - |
| [`import`](#reliant-forge-project-migrate-import) | Import migrations from another format (e.g. goose) into golang-migrate shape |
| [`tdd`](#reliant-forge-project-migrate-tdd) | Rewrite hand-rolled handler tests to use tdd.RunRPCCases |

***

#### reliant forge project migrate import

Import migrations from another format (e.g. goose) into golang-migrate shape

Import SQL migrations from another tool's format into forge's
forward-only golang-migrate shape (one .up.sql per migration).

Currently supports:
\--from goose    One-file goose migrations with -- +goose Up / -- +goose Down

For each \*.sql file in --src-dir, the importer:

1. Keeps the -- +goose Up section and DROPS the -- +goose Down section.
   Forge rolls forward only and never runs down SQL, so the importer
   writes no .down.sql; each dropped Down section is listed so you can
   see what was discarded.
2. Drops -- +goose StatementBegin / -- +goose StatementEnd markers.
3. Carries -- +goose NO TRANSACTION over to a golang-migrate x-no-tx-wrap
   header.
4. Renumbers starting from the next-available index in --dest-dir, so
   pack-installed migrations (00001-0000N) keep their slots.

Files with no goose markers are skipped.

Examples:
forge project migrate import --from goose --src-dir ../old-project/migrations
forge project migrate import --from goose --src-dir ./legacy --dry-run
forge project migrate import --from goose --src-dir ./legacy --force

```
reliant forge project migrate import [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dest-dir` | `string` | - | Destination migrations directory (default: project's migrations dir) |
| `--dry-run` | `bool` | - | Print planned writes without touching disk |
| `--force` | `bool` | - | Overwrite existing target files (default refuses) |
| `--from` | `string` | - | Source format (currently only 'goose' supported) |
| `--src-dir` | `string` | - | Directory containing source migration files |

***

#### 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 migrate tdd [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dry-run` | `bool` | - | Print actions without writing files |
| `--path` | `string` | - | Project root or subtree to walk (default: cwd) |

***

#### 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 new [project-name] --mod [module-path] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--binary` | `string` | `per-service` | Binary packaging: 'per-service' (default — canonical cmd/server.go cobra root, one Application per service) or 'shared' (one Go binary, cobra subcommand per service, KCL MultiServiceApplication for deploy) |
| `--buf-plugins` | `string` | `local` | Default proto plugin source: 'local' (resolved from PATH; no BSR auth needed) or 'remote' (BSR-hosted, requires login under load) |
| `--disable` | `stringSlice` | `[]` | Features to leave out of the scaffold (comma-separated): orm, codegen, migrations, ci, build, deploy, contracts, frontend, observability, hot\_reload. Not persisted: features derive from the tree that gets written. |
| `--force` | `bool` | - | With --in-place: scaffold over an existing forge.yaml, and REPLACE every pre-existing file the scaffold writes (README.md, go.mod, Taskfile.yml, …) with forge's version. Without it, existing files are kept and listed. .gitignore is always merged, never replaced |
| `--frontend` | `stringSlice` | `[]` | Name(s) of Next.js frontends (can be repeated or comma-separated) |
| `--frontend-workspaces` | `bool` | - | Opt into pnpm-workspaces layout: emit packages/api + packages/hooks + packages/ui-web shared across all frontends. Off by default; recommended once you have 2+ frontends (web + mobile). |
| `--go-version` | `string` | - | Go version to use in go.mod (e.g., 1.24); defaults to detected version |
| `--harness` | `string` | `reliant` | AI harness conventions to scaffold for. Each writes a memory file; only claude also receives on-disk skills. reliant (default): reliant.md — skills are read from the forge binary (`forge skill load <name>`) and discovered via forge.yaml, so NO skill files are written. claude: CLAUDE.md + .claude/skills/ (regenerated every `forge generate`). cursor: .cursorrules. copilot: .github/copilot-instructions.md. codex: AGENTS.md. Recorded as `harness:` in forge.yaml and honored by every later generate |
| `--in-place` | `bool` | - | Create project in current directory instead of a new subdirectory |
| `--kind` | `string` | `service` | Project kind: service (default), cli, library |
| `--link-forge` | `bool` | - | Bridge the project to the forge checkout this binary was built from (gitignored go.work 'use' + .forge-link/ for the web runtime). Off by default; required for a service scaffold from an unreleased forge build (also: FORGE\_LINK\_FORGE=1) |
| `--mod` | `string` | - | Go module path (e.g., github.com/example/my-project). Defaults to example.com/`<name>`, so a first project needs no flag; set it to the path you will publish under |
| `--name` | `string` | - | Project name. Same as the positional arg, and the way to name an --in-place project whose directory (a worktree, a branch checkout) is not the product name; defaults to the directory name |
| `--path`, `-p` | `string` | `.` | Path where to create the project |
| `--service` | `stringSlice` | `[]` | Name(s) of initial Go services (repeatable or comma-separated). Name services after domain entities (item, order), not the binary. Omit to scaffold zero services and add them later via 'forge scaffold service `<entity>`' |
| `--skip-tools` | `bool` | - | Skip auto-installing protoc-gen-go / protoc-gen-connect-go (run 'forge tools install' later) |

***

#### reliant forge project rescaffold

Re-create scaffold-once files you deleted, as forge would scaffold them today

Re-create one or more scaffold-once ("yours") files.

forge writes a scaffold-once file exactly once. Deleting it is an act of
ownership that .forge/scaffolded.json records, so `forge generate` leaves it deleted —
in this checkout and every clone. To get forge's version back, name it here:

forge project rescaffold .github/workflows/ci.yml

For each path, rescaffold drops its birth record, writes the file the way forge
scaffolds it for this project today, and records the birth again: the file is
yours once more, and deleting it again sticks.

Every file forge scaffolds can be re-created this way — CI workflows (through
the same mapper `forge generate` uses, so only workflows this project has),
.pre-commit-config.yaml, the devcontainer, the command tree, internal/app, the
frontend pages, and the rest.

A path that still exists is refused: rescaffold never overwrites your bytes. To
replace a file you have edited with the current template, use
`forge project upgrade --force <path>`, which shows the diff first.

Examples:
forge project rescaffold .github/workflows/e2e.yml .pre-commit-config.yaml
forge project rescaffold internal/handlers/item/handlers\_crud\_test.go

```
reliant forge project rescaffold <path>...
```

***

#### reliant forge project scaffolded

List the scaffold-once files forge has written, and which are absent

List every scaffold-once ("yours") file forge has written in this project,
from the birth ledger at .forge/scaffolded.json.

forge writes a scaffold-once file exactly once and then it is yours. Deleting
one is an act of ownership the ledger records, so `forge generate` leaves it
deleted — here and in every clone. `forge generate` reports that set only when
it CHANGES; this command answers it any time.

present   the file is on disk — your bytes, forge leaves them alone
ABSENT    you deleted it, and forge is respecting that

To have forge write an absent one again, fresh against the project as it
stands today:

cli forge project rescaffold `<path>`

Examples:
cli forge project scaffolded
cli forge project scaffolded --absent

```
reliant forge project scaffolded [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--absent` | `bool` | - | List only the files that are absent (deleted on purpose) |

***

#### reliant forge project shapes

List every API shape — RPCs, messages, enums, tables, handlers, hooks — with file:line

List every API shape in the project with its source location.

Derived LIVE from .proto, db/migrations and source on every call — never from
gen/forge\_descriptor.json, which is a cache and can be stale. Answers "where is
X and what shape is it" in ONE call instead of a sequence of greps.

Start recon here. A measured fan-out spent 28 minutes across 123 calls
re-deriving this by hand, mostly paging one large proto by symbol.

\--grep is a REGEX, so ask about every entity you own in ONE call. A later run
adopted this command and still invoked it once per entity — 11 turns of
\--grep Property then --grep Customer — which is the old grep loop wearing a
new command's name.

Examples:
forge project shapes                          # everything
forge project shapes --grep Estimate          # one entity across all layers
forge project shapes --grep 'Invoice|Payment' # SEVERAL entities in one call
forge project shapes --kind rpc,handler       # what is declared vs implemented
forge project shapes --kind deploy-target     # what a workload's deploy= accepts

```
reliant forge project shapes [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--grep` | `string` | - | case-insensitive regex filter over name and detail |
| `--kind` | `string` | - | comma-separated kinds: rpc,message,enum,table,store,handler,hook,deploy-target |

***

#### reliant forge project upgrade

Update frozen project files from latest Forge templates

Detect template drift on frozen files (files written at 'forge project new' time but
not updated by 'forge generate') and apply updates from newer Forge templates.

Files that haven't been modified by the user are updated automatically.
User-modified files show a diff and are skipped unless --force is used.

\--force is per-path. Naming paths after --force restricts the overwrite to
exactly those files, so you can adopt one new template without stomping every
other file you have customized. Bare --force keeps its whole-project meaning.

A file you have deliberately taken over is better claimed than defended:
'forge project disown `<path>` --reason ...' records the ownership transfer, and
upgrade then skips the file permanently — even under --force.

Scaffold-once files (each frontend's shared mechanism modules under src/lib and
src/hooks, plus the .github starters) get their own section and are NEVER written
implicitly. They are yours from birth; upgrade only reports what their templates
gained, how far behind you are in lines, and how many lines in your copy no
template accounts for. Adopt one by naming it:
'forge project upgrade --force `<path>`'. Bare --force does not reach them.

After a successful upgrade the project's forge\_version field in forge.yaml
is bumped to the current binary version (or to --to when provided). Migrations
whose declared version range AND detection script both match this project are
surfaced first, so the LLM running upgrade can follow them step-by-step.

\--check SUMMARIZES; --check `<path>` DETAILS. The report names each file that
differs and sizes the difference in lines — it does NOT print diffs inline,
because on a real project that is thousands of lines nobody reads. Files are
grouped and ranked by what adopting them costs: the ones with no local edits
come first, since for those a single --force is the whole job. Name a path
after --check to see that one file's full diff, or pass --all to list every
file the groups summarized.

Examples:
forge project upgrade                        # Upgrade to latest, run all needed migrations
forge project upgrade --to 1.5.0             # Upgrade to a specific target version
forge project upgrade --dry-run              # Show what would change (alias for --check)
forge project upgrade --check                # Dry-run summary: what differs, and by how much
forge project upgrade --check buf.yaml       # That one file's full diff
forge project upgrade --check --all          # Every reported file (still no inline diffs)
forge project upgrade --force                # Apply all updates, even for user-modified files
forge project upgrade --force buf.yaml       # Overwrite ONLY buf.yaml; leave other edits alone

```
reliant forge project upgrade [--force <path>...] [flags]
```

**Subcommands:**

| Command | Description |
| - | - |
| [`apply`](#reliant-forge-project-upgrade-apply) | Record a migration as applied (writes .forge/migrations.json) |
| [`list`](#reliant-forge-project-upgrade-list) | List pending forge migrations for this project |

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--all` | `bool` | - | List every reported file instead of the first few per group |
| `--check` | `bool` | - | Dry-run: only show what would change, don't write files |
| `--dry-run` | `bool` | - | Alias for --check |
| `--force` | `bool` | - | Overwrite user-modified files. Name paths after it to overwrite only those |
| `--to` | `string` | - | Target forge version (defaults to the current binary version) |

***

#### 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 apply <migration-id>
```

***

#### 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 project upgrade list [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output JSON instead of human-readable text |

***

### reliant forge registry

Log in to and read refs from the registries an env's workloads declare

Work with the image registries an environment's workloads DECLARE — each
workload's `image` in deploy/kcl/workloads.k carries its own registry, and no
environment declares one.

The registry is never passed to forge: these commands read it from the workload
declarations, exactly as `forge env build <env> --push` and `forge env deploy <env>` do.
An env whose workloads name two registries is handled by both commands without
forge needing a concept for it.

```
reliant forge registry
```

**Subcommands:**

| Command | Description |
| - | - |
| [`login`](#reliant-forge-registry-login) | docker login to every registry the env's workloads declare |
| [`ref`](#reliant-forge-registry-ref) | Print the digest-pinned refs `forge env build <env> --push` pushed |

***

#### reliant forge registry login

docker login to every registry the env's workloads declare

Log docker in to each registry host named by the images the env's workloads
declare, so that `forge env build <env> --push` and any signing / SBOM / scanning step
that follows can reach them.

The registry HOSTS come from the workload declarations and nowhere else — there
is no host argument and no flag that takes one. An env whose workloads push to
two registries logs in to both, in one command.

THE PLATFORM REGISTRY NEEDS NO CREDENTIAL FROM YOU. For the host named by an
env's forge.ControlPlane registry\_host (Reliant's registry by default),
forge presents the SAME control-plane credential it reaches the control plane
with — `--token`, then the env's declared token\_env, then what `forge login`
stored, then the credential helper \$FORGE\_CREDENTIAL\_HELPER (a host application's
session — Reliant sets it). One token, so there is nothing to mint and nothing
to rotate:

forge registry login prod

`forge env build <env> --push` and `forge env deploy <env>` do this for
themselves before their first push, so a hosted pipeline usually needs no login
step at all.

FOR ANYBODY ELSE'S REGISTRY the credential is the only thing you pass, and it is
not a registry pointer:

echo "$GITHUB_TOKEN" | forge registry login prod --username "$GITHUB\_ACTOR" --password-stdin

or name the environment variable holding it, the way forge.ControlPlane names
its token\_env — so a CI config states a variable NAME (non-sensitive, belongs in
git) rather than piping a secret through a shell:

forge registry login prod --username "\$GITHUB\_ACTOR" --password-env GITHUB\_TOKEN

One credential is used for every foreign host. That is correct for the
overwhelmingly common case (one org, one registry, one token) and honest about
the rest: for two foreign registries needing two credentials, run the command
twice, or log the second one in with plain `docker login` — forge holds no
credential store and inventing one here would be a secrets manager, not a build
tool.

An env that pushes to the platform registry AND a foreign one logs in to both in
one run: ours from the control-plane credential, theirs from the flags.

A k3d-local registry (localhost / \*.localhost) takes no credentials, so it is
skipped.

```
reliant forge registry login <environment> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--password-env` | `string` | - | Name of the environment variable holding a foreign registry's credential (the forge.ControlPlane token\_env convention: the NAME is in git, the VALUE never is) |
| `--password-stdin` | `bool` | - | Read a foreign registry's credential from stdin (a credential never belongs on the command line) |
| `--token` | `string` | - | Control-plane credential to present to the PLATFORM registry, overriding the env's token\_env and the stored `forge login` (one-off / debugging) |
| `--username`, `-u` | `string` | - | Registry user the credential belongs to, for a registry that is NOT the platform's (e.g. \$GITHUB\_ACTOR, \_json\_key, oauth2accesstoken, AWS) |

***

#### reliant forge registry ref

Print the digest-pinned refs `forge env build <env> --push` pushed

Print `<image>@<digest>` for each image the last `forge env build <env> --push`
pushed — the immutable references a signing, SBOM, provenance or vulnerability
scan step should act on. They are read from .forge/state/ (what the build
recorded), so each ref names the registry its own workload declared.

By default every built image is printed, one per line, prefixed with the
workload that declared it. --image narrows to one.

\--github-output also appends ref=, image= and digest= to the file GitHub
Actions names in \$GITHUB\_OUTPUT. With several images it writes the `<workload>`\_
prefixed form as well, so a later step can address a specific one.

```
reliant forge registry ref <environment> [--image <name>] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--github-output` | `bool` | - | Also append ref=, image= and digest= to \$GITHUB\_OUTPUT |
| `--image` | `string` | - | Print only this image (default: every image the build pushed) |

***

### reliant forge release

Inspect and verify release ledgers

Work with the release ledgers `forge env build <env> --release <version>` writes.

A release ledger (.forge/releases/`<version>`.json) names every artifact a
release ships — container images, npm packages, Go modules, published files —
with the coordinate and hash each one was cut with.

```
reliant forge release
```

**Subcommands:**

| Command | Description |
| - | - |
| [`verify`](#reliant-forge-release-verify) | Prove every artifact a release names actually exists and matches |
| [`where`](#reliant-forge-release-where) | Show which environments are currently bound to a release |

***

#### 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 verify <version> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--concurrency` | `int` | `8` | Maximum simultaneous registry requests |
| `--json` | `bool` | - | Emit the verification report as JSON (exit code unchanged) |
| `--strict` | `bool` | - | Treat UNVERIFIABLE artifacts as failures (exit 1) |
| `--timeout` | `duration` | `30s` | Per-request timeout for registry reads |

***

#### 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 release where <version> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | - | Ask only this env's ledger (default: every env this checkout declares) |
| `--json` | `bool` | - | Emit machine-readable JSON (same exit codes as text mode) |
| `--token` | `string` | - | Credential for a hosted ledger, ahead of the env var and the credentials file |

***

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

```
reliant forge scaffold [flags]
```

**Subcommands:**

| Command | Description |
| - | - |
| [`adapter`](#reliant-forge-scaffold-adapter) | Scaffold an outbound adapter (HTTP client, queue producer, storage gateway) |
| [`binary`](#reliant-forge-scaffold-binary) | Scaffold a non-server long-running binary |
| [`crd`](#reliant-forge-scaffold-crd) | Scaffold a Custom Resource Definition + reconciler on an operator |
| [`entity`](#reliant-forge-scaffold-entity) | Birth a database entity from its already-authored proto message: the owned forward migration + the CRUD wire contract |
| [`frontend`](#reliant-forge-scaffold-frontend) | Scaffold a new frontend |
| [`handler-file`](#reliant-forge-scaffold-handler-file) | Scaffold an additional RPC-group file in an existing handler directory |
| [`library`](#reliant-forge-scaffold-library) | Scaffold a library-shaped Go package (no contract.go) and pre-register the contracts.exclude entry |
| [`operator`](#reliant-forge-scaffold-operator) | Scaffold a new Kubernetes operator |
| [`package`](#reliant-forge-scaffold-package) | Scaffold a new internal package (alias for 'forge package new') |
| [`rpc`](#reliant-forge-scaffold-rpc) | Scaffold a custom RPC on the service's handler package |
| [`scenario`](#reliant-forge-scaffold-scenario) | Scaffold a new frontend mock scenario |
| [`service`](#reliant-forge-scaffold-service) | Scaffold one or more Go services |
| [`webhook`](#reliant-forge-scaffold-webhook) | Scaffold a webhook endpoint on an existing service |
| [`worker`](#reliant-forge-scaffold-worker) | Scaffold a new background worker |

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dry-run` | `bool` | - | print the full plan (phase 1+2 predictions) and write nothing |
| `--service` | `string` | - | narrow every phase of the sweep to one service |

***

#### 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 adapter <name>
```

**Flags:**

***

#### reliant forge scaffold binary

Scaffold a non-server long-running binary

Scaffold a non-server long-running binary into an existing Forge project.

A binary is a process with its own Deployment shape. Use this when:

* You need a reverse proxy / gateway in front of pods.
* You want an off-service NATS consumer that isn't an in-process worker.
* You need a sidecar with its own deploy lifecycle.

For in-process background work, use 'forge scaffold worker' instead — workers
share the canonical server's lifecycle and Deps.

This creates:
cmd/`<name>`.go                       Cobra subcommand (registered against the shared root)
internal/`<name>`/contract.go          Deps, Service, New(deps) (\*Runner, error)
internal/`<name>`/`<name>`.go            Runner.Run(ctx) lifecycle body
internal/`<name>`/`<name>`\_test.go       Lifecycle + validateDeps tests

And an entry under 'binaries:' in forge.yaml so deploy emits a
Deployment for the binary. See the binaries skill (`forge skill load binaries`)
for when to choose a binary vs worker vs service.

Example:
forge scaffold binary workspace-proxy

```
reliant forge scaffold binary <name>
```

***

#### reliant forge scaffold crd

Scaffold a Custom Resource Definition + reconciler on an operator

Scaffold a Kubernetes Custom Resource Definition and its reconciler onto an
existing operator.

Generates:

* api/`<version>`/`<name>`\_types.go              CRD spec + status types
* operators/`<operator>`/`<name>`\_controller.go  thin reconciler shim
* operators/`<operator>`/`<name>`\_controller\_test.go fake-client unit test

The reconciler shim embeds forge/pkg/controller.Reconciler\[T] which
provides fetch / NotFound / finalizer / dispatch lifecycle automatically.
You implement ReconcileSpec (and FinalizeSpec, when finalization needs
cleanup) for the domain logic.

Shapes:
state-machine  Spec.State drives the loop through observable phases (default).
config         Declarative-only — Spec describes a configuration to project.
composite      Spec owns sub-resources whose lifetime is coupled to the parent.

Example:
forge scaffold crd Workspace
forge scaffold crd Database --shape config
forge scaffold crd Cluster --shape composite --operator manager

```
reliant forge scaffold crd <Name> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--group` | `string` | - | API group (default: parent operator's group) |
| `--operator` | `string` | - | Target operator name (default: only operator in project) |
| `--shape` | `string` | `state-machine` | Reconciler scaffold style: state-machine, config, composite |
| `--version` | `string` | - | API version (default: parent operator's version) |

***

#### 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 entity <name> --from-proto <svc>[.<Message>] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dry-run` | `bool` | - | print the --from-proto birth plan (migrations + quintet completion) and write nothing |
| `--from-proto` | `string` | - | derive the create-table migration from an already-authored proto message: `<svc>` or `<svc>`.`<Message>` (one-time; the migration is yours at birth) |
| `--no-timestamps` | `bool` | - | skip the managed created\_at/updated\_at columns |
| `--soft-delete` | `bool` | - | add deleted\_at TIMESTAMPTZ — deletes become UPDATEs, reads filter IS NULL |
| `--table` | `string` | - | table name override (default: pluralized snake\_case of `<name>`) |

***

#### 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 frontend <name> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--auth-mode` | `string` | - | Sign-in flow for this frontend. Only `native` (the default) is scaffolded: your own form POSTs credentials to your own API and gets an HttpOnly session cookie; the server runs the OIDC flow (internal/app/login\_broker.go) and the browser never contacts the IdP. |
| `--base-path` | `string` | - | URL prefix the frontend is mounted under (e.g. "/admin"). Only applies to --kind web. |
| `--kind` | `string` | - | frontend kind (web, mobile, or vite-spa) |
| `--output` | `string` | - | Next.js output shape: static (default — a static export into out/), standalone, or server. Only applies to --kind web. |
| `--port` | `int` | `0` | Pin the frontend dev-server port. Default (unset) allocates a free port at launch — set this only when the port is externally fixed (e.g. an OAuth redirect URI registered with an IdP). |
| `--routes` | `stringSlice` | `[]` | Only generate CRUD pages for these entity route slugs (e.g. --routes users,usage-events), or `--routes none` for no generated pages at all. Default (unset) generates a page set for EVERY entity. Persisted as frontends\[].routes and honored by every later generate run. |

***

#### 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 handler-file <svc> <name>
```

***

#### 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 library <name> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--force` | `bool` | - | Overwrite the target directory if it already exists |
| `--no-exclude` | `bool` | - | Skip adding the path to forge.yaml's contracts.exclude |
| `--path` | `string` | - | Target directory (default: internal/`<name>`/) |

***

#### 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 operator <name> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--api-package` | `string` | - | (legacy) Go package name for the placeholder CRD types |
| `--crd-type` | `string` | - | (legacy) Placeholder CRD Go type name |
| `--group` | `string` | - | API group (default: `<project-name>`.io) |
| `--version` | `string` | `v1alpha1` | API version |
| `--with-placeholder-crd` | `bool` | - | Emit legacy types.go + controller.go scaffold (use 'forge scaffold crd' for new CRDs) |

***

#### 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 package <name> [flags]
```

**Flags:**

| Flag | Type | Default | Description | |
| - | - | - | - | - |
| `--kind` | `string` | - | package kind template (e.g. eventbus, client) | |
| `--type` | `string` | `service` | package shape: service | adapter (see --help) |

***

#### 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 rpc <svc> <Name> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--stream` | `string` | - | stream mode (server, client, bidi); omit for unary — only used when the RPC is not in the proto yet |

***

#### 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 scenario <name> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--description` | `string` | - | Optional human-readable description embedded in the generated file |
| `--from` | `string` | - | Copy an existing scenario file as a starting point (by name, no .ts suffix) |
| `--frontend` | `string` | - | Target frontend name (required when the project has multiple frontends) |

***

#### 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 service <name>... [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--force` | `bool` | - | Force-overwrite every scaffold output file |
| `--resume` | `bool` | - | Resume a partial scaffold: skip files that already exist |

***

#### reliant forge scaffold webhook

Scaffold a webhook endpoint on an existing service

Scaffold a webhook ingestion endpoint onto an existing Go service.

This scaffolds a webhook handler with signature verification and idempotency,
along with a test file. The handler is added to the service's handler directory.

Example:
forge scaffold webhook stripe --service payments
forge scaffold webhook github --service notifications

```
reliant forge scaffold webhook <name> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--service` | `string` | - | Target service name (required) |

***

#### reliant forge scaffold worker

Scaffold a new background worker

Scaffold a new background worker into an existing Forge project.

A worker is a long-running process that doesn't serve HTTP but participates
in the single-binary lifecycle. It has Start(ctx)/Stop(ctx) methods, health
reporting, and the same Deps injection as services.

The --no-generate flag suppresses the post-scaffold `forge generate` run.
The scaffold itself (workers/`<name>`/\*.go + forge.yaml services append) is the
only step the verb promises; the pipeline run is a convenience that becomes
hostile under parallel-agent work (see kalshi-trader migration round friction
forge-add-worker-runs-full-pipeline). Pass --no-generate when staging
scaffold-only changes in a multi-lane round and follow up with an explicit
`forge generate` at a coordination point.

Example:
forge scaffold worker email\_sender
forge scaffold worker order\_processor
forge scaffold worker cleanup --kind cron --schedule "\*/5 \* \* \* \*"
forge scaffold worker engine\_shadow --kind cron --schedule "0 3 \* \* \*" --no-generate

```
reliant forge scaffold worker <name> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--kind` | `string` | - | worker kind (use cron for scheduled workers) |
| `--no-generate` | `bool` | - | skip the post-scaffold `forge generate` run (scaffold-only mode for parallel-agent rounds) |
| `--schedule` | `string` | - | cron schedule for --kind cron workers |

***

### reliant forge secret

Manage an environment's secret store (local file or hosted control plane)

Manage the secret store an environment's secret\_provider declares:

forge.FileSecrets    the gitignored YAML store (a local env) — a flat map of
env-var NAME to value.
forge.HostedSecrets  the env's control plane (control\_plane). set / unset /
list go through its API. A hosted env's values are
never read back by these commands; a LOCAL env's
(control\_plane with no hosted tier) are pulled into
memory by `forge env up` and nothing else.

Every command names its environment with a REQUIRED --env flag. There is no
default and no positional form: the env is the one thing a secret command
must never guess.

A secret is declared ONCE in KCL as a reference (EnvVar.secret\_ref); its
value lives here and never enters git or KCL render output. A value only
reaches a service that DECLARES it, so putting something here that no
service references does nothing — config belongs in deploy/kcl/`<env>`/config.k.

forge secret set   --env dev STRIPE\_SECRET\_KEY   # value on stdin; add, or replace (= rotate)
forge secret unset --env dev STRIPE\_SECRET\_KEY
forge secret list  --env dev                     # names + presence, never values
forge secret ensure --env dev                    # FileSecrets: create the file + report missing
forge secret migrate --env dev                   # FileSecrets: convert a legacy .env file

```
reliant forge secret
```

**Subcommands:**

| Command | Description |
| - | - |
| [`ensure`](#reliant-forge-secret-ensure) | Create the FileSecrets store and report missing values |
| [`list`](#reliant-forge-secret-list) | List declared secrets and whether each has a value |
| [`migrate`](#reliant-forge-secret-migrate) | Convert a legacy .env secrets file into the YAML store |
| [`set`](#reliant-forge-secret-set) | Add or replace one secret value (read from stdin) |
| [`unset`](#reliant-forge-secret-unset) | Remove one secret from the store |

***

#### 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 ensure --env <environment> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | - | Environment whose secret store to act on (required; deploy/kcl/`<env>`/) |

***

#### 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 list --env <environment> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | - | Environment whose secret store to act on (required; deploy/kcl/`<env>`/) |
| `--json` | `bool` | - | Emit machine-readable JSON (names/presence/versions/declaring workloads/inert keys — never values) |

***

#### 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 migrate --env <environment> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dry-run` | `bool` | - | Show what would move without writing anything |
| `--env` | `string` | - | Environment whose secret store to act on (required; deploy/kcl/`<env>`/) |

***

#### 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 set --env <environment> <KEY> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | - | Environment whose secret store to act on (required; deploy/kcl/`<env>`/) |
| `--from-file` | `string` | - | Read the value from a file instead of stdin |

***

#### reliant forge secret unset

Remove one secret from the store

```
reliant forge secret unset --env <environment> <KEY> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--env` | `string` | - | Environment whose secret store to act on (required; deploy/kcl/`<env>`/) |

***

### reliant forge skill

Manage Forge skills — conventions and playbooks for LLM agents

```
reliant forge skill
```

**Subcommands:**

| Command | Description |
| - | - |
| [`list`](#reliant-forge-skill-list) | List available skills (forge-shipped, project, and user-global) |
| [`load`](#reliant-forge-skill-load) | Print a skill's content to stdout (resolves user > project > forge) |
| [`search`](#reliant-forge-skill-search) | Search skills across all scopes by keyword (path/name=3, desc/body=1) |
| [`write`](#reliant-forge-skill-write) | Write every bundled skill to a directory |

***

#### reliant forge skill list

List available skills (forge-shipped, project, and user-global)

List available skills (forge-shipped, project, and user-global).

The default view is GROUPED and fits on one screen: the start-here skills
first, then one row per skill group with its sub-skills collapsed into a
'+N' count. Every collapsed sub-skill is still loadable by its exact path
('forge skill load db/seeding'). Pass --all for the exhaustive flat table.

'forge skill search `<keyword>`' is usually faster than reading either view —
it scores paths, descriptions and BODIES, so it finds skills whose title
never mentions your term.

One-time migration playbooks (relevance: migration — the skills under
migrations/) are hidden by default; they only matter while crossing a
specific forge version transition. Pass --include-migrations to list
them, or use 'forge project upgrade list' to see the migrations that actually
apply to this project's pinned forge\_version.

```
reliant forge skill list [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--all` | `bool` | - | Print the exhaustive flat table (every sub-skill, full descriptions) instead of the grouped view |
| `--include-migrations` | `bool` | - | Include one-time migration skills (relevance: migration) in the listing |
| `--json` | `bool` | - | Output JSON instead of a tab-separated table |

***

#### reliant forge skill load

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

```
reliant forge skill load <name>
```

***

#### reliant forge skill search

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

```
reliant forge skill search <query> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output JSON instead of a tab-separated table |

***

#### 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 skill write --out <dir> [--style claude|forge|md] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--include-migrations` | `bool` | - | Also export one-time migration skills (relevance: migration) |
| `--out` | `string` | - | Target directory (created if missing) — required |
| `--style` | `string` | `forge` | Output layout: forge (default), claude, md |

***

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

***

### reliant forge storage

Inspect and bound local caches; preserve persistent application data

```
reliant forge storage
```

**Subcommands:**

| Command | Description |
| - | - |
| [`check`](#reliant-forge-storage-check) | Refuse a build below the physical host free-space reserve |
| [`configure-nodes`](#reliant-forge-storage-configure-nodes) | Preview kubelet GC migration for existing registered nodes; --apply restarts them |
| [`daemon`](#reliant-forge-storage-daemon) | Run maintenance periodically, reloading policy for every pass |
| [`gc`](#reliant-forge-storage-gc) | Preview cleanup; --apply deletes only eligible cache and registry versions |
| [`install`](#reliant-forge-storage-install) | Install daily maintenance (launchd, systemd timer, or Windows Task Scheduler) using a stable copy of this executable |
| [`policy`](#reliant-forge-storage-policy) | Print the effective storage policy |
| [`register`](#reliant-forge-storage-register) | Register local cluster contexts, declared repositories or release pins |
| [`status`](#reliant-forge-storage-status) | Report physical host capacity, Docker usage and node cleanup settings |
| [`worktrees`](#reliant-forge-storage-worktrees) | Preview idle, clean, pushed worktrees of a repo; --apply removes them |

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--policy` | `string` | - | machine storage policy (default: user config directory/forge/storage.json) |

***

#### reliant forge storage check

Refuse a build below the physical host free-space reserve

```
reliant forge storage check [path...]
```

***

#### reliant forge storage configure-nodes

Preview kubelet GC migration for existing registered nodes; --apply restarts them

```
reliant forge storage configure-nodes [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--apply` | `bool` | - | install configuration and restart local k3d nodes sequentially |

***

#### reliant forge storage daemon

Run maintenance periodically, reloading policy for every pass

```
reliant forge storage daemon [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--interval` | `duration` | `24h0m0s` | time between maintenance passes |

***

#### reliant forge storage gc

Preview cleanup; --apply deletes only eligible cache and registry versions

```
reliant forge storage gc [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--apply` | `bool` | - | execute the cleanup plan |
| `--dry-run` | `bool` | - | preview only (the default) |

***

#### reliant forge storage install

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

```
reliant forge storage install
```

***

#### reliant forge storage policy

Print the effective storage policy

```
reliant forge storage policy
```

***

#### reliant forge storage register

Register local cluster contexts, declared repositories or release pins

```
reliant forge storage register [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--builder` | `stringSlice` | `[]` | local Docker builders whose cache to maintain |
| `--context` | `stringSlice` | `[]` | local k3d contexts that can use these images |
| `--ledger` | `string` | `.forge/releases` | local release ledger directory to import before enabling cleanup |
| `--pin` | `stringSlice` | `[]` | image references that must remain available |
| `--repository` | `stringSlice` | `[]` | declared local image repositories, including registry host |

***

#### reliant forge storage status

Report physical host capacity, Docker usage and node cleanup settings

```
reliant forge storage status
```

***

#### reliant forge storage worktrees

Preview idle, clean, pushed worktrees of a repo; --apply removes them

```
reliant forge storage worktrees [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--apply` | `bool` | - | remove removable worktrees without forcing or deleting branches |
| `--base` | `string` | - | integration branch for the pushed test (default: origin/HEAD, else origin/main) |
| `--idle` | `duration` | `24h0m0s` | minimum time since any activity in the worktree (at least 24h) |
| `--repo` | `string` | `.` | repository whose worktrees to inspect |

***

### reliant forge tools

Manage developer tooling forge depends on (proto plugins, etc.)

Manage developer tooling that forge expects on PATH but does not ship.

Subcommands:
install   Install the codegen tools forge runs (protoc-gen-go,
protoc-gen-connect-go, goimports) via 'go install', at the
versions this project's go.mod resolves, and check that every
frontend declares its TypeScript plugin.

Forge scaffolds buf.gen.yaml with 'local:' plugins by default so that
'forge generate' works without any BSR (buf.build) authentication.
Those local plugins must be on PATH; this command installs them.

```
reliant forge tools
```

**Subcommands:**

| Command | Description |
| - | - |
| [`install`](#reliant-forge-tools-install) | Install codegen tools (protoc-gen-go, protoc-gen-connect-go, goimports) |

***

#### reliant forge tools install

Install codegen tools (protoc-gen-go, protoc-gen-connect-go, goimports)

Install the codegen tools forge needs on PATH for the default
local:-plugin buf.gen.yaml workflow, plus goimports, which formats every Go
file forge generates.

Each tool is installed at the version this project's go.mod resolves for it
(google.golang.org/protobuf, connectrpc.com/connect, golang.org/x/tools) —
the version the committed generated code was produced with — falling back to
@latest when go.mod does not contain the module. --version overrides that for
every tool.

By default, tools already present on PATH are skipped. Use --force to
re-install them.

The TypeScript plugin, @bufbuild/protoc-gen-es, is not installed here. It
is an ordinary devDependency of each frontend, so the frontend's own
'npm ci' installs it at the version its lockfile pins. This command only
checks that every frontend whose buf.gen.yaml runs it declares it, and
fails with the edit to make when one does not. It never runs npm and never
writes a frontend's package.json or package-lock.json — with or without
\--force.

Examples:
forge tools install
forge tools install --version v1.34.2
forge tools install --force

```
reliant forge tools install [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--force` | `bool` | - | Reinstall the Go tools even when already on PATH (never touches a frontend's npm dependencies) |
| `--version` | `string` | - | Version passed to 'go install' for every tool (e.g. latest, v1.34.2); default: the version go.mod resolves, else latest |

***

### reliant forge version

Print the forge version and build identity

```
reliant forge version
```

***

## reliant open

Open a project in Reliant

Opens a project in the Reliant cloud platform. This is the primary command
for starting a Reliant session:

1. Detects the project from the given path (defaults to current directory)
2. Ensures you are authenticated (runs login flow if needed)
3. Registers the project with the cloud API
4. Starts the tools daemon for local tool execution
5. Opens the Reliant web UI in your browser

This is the "reliant ." command.

```
reliant open [path] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--background` | `bool` | - | Start daemon in background and return to shell |
| `--no-daemon` | `bool` | - | Skip starting the tools daemon |

***

## 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 preview-url <port> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--built` | `string` | - | What was built (included in --json output) |
| `--json` | `bool` | - | Emit the structured deliverable shape `{built, run_command, port, url, listening, access_level}` |
| `--require-listening` | `bool` | - | Fail if nothing is listening on the port |
| `--run` | `string` | - | The command that runs it (included in --json output) |

***

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

**Subcommands:**

| Command | Description |
| - | - |
| [`create`](#reliant-project-create) | Create a project and print its ID |
| [`list`](#reliant-project-list) | List your projects |

***

### 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 create [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--description` | `string` | - | Optional project description |
| `--name` | `string` | - | Project name (defaults to the base name of --path) |
| `--path` | `string` | `.` | Project directory path |

***

### 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 project list [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output in JSON format |

***

## reliant server

Run cloud server components

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

```
reliant server
```

**Subcommands:**

| Command | Description |
| - | - |
| [`api`](#reliant-server-api) | Run the stateless HTTP + gRPC API server |
| [`gateway`](#reliant-server-gateway) | Run the daemon connection gateway |
| [`worker`](#reliant-server-worker) | Run the Temporal workflow worker |

***

### 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 api [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--bind-address` | `string` | `0.0.0.0` | Network address to bind to |
| `--cors-origins` | `string` | `*` | Comma-separated CORS allowed origins, or \* for all |
| `--data-dir` | `string` | `./data` | Data directory for logs and certs |
| `--db-driver` | `string` | `postgres` | Database driver (postgres) |
| `--db-url` | `string` | - | Database connection URL (required) |
| `--disable-tls` | `bool` | - | Disable TLS (use plaintext HTTP) |
| `--grpc-port` | `int` | `9090` | gRPC/ConnectRPC listen port |
| `--health-port` | `int` | `8081` | Health/readiness HTTP endpoint port |
| `--jwks-url` | `string` | - | JWKS endpoint URL for JWT validation (alternative to PEM key) |
| `--jwt-public-key` | `string` | - | JWT public key PEM for token validation |
| `--jwt-public-key-file` | `string` | - | Path to JWT public key PEM file |
| `--nats-url` | `string` | - | NATS server URL (required) |
| `--pprof-port` | `int` | `6060` | pprof debug server port |
| `--streaming-driver` | `string` | `nats` | Streaming driver (memory or nats) |
| `--temporal-host` | `string` | `localhost` | Temporal server host |
| `--temporal-namespace` | `string` | `reliant` | Temporal namespace |
| `--temporal-port` | `int` | `7233` | Temporal server port |
| `--tls-cert` | `string` | - | TLS certificate file path |
| `--tls-key` | `string` | - | TLS key file path |

***

### 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 gateway [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--bind-address` | `string` | `0.0.0.0` | Network address to bind to |
| `--daemon-port` | `int` | `9190` | Daemon bidi-streaming gRPC listen port |
| `--data-dir` | `string` | `./data` | Data directory for logs and certs |
| `--db-driver` | `string` | `postgres` | Database driver (postgres) |
| `--db-url` | `string` | - | Database connection URL (required) |
| `--disable-tls` | `bool` | - | Disable TLS (use plaintext HTTP) |
| `--health-port` | `int` | `8080` | Health/readiness HTTP endpoint port |
| `--nats-url` | `string` | - | NATS server URL (required) |
| `--tls-cert` | `string` | - | TLS certificate file path |
| `--tls-key` | `string` | - | TLS key file path |

***

### 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 server worker [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--data-dir` | `string` | `./data` | Data directory for logs |
| `--db-driver` | `string` | `postgres` | Database driver (postgres) |
| `--db-url` | `string` | - | Database connection URL (required) |
| `--health-port` | `int` | `8081` | Health check endpoint port |
| `--nats-url` | `string` | - | NATS server URL (required) |
| `--pprof-port` | `int` | `6060` | pprof debug server port (binds 127.0.0.1 only) |
| `--streaming-driver` | `string` | `nats` | Streaming driver (memory or nats) |
| `--temporal-host` | `string` | `localhost` | Temporal server host |
| `--temporal-namespace` | `string` | `reliant` | Temporal namespace |
| `--temporal-port` | `int` | `7233` | Temporal server port |

***

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

**Subcommands:**

| Command | Description |
| - | - |
| [`create`](#reliant-trigger-create) | Create a schedule trigger |
| [`delete`](#reliant-trigger-delete) | Delete a trigger and its schedule |
| [`disable`](#reliant-trigger-disable) | Pause a trigger's schedule |
| [`enable`](#reliant-trigger-enable) | Resume a trigger's schedule |
| [`events`](#reliant-trigger-events) | List a trigger's firings, newest first |
| [`fire`](#reliant-trigger-fire) | Run a trigger now |
| [`get`](#reliant-trigger-get) | Show one trigger |
| [`list`](#reliant-trigger-list) | List your triggers |
| [`update`](#reliant-trigger-update) | Replace a trigger's definition |

***

### reliant trigger create

Create a schedule trigger

```
reliant trigger create [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--catchup-window` | `string` | - | How late a fire missed during an outage may still run (default: 10m) |
| `--cron` | `stringArray` | `[]` | 5-field cron expression; repeatable, and the union of all of them |
| `--daemon` | `string` | - | Daemon the runs execute on, by id or hostname (required) |
| `--disabled` | `bool` | - | Create the trigger paused |
| `--interval` | `string` | - | Fire every interval, as a Go duration (e.g. 30m); at least 1m |
| `--message` | `string` | - | The prompt each run starts from (required) |
| `--name` | `string` | - | Trigger name, unique per project (defaults to the workflow name) |
| `--overlap` | `string` | - | What to do when the previous run is still going: skip (default) or allow |
| `--param` | `stringArray` | `[]` | Workflow input as key=value; repeatable. Values parse as JSON, else as a string |
| `--preset` | `stringArray` | `[]` | Preset assignment as group=name, or just name for the top level; repeatable |
| `--project` | `string` | - | Project path (resolved to a project id; defaults to the working directory) |
| `--project-id` | `string` | - | Project id, if you already have it |
| `--timezone` | `string` | - | IANA zone the cron expressions are read in (default: UTC) |
| `--workflow` | `string` | - | Workflow to run (default: your default workflow) |
| `--worktree` | `string` | - | Worktree id to run in (default: the project's main worktree) |

***

### reliant trigger delete

Delete a trigger and its schedule

Delete a trigger and its schedule.

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

```
reliant trigger delete <id-or-name>
```

***

### 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 disable <id-or-name>
```

***

### 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 enable <id-or-name>
```

***

### reliant trigger events

List a trigger's firings, newest first

```
reliant trigger events <id-or-name> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--limit` | `int32` | `0` | Maximum firings to return (0 applies the server default) |

***

### reliant trigger fire

Run a trigger now

Run a trigger now, out of band with its schedule.

A manual fire skips the enabled and overlap checks: asking for it is the
decision those policies exist to make for the unattended case. The firing is
asynchronous — use `trigger events` to see what it did.

```
reliant trigger fire <id-or-name>
```

***

### reliant trigger get

Show one trigger

```
reliant trigger get <id-or-name>
```

***

### reliant trigger list

List your triggers

```
reliant trigger list [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--all` | `bool` | - | List triggers across every project |
| `--project-id` | `string` | - | List one project's triggers |

***

### reliant trigger update

Replace a trigger's definition

Replace a trigger's definition.

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

```
reliant trigger update <id-or-name> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--catchup-window` | `string` | - | How late a fire missed during an outage may still run (default: 10m) |
| `--cron` | `stringArray` | `[]` | 5-field cron expression; repeatable, and the union of all of them |
| `--daemon` | `string` | - | Daemon the runs execute on, by id or hostname (required) |
| `--disabled` | `bool` | - | Pause the trigger (omit to leave its current state alone) |
| `--interval` | `string` | - | Fire every interval, as a Go duration (e.g. 30m); at least 1m |
| `--message` | `string` | - | The prompt each run starts from (required) |
| `--name` | `string` | - | Trigger name, unique per project (defaults to the workflow name) |
| `--overlap` | `string` | - | What to do when the previous run is still going: skip (default) or allow |
| `--param` | `stringArray` | `[]` | Workflow input as key=value; repeatable. Values parse as JSON, else as a string |
| `--preset` | `stringArray` | `[]` | Preset assignment as group=name, or just name for the top level; repeatable |
| `--project` | `string` | - | Project path (resolved to a project id; defaults to the working directory) |
| `--project-id` | `string` | - | Project id, if you already have it |
| `--timezone` | `string` | - | IANA zone the cron expressions are read in (default: UTC) |
| `--workflow` | `string` | - | Workflow to run (default: your default workflow) |
| `--worktree` | `string` | - | Worktree id to run in (default: the project's main worktree) |

***

## reliant version

Print version information

```
reliant version [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output in JSON format |
| `--short` | `bool` | - | Print only the version number |

***

## reliant workflow

Manage and validate workflows

Commands for validating, listing, and running Reliant workflows.

```
reliant workflow
```

**Subcommands:**

| Command | Description |
| - | - |
| [`answer`](#reliant-workflow-answer) | Answer a pending question on a workflow execution |
| [`follow`](#reliant-workflow-follow) | Follow a workflow execution and stream NDJSON lifecycle events |
| [`list`](#reliant-workflow-list) | List available workflows |
| [`pause`](#reliant-workflow-pause) | Pause a running workflow execution (keeps it resumable) |
| [`questions`](#reliant-workflow-questions) | List the open question(s) awaiting input on a workflow execution |
| [`resume`](#reliant-workflow-resume) | Resume a paused or expired workflow execution |
| [`run`](#reliant-workflow-run) | Run a workflow |
| [`scenario`](#reliant-workflow-scenario) | Run and manage workflow scenarios |
| [`status`](#reliant-workflow-status) | Show a one-shot snapshot of a workflow execution |
| [`terminate`](#reliant-workflow-terminate) | Terminate a running workflow execution |
| [`validate`](#reliant-workflow-validate) | Validate workflow YAML files |
| [`validate-tree`](#reliant-workflow-validate-tree) | Validate a workflow tree with preset-aware cross-workflow checks |
| [`wait-for-gate`](#reliant-workflow-wait-for-gate) | Block until the workflow needs you — the next open question/approval |
| [`watch`](#reliant-workflow-watch) | Watch a workflow execution and print meaningful boundaries as they happen |

***

### 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 answer <execution-id> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--interactive` | `bool` | - | Prompt for each sub-question interactively (default when no --select) |
| `--question` | `string` | - | Question id to answer (default: the pending question) |
| `--select` | `stringArray` | `[]` | Option label to select (repeatable, one per sub-question) |
| `--text` | `string` | - | Free-text answer/feedback |

***

### 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 follow <execution-id> [flags]
```

**Flags:**

| Flag | Type | Default | Description | | | | | | | |
| - | - | - | - | - | - | - | - | - | - | - |
| `--exit-on-gate` | `bool` | - | Stop and exit 3 as soon as a question/approval gate opens (for scripted supervision) | | | | | | | |
| `--hook` | `stringArray` | `[]` | Exec hook 'on=`<event>` cmd=`<shell>`' (repeatable); event is one of node\_started | node\_completed | node\_failed | workflow\_completed | workflow\_failed | question | approval | any |
| `--interval` | `duration` | `2s` | Poll interval | | | | | | | |
| `--tail` | `bool` | - | Skip historical events and follow from now | | | | | | | |
| `--timeout` | `duration` | `0s` | Stop following after this long (exit code 2); 0 means no timeout | | | | | | | |

***

### reliant workflow list

List available workflows

Lists workflows in the current project and builtin workflows.

```
reliant workflow list [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--builtins-only` | `bool` | - | Only show builtin workflows |
| `--json` | `bool` | - | Output in JSON format |
| `--project-only` | `bool` | - | Only show project 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 pause <execution-id>
```

***

### 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 questions <execution-id> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output as JSON |

***

### 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 resume <execution-id>
```

***

### 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 run <workflow-name> [flags]
```

**Flags:**

| Flag | Type | Default | Description | | | | | | | |
| - | - | - | - | - | - | - | - | - | - | - |
| `--exit-on-gate` | `bool` | - | Stop and exit 3 as soon as a question/approval gate opens (for scripted supervision) | | | | | | | |
| `--follow`, `-f` | `bool` | - | Follow the execution and stream NDJSON lifecycle events | | | | | | | |
| `--hook` | `stringArray` | `[]` | Exec hook 'on=`<event>` cmd=`<shell>`' (repeatable); event is one of node\_started | node\_completed | node\_failed | workflow\_completed | workflow\_failed | question | approval | any |
| `--input`, `-i` | `stringArray` | `[]` | Workflow input as key=value (repeatable) | | | | | | | |
| `--input-file` | `string` | - | JSON file containing workflow inputs | | | | | | | |
| `--interval` | `duration` | `2s` | Poll interval | | | | | | | |
| `--message`, `-m` | `string` | - | First chat message that kicks the workflow (falls back to inputs.message/prompt, then a default) | | | | | | | |
| `--project-id` | `string` | - | Project ID (takes precedence over --project-path) | | | | | | | |
| `--project-path` | `string` | - | Project directory path; resolves (find-or-create) to a project ID when --project-id is absent | | | | | | | |
| `--tail` | `bool` | - | Skip historical events and follow from now | | | | | | | |
| `--timeout` | `duration` | `0s` | Stop following after this long (exit code 2); 0 means no timeout | | | | | | | |

***

### reliant workflow scenario

Run and manage workflow scenarios

Commands for running and listing workflow scenario tests.

```
reliant workflow scenario
```

**Subcommands:**

| Command | Description |
| - | - |
| [`list`](#reliant-workflow-scenario-list) | List available scenarios for workflows |
| [`run`](#reliant-workflow-scenario-run) | Run scenario tests against workflows |

***

#### 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 list [workflow-path] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dir` | `string` | `.reliant/workflows` | Directory containing workflow YAML files |
| `--include-builtins` | `bool` | - | Also list builtin workflow scenarios |
| `--json` | `bool` | - | Output in JSON format |

***

#### 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 scenario run [workflow-path] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dir` | `string` | `.reliant/workflows` | Directory containing workflow YAML files |
| `--fail-fast` | `bool` | - | Stop on first scenario failure |
| `--filter` | `string` | - | Only run scenarios whose name contains this string |
| `--include-builtins` | `bool` | - | Also run builtin workflow scenarios |
| `--json` | `bool` | - | Output in JSON format (for CI) |
| `--verbose`, `-V` | `bool` | - | Show detailed output for each scenario |

***

### 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 status <execution-id> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--json` | `bool` | - | Output as JSON |

***

### 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 terminate <execution-id>
```

***

### 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 [path] [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dir` | `string` | `.reliant/workflows` | Directory containing workflow YAML files |
| `--fail-fast` | `bool` | - | Stop on first validation error |
| `--include-builtins` | `bool` | - | Also validate builtin workflows |
| `--json` | `bool` | - | Output in JSON format (for CI) |
| `--verbose`, `-V` | `bool` | - | Show detailed output for each workflow |

***

### 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 validate-tree <path-or-builtin-ref> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--dir` | `string` | `.reliant/workflows` | Directory containing workflow YAML files |
| `--include-builtins` | `bool` | `true` | Also resolve and validate references into builtin workflows |
| `--input`, `-i` | `stringArray` | `[]` | Input binding as key=value (repeatable) |
| `--input-file` | `string` | - | JSON file containing workflow inputs |
| `--json` | `bool` | - | Output in JSON format (for CI) |
| `--preset-dir` | `string` | `.reliant/presets` | Directory containing project preset YAML files |
| `--verbose`, `-V` | `bool` | - | Show detailed output |

***

### 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 wait-for-gate <execution-id> [flags]
```

**Flags:**

| Flag | Type | Default | Description |
| - | - | - | - |
| `--interval` | `duration` | `2s` | Poll interval |
| `--json` | `bool` | - | Print the gate (or outcome) as JSON |
| `--tail` | `bool` | - | Skip historical events (an already-open gate is still reported) |
| `--timeout` | `duration` | `0s` | Give up after this long (exit code 2); 0 waits indefinitely |

***

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

```
reliant workflow watch <execution-id> [flags]
```

**Flags:**

| Flag | Type | Default | Description | | | | | | | |
| - | - | - | - | - | - | - | - | - | - | - |
| `--exit-on-gate` | `bool` | - | Stop and exit 3 as soon as a question/approval gate opens (for scripted supervision) | | | | | | | |
| `--hook` | `stringArray` | `[]` | Exec hook 'on=`<event>` cmd=`<shell>`' (repeatable); event is one of node\_started | node\_completed | node\_failed | workflow\_completed | workflow\_failed | question | approval | any |
| `--interval` | `duration` | `2s` | Poll interval | | | | | | | |
| `--tail` | `bool` | - | Skip historical events and follow from now | | | | | | | |
| `--timeout` | `duration` | `0s` | Stop following after this long (exit code 2); 0 means no timeout | | | | | | | |

***


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.