# Config reference

`rampway.config.mjs` is an ESM module that exports a default config object.

## Top-level fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `application` | string | yes | Name of the application. Used in deploy metadata and reports. |
| `stages` | object | yes | Map of stage names to stage configs. |
| `deployLog` | object \| boolean | no | Per-deploy log settings, or a boolean to enable/disable with defaults. See Deploy log below. |

### Deploy log

Every `rampway <stage> deploy` writes a full transcript of its lifecycle steps,
full shell commands, and redacted child stdout/stderr to a timestamped file on
the machine running the deploy. With this default enabled, the human console
shows timed step progress and one concise final summary instead of raw child
output. The log path is printed at the start of the run and in the final
summary. Dry runs are not logged.

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `enabled` | boolean | no | `true` | Set to `false` (or set `deployLog: false`) to disable per-deploy logging. Human deploys then stream redacted child output live to stderr. |
| `dir` | string | no | `"log/deploys"` | Log directory. Relative paths resolve against the deploy source; absolute paths are used as-is. |
| `keep` | number | no | `20` | Number of most-recent deploy logs to retain; older ones are pruned. |

Log files are named `<UTC-timestamp>-<stage>.log` (e.g. `20260722162000-production.log`). Only files matching that pattern are pruned, so unrelated files in the directory are never removed.

Malformed `deployLog` config fails loudly: `enabled` must be a boolean, `dir` a non-empty string, and `keep` a positive integer.

`--json` always suppresses lifecycle and child stderr and writes exactly one
final JSON document to stdout. On failure, the document identifies the failed
step, exit status or signal when available, and deploy-log path. Human failures
show the same concise information; the log retains the detailed diagnostics.

## Stage fields

Each stage under `stages` supports:

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `deployTo` | string | yes | — | Absolute path to the deploy root directory (e.g. `/home/dev/myapp`). |
| `namespace` | string | no | — | A 1–64 character path-safe deployment namespace. Isolates release, current, shared, lock, state, report, and runtime paths below one `deployTo`. |
| `strategy` | string | no | Transport-dependent | Deploy strategy: `"local-copy"`, `"remote-git"`, or reserved `"archive-upload"`. Defaults to `"local-copy"` for local transport and `"remote-git"` for SSH transport. `archive-upload` is recognized but not yet deployable. |
| `transport` | object | no | `{type: "ssh"}` | Transport configuration. See Transport below. |
| `hosts` | array | no | `[]` | For SSH transport: list of hosts with `host`, `user`, and `roles`. |
| `http` | object | no | — | Optional control-plane transport config. Enables `rampway <stage> deploy --transport=http|auto`. See HTTP control plane below. |
| `keepReleases` | number | no | `5` | Number of managed releases to retain after cleanup, excluding extra current/runtime-referenced/pinned protection. A post-publish built-in cleanup failure is recoverable with the revision-fenced `cleanup-releases` command. |
| `linkedFiles` | array | no | `[]` | Files symlinked from `shared/` into each release. See Linked files below. |
| `linkedDirs` | array | no | `[]` | Directories symlinked from `shared/` into each release. |
| `tasks` | object | no | `{}` | Named task groups executed in order during deploy. See Tasks below. |
| `operationalTasks` | object | no | `{}` | Explicit standalone tasks that run against the active release. See Operational tasks below. |
| `hooks` | object | no | `{}` | Lifecycle-phase hooks executed at specific points in the deploy pipeline. See Hooks below. |
| `runtime` | object | no | `{type: "none"}` | Runtime adapter config. See Runtime below. |
| `healthChecks` | array | no | `[]` | Checks run after tasks and before publish. See Health checks below. |

### Deployment namespaces

Use `namespace` when independent runtimes or roles share one project deploy root
but must not share lifecycle state or filesystem ownership:

```js
stages: {
  productionMain: {
    deployTo: "/srv/app",
    namespace: "main",
    // ...
  },
  productionStreaming: {
    deployTo: "/srv/app",
    namespace: "file-streaming",
    // ...
  }
}
```

The example uses `releases/main/<release>`, `current/main`, and
`releases/file-streaming/<release>`, `current/file-streaming`. Namespace shared
content is stored under `shared/namespaces/<namespace>`, while Rampway-owned
locks, known-good runtime state, immutable runtime material, boot state, and
owner handoff requests are stored under
`shared/rampway/namespaces/<namespace>`. Deployment reports are also scoped to
the namespace shared directory.

