# `fjx.json` — per-project config Each project directory the supervisor manages must contain an `fjx.json` at its root. It is loaded once at supervisor startup and re-read on SIGHUP / `fjx supervise reload`. The loader lives in `src/project-config.ts`. ## Shape ```json { "images": { "pm": "fjx-base", "dev": "fjx-base", "qa": "fjx-base" } } ``` The top-level value MUST be a JSON object. Unknown top-level fields are tolerated and preserved verbatim — a newer supervisor writing extra keys won't break an older `fjx` reading the same file. ## Fields ### `images` *(object, optional)* Maps a role name to the container image the supervisor spawns for that role's tick. Keys are role identifiers (`pm`, `dev`, `qa` today; more may be added). Values are image references resolvable by the host's container runtime — `fjx-base`, `ghcr.io/example/fjx-dev:v1.2.3`, a local tag from `just images::build`, etc. - Missing `images` is equivalent to `{}`: no role has an image override. - A role without an entry falls back to the supervisor's default image. If there is no default and no entry, that role's ticks are skipped (the supervisor logs and moves on). - Values MUST be strings; an object or array under `images.` is a hard error. - Unknown role keys are preserved but ignored by the current supervisor. ## Validation errors The loader fails fast with a `ProjectConfigError` when: - The file exists but is not valid JSON. - The top-level value is not a JSON object (a bare array, string, or `null`). - `images` is present but not an object. - `images.` is present but not a string. A missing `fjx.json` is **not** an error — `loadProjectConfig` returns `undefined` and the supervisor treats the project as having no overrides. This is intentional so a fresh project directory can be added to the supervisor's project list before its config lands. ## Reloading The supervisor re-reads each project's `fjx.json` on: - `SIGHUP` to the daemon process - `fjx supervise reload` over the control socket (see [supervisor setup](./supervise-setup.md)) In-flight ticks finish under their previous config; the next scheduled tick picks up the new values. There is no partial reload — the whole file is re-parsed atomically.