# Agent roles catalog This directory is **data, not application code**. It holds the content of an "agent roles catalog": reusable agent role definitions (system prompts plus a little metadata), grouped into bundles and translated into one or more languages. A separate server reads these files and serves them; nothing here is executable application logic except the validation script. ## File layout ``` agent-roles-catalog/ index.json # the catalog manifest: bundles, languages, role versions bundles/ / .json # one file per declared language (e.g. ru.json, en.json) scripts/ check.mjs # validates the catalog (no dependencies) content-hashes.json # check artifact: per-role content-hash lock (NOT served) package.json # defines the `check` script README.md ``` Currently shipped bundles: - `editorial` — the editorial suite (structural-editor, line-editor, fact-checker, proofreader, narrator), languages `ru`, `en`. - `research` — a single `researcher` role, languages `ru`, `en`. ## How it's served The server does not bundle this data; it reads it at request time from a single configured location, the `AI_AGENT_ROLES_CATALOG_URL` env var (`EnvironmentService.getAiAgentRolesCatalogSource()`), an `http(s)://` base URL to the catalog's raw files. The server fetches `/index.json` for the manifest and `/bundles//.json` for each opened bundle file (REMOTE only). That base URL is provided as a per-branch default in the Docker image (set in CI: a `develop` build points at the `develop` raw URL, a release build at the `main` raw URL) and can be overridden at runtime via the `AI_AGENT_ROLES_CATALOG_URL` env var. Local-filesystem sources are no longer supported; if the value is unset the catalog is unavailable. The fetched JSON is re-validated server-side (the catalog is treated as untrusted input). See `.env.example` for the variable and the CHANGELOG for the rollout. ## `index.json` schema ```jsonc { "schemaVersion": 1, "bundles": [ { "id": "editorial", // unique bundle id; matches bundles// "name": { "ru": "...", "en": "..." }, // localized display name "description": { "ru": "...", "en": "..." }, "languages": ["ru", "en"], // which .json files must exist "roles": [ { "slug": "structural-editor", "version": 1 } // ... ] } ] } ``` `version` lives **here, in index.json**, per role. Bump it whenever a role's content (instructions, name, description, etc.) changes, so consumers can detect updates. ## Bundle (`.json`) schema ```jsonc { "schemaVersion": 1, "language": "ru", "roles": [ { "slug": "structural-editor", // REQUIRED, unique across the whole catalog "emoji": "🧱", "name": "...", // REQUIRED, localized "description": "...", // localized "instructions": "...", // REQUIRED, the system prompt, localized "autoStart": true, // whether the role starts working immediately "launchMessage": "..." // first message sent on launch (or null) } ] } ``` Notes: - `modelConfig` is intentionally absent; the server treats an absent `modelConfig` as `null`. - A role's `slug`, `emoji`, and `autoStart` are identical across all language files of the same bundle. Only `name`, `description`, `instructions`, and `launchMessage` are translated. ## Slug uniqueness **Every `slug` must be UNIQUE ACROSS THE WHOLE CATALOG**, not just within a bundle. A slug appears once per language file of its bundle (same slug in `ru.json` and `en.json`), but no two different bundles may share a slug. `scripts/check.mjs` enforces this. ## How to add things ### Add a role to an existing bundle 1. Add an entry to that bundle's `roles[]` in `index.json` with a new unique `slug` and `version: 1`. 2. Add a role object with the same `slug` to **every** `.json` of the bundle, translating `name`, `description`, `instructions`, and `launchMessage`. 3. Run the check (see below). ### Add a bundle 1. Add a bundle object to `index.json` (`id`, `name`, `description`, `languages`, `roles`). 2. Create `bundles//.json` for each declared language, with one role object per `roles[]` entry. 3. Run the check. ### Add a language to a bundle 1. Add the language code to that bundle's `languages[]` in `index.json`. 2. Create `bundles//.json` containing every role of the bundle, translated. 3. Run the check. ### Change a role's content Edit the role in the relevant `.json` file(s) and **bump that role's `version`** in `index.json`. Then run `node scripts/check.mjs --update-hashes` to refresh the content-hash lock (`scripts/content-hashes.json`). `check.mjs` now **fails if a role's content changed but its `version` was not bumped**, so this step is mandatory — the lock can only be refreshed after the bump. ## Validating From this directory: ```sh node scripts/check.mjs # or: npm run check ``` It fails (exit code 1) if any slug is duplicated across the catalog, if a bundle's index `roles[]` don't match the slugs present in each language file, if a declared language file is missing, or if any role is missing a required field (`slug`, `name`, `instructions`). It prints `OK` on success. ### Content-hash guard `check.mjs` also guards against changing a role's content without bumping its `version`. It keeps a lockfile, `scripts/content-hashes.json`, mapping each role `slug` to `{ version, hash }`, where `hash` is a SHA-256 over the role's content fields (`emoji`, `autoStart`, `name`, `description`, `instructions`, `launchMessage`) across all of its language files, in a deterministic canonical form. This lockfile is a **check artifact only** — the server fetches only `index.json` and the bundle `.json` files, never this file, so it has no effect on the served catalog or its schema. On a normal run, for every role the check recomputes the hash and compares it against the lock: - content unchanged and versions agree → OK; - content changed but `version` not bumped above the lock → **error** asking you to bump and refresh; - content changed and `version` bumped → **error** asking you to record it by refreshing the lock; - role missing from the lock, or a lock entry for a role that no longer exists → **error** asking you to refresh. Refresh the lock with: ```sh node scripts/check.mjs --update-hashes # alias: --fix ``` This recomputes the lock from the current catalog, prunes entries for removed roles, and prints what changed — but it **refuses to write** (exit 1) if any role's content changed while its `index.json` version was not bumped, so the version bump is always enforced first. The check also requires every `index.json` role to carry a finite numeric `version` (the server requires the same). Known, accepted limitation: a deliberate prune-then-readd of a slug (remove the role and run `--update-hashes`, then re-add it with changed content at the same version) is **not** caught, because a brand-new slug has no lock baseline to enforce a bump against.