Names must match `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`; path separators,
traversal segments, whitespace, and empty values fail configuration validation.
Rampway rejects duplicate namespaces and rejects any config that mixes
namespaced and unnamespaced stages at the same effective `deployTo`, because
legacy `current` is a symlink while namespaced `current` is a directory. It also
fails before filesystem mutation when an existing `current` layout conflicts
with the configured topology. Omitting `namespace` preserves the existing
`releases/`, `current`, `shared/`, `shared/rampway/*`, lock, persisted-state,
and report paths exactly.

### Strategy: remote-git

The `remote-git` strategy creates a temporary checkout directory on the deployment target, removes its `.git` directory, then atomically renames it into the release directory before publish. A branch deployment performs a shallow `git clone --branch`; a pinned revision initializes the temporary directory, adds the configured repository as `origin`, and shallow-fetches that revision. The target host needs `git` and repository access. Required fields:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `repo` | string | yes | Git repository URL (or local path). Used as the `origin` remote on the target. Avoid embedding credentials: plan and error output redact HTTPS URL user-info, but a credential helper or SSH deploy key is safer. |
| `branch` | string | no | Branch to fetch and check out. Default: `"master"`. |
| `revision` | string | no | Specific commit SHA to fetch and check out. When unset, the HEAD of `branch` is checked out. |

```js
strategy: "remote-git",
repo: "git@github.com:org/repo.git",
branch: "main"
```

## Transport

### local

No-op transport for test deployments on the same machine.

```js
transport: {type: "local"}
```

### ssh

SSH transport using the local `ssh` client. Requires `hosts`.

```js
transport: {type: "ssh"},
hosts: [{host: "server.example", user: "dev", roles: ["app", "web"]}]
```

No credential or known-host paths are required in an ordinary project config.
For every operational CLI or Velocious worker command, Rampway resolves the
same secure SSH profile before the first SSH subprocess:

- host verification always uses `StrictHostKeyChecking=yes`;
- user host keys use `~/.ssh/known_hosts` and `~/.ssh/known_hosts2`, with the
  standard global OpenSSH files retained;
- `deploy.key` beside the loaded config is preferred for the direct deployment
  host, followed by readable standard user identities;
- a usable inherited `SSH_AUTH_SOCK` is preserved for onward authentication,
  or as the direct fallback when no identity file is available;
- without a usable inherited agent, Rampway owns a private agent for the command
  and loads the selected deploy identity plus the other readable standards;
- the selected identity must load successfully, while standard identities that
  `ssh-add` rejects are skipped without discarding the working selected key;
- missing usable agent/identity material fails before SSH; and
- `remote-git` with an SSH-form repository enables agent forwarding, while
  HTTPS/local repositories and other strategies explicitly disable it.

When the Velocious client resolves an SSH-form repository branch with local
`git ls-remote`, Git's SSH child receives this same strict user/global
known-host profile, including an explicit `knownHostsFile` override. Both that
Git child and direct deployment SSH receive `IdentityAgent=<resolved socket>` so
user SSH configuration cannot redirect the command away from Rampway's private
or inherited agent. A pinned-revision `--transport=http` or successful pinned
`auto` control-plane deploy enters no local SSH credential scope; an actual
SSH-form branch lookup and any direct fallback still do.

The normal configuration therefore remains project facts only:

```js
transport: {
  type: "ssh"
},
hosts: [{host: "server.example", user: "deploy", port: 22}]
```

Default deploy-identity discovery checks these paths in order:

1. `deploy.key` beside the loaded Rampway config file;
2. OpenSSH-current's standard private-key names under the current user's
   `~/.ssh` directory, in order: `id_rsa`, `id_ecdsa`, `id_ecdsa_sk`,
   `id_ed25519`, `id_ed25519_sk`, and `id_mldsa44_ed25519`.

Rampway writes each checked, missing, found, and selected path to stderr, keeping
`--json` stdout machine-readable. Keep config-local `deploy.key` untracked and
readable only by its intended user. The selected deploy identity is loaded
first, followed by the other readable standard identities so forwarded
SSH-form repository access can use its appropriate key. With a usable inherited
agent, the selected identity file is still used directly for the deployment
host while the inherited credentials remain available for the forwarded
repository connection.

Exceptional deterministic overrides remain available:

