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

# ready.json session contract

> Full schema v2 field reference for ready.json, the Extension.js readiness contract that scripts and agents poll instead of parsing terminal output.

Poll one JSON file to know when a session is ready, broken, or gone.

Every `dev`, `start`, `preview`, and `build` run writes `dist/extension-js/<browser>/ready.json` atomically on each compile. The current contract is `schemaVersion: 2`.

A `preview` with no build loads the source folder and still writes its contract to the project's `dist/extension-js/<browser>/ready.json`, with every browser field a built preview carries.

## Statuses

| Status | Meaning |
| - | - |
| `starting` | The run began. The file resets, and `events.ndjson` is truncated for the run. |
| `ready` | The latest compile succeeded and the output is on disk. |
| `error` | The compile failed, the browser never started or exited before the extension loaded, or the browser refused or lost the extension. |
| `stopped` | The watch closed. Stamped with `code: "shutdown"` so a dead session can never advertise `ready`. |

## Two-phase readiness

`status: "ready"` means compiled, nothing more. The browser may still be launching, and the service worker may not be connected yet.

Act tooling (`eval`, `storage`, `reload`, `open`, `inspect`) needs the second phase. Wait until the contract carries `runtime: "attached"` and an `executorAttachedAt` timestamp before you drive the extension.

The attach stamp is idempotent and survives recompiles. An attach also clears any earlier `extension_load_refused` state, because the executor runs inside the guest.

## Field reference (schema v2)

Fields that are always present:

| Field | Type | Meaning |
| - | - | - |
| `schemaVersion` | `2` | The ready contract's own version. |
| `schema` | `1` | Advertises that this engine speaks the schema-1 [result envelope](/docs/contracts/result-envelope). |
| `status` | string | `starting`, `ready`, `error`, or `stopped`. |
| `command` | string | `dev`, `start`, `preview`, or `build`. |
| `browser` | string | The browser target that this session serves. |
| `runId` | string | The session identity. Join key across `ready.json`, `events.ndjson`, and `logs.ndjson`. |
| `startedAt` | ISO string | When the run began. |
| `distPath` | string | Absolute path to the compiled extension. |
| `manifestPath` | string | Absolute path to the source manifest. |
| `port` | number or null | The port that the dev server actually bound. |
| `pid` | number | The dev server process id. Check liveness before trusting the contract. |
| `ts` | ISO string | When this document was last written. |
| `compiledAt` | string or null | When the latest successful compile finished. |
| `errors` | string\[] | ANSI-stripped compile errors, capped at 10 entries. |
| `toolchainVersion` | string | The Extension.js version that produced this tree. |

Fields that appear when known:

| Field | Type | Meaning |
| - | - | - |
| `host` | string | The dev server host. |
| `code` | string | Machine name for the error state, see below. |
| `message` | string | Human sentence beside `code`. |
| `instanceId` | string | The dev instance identity for multi-instance setups. |
| `instanceExplicit` | boolean | Whether the instance id was user-supplied. |
| `controlPort` | number or null | The control bridge WebSocket port. |
| `controlPath` | string | The control bridge WebSocket path (`/extjs-control`). |
| `logsPath` | string | Relative path to `logs.ndjson`. |
| `cdpPort` | number | Chromium launches: the CDP port, stamped post-launch. |
| `rdpPort` | number | Gecko launches: the RDP debugger-server port, stamped post-launch. |
| `cdpFaultCode` | string | Chromium `dev` sessions: stamped when the CDP wire failed after the browser came up, with one of the debug protocol codes such as `E_CDP_TIMEOUT` or `E_CDP_NOT_CONNECTED`. The session stays `ready` because the browser is up, but reload and HMR cannot attach until the fault is fixed. Preserved across recompiles of the same run. |
| `cdpFaultMessage` | string | The plain line beside `cdpFaultCode`. The `ready` frame under `--output json` carries the same fault on `warnings` as `E_CDP_...: message`. |
| `profilePath` | string | The resolved profile directory. Ephemeral profile names are generated, so read them here. |
| `binary` | string | Absolute path to the browser binary this session actually launched. Stamped for every run, including the ones that did not name a binary: those are the runs where the resolver chose for you and the path is not otherwise visible. |
| `binaryProvenance` | string | How that binary was chosen: `managed` (the Extension.js cache), `pinned` (`--chromium-binary`), `system` (an installed browser), or `snapshot` (a cached dev-channel build). |
| `browserPid` | number | The browser process id. The supported handle for tearing the browser down. |
| `extensionId` | string | The id that the browser serves the dist under. Browser-confirmed when available, derived otherwise. |
| `extensionName` | string | The extension's name, as build provenance. |
| `extensionVersion` | string | The extension's version, as build provenance. |
| `browserExitedAt` | ISO string | Stamped when the browser exits mid-session without being asked. Preserved across recompiles. |
| `browserExitCode` | number or null | The exit code beside `browserExitedAt`. |
| `browserExitSignal` | string or null | The signal beside `browserExitedAt` when the browser died of one, such as `SIGTRAP` for a crash. A crash carries no exit code, so this is the one clue about why it went. |
| `browserLaunchFailedAt` | ISO string | Stamped when no browser process came up at all. Preserved across recompiles. |
| `browserLaunchFailedReason` | string | The reason beside `browserLaunchFailedAt` as one plain line, such as the spawn error with the binary path. |
| `runtime` | `"attached"` or `"detached"` | `attached` once the service worker has connected and can be driven. From 4.1.20, `detached` once the last connected extension context disconnects, and `attached` again on reconnect. |
| `executorAttachedAt` | ISO string | When the service worker first connected. Kept as provenance after a detach. |
| `executorDetachedAt` | ISO string | From 4.1.20, when the last connected extension context disconnected. |
| `managedExtensions` | array | Every extension that the engine loads besides yours, as `{path, id?}` records. Subtract them by id in a census. |

