MUST-FIX - isSourceUniqueViolation read the wrong error field: kysely-postgres-js (postgres@3.4.8) puts the violated constraint on `constraint_name`, not node-postgres' `.constraint`, so a concurrent same-slug+language import's 23505 was never recognized as a source-collision and surfaced a false "name already exists" error. Now read `constraint_name` (with `.constraint` as a fallback for other drivers). Fix the faked test fixture (it built the error with the same wrong `.constraint` field, masking the bug): it now uses `constraint_name`, so the test genuinely exercises the skip path and FAILS against the unfixed code. - Extract the catalog modal's role-state computation into a pure `catalogRoleInstallState(role, workspaceRoles, language)` helper (mirrors role-launch.ts) and cover it with vitest: import / installed / update / same-slug-different-language. SUGGESTIONS - Restore IAiRoleUpdateFromCatalogResult as a discriminated union mirroring the server; narrow the consumer via `"reason" in result` (the boolean discriminant does not narrow under strictNullChecks:false). - README: add a "How it's served" section documenting AI_AGENT_ROLES_CATALOG_URL (remote http(s) base / local path / empty => in-repo folder). - check.mjs: drop the redundant `const key = slug` alias. - Cover the reason->message mapping in useUpdateAiRoleFromCatalogMutation (4 branches) via renderHook with a mocked service. - Cover importFromCatalog "bundle not in index" => BadGateway. - Cover updateFromCatalog "slug in index but missing in bundle file" => not-in-catalog. ARCHITECTURE - Extract the shared catalog read prefix: a private `loadBundleById` (fetchIndex -> meta -> fetchBundle -> versionMap) reused by getCatalogBundle and importFromCatalog, and a `catalogRoleContentFields` mapper shared by the import insert and update patch. The three orchestrations and their distinct write paths stay separate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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/
<bundle-id>/
<lang>.json # one file per declared language (e.g. ru.json, en.json)
scripts/
check.mjs # validates the catalog (no dependencies)
package.json # defines the `check` script
README.md
Currently shipped bundles:
editorial— the editorial suite (structural-editor, line-editor, copy-editor, fact-checker, proofreader, narrator), languagesru,en.research— a singleresearcherrole, languagesru,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()). The value selects one of
three sources:
http(s)://…— a REMOTE base URL. The server fetches<base>/index.jsonfor the manifest and<base>/bundles/<bundle-id>/<lang>.jsonfor each opened bundle file (e.g. the raw GitHub base of the catalog repo in production).- any other non-empty value — a LOCAL filesystem directory; the same
index.json/bundles/<id>/<lang>.jsonpaths are read from disk. - empty / unset (the default) — the in-repo
agent-roles-catalog/folder (this directory), i.e. local dev reads these files directly.
In every case the layout below is what the server expects, and 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
{
"schemaVersion": 1,
"bundles": [
{
"id": "editorial", // unique bundle id; matches bundles/<id>/
"name": { "ru": "...", "en": "..." }, // localized display name
"description": { "ru": "...", "en": "..." },
"languages": ["ru", "en"], // which <lang>.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 (<lang>.json) schema
{
"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:
modelConfigis intentionally absent; the server treats an absentmodelConfigasnull.- A role's
slug,emoji, andautoStartare identical across all language files of the same bundle. Onlyname,description,instructions, andlaunchMessageare 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
- Add an entry to that bundle's
roles[]inindex.jsonwith a new uniqueslugandversion: 1. - Add a role object with the same
slugto every<lang>.jsonof the bundle, translatingname,description,instructions, andlaunchMessage. - Run the check (see below).
Add a bundle
- Add a bundle object to
index.json(id,name,description,languages,roles). - Create
bundles/<id>/<lang>.jsonfor each declared language, with one role object perroles[]entry. - Run the check.
Add a language to a bundle
- Add the language code to that bundle's
languages[]inindex.json. - Create
bundles/<id>/<lang>.jsoncontaining every role of the bundle, translated. - Run the check.
Change a role's content
Edit the role in the relevant <lang>.json file(s) and bump that role's
version in index.json.
Validating
From this directory:
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.