| Field | Behavior |
|-------|----------|
| `identityFile` | Absolute readable deploy-host identity passed with `IdentitiesOnly=yes`. It cannot be combined with `credentials`. |
| `knownHostsFile` | Absolute readable regular file replacing only the standard user known-host files. Strict checking and standard global files remain enabled. |
| `forwardAgent` | Explicit boolean override. Set `false` to disable the SSH-repository default for a trusted deployment host. |
| `sshOptions` | Nonempty array of extra OpenSSH options. NUL/empty values and attempts to weaken strict checking or override known-host, forwarding, identity-file, or command-scoped `IdentityAgent` fields are rejected. |
| `credentials` | Explicit `{type: "ephemeral-agent", ...}` private-agent selection that replaces even a usable inherited agent for this command. |

For the explicit `credentials` override, `deployKey: "auto"` retains the same
first-readable discovery order. An absolute `deployKey` fails without fallback
when missing, unreadable, or not a regular file. `identityFiles` is an optional
nonempty list of distinct absolute additional identities. Omitting `deployKey`
retains `identityFiles`-only behavior.

Rampway owns a private agent for the whole command, loads identities through
`ssh-add` stdin, and removes it afterward. The identities are available to
every SSH subprocess in that command, including archive-upload pipelines.
Rampway also isolates the command's subprocess groups. On completion or
HUP/INT/TERM cancellation it terminates any surviving descendants with bounded
TERM-then-KILL escalation before removing the private agent. Processes and SSH
agents inherited from the caller remain outside that ownership boundary.

The `--ssh-forward-agent` CLI flag remains an explicit per-invocation `true`
override and fails on non-SSH stages. For deploys it selects the direct SSH path
instead of the default `auto` HTTP control-plane path; combining it with
`--transport=http` is an error. Forward only to deployment hosts you trust:
while a session lives, the forwarded agent's keys are usable on that host.

### http (control plane)

Optional transport that runs the deploy through the authenticated Velocious
deployment API mounted by the application. When `http` is configured,
`rampway <stage> deploy` defaults to `--transport=auto`; see Deploy transports
below.

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `endpoint` | string | yes | — | Base URL where the API is mounted, e.g. `https://app.example/rampway/deployments`. Must be an `http(s)` URL. |
| `accessToken` | string | no | — | Bearer token to authenticate the client. Exactly one of `accessToken` or `accessTokenEnv` is required. |
| `accessTokenEnv` | string | no | — | Name of an environment variable holding the bearer token. Exactly one of `accessToken` or `accessTokenEnv` is required. The token value is never printed. |
| `project` | string | no | `stage.application` | Project the control plane is allowlisted for. |
| `pollIntervalMs` | positive integer | no | `2000` | How often to poll the run while it is pending/running. |
| `timeoutMs` | positive integer | no | `1800000` | How long to keep polling before failing the deploy. |

```js
http: {
  endpoint: "https://app.example/rampway/deployments",
  accessTokenEnv: "RAMPWAY_DEPLOY_TOKEN",
  project: "my-app",
  pollIntervalMs: 2000,
  timeoutMs: 1800000
}
```

The CLI resolves the release revision as a full 40-character SHA; a branch name
is resolved with `git ls-remote` against `stage.repo`. The control plane only
accepts full SHAs reachable from the stage's approved release branch. Malformed
`http` config fails loudly: a missing or non-`http(s)` endpoint, an absent or
invalid token source, a non-string `project`, or non-positive interval/timeout
values are all rejected.

### Deploy transports

`rampway <stage> deploy --transport=http|ssh|auto` selects how the deploy is
executed:

- `http` — requires `stage.http`; runs entirely through the control plane and
  fails if the stage has no `http` config.
- `ssh` — runs directly over the stage's `transport` (ssh or local),
  bypassing the control plane.
- `auto` (default) — uses the control plane when `stage.http` is configured,
  and falls back to the direct transport only when the control plane is
  unreachable (network failure), not deployed for the project/stage (`404`),
  or unavailable (`5xx` without an admitted run). It never falls back on
  authentication or validation errors (`401`, `403`, `409`, `422`), on a
  `5xx` that carries an admitted run, or once a run has been admitted and is
  being polled.

Dry runs (`--dry-run`) always print a direct-transport plan and never contact
the control plane.

### Structural deploy plans

`rampway <stage> plan [revision]` renders the deploy structure after loading
and validating configuration. With no positional revision, selection remains
`--revision`, then `--branch`, then `stage.revision`, then `stage.branch`. A
positional revision takes precedence over all four sources and must contain 7
to 64 hexadecimal characters. The selected value appears in both human and
JSON output.

