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>
150 lines
5.1 KiB
Markdown
150 lines
5.1 KiB
Markdown
# 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), 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()`). The value selects one of
|
|
three sources:
|
|
|
|
- **`http(s)://…`** — a REMOTE base URL. The server fetches `<base>/index.json`
|
|
for the manifest and `<base>/bundles/<bundle-id>/<lang>.json` for 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>.json` paths 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
|
|
|
|
```jsonc
|
|
{
|
|
"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
|
|
|
|
```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** `<lang>.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/<id>/<lang>.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/<id>/<lang>.json` containing every role of the bundle,
|
|
translated.
|
|
3. 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:
|
|
|
|
```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.
|