## Error states

The `code` field names the failure class. Four codes matter for automation:

| `code` | What happened |
| - | - |
| `extension_load_refused` | The session runs, but the browser threw the extension out. Every other surface looks healthy, only the contract says so. Carries `extensionLoadRefusedAt` and `extensionLoadRefusedReason`. |
| `profile_locked` | Another live session holds the profile, so the browser never started. Carries `profileLockedAt` and a `profileLockOwner` with `host` and `pid`. |
| `browser_exited` | The browser process died. `start` and `preview` flip to `error`. `dev` flips to `error` only when the browser exits before the extension loaded. After that point `dev` keeps its compile status and only stamps `browserExitedAt`. A Firefox that exits before its debugger answers lands here within about a second. |
| `browser_launch_failed` | No browser process came up: the binary is missing, cannot be executed, or the pin is wrong. A Firefox that runs but whose debugger never answers lands here too, and Extension.js stops that process. `dev`, `start`, and `preview` all flip to `error`. Carries `browserLaunchFailedAt` and `browserLaunchFailedReason`. A launch refused for a bad `--chromium-binary` or `--gecko-binary` pin also carries `browserLaunchFailedCode: "E_BROWSER_BINARY_INVALID"`. A Gecko launch refused in system profile mode carries `browserLaunchFailedCode: "E_FLAG_NOT_SUPPORTED_HERE"`. |

A load refusal outlives the next successful compile. Only a new run (`starting`) or a real executor attach clears it.

A `dev` session whose browser exited before the extension loaded stays in `error` across recompiles, because a recompile does not bring the browser back. A launch failure outlives recompiles too, until a browser does start or a new run begins.

## Waiting with --wait

`extension dev --wait` and `extension start --wait` poll the contract every 250 ms and exit when it reports `ready`. Pair them with `--output json` for a machine result.

The wait loop refuses to trust stale files. Three checks run on every read:

1. The `command` field must match the waiting command.
2. The `pid` must be alive. For `dev`, a dead producer always means stale, so polling continues.
3. For `start`, a dead pid is accepted only when the contract is fresh. Freshness means `ts`, `compiledAt`, or `startedAt` is within the last 60 seconds.

A timeout exits with `E_READY_TIMEOUT`. A contract in `error` status fails the wait with its message, and the `error.code` of the result resolves the contract's own code:

| Contract `code` | `--wait` error code |
| - | - |
| `compile_error` | `E_COMPILE` |
| `compile_failed` | `E_COMPILE_FATAL` |
| `dev_server_start_failed` | `E_DEV_SERVER_START` |
| `browser_launch_failed` | `E_BROWSER_LAUNCH` |
| `browser_exited` | `E_BROWSER_EXITED` |
| `profile_locked` | `E_PROFILE_LOCKED` |
| `extension_load_refused` | `E_EXTENSION_LOAD_REFUSED` |
| A code outside this list | `E_READY_ERROR_STATUS` |

A `browser_launch_failed` contract that carries `browserLaunchFailedCode: "E_BROWSER_BINARY_INVALID"` or `"E_FLAG_NOT_SUPPORTED_HERE"` fails the wait with that code and `status: "usage"` instead of `E_BROWSER_LAUNCH`.

## Joining the session files

The same directory holds `events.ndjson` (compile timeline) and `logs.ndjson` (extension console output). Every row in both carries the `runId` from `ready.json`.

`events.ndjson` is truncated at every run start, so it only ever describes the current run. Event types are `compile_start`, `compile_success`, `compile_error`, `browser_exited`, `browser_launch_failed`, and `shutdown`. A `browser_exited` row mirrors the launcher's exit stamp, with `exitCode`, `exitSignal`, and `browserExitedAt`, so the timeline says when and how the browser went. After it, every act verb answers `E_CONTROL_UNAVAILABLE` naming the exit and the restart, since no service worker reconnects until a new `dev` session relaunches the browser. A `browser_launch_failed` row mirrors the launch failure stamp, with `browserLaunchFailedAt` and `reason`.

## Example

```json theme={null}
{
  "schemaVersion": 2,
  "schema": 1,
  "status": "ready",
  "command": "dev",
  "browser": "chromium",
  "runId": "mdyq3k2p-a1b2c3d4",
  "startedAt": "2026-08-03T14:05:12.000Z",
  "distPath": "/home/dev/my-extension/dist/chromium",
  "manifestPath": "/home/dev/my-extension/manifest.json",
  "port": 8080,
  "host": "127.0.0.1",
  "pid": 51234,
  "ts": "2026-08-03T14:05:19.412Z",
  "compiledAt": "2026-08-03T14:05:19.401Z",
  "errors": [],
  "instanceId": "i-4b9a77",
  "controlPort": 8081,
  "controlPath": "/extjs-control",
  "logsPath": "dist/extension-js/chromium/logs.ndjson",
  "cdpPort": 9222,
  "profilePath": "/tmp/extension-js/profiles/brisk-amber-fox",
  "browserPid": 51302,
  "extensionId": "abcdefghijklmnopabcdefghijklmnop",
  "toolchainVersion": "4.0.22",
  "runtime": "attached",
  "executorAttachedAt": "2026-08-03T14:05:21.007Z"
}
```

## Next steps

* Read command results with the [Result envelope](/docs/contracts/result-envelope).
* Stream the same session as frames with the [Lifecycle stream](/docs/contracts/lifecycle-stream).
* Wire the whole loop into tests with [Playwright E2E](/docs/workflows/playwright-e2e).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.