Planning is structural only: it does not prove that a SHA exists, check branch
or remote-git reachability, or obtain HTTP control-plane approval. It performs
no SSH, Git fetch/clone, local-copy, temporary checkout, control-plane request,
deploy lock, release-directory, runtime, filesystem deployment, or transport
operation. `local-copy` and `remote-git` steps are descriptions of what a later
deploy would do, not actions or admission checks.

## Linked files and directories

Linked files and directories are symlinked from `shared/` into every release before tasks run.

### linkedFiles

Array of objects with `path` (required) and `required` (default `false`):

```js
linkedFiles: [
  {path: ".env", required: true},
  {path: "backend/src/config/secrets.js", required: false}
]
```

Required files that don't exist in `shared/` cause the deploy to fail. Optional missing files are silently skipped.

### linkedDirs

Array of directory paths relative to the release root:

```js
linkedDirs: ["log", "tmp/pids", "tmp/cache", "storage"]
```

## Tasks

Task groups are named objects in `tasks`. Each value is a string or an array of task entries. Task groups run in config order.

### Task entries

A task entry can be:

- A **string**: the shell command to run.
- An **object**: with `command` (required), plus optional `cwd`, `env`, `optional`, and `onlyIfChanged`.

```js
tasks: {
  install: [
    "echo installing packages",
    {command: "npm ci --omit=dev", cwd: "backend", env: {NODE_ENV: "production"}},
    {command: "warm cache", optional: true}
  ],
  migrate: ["NODE_ENV=production npm run db:migrate"]
}
```

### Task helpers

Import task helpers from `rampway/tasks`:

```js
import {npmInstall, pnpmInstall, yarnInstall, bunInstall} from "rampway/tasks"
import {checkExpoWebArtifacts, velociousMigrate, onlyIfChanged} from "rampway/tasks"
```

#### npmInstall / pnpmInstall / yarnInstall / bunInstall

Return a task entry with the install command for the given package manager.

Options: `cwd`, `env`, `optional`, `production`, `clean` (npm), `frozenLockfile` (pnpm/yarn/bun).

```js
npmInstall({cwd: "backend", production: true})
// → {command: "npm ci --omit=dev", cwd: "backend"}

pnpmInstall({cwd: "app", production: true})
// → {command: "pnpm install --frozen-lockfile --prod", cwd: "app"}
```

#### velociousMigrate

Returns a task entry that runs `npx velocious db:migrate`.

Options: `command` (override), `cwd`, `env`, `optional`.

```js
velociousMigrate({cwd: "backend", env: {NODE_ENV: "production"}})
// → {command: "npx velocious db:migrate", cwd: "backend", env: {NODE_ENV: "production"}}
```

#### checkExpoWebArtifacts

Returns a task entry that fails unless `index.html` exists in the Expo web build directory.

Options: `dir` (default `"dist"`), `cwd`, `optional`.

```js
checkExpoWebArtifacts({dir: "app/dist"})
// → {command: 'test -f "app/dist/index.html"'}
```

#### onlyIfChanged

Wraps a task entry so it is skipped when none of the given paths have changed since the previous deploy. First deploys always run.

```js
onlyIfChanged(["backend"], npmInstall({cwd: "backend", production: true}))
// → {command: "npm ci --omit=dev", cwd: "backend", onlyIfChanged: ["backend"]}
```

### Task execution model

1. Task groups run in config order.
2. Tasks within a group run in array order.
3. Required task failures stop the deploy before publish.
4. Optional task failures (`optional: true`) are recorded and the deploy continues.
5. Tasks with `onlyIfChanged` are skipped when their paths haven't changed since the last deploy.
6. Tasks with `cwd` resolve relative to the release root.

### Listing tasks

`rampway <environment> tasks` lists a stage's configured task groups and their entries in config order, so you can discover what a deploy will run without deploying:

```bash
rampway production tasks
rampway production tasks --json
```

Each entry shows its command plus any `cwd`, `env`, `optional`, and `onlyIfChanged` metadata. Secret-like env values and `KEY=value` assignments in commands are redacted in both the human and `--json` output.

## Operational tasks

`operationalTasks` is a strict namespace for reusable standalone operations. It is separate from deploy lifecycle `tasks`: an operational task runs only when its exact configured name is invoked.

```js
operationalTasks: {
  "cache:warm": [
    {
      command: "npm run cache:warm",
      cwd: "backend",
      env: {NODE_ENV: "production"},
      forwardEnv: ["CACHE_WARM_CREDENTIAL"]
    }
  ]
}
```

Each value is a command string, one entry object, or a non-empty ordered array of strings and entry objects. Entry objects accept only:

- `command` — required non-empty shell command from trusted configuration.
- `cwd` — optional non-empty release-relative directory; absolute and escaping paths are rejected.
- `env` — optional string-valued environment literals with safe shell names. Secret-like keys are rejected; use `forwardEnv`.
- `forwardEnv` — optional unique allowlist of safe environment names. Every named value must exist in Rampway's invocation environment and is always treated as sensitive.

Task names are 1–128 characters and match `^[A-Za-z0-9][A-Za-z0-9:_-]*$`. Selection is an exact name lookup; command text and arbitrary trailing arguments cannot come from the CLI.

```bash
CACHE_WARM_CREDENTIAL=... rampway production task cache:warm
CACHE_WARM_CREDENTIAL=... rampway production task cache:warm --json
```

Rampway resolves all forwarded values before transport work, acquires the same exclusive lock used by deploy and rollback, then strictly resolves `current` to one existing direct child of `releases/`. Every entry is required and runs in order against that immutable release path. Missing configuration, forwarded values, lock ownership, active release, command failures, and transport failures all fail closed. There is no task `--force`.

For SSH, forwarded values are sent to a fixed remote shell through stdin rather than SSH argv. Known values are redacted from streamed output, returned output, and errors even when echoed without their variable name or split across chunks. Human output identifies the application, stage, task, and release. JSON mode emits only:

```json
{"application":"demo","stage":"production","task":"cache:warm","releaseId":"20260728120000","status":"passed"}
```

Standalone task runs are not appended to the deployment report. The plural `rampway <stage> tasks` command remains a read-only inventory of deploy task groups.

## Hooks

Hooks are lifecycle-phase callbacks that run commands at specific points during the deploy pipeline, outside the fixed task-group slot. Each hook phase accepts an array of task entries (strings or objects, same format as tasks).

Hooks before the release directory is created cannot use `cwd` (since there is no release path to resolve against). Hooks after `after_source` run with the release directory as their working directory and support `cwd`, `env`, `optional`, and `onlyIfChanged` like regular task entries.

| Phase | When it fires |
|-------|--------------|
| `before_deploy` | Before base directories are ensured |
| `before_lock` | Before deploy lock acquisition |
| `after_lock` | After deploy lock acquisition |
| `before_source` | Before source code copy |
| `after_source` | After source code is copied into the release directory |
| `before_tasks` | Before task groups execute |
| `after_tasks` | After task groups execute |
| `before_runtime` | Before runtime adapter deploy |
| `after_runtime` | After runtime adapter deploy |
| `before_health` | Before health checks |
| `after_health` | After health checks |
| `before_publish` | Before `current` symlink is updated |
| `after_publish` | After `current` symlink is updated |
| `before_cleanup` | Before old release cleanup |
| `after_cleanup` | After old release cleanup |
| `on_pre_publish_failure` | After Rampway begins runtime processing for a candidate but before `current` is updated; use idempotent, candidate-specific recovery such as stopping a newly handed-off daemon |

`on_pre_publish_failure` runs only after Rampway invokes the runtime adapter for a candidate release and only while `current` still points at the previous release. The hook must be idempotent and prove that a candidate handoff occurred before it mutates runtime state. If it fails, Rampway preserves the original deploy error and appends the hook failure for diagnosis.

Other hook failures follow task failure semantics: required-hook failures stop the deploy, and optional-hook failures (`optional: true`) are recorded and the deploy continues. Failures after publish include recovery hints in the error message since `current` may have changed. Only failure of Rampway's built-in retention operation after publication is classified as deployed and active with cleanup pending; `before_cleanup` and `after_cleanup` hook failures remain fatal.

```js
hooks: {
  before_deploy: ["echo 'deploy starting'"],
  after_publish: [{command: "notify-slack.sh", optional: true, env: {CHANNEL: "deploys"}}],
  before_cleanup: [npmInstall({cwd: "backend"})]
}
```

Hooks and tasks can coexist. Tasks run at a fixed point (after shared links, before runtime). Hooks let you insert commands at any lifecycle phase without reordering task groups.

## Runtime

Configures how the deployed application is handed off to a process manager.

### none

No runtime — just deploy files.

```js
runtime: {type: "none"}
```

### rollbridge

Rollbridge runtime adapter. Handles `deploy`, `validate`, `status`, `logs`, and `recover`.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | yes | `"rollbridge"` |
| `command` | string | no | Rollbridge CLI command. Default: `"npx rollbridge"`. |
| `packageDir` | string | no | Release-relative directory containing the Rollbridge package. Default: `"."`. |
| `config` | string | no | Rollbridge config file, relative to `packageDir`, or an absolute trusted path. Required for stable rollback. |
| `daemonLogPath` | string | no | Path to the daemon log file in shared storage. |
| `daemonPidPath` | string | no | Path to the daemon PID file in shared storage. |
| `daemonStartTimeoutMs` | positive integer | no | Maximum daemon startup time passed to Rollbridge. |
| `rollbackCompatibility` | string | no | Application runtime protocol identifier persisted with each release. Rollback and automatic restoration fail before activation when the retained identifier differs from the current configuration. |
| `socketPath` | string | no | Path to the Rollbridge Unix socket. |
| `versionPath` | string | no | Path to a version file used for stale-daemon detection. |
| `env` | object | no | String environment values passed to Rollbridge. Secret-like values are transported and redacted separately. |
| `externalOwner` | object | no | Opt in to a persistent external foreground owner with `{type: "foreground"}`. Requires `config`, `socketPath`, and Rollbridge 0.1.38 or newer so Rampway can accept its exact content-addressed daemon runtime; upgrading a retained owner running an older Rollbridge fails closed (see below). |

```js
runtime: {
  type: "rollbridge",
  packageDir: "backend",
  config: "config/rollbridge.production.mjs",
  daemonLogPath: "/home/dev/myapp/shared/log/rollbridge.log",
  daemonPidPath: "/home/dev/myapp/shared/tmp/pids/rollbridge.pid",
  rollbackCompatibility: "worker-protocol-v1",
  socketPath: "/tmp/rollbridge-myapp.sock",
  versionPath: "/home/dev/myapp/shared/tmp/rollbridge-version",
  externalOwner: {
    type: "foreground",
    handoffTimeoutMs: 30000,
    pollIntervalMs: 250
  }
}
```

`externalOwner.handoffTimeoutMs` and `pollIntervalMs` are optional positive
integer millisecond bounds (defaults: 30000 and 250). `handoffTimeoutMs` is
one deadline that bounds the authenticated `retire-owner` socket round trip
and the subsequent replacement polling after initial owner identity
verification; a stalled, truncated, malformed, or non-success
retirement response fails the handoff deterministically without retrying
retirement or taking over the owner. Every accepted deploy or
rollback must establish the new boot digest, including first adoption and a
same-contract release change. Rampway waits for health, `current` publication,
and atomic boot-state persistence, then writes a request bound to the exact
deploy lock and accepted boot digest. An owner already reporting the exact
accepted release, runtime, and bootstrap attestation remains untouched.
Otherwise Rampway sends one authenticated `retire-owner` request only to a daemon
whose status first proves the accepted release and runtime identity, then waits
for the replacement foreground owner with the exact boot attestation. Retirement
must release ownership and return without waiting for retained generations or
connections to drain. Success removes the request; timeout preserves the
accepted boot state and request for recovery.

Runtime replacement capability is selected only from the target snapshot's
Rollbridge package version. With a Rollbridge 0.1.29-or-newer candidate,
`rollbridge deploy --ensure-daemon` owns atomic replacement of an incompatible
config, socket, package, or runtime owner. Rampway neither pre-shuts down the
incumbent nor applies its destructive legacy shutdown/retry fallback if that
candidate fails closed. A typed replacement refusal also tells the deploy and
rollback lifecycles that the incumbent remains authoritative, so they preserve
the failed result and original cause without automatically redeploying the
previous snapshot. `current` and accepted runtime state remain unchanged, while
normal pre-publish failure hooks and release diagnostics still run. Candidates
before 0.1.29 keep the legacy compatibility path: Rampway shuts down a responsive
daemon before deploying a changed runtime contract and can retry once after a
reported legacy runtime mismatch. The first upgrade from a pre-split owner to
0.1.29+ may still be disruptive under Rollbridge's documented, narrowly
authorized one-time pre-split bridge, even though Rampway leaves that replacement
transaction and guardian-owned process state to Rollbridge.

Deploy success and deploy-lock release follow healthy activation and replacement
owner readiness. They must not wait for retired jobs generations, workers, jobs,
HTTP/WebSocket connections, or other retained services to finish. Rollbridge
durably supervises those drains after the command returns and reports every
release directory still referenced by a retained process. Rampway owns the
corresponding retention metadata and must exclude those pinned releases from
cleanup until all references have ended. HTTP/WebSocket drain completion or
timeout does not release a jobs-generation pin.
Rampway atomically records the verified owner's referenced-release set before
asynchronous retirement and replaces it from later owner status, so cleanup pins
survive separate deploy invocations and end when Rollbridge drops the reference.

Deploying over a retained external owner running Rollbridge older than 0.1.21
fails closed before any shutdown or deploy: those versions have no
authenticated `retire-owner` contract, so replacing them requires a full
shutdown that interrupts service while long-running workers drain. Stage the
migration through an intermediate 0.1.21+ release with `runtime.externalOwner`
disabled in an explicitly planned maintenance restart, scheduled only after
accepted long-running workers have drained, then upgrade to Rollbridge 0.1.38+
before re-enabling it.

Rolling back to a retained release whose runtime snapshot predates the
`runtime.socketPath` requirement also fails closed, before any runtime
activation: redeploy that release once with the current Rampway so its
snapshot records the control socket path, then roll back. `runtime.socketPath`
itself must be a non-empty string without NUL bytes and is validated at config
load.

Configure the service manager to run `rampway <stage> runtime-boot --config
<absolute-path>` with `Restart=always` and a bounded restart delay. Early
attempts may collide with the temporary daemon and exit; retries converge after
Rampway relinquishes it. `Restart=on-failure` is insufficient because the old
foreground owner can exit cleanly. Rampway does not invoke a service manager or
supervise the daemon.

An explicit config is snapshotted before handoff into immutable
`shared/rampway/runtime/rollbridge/<digest>/` material. The effective contract
digest covers the config bytes, Rollbridge package version, command, package
directory, daemon paths/options, and environment values. Only the digest,
environment names, and non-secret activation locators are atomically published
to `shared/rampway/state.json`, indexed by release, after both runtime handoff
and configured health checks succeed. Missing or changed known-good digests
restart an active daemon; a matching digest preserves it. `versionPath` remains
supported for package version compatibility and is updated only when the
lifecycle commits health.

Rampway also records the accepted deploy command in that release snapshot. A
later stable local `runtime-boot` config may therefore use a different command
for staging or supervisor integration without changing the already accepted
contract. This does not relax config, environment, package, or runtime identity:
new external-owner snapshots bind the exact `daemonRuntime` reported by
Rollbridge, copy its manifest into immutable
`shared/rampway/runtime/rollbridge-daemon/<digest>/runtime.json`, and verify both
that copy and Rollbridge's original content-addressed runtime before acceptance
and every boot. The foreground command is always the accepted runtime's
`bin/rollbridge`; it never resolves through `current`, a release package, `npx`,
or a package cache. Malformed, redirected, missing, changed, or differently
reported runtime material fails closed. Verification independently reproduces
Rollbridge daemon-runtime format 1's raw-byte digest without executing the
candidate runtime. Executable bytes, source/dependency bytes, symlinks, special
filesystem entries, and package-version mismatches are rejected before spawn.

Keep the configured Rollbridge file self-contained. Rampway snapshots that file,
not arbitrary imports from an application checkout. Do not put secrets in the
file or command; use `env` or shared application secret files. A Rollbridge
deploy without `config` retains its existing forward-deploy behavior, but
runtime-complete rollback fails closed until a successful deploy with explicit
stable config has recorded known-good state.

Set `rollbackCompatibility` when an application runtime protocol changes
persisted work that older workers cannot safely consume. Rampway records the
identifier in each successful release snapshot and rejects a rollback whose
identifier does not exactly match the currently configured value. The same
check protects automatic restoration after a failed deployment or rollback and
foreground `runtime-boot` of the accepted current release.
Releases without an identifier remain mutually compatible, preserving existing
behavior.

### compose

Compose runtime adapter for a pre-existing, trusted Docker Compose deployment.
Rampway runs only the two literal commands configured here from a fixed absolute
Compose root; it does not construct Compose arguments from release IDs, hooks,
or command-line input.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | yes | `"compose"` |
| `composeRoot` | absolute path | yes | Fixed directory used as the working directory for both commands. |
| `handoffCommand` | string | yes | Static trusted command that selects/starts the candidate runtime. |
| `healthCommand` | string | yes | Static trusted command that performs the runtime health check after handoff. |

```js
runtime: {
  type: "compose",
  composeRoot: "/srv/myapp/compose",
  handoffCommand: "docker compose up -d --wait web",
  healthCommand: "docker compose exec -T web node scripts/healthcheck.mjs"
}
```

The adapter passes the immutable candidate to both commands through the trusted
`RAMPWAY_RELEASE_ID` and `RAMPWAY_RELEASE_PATH` environment variables, then runs
the candidate handoff followed by the configured health command before Rampway runs stage health checks or publishes `current`. Its
snapshot stores only the fixed Compose root and a contract digest: it never
stores command text or secrets. Configuration is intentionally conservative:
all three fields are required, `composeRoot` must be absolute, and no runtime
`env` or extra adapter fields are accepted. Put sensitive values in the trusted
Compose environment/secret mechanism, not in these commands.

A failed candidate handoff or health check, and a failed explicit rollback,
reactivate and health-check the previous Compose runtime through the same
runtime deploy API before returning the original failure.

## Rollback and retained releases

`rampway <stage> rollback` selects the latest other safe 14-digit release, or
`--to <exact-release-id>` for any named legacy release. Under the deploy lock it activates the target runtime,
runs the stage health checks, and publishes `current` only after both pass. It
never runs `tasks`, deploy migrations, or orchestration from the target checkout.
Failure after an attempted handoff reactivates and health-checks the previous
release; if restoration also fails, the error contains both causes and the lock
is still released. Deploy applies the same restoration as soon as a candidate
handoff starts, including candidate activation, health, and pre-publish failures.

Retained discovery accepts bounded direct-child names used by Rampway and
Capistrano. Traversal, nested paths, leading-dot temporary names, non-directories,
and symlinks resolving outside `releases/` fail closed. Discovery does not grant
cleanup ownership: only valid Rampway metadata for this application/stage makes
a release managed and count toward `keepReleases`.

Runtime maintenance is adapter-generic:

```bash
rampway production runtime-validate
rampway production runtime-status
rampway production runtime-logs
rampway production runtime-recover --yes
rampway production runtime-recover-generation-transition <failed-release-id> --yes
rampway production runtime-boot --config /absolute/path/rampway.config.mjs
```

These commands strictly resolve the active release and reuse its known-good
snapshot when present. The `rollbridge-*` command names remain compatibility
aliases.

`runtime-recover-generation-transition` is the guarded incident-recovery path
for a failed Rollbridge candidate that never became `current`. It verifies the
retained candidate and incumbent identities, stable Rampway state, and live
Rollbridge transition before asking Rollbridge to retire the exact failed
candidate. Config-less forward-deploy snapshots are supported without relaxing
the immutable-config requirement for rollback or foreground boot. It never
deploys, publishes, or runs hooks; it updates only live release-retention pins.
Later successful Rollbridge deploys reconcile those pins from owner status before
cleanup. Production requires `--yes`. See
[Failed generation recovery](failed-generation-recovery.md).

`runtime-boot` is different from the maintenance commands: it is a local-only,
foreground boot entrypoint intended for a host service manager. It reads only
Rampway's digest-protected `shared/rampway/runtime-boot.json`, requires its
accepted release to be the exact retained `current` release, verifies release
metadata, revision, identical active and retained runtime snapshots, their full
contract identity and rollback compatibility, and immutable runtime config
identity, and then calls the adapter's foreground boot contract. A valid
schema-1 boot record is atomically upgraded using that verified matching
snapshot before the foreground process starts. An exact lock-bound
external-owner handoff instead preserves the schema-1 record and attestation;
the next unlocked restart performs the migration. Other unsupported schemas
fail closed. It uses only the local transport and
coordinates with the deploy lock; it does no source preparation, tasks, hooks,
application migration, publication, cleanup, or fallback selection. Rollbridge support
requires an explicit `runtime.config` and Rollbridge 0.1.17 or newer; a
successful deploy without stable config remains supported but cannot seed
runtime boot state. The optional external-owner handoff requires Rollbridge
0.1.38 or newer and passes the accepted boot-state digest as
`--boot-attestation` on an ordinary foreground daemon boot. A matching owner is
a no-op; a stale or absent bootstrap on the exact accepted release/runtime
receives one authenticated `retire-owner` control command through the
configured `runtime.socketPath`, which quiesces the old owner and releases its
proxy/control listeners promptly — without waiting for long-draining workers —
so a serial outer supervisor can start the replacement. The retirement socket
round trip and the replacement wait share one `handoffTimeoutMs` deadline; a
stalled, truncated, malformed, or non-success response fails closed. Status/read identity
mismatches fail closed without shutdown.

## Health checks

Each health check is an object with one of:

- `path`: file to check exists, relative to the release root.
- `command`: shell command; exits 0 = healthy.

```js
healthChecks: [
  {name: "package", path: "package.json"},
  {name: "migrations-table", command: "node -e 'require(\"velocious\")'"}
]
```
