@kontextmind/kxm 0.7.125 → 0.7.127

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/roles/planner.yaml +6 -7
  3. package/.kxm/roles/reviewer-arch.yaml +6 -7
  4. package/.kxm/roles/reviewer-cli.yaml +6 -7
  5. package/.kxm/roles/writer.yaml +8 -17
  6. package/CHANGELOG.md +17 -0
  7. package/docs/README.md +2 -0
  8. package/docs/architecture/access.md +2 -4
  9. package/docs/architecture/inventory.md +4 -0
  10. package/docs/contracts/validation.md +1 -1
  11. package/docs/contributing/harness-routing-internals.md +1 -1
  12. package/docs/contributing/learnings.md +105 -0
  13. package/docs/contributing/operating-rules.md +107 -0
  14. package/docs/contributing/test-matrix.md +1 -1
  15. package/docs/glossary.md +1 -1
  16. package/docs/guides/agent-skills.md +1 -1
  17. package/docs/operations/backup-and-restore.md +2 -2
  18. package/docs/reference/cli-reference.md +31 -74
  19. package/docs/reference/config-reference.md +83 -118
  20. package/docs/reference/harness-routing.md +15 -8
  21. package/examples/project/.kxm/models/critic-claude.yaml +6 -2
  22. package/examples/project/.kxm/models/critic-gemini.yaml +6 -2
  23. package/examples/project/.kxm/models/critic-grok.yaml +6 -2
  24. package/examples/project/.kxm/models/implementation.yaml +6 -2
  25. package/examples/project/.kxm/models/primary.yaml +6 -2
  26. package/examples/project/.kxm/roles/planner.yaml +8 -0
  27. package/examples/project/.kxm/roles/reviewer-arch.yaml +8 -0
  28. package/examples/project/.kxm/roles/reviewer-cli.yaml +8 -0
  29. package/examples/project/.kxm/roles/writer.yaml +8 -0
  30. package/package.json +1 -1
  31. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  32. package/plugins/kxm/dist/cli.js +1759 -1306
  33. package/plugins/kxm/dist/mcp-server.js +1 -1
  34. package/plugins/kxm/dist/runtime-supervisor.js +994 -245
  35. package/plugins/kxm/dist/runtime.js +1018 -269
  36. package/plugins/kxm/dist/server.js +55 -1
  37. package/plugins/kxm/package.json +1 -1
  38. package/plugins/kxm/skills/kxm/SKILL.md +1 -1
  39. package/plugins/kxm/skills/kxm-definitions/SKILL.md +3 -7
  40. package/plugins/kxm/src/autocomplete.ts +1 -1
  41. package/plugins/kxm/src/cli/project.ts +7 -1
  42. package/plugins/kxm/src/cli/roles.ts +46 -150
  43. package/plugins/kxm/src/cli.ts +8 -22
  44. package/plugins/kxm/src/engine.ts +24 -1
  45. package/plugins/kxm/src/init-guide-setup.ts +38 -4
  46. package/plugins/kxm/src/mcp-server.ts +1 -1
  47. package/plugins/kxm/src/permission.ts +11 -0
  48. package/plugins/kxm/src/policy-draft.mjs +56 -16
  49. package/plugins/kxm/src/project-config.ts +121 -10
  50. package/plugins/kxm/src/repair.ts +1 -0
  51. package/plugins/kxm/src/role.ts +40 -440
  52. package/plugins/kxm/src/routes.ts +28 -5
  53. package/plugins/kxm/src/template.ts +34 -1
  54. package/schemas/README.md +2 -1
  55. package/schemas/model.schema.json +111 -13
  56. package/schemas/role.schema.json +61 -31
  57. package/schemas/policy-draft/README.md +0 -17
  58. package/schemas/policy-draft/model.v2.schema.json +0 -140
  59. package/schemas/policy-draft/role.v2.schema.json +0 -91
@@ -100,7 +100,7 @@ kxm -V
100
100
 
101
101
  `--dry-run` changes nothing: no file is written, deleted, or moved, no request that changes hub or Runtime state is sent, no process is started, and no remote command runs. A command that cannot say what it would do without doing some of it refuses the flag instead of acting.
102
102
 
103
- - Plan with `planned`: `backup`, `restore`, `config set`, `role add|remove|modify|set-host|resume`, `workflow add|remove|modify`, `goal create`, `task create|run|sync`, `memory note|sync`, `skills evaluate|promote|reject`, `context promote`, `context wiki-compile --out`, `auth token`, `session token`, `session brief`, and `ssh run|file|close`. Each prints its normal result plus `dryRun: true` and `planned`, a list of `{action, target}` entries whose `action` is `write`, `delete`, `move`, `request`, or `ssh`. Text mode prints `dry run: <summary>` and one indented `would <action> <target>` line per entry (`session brief` appends `dry run: would write <file>` lines to the brief instead).
103
+ - Plan with `planned`: `backup`, `restore`, `config set`, `role add|remove|modify|resume`, `workflow add|remove|modify`, `goal create`, `task create|run|sync`, `memory note|sync`, `skills evaluate|promote|reject`, `context promote`, `context wiki-compile --out`, `auth token`, `session token`, `session brief`, and `ssh run|file|close`. Each prints its normal result plus `dryRun: true` and `planned`, a list of `{action, target}` entries whose `action` is `write`, `delete`, `move`, `request`, or `ssh`. Text mode prints `dry run: <summary>` and one indented `would <action> <target>` line per entry (`session brief` appends `dry run: would write <file>` lines to the brief instead).
104
104
  - Plan in their own shape (described in each section): `init`, `run`, `runs drive|cancel`, `docs build|serve`, `runtime start|stop|sync-retry`, `hub start|stop|bind|unbind`, `session start|stop`, `agent worker`, `dash`, `studio serve`, `models inventory-refresh`, `routes admit|disable`, `update`, `completion install`, `workflow start|signal|export|checkpoint|record|wait`, every `peer` subcommand, `gate degrade|signal`, `skills create`, and `improve report`. `gate github watch --dry-run` still polls GitHub but does not post the signal.
105
105
  - Read-only commands run as usual, without leaving a trace: a local SQLite store is opened without creating `-wal` or `-shm` files, and `runs status|list|receipt` only attach to a running Runtime supervisor. With no supervisor running they exit 2 with `dry_run_unsupported` instead of starting one. `context wiki-compile --dry-run` still asks the hub to compile, which is a read.
106
106
  - Refused: `kxm models` (the interactive screen) and `kxm prices acknowledge` exit 2 with `dry_run_unsupported`. The CLI keeps one list of the commands that answer `--dry-run` and refuses every other command the same way before it runs, so a command added without dry-run support fails closed.
@@ -142,10 +142,10 @@ Exit 2 covers an unknown command or option, a missing argument or required optio
142
142
 
143
143
  | Location | Contents | Used by |
144
144
  |---|---|---|
145
- | Project files under `<project>/.kxm/` (reviewed in Git) | `project.yaml`, `agents/`, `workflows/`, `gates.yaml`, `repo/`, `template-provenance.yaml`, `routes.yaml`, `models/inventory.yaml`, `roles/`, `role-hosts.yaml`, `modes.yaml`, `prices.yaml` | `init`, `trust`, `run`, `models`, `routes`, `role`, `workflow definitions\|add\|remove\|modify`, `explain`, `studio` |
145
+ | Project files under `<project>/.kxm/` (reviewed in Git) | `project.yaml`, `agents/`, `workflows/`, `gates.yaml`, `repo/`, `template-provenance.yaml`, `routes.yaml`, `models/inventory.yaml`, `roles/`, `roles`, `modes.yaml`, `prices.yaml` | `init`, `trust`, `run`, `models`, `routes`, `role`, `workflow definitions\|add\|remove\|modify`, `explain`, `studio` |
146
146
  | Local project records under `<project>/.kxm/` | `config.yaml` (project scope), `goals/`, `tasks/`, `memory/`, `skills/`, `candidates/`, `backups/`, `run/ssh-sockets/` | `config`, `goal`, `task`, `memory`, `skills`, `improve`, `backup`, `ssh` |
147
147
  | Workspace directories (`.kxm/state`, `.kxm/logs`, `.kxm/assets`, `.kxm/config`; moved by `--workspace` or `KXM_*_DIR`) | hub SQLite store `state/kxm.db` (or `KXM_DATA_PATH`), `state/hub.pid`, `state/session-brief.json`, `state/lanes.json` (mode 0600 lane records), `logs/telemetry.jsonl`, `logs/kxm-hub.jsonl`, `assets/sessions/`, `assets/workflows/`, `assets/improvements/`, `assets/retrospectives/`, legacy `config/agents.json` and `config/gates.json` | `hub`, `session`, `dash`, `lane`, `agent worker`, `workflow list\|get\|export`, `gate`, `improve`, `routing report` |
148
- | User config directory (`KXM_USER_CONFIG_DIR`, default `~/.config/kxm`) | `config.yaml` (user scope), `session.token`, global `roles/` and `workflows/`, `role-hosts.yaml`, `completions/` | `config --scope user`, `auth token`, `session brief\|token`, `role`/`workflow` with `--scope global`, `studio serve`, `completion install` |
148
+ | User config directory (`KXM_USER_CONFIG_DIR`, default `~/.config/kxm`) | `config.yaml` (user scope), `session.token`, global `roles/` and `workflows/`, `roles`, `completions/` | `config --scope user`, `auth token`, `session brief\|token`, `role`/`workflow` with `--scope global`, `studio serve`, `completion install` |
149
149
  | User state root (`KXM_STATE_HOME`; macOS `~/Library/Application Support/KXM`; Linux `$XDG_STATE_HOME/kxm` or `~/.local/state/kxm`; Windows `%LOCALAPPDATA%\KXM`) | `hub-env.json` (persisted hub credentials), `hub-binding.json`, `runtime/` (Runtime supervisor registry and per-project run stores), `update.yaml`, repository bindings | `hub start\|bind\|unbind`, every hub client, `run`, `runs`, `runtime`, `tenant status`, `update`, `init --repository`, and (read-only, the project's run store) `improve` and `routing report` |
150
150
 
151
151
  `init`, `trust`, `run`, `runs`, `docs build`, `docs serve`, `runtime sync-retry`, `tenant status`, and `studio layout` find the project root by walking up from the current directory. `improve` and `routing report` use the current directory's Git root when it holds `.kxm/project.yaml`, to find the project's Runtime run store (and, for `improve`, its configuration and default candidate directory). `config`, `role`, `workflow definitions|add|remove|modify`, `goal`, `task`, `memory`, `skills`, `backup`, `restore`, `studio serve`, and `ssh` (socket directory) use `.kxm` in the current directory. Run those from the project root.
@@ -1023,7 +1023,7 @@ Opens an interactive screen over `.kxm/models/inventory.yaml` that shows each mo
1023
1023
 
1024
1024
  - Needs an interactive terminal. With `--json` or without a TTY it exits 2 with `interactive_tty_required`.
1025
1025
  - `r` and `x` also mark the model `admitted` in `.kxm/routes.yaml`. So `x` re-admits a disabled route while it removes the role binding, and `r` admits the route as well as binding it.
1026
- - `r` writes a roster entry `{model: <inventory id>, enabled: true}` with no harness, creating the role file if needed. The inventory id is often a bare model (`grok-4.6`), which the Runtime's roster check does not match against an agent's `provider/model`; edit the entry to the full selector.
1026
+ - `r` binds the role only when a `kxm.model.v2` file matches the inventory id (`model`, or `vendor/model`). It appends `{route: <route-id>}` and creates a v2 role file when the role is new. A selector with no matching model file is refused with `unknown route` before either file is written. `kxm models` does not write a v1 roster entry.
1027
1027
 
1028
1028
  ```bash
1029
1029
  kxm models --json
@@ -1151,7 +1151,9 @@ kxm routes disable --dry-run --json
1151
1151
 
1152
1152
  ## `kxm role`
1153
1153
 
1154
- Manages role definitions (`kxm.role.v1`) and role-seat host bindings (`kxm.role-hosts.v1`). Local scope is `.kxm/roles/` and `.kxm/role-hosts.yaml` in the current directory; global scope is `<KXM_USER_CONFIG_DIR>/roles/` and `<KXM_USER_CONFIG_DIR>/role-hosts.yaml`. A local role with the same ID overrides a global one. `kxm role` with no subcommand runs `role list`. For `--pick` without a value on a non-interactive shell, set `KXM_PICK_SELECT` to an index or ID. Errors from this group are plain text on stderr, even with `--json`.
1154
+ Manages role definitions (`kxm.role.v2`). A role file is `.kxm/roles/<role>.yaml` in the project, or `<KXM_USER_CONFIG_DIR>/roles/<role>.yaml` for global scope. A local role with the same ID overrides a global one. `kxm role` with no subcommand runs `role list`. For `--pick` without a value on a non-interactive shell, set `KXM_PICK_SELECT` to an index or ID. Errors from this group are plain text on stderr, even with `--json`.
1155
+
1156
+ A roster entry is `{route, effort, mode}`. `route` names `.kxm/models/<route-id>.yaml`. The first entry is the primary route. `kxm role add --route` writes that roster. `kxm role modify --add-route` and `--remove-route` change it.
1155
1157
 
1156
1158
  ### `kxm role list`
1157
1159
 
@@ -1193,35 +1195,32 @@ Prints a role definition as YAML.
1193
1195
  kxm role get writer --scope local
1194
1196
  ```
1195
1197
 
1196
- Not run with a configured role; in a fresh project it exits 1 with `kxm: role 'writer' not found`.
1198
+ In a fresh project this exits 1 with `kxm: role 'writer' not found`. This checkout's writer is the `kxm role get writer --json` example under [`kxm role modify`](#kxm-role-modify).
1197
1199
 
1198
1200
  ### `kxm role add`
1199
1201
 
1200
1202
  ```text
1201
- kxm role add [roleId] [--file <path>] [--description <text>] [--skills <skills>] [--harness <harness>] [--model <model>] [--scope global|local] [--overwrite] [--pick [selection]]
1203
+ kxm role add [roleId] [--file <path>] [--description <text>] [--skills <skills>] [--route <route-id>] [--scope global|local] [--overwrite] [--pick [selection]]
1202
1204
  ```
1203
1205
 
1204
- Adds a role definition. Without a role ID, or with `--pick`, you choose from the built-in templates (`writer`, `planner`, `critic-arch`, `critic-cli`, `verifier`) and, for local scope, existing global roles; a global role with a template's ID is not offered. The choice is written under its own ID with its content: the template, or a copy of the global role's file, with `--description`, `--skills`, and `--model` (with `--harness`) replacing its description, skills, and roster. With `--file`, the YAML file is used and its `id` is replaced by the role ID. Otherwise a role with only the given options is written.
1206
+ Adds a role definition. Without a role ID, or with `--pick`, local scope offers existing global roles. The choice is written under its own ID: a copy of the global role file, with `--description`, `--skills`, and `--route` replacing its description, skills, and roster. Repeat `--route`. The first id is the primary roster entry. With `--file`, the YAML file is used and its `id` is replaced by the role ID. `--route` does not replace the file's roster. Otherwise a role with only the given options is written.
1205
1207
 
1206
1208
  | Option | Argument | Default | Description |
1207
1209
  |---|---|---|---|
1208
1210
  | `--file` | `<path>` | none | Path to YAML role definition file |
1209
1211
  | `--description` | `<text>` | `Role <id>` | Role description |
1210
1212
  | `--skills` | `<skills>` | none | Comma-separated skills list |
1211
- | `--harness` | `<harness>` | `pi` when `--model` is set | Primary harness name (e.g. grok, claude, agy, pi) |
1212
- | `--model` | `<model>` | none | Primary model identifier (e.g. grok-4.6, fable, gemini-3.8-flash-high) |
1213
+ | `--route` | `<route-id>` | none | Route id under `.kxm/models/`. Repeatable. The first id is primary |
1213
1214
  | `--scope` | `<scope>` | `local` | Configuration scope: global or local (default: local) |
1214
1215
  | `--overwrite` | none | off | Overwrite existing role definition if present |
1215
- | `--pick` | `[selection]` | none | Pick from available role templates (index or id) |
1216
+ | `--pick` | `[selection]` | none | Pick a global role to copy (index or id) |
1216
1217
 
1217
- - Writes `<scope dir>/roles/<id>.yaml`. `--dry-run` plans the write and writes nothing.
1218
- - Local scope belongs to a KXM project: the file lands in the project root's `.kxm/roles/` from any subdirectory, and outside a project the command refuses with `project_not_found` and creates nothing. Before writing, the project loader checks the project with the new role in place of any file of that ID. The loader reads only `writer.yaml`, whose enabled roster must include the `implementer` agent's model (see [Roles](config-reference.md#kxmrolesroleyaml-kxmrolev1)). If the project would not load, the command refuses with `role_invalid`, lists each issue and writes nothing, also under `--dry-run`, and `--overwrite` replaces a `writer.yaml` the loader refuses. Global scope is not checked, because no loader reads it.
1219
- - Refusals exit 2 and honor `--json`: `project_not_found` and `role_invalid` (with `issues`, each `{phase, code, file, message}`). An existing role without `--overwrite` exits 1 with a plain `role add failed: role_already_exists: ...` line, also under `--dry-run`.
1218
+ - Writes `.kxm/roles/<id>.yaml` in local scope, or `<KXM_USER_CONFIG_DIR>/roles/<id>.yaml` in global scope. `--dry-run` plans the write and writes nothing.
1219
+ - Each `--route` is checked the same way as `kxm role modify --add-route`. If `.kxm/models/<route-id>.yaml` is missing, the command exits 1 and writes `kxm: route '<route-id>' is not a file under .kxm/models/` to stderr. It does not write the role file, including under `--dry-run`. `--file` checks every `roster[].route` the same way before writing, in local scope and in global scope.
1220
+ - Local scope belongs to a KXM project: the file lands in the project root's `.kxm/roles/` from any subdirectory, and outside a project the command refuses with `project_not_found` and creates nothing. Before writing, the project loader checks the project with the new role in place of any file of that ID. The loader reads only `writer.yaml`, whose roster must name a route whose model is the `implementer` agent's model (see [Roles](config-reference.md#kxmrolesroleyaml-kxmrolev2)). If the project would not load, the command refuses with `role_invalid`, lists each issue and writes nothing, also under `--dry-run`, and `--overwrite` replaces a `writer.yaml` the loader refuses. Global scope is not checked, because no loader reads it.
1221
+ - Refusals exit 2 and honor `--json`: `project_not_found` and `role_invalid` (with `issues`, each `{phase, code, file, message}`). A missing `--route` file, and an existing role without `--overwrite`, exit 1 with a plain stderr line, also under `--dry-run` (`kxm: route '<route-id>' is not a file under .kxm/models/`, or `role add failed: role_already_exists: ...`).
1220
1222
  - JSON keys: `roleId`, `id`, `filePath`, `scope`.
1221
1223
 
1222
- > [!WARNING]
1223
- > Most built-in template entries, and `--model` as you type it, are bare model IDs such as `grok-4.6`, `fable`, and `gemini-2.5-pro`. The Runtime's roster check needs the agent's full `provider/model` selector, so a live attempt under a local `writer.yaml` copied from the template is refused (`producer_route_unsupported: … not in role 'writer' roster`). Write `--model xai/grok-4.6`, or edit the entries to full selectors, before you drive live runs.
1224
-
1225
1224
  ```bash
1226
1225
  kxm role add demo-role --description "Demo role" --dry-run --json
1227
1226
  ```
@@ -1230,14 +1229,14 @@ kxm role add demo-role --description "Demo role" --dry-run --json
1230
1229
  {"schema":"kxm.cli-result.v1","ok":true,"command":"role add","roleId":"demo-role","id":"demo-role","filePath":"/work/proj/.kxm/roles/demo-role.yaml","scope":"local","dryRun":true,"planned":[{"action":"write","target":"/work/proj/.kxm/roles/demo-role.yaml"}]}
1231
1230
  ```
1232
1231
 
1233
- Add a reviewer role with a Claude model, and copy a built-in template into global scope (Not run):
1232
+ Add a reviewer whose primary route is `fable-claude`, with `grok-native` second (Not run):
1234
1233
 
1235
1234
  ```bash
1236
- kxm role add reviewer --description "Independent reviewer" --harness claude --model fable --skills kxm
1235
+ kxm role add reviewer --description "Independent reviewer" --route fable-claude --route grok-native --skills kxm
1237
1236
  ```
1238
1237
 
1239
1238
  ```bash
1240
- kxm role add --pick critic-arch --scope global
1239
+ kxm role add --pick reviewer --scope local
1241
1240
  ```
1242
1241
 
1243
1242
  ### `kxm role remove`
@@ -1267,88 +1266,46 @@ kxm role remove demo-role --dry-run --json
1267
1266
  ### `kxm role modify`
1268
1267
 
1269
1268
  ```text
1270
- kxm role modify [roleId] [--description <text>] [--add-skill <skill>] [--remove-skill <skill>] [--add-model <harness:model>] [--remove-model <model>] [--scope global|local] [--pick [selection]]
1269
+ kxm role modify [roleId] [--description <text>] [--add-skill <skill>] [--remove-skill <skill>] [--add-route <route-id>] [--remove-route <route-id>] [--scope global|local] [--pick [selection]]
1271
1270
  ```
1272
1271
 
1273
- Updates an existing role's description, skills, or model roster and rewrites its file.
1272
+ Updates an existing role's description, skills, or route roster and rewrites its file.
1274
1273
 
1275
1274
  | Option | Argument | Default | Description |
1276
1275
  |---|---|---|---|
1277
1276
  | `--description` | `<text>` | unchanged | Updated description |
1278
1277
  | `--add-skill` | `<skill>` | none | Skill to add |
1279
1278
  | `--remove-skill` | `<skill>` | none | Skill to remove |
1280
- | `--add-model` | `<harness:model>` | none | Model to add to roster |
1281
- | `--remove-model` | `<model>` | none | Model to remove from roster |
1279
+ | `--add-route` | `<route-id>` | none | Route id to add. Must be `.kxm/models/<route-id>.yaml` |
1280
+ | `--remove-route` | `<route-id>` | none | Route id to remove from the roster |
1282
1281
  | `--scope` | `<scope>` | first match | Configuration scope: global or local |
1283
1282
  | `--pick` | `[selection]` | none | Pick a role to modify (index or id) |
1284
1283
 
1285
- - `--add-model` without a colon uses harness `pi`. `--dry-run` returns the modified role and plans the write without making it.
1284
+ - `--add-route` checks that `.kxm/models/<route-id>.yaml` exists in the project. If it does not, the command exits 1 and writes `kxm: route '<route-id>' is not a file under .kxm/models/` to stderr. It does not write the role file. `--remove-route` drops a roster entry by id and does not require the model file. `--dry-run` returns the modified role and plans the write without making it, after the same existence check.
1286
1285
  - JSON keys: `roleId`, `id`, `role`, `filePath`, `scope`.
1287
1286
 
1288
1287
  ```bash
1289
- kxm role modify demo-role --add-skill kxm --dry-run --json
1290
- ```
1291
-
1292
- ```text
1293
- {"schema":"kxm.cli-result.v1","ok":true,"command":"role modify","roleId":"demo-role","id":"demo-role","role":{"schema":"kxm.role.v1","id":"demo-role","description":"Demo role","skills":["kxm"],"roster":[]},"filePath":"/work/proj/.kxm/roles/demo-role.yaml","scope":"local","dryRun":true,"planned":[{"action":"write","target":"/work/proj/.kxm/roles/demo-role.yaml"}]}
1294
- ```
1295
-
1296
- Add a Claude model to a role's roster (Not run):
1297
-
1298
- ```bash
1299
- kxm role modify reviewer --add-model claude:fable --add-skill kxm-peer
1288
+ kxm role get writer --json
1300
1289
  ```
1301
1290
 
1302
- ### `kxm role hosts`
1303
-
1304
1291
  ```text
1305
- kxm role hosts [--scope all|global|local]
1292
+ {"schema":"kxm.cli-result.v1","ok":true,"command":"role get","roleId":"writer","scope":"local","filePath":"/work/kxm/.kxm/roles/writer.yaml","role":{"schema":"kxm.role.v2","id":"writer","purpose":"writer","permission":"edit","description":"Primary implementation agent.","skills":[],"roster":[{"route":"grok-native","effort":"medium"},{"route":"qwen-openrouter-pi","effort":"medium"},{"route":"gemini-agy"}]}}
1306
1293
  ```
1307
1294
 
1308
- Lists role seats (`critic-arch`, `critic-cli`, `planner`, `verifier`, `writer`, plus any configured seat) and the host, model, and effort each resolves to, with the source of the decision (`override`, `role-hosts`, `seat-default`, `role-roster`, or `fallback`). The listing is display-only: no dispatch path reads seats or `role-hosts.yaml`, so a run's harness and model still come from the agent file.
1309
-
1310
- | Option | Argument | Default | Description |
1311
- |---|---|---|---|
1312
- | `--scope` | `<scope>` | `all` | Filter by scope: all, global, or local |
1313
-
1314
- - Reads only. JSON keys: `scope`, `filePath`, `seats` (`seatId`, `host`, `model`, `provider`, `effort`, `source`, `configuredHost`, `configuredModel`), `hostProviders`.
1295
+ Captured from this checkout with `node scripts/kxm.mjs role get writer --json`. The path is shortened to `/work/kxm`.
1315
1296
 
1316
1297
  ```bash
1317
- kxm role hosts
1298
+ kxm role modify writer --add-skill kxm --dry-run --json
1318
1299
  ```
1319
1300
 
1320
1301
  ```text
1321
- ROLE SEATS (default):
1322
- critic-arch -> host: pi [anthropic/claude-fable-5.1] (via seat-default)
1323
- critic-cli -> host: pi [openai/gpt-5.6-sol] (via seat-default)
1324
- planner -> host: pi [anthropic/claude-fable-5.1] (via seat-default)
1325
- verifier -> host: pi [evaluator] (via seat-default)
1326
- writer -> host: grok [x-ai/grok-4.6] (via seat-default)
1302
+ {"schema":"kxm.cli-result.v1","ok":true,"command":"role modify","roleId":"writer","id":"writer","role":{"schema":"kxm.role.v2","id":"writer","purpose":"writer","permission":"edit","description":"Primary implementation agent.","skills":["kxm"],"roster":[{"route":"grok-native","effort":"medium"},{"route":"qwen-openrouter-pi","effort":"medium"},{"route":"gemini-agy"}]},"filePath":"/work/kxm/.kxm/roles/writer.yaml","scope":"local","dryRun":true,"planned":[{"action":"write","target":"/work/kxm/.kxm/roles/writer.yaml"}]}
1327
1303
  ```
1328
1304
 
1329
- ### `kxm role set-host`
1330
-
1331
- ```text
1332
- kxm role set-host <seatId> <host> [--model <model>] [--effort low|medium|high|xhigh] [--scope global|local]
1333
- ```
1334
-
1335
- Binds a role seat to a host in `role-hosts.yaml`. The binding changes what `kxm role hosts` shows, not what runs.
1336
-
1337
- | Option | Argument | Default | Description |
1338
- |---|---|---|---|
1339
- | `--model` | `<model>` | none | Model identifier for this seat |
1340
- | `--effort` | `<effort>` | none | Effort level: low, medium, high, xhigh |
1341
- | `--scope` | `<scope>` | `local` | Configuration scope: global or local (default: local) |
1342
-
1343
- - Writes `.kxm/role-hosts.yaml` (or the global file). `--dry-run` plans the write and writes nothing.
1344
- - JSON keys: `seatId`, `host`, `binding`, `filePath`, `scope`.
1305
+ Add an existing route to a role's roster (Not run):
1345
1306
 
1346
1307
  ```bash
1347
- kxm role set-host writer claude --model fable --effort high --dry-run --json
1348
- ```
1349
-
1350
- ```text
1351
- {"schema":"kxm.cli-result.v1","ok":true,"command":"role set-host","seatId":"writer","host":"claude","binding":{"host":"claude","model":"fable","effort":"high"},"filePath":"/work/proj/.kxm/role-hosts.yaml","scope":"local","dryRun":true,"planned":[{"action":"write","target":"/work/proj/.kxm/role-hosts.yaml"}]}
1308
+ kxm role modify reviewer --add-route fable-claude --add-skill kxm-peer
1352
1309
  ```
1353
1310
 
1354
1311
  ### `kxm role resume`
@@ -26,11 +26,10 @@ Related pages:
26
26
  | `.kxm/repo/repo.yaml` (in each repository) | `kxm.repository.v1` | Per-repository definition | You; `kxm init` creates the control one | Yes, in the repository it describes |
27
27
  | `.kxm/project/env.yaml`, `.kxm/repo/env.yaml` | `kxm.environment.v1` | Portable, non-secret environment | You | Yes |
28
28
  | `.kxm/agents/<id>.yaml` | `kxm.agent.v1` | Agent harness, model, and permission ceilings | You; `kxm init` creates two | Yes |
29
- | `.kxm/models/<id>.yaml` | `kxm.model.v1` | Named model profiles for selectors | You | Yes |
29
+ | `.kxm/models/<id>.yaml` | `kxm.model.v2` | One model route (harness, vendor, status, permissions) | You; `kxm init` | Yes |
30
30
  | `.kxm/workflows/<id>.yaml` | `kxm.workflow.v1` | Ordered steps and typed transitions | You; `kxm init` creates `default` | Yes |
31
31
  | `.kxm/gates.yaml` | `kxm.gate-registry.v1` | The executable gate registry | You; `kxm init` creates it | Yes |
32
- | `.kxm/roles/<role>.yaml` | `kxm.role.v1` | Model rosters per role | You, `kxm role`, `kxm models` | Yes |
33
- | `.kxm/role-hosts.yaml` | `kxm.role-hosts.v1` | Role seat to host bindings (display only) | `kxm role set-host` | Yes, unless you ignore it |
32
+ | `.kxm/roles/<role>.yaml` | `kxm.role.v2` | Model rosters per role | You, `kxm role`, `kxm models` | Yes |
34
33
  | `.kxm/routes.yaml` | `kxm.routes.v2` | Admitted and disabled model routes | You, `kxm routes`, `kxm models` | Yes |
35
34
  | `.kxm/roster.yaml` | `kxm.developer-roster.v1` | Developer assignment roster for the KXM source repository | Maintainers | Yes, and it must be committed |
36
35
  | `.kxm/prices.yaml` | `kxm.prices.v1` | Dated, hash-pinned list prices | You | Yes |
@@ -86,7 +85,7 @@ Cost accounting:
86
85
  | Revisions pinned on every run | `configRevision` (the bundle), memory revision (`.kxm/memory` without `candidates/`, plus `.kxm/skills/promoted`), executor policy, tool policy (agent and step `tools` plus the gate registry) | `kxm run` |
87
86
 
88
87
  Nothing validates `routes.yaml`, `roles/` (other than `writer.yaml`),
89
- `role-hosts.yaml`, `roster.yaml`, `prices.yaml`, `inventory.yaml`,
88
+ `roles`, `roster.yaml`, `prices.yaml`, `inventory.yaml`,
90
89
  `config.yaml`, `modes.yaml`, memory, tasks, goals, or webhook JSON during
91
90
  `kxm init`. Their own readers report problems when they run. None of them are
92
91
  part of `configRevision`, and `kxm trust check` does not see them: a change to
@@ -454,7 +453,7 @@ Error codes: `executor_unknown`, `harness_unknown`, `tool_preset_unknown`,
454
453
  `model_tag_unresolved`, `harness_unhosted_model`,
455
454
  `pi_native_impersonation_blocked`, the path codes under
456
455
  [Rules shared by the project bundle](#rules-shared-by-the-project-bundle), and
457
- `role_roster_conflicts_with_agent` (see [Roles](#kxmrolesroleyaml-kxmrolev1)).
456
+ `role_roster_conflicts_with_agent` (see [Roles](#kxmrolesroleyaml-kxmrolev2)).
458
457
 
459
458
  Commands: `kxm init` creates `coordinator` (`claude`, `anthropic/fable`) and
460
459
  `implementer` (`grok`, `xai/grok-4.6`) and admits both selectors in
@@ -464,47 +463,48 @@ selectors too, but skips Google guide candidates because the Runtime cannot
464
463
  reach the `antigravity` Pi provider yet; `kxm run` and the Runtime read them;
465
464
  `kxm trust` diffs them.
466
465
 
467
- ## `.kxm/models/<id>.yaml` (`kxm.model.v1`)
466
+ ## `.kxm/models/<id>.yaml` (`kxm.model.v2`)
468
467
 
469
- Named model profiles that agent and step selectors can reference by `profile`
470
- or `tag`. The filename is the profile ID; `inventory.yaml` in the same
471
- directory is reserved for the generated inventory and is skipped by this
472
- loader.
468
+ One model route. The filename is the route id (`inventory.yaml` in the same
469
+ directory is the generated catalog and is skipped). `kxm config` validation
470
+ checks the file against `schemas/model.schema.json`. `origin` is required when
471
+ `harness` is `pi` and optional otherwise. When `origin` is present, `source`
472
+ is a project-relative evidence file and `sha256` is the SHA-256 of that file's
473
+ bytes.
473
474
 
474
475
  | Field | Type and allowed values | Required, default | What reads it |
475
476
  |---|---|---|---|
476
- | `schema` | `kxm.model.v1` | Required | Loader |
477
- | `provider` | Identifier | Required | Loader: selector resolution, harness pairing, provider diversity |
478
- | `model` | String, 1 to 200 characters | Required | Loader: same |
479
- | `thinking` | String, 1 to 64 characters | Optional | Not read by any code path yet |
480
- | `tags` | Unique identifiers, at most 32 | Optional | Loader: `tag` selectors |
481
- | `capabilities` | Unique identifiers, at most 32 | Optional | Loader: `tag` selectors with `capabilities` |
482
- | `priority` | Integer, -10,000 to 10,000 | Optional | Not read by any code path yet |
483
- | `fallbacks` | Up to 8 selectors | Optional | Loader checks references (`model_profile_unknown`, `model_tag_unresolved`) and cycles (`model_fallback_cycle`); nothing fails over yet |
484
- | `limits.contextTokens`, `limits.outputTokens` | Integer, at least 1 | Optional | Not read by any code path yet |
485
- | `limits.timeoutMs` | Integer, 0 to 31,536,000,000 | Optional | Not read by any code path yet |
486
-
487
- Profiles are load-time data only. The Runtime's live route resolution reads
488
- the agent file directly and does not consult profiles.
477
+ | `schema` | `kxm.model.v2` | Required | Loader |
478
+ | `id` | Kebab identifier, 1 to 64 characters | Optional; the filename is the id | Loader |
479
+ | `harness` | Identifier, 1 to 64 characters | Required | Developer ceilings and dispatch membership |
480
+ | `model` | String, 1 to 200 characters | Required | Dispatch membership (`model`, `vendor/model`, `harness/model`) |
481
+ | `vendor` | String, 1 to 64 characters | Required | The lab that trained the model, not the biller |
482
+ | `status` | `admitted`, `candidate`, or `retired` | Required | A role roster may use only `admitted` |
483
+ | `permissions` | One or two of `edit`, `read-only` | Required | Must stay inside the harness ceiling |
484
+ | `origin.source` | String, 1 to 512 characters | Required with `origin.sha256` when `origin` is set; required for `harness: pi` | Evidence path |
485
+ | `origin.sha256` | 64 hex characters | Required with `origin.source` | Must match the evidence file |
486
+ | `thinking` | String, 1 to 64 characters | Optional | Recorded on the route |
487
+ | `tags`, `capabilities` | Unique identifiers, at most 32 | Optional | Selector tags |
488
+ | `priority` | Integer, -10,000 to 10,000 | Optional | Recorded on the route |
489
+ | `fallbacks` | Up to 8 `profile`, `tag`, or `provider`+`model` objects | Optional | Reference check |
490
+ | `limits.contextTokens`, `limits.outputTokens` | Integer, at least 1 | Optional | Recorded on the route |
491
+ | `limits.timeoutMs` | Integer, 0 to 31,536,000,000 | Optional | Recorded on the route |
489
492
 
490
493
  ```yaml
491
- # .kxm/models/critic-claude.yaml — the filename is the profile id.
492
- schema: kxm.model.v1
493
- provider: anthropic
494
- model: fable
495
- thinking: high
496
- tags: [critic]
497
- capabilities: [tools, structured-output]
498
- priority: 100
499
- fallbacks:
500
- - profile: critic-sol
501
- limits:
502
- contextTokens: 200000
503
- outputTokens: 32000
504
- timeoutMs: 1800000
494
+ schema: kxm.model.v2
495
+ id: grok-native
496
+ harness: grok
497
+ model: grok-4.7
498
+ vendor: xai
499
+ status: admitted
500
+ permissions:
501
+ - edit
502
+ origin:
503
+ source: .kxm/roster.yaml
504
+ sha256: b2bd628604e8fec5afdff2a1f2c104ed14ff785686bb1f38272bc972a310c806
505
505
  ```
506
506
 
507
- Commands: the loader in `kxm init`, `kxm run`, and `kxm trust`.
507
+ Commands: `kxm init`, `kxm run`, and `kxm trust` load these files. `kxm role modify --add-route` refuses a route id that has no file here (exit 1). Dispatch membership for a role reads `harness`, `model`, and `vendor`. The developer assignment runner still reads `.kxm/roster.yaml` until P2.
508
508
 
509
509
  ## `.kxm/workflows/<id>.yaml` (`kxm.workflow.v1`)
510
510
 
@@ -957,98 +957,63 @@ policy revision pinned on each run. `kxm gate artifacts-exist --path <file>` is
957
957
  separate CLI check against the workspace assets directory (`KXM_ASSETS_DIR`),
958
958
  not this registry.
959
959
 
960
- ## `.kxm/roles/<role>.yaml` (`kxm.role.v1`)
961
-
962
- Role files hold model rosters. Three different readers use them, and they
963
- read different fields:
964
-
965
- | Reader | File | Fields it reads | Effect |
966
- |---|---|---|---|
967
- | Project loader (`validateBundle`) | `.kxm/roles/writer.yaml` only | `roster[].model`, `roster[].enabled` | Cross-checks the writer roster against the `implementer` agent's model (see below). Parsed as restricted YAML. |
968
- | Runtime route check (`listRoleBindings` in `plugins/kxm/src/routes.ts`) | `.kxm/roles/<role>.yaml`, role = agent ID, `writer` for `implementer` | `roster[].model` | The agent's `provider/model` must appear exactly. `enabled` is ignored, so a disabled entry still admits. The role name comes from the filename. |
969
- | `kxm role` commands (`plugins/kxm/src/role.ts`) | `.kxm/roles/*.yaml` and `~/.config/kxm/roles/*.yaml` | Everything below | Listing and editing only. A file without `schema: kxm.role.v1` is silently skipped. A local file overrides a global one with the same ID. A local `kxm role add` runs the project loader check above first and refuses with `role_invalid` a role the loader would reject. |
960
+ ## `.kxm/roles/<role>.yaml` (`kxm.role.v2`)
970
961
 
971
- The loader check applies when the agent `implementer` (or else `writer`)
972
- declares a model and the roster has at least one enabled entry. One enabled
973
- entry must then equal `provider/model`, equal the bare model, or end with
974
- `/<model>`; otherwise the load fails with `role_roster_conflicts_with_agent`.
962
+ One role. The filename is the role id. `kxm config` validation checks the file
963
+ against `schemas/role.schema.json`.
975
964
 
976
- Write roster models as the full `provider/model` string. `kxm role add --model
977
- grok-4.6` writes a bare model ID, which satisfies the loader check but not the
978
- Runtime route check. `kxm role modify <role> --add-model grok:xai/grok-4.6`
979
- writes the full form.
965
+ `listRoleBindings` reads `roster[].route`. Dispatch treats `implementer` as
966
+ `writer` and requires the agent selector to be one of the selectors named by
967
+ those route files (`model`, `vendor/model`, or `harness/model`). The writer
968
+ cross-check (`role_roster_conflicts_with_agent`) does the same comparison
969
+ against the `implementer` agent's model, or the `writer` agent when there is
970
+ no implementer. `kxm role` reads and writes these files. A file without
971
+ `schema: kxm.role.v2` is skipped by `kxm role`. A local file overrides a
972
+ global one with the same id.
980
973
 
981
974
  | Field | Type | Required, default | What reads it |
982
975
  |---|---|---|---|
983
- | `schema` | `kxm.role.v1` | Required by `kxm role` | `kxm role` commands |
984
- | `id` | String | Optional, the filename | `kxm role` commands; the loader and Runtime use the filename |
985
- | `description` | String | Optional | `kxm role` commands |
986
- | `roster[].model` | String, `provider/model` | Required per entry | Loader (writer only), Runtime route check |
987
- | `roster[].enabled` | Boolean | Optional, `true` | Loader writer check only |
988
- | `roster[].harness` | String | Optional | `kxm role list` and `kxm role hosts` display |
989
- | `roster[].provider` | String | Optional | `kxm role hosts` display |
990
- | `roster[].effort` | `low`, `medium`, `high`, or `xhigh` | Optional | `kxm role hosts` display; not passed to any producer |
991
- | `roster[].mode` | `headless`, `interactive`, or `either` | Optional | Not read by any code path yet |
992
- | `skills`, `tools`, `produces`, `consumes`, `policy` | See `schemas/role.schema.json` | Optional | Stored and shown by `kxm role`; not read by any code path yet |
993
-
994
- Roster order is priority by convention, and `kxm role list` shows the first
995
- entry as the primary. No code path fails over along the roster yet: the
996
- Runtime only checks membership, and the model comes from the agent file.
997
-
998
- `schemas/role.schema.json` describes a stricter shape (required
999
- `description`, required `harness` per entry, `model` as a selector object, no
1000
- `enabled`). No loader enforces it, and the role files the Runtime reads do not
1001
- follow it.
976
+ | `schema` | `kxm.role.v2` | Required | Loader and `kxm role` |
977
+ | `purpose` | `writer`, `planner`, `reviewer-arch`, `reviewer-cli`, or `experiment` | Required | Developer ceilings |
978
+ | `permission` | `edit` or `read-only` | Required | Must be granted by every roster route |
979
+ | `description` | String, 1 to 2,000 characters | Required | `kxm role` |
980
+ | `roster[].route` | Kebab route id, 1 to 64 characters | Required per entry | Must name `.kxm/models/<route>.yaml` |
981
+ | `id` | Identifier | Optional; the filename | `kxm role` |
982
+ | `extends` | Identifier of another role in the same project | Optional | Refused when the chain cycles |
983
+ | `roster[].effort` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` | Optional | Must be inside the harness ceiling when set |
984
+ | `roster[].mode` | `headless`, `interactive`, or `either` | Optional | Recorded on the entry |
985
+ | `policy.fallback.onError` | Unique `rate_limit`, `transport`, `provider_unavailable` | Optional | Recorded on the role |
986
+ | `policy.fallback.maxSwitches` | Integer, at least 0 | Optional | Recorded on the role |
987
+ | `policy.fallback.revert` | `next_run` or `never` | Optional | Recorded on the role |
988
+ | `skills`, `tools`, `produces`, `consumes` | See `schemas/role.schema.json` | Optional | `kxm role` |
989
+ | `policy.vendorIndependenceRequired`, `policy.maxTransitions`, `policy.requiresGateVerification` | Boolean, or a positive integer for `maxTransitions` | Optional | Recorded on the role |
990
+
991
+ Roster order is preference. `kxm role list` shows the first route as the
992
+ primary. The Runtime checks membership. The model that runs still comes from
993
+ the agent file. The developer assignment runner still reads `.kxm/roster.yaml`
994
+ until P2.
1002
995
 
1003
996
  ```yaml
1004
- # .kxm/roles/writer.yaml — the filename is the role id the Runtime looks up.
1005
- schema: kxm.role.v1
997
+ schema: kxm.role.v2
1006
998
  id: writer
1007
- description: Primary implementation role.
1008
- roster: # order is priority
1009
- - model: xai/grok-4.6 # full provider/model string
999
+ purpose: writer
1000
+ permission: edit
1001
+ description: Primary implementation agent.
1002
+ roster:
1003
+ - route: grok-native
1010
1004
  effort: medium
1011
- enabled: true
1012
- - model: openrouter/qwen/qwen3-coder-plus
1005
+ - route: qwen-openrouter-pi
1013
1006
  effort: medium
1014
- enabled: true
1015
1007
  ```
1016
1008
 
1017
- Validated with `kxm init --json` (writer cross-check), `kxm role list --json`,
1018
- and the Runtime's `listRoleBindings`.
1019
-
1020
- Commands: `kxm role list|get|add|remove|modify` (`--scope global|local`);
1021
- `kxm models` (interactive) adds or removes `{model, enabled: true}` entries
1022
- while admitting a route; the Runtime reads rosters on every live attempt.
1023
-
1024
- ## `.kxm/role-hosts.yaml` (`kxm.role-hosts.v1`)
1025
-
1026
- Seat-to-host bindings for display. Read and written only by `kxm role hosts`
1027
- and `kxm role set-host`; no dispatch path reads it. Location:
1028
- `.kxm/role-hosts.yaml` (or `.yml`, or `role-hosts.json`) locally and
1029
- `~/.config/kxm/role-hosts.yaml` globally; local seats override global ones.
1009
+ `kxm role add --route <route-id>` writes the roster. Repeat the option; the first id is primary. `kxm role modify <role> --add-route <route-id>` appends a route that already
1010
+ has a model file. `--remove-route <route-id>` drops one. Validated with
1011
+ `kxm config` and `kxm role list --json`.
1030
1012
 
1031
- | Field | Type | Notes |
1032
- |---|---|---|
1033
- | `schema` | `kxm.role-hosts.v1` | A file with a different `schema` is ignored |
1034
- | `seats.<seat>.host` | String | Harness shown for the seat |
1035
- | `seats.<seat>.model` | String | Model shown for the seat |
1036
- | `seats.<seat>.effort` | `low`, `medium`, `high`, or `xhigh` | |
1037
- | `hostProviders.<host>` | String | Provider shown for a host |
1038
-
1039
- Seats without a binding fall back to built-in defaults (`planner`, `writer`,
1040
- `critic-arch`, `critic-cli`, `verifier`), then to the role roster's first entry.
1041
-
1042
- ```yaml
1043
- schema: kxm.role-hosts.v1
1044
- seats:
1045
- writer:
1046
- host: grok
1047
- model: xai/grok-4.6
1048
- effort: medium
1049
- ```
1013
+ `kxm models` does not write a v1 roster entry (a model id with an enabled flag). Its `r` and `x` keys call `setRouteState`, which appends `{route}` only when a `kxm.model.v2` file matches the inventory id. A selector with no matching model file is the `unknown route` path: the role file is left unchanged.
1050
1014
 
1051
- Written by `kxm role set-host writer grok --model xai/grok-4.6 --effort medium`.
1015
+ Commands: `kxm role list|get|add|remove|modify` (`--scope global|local`, and `add --route`, `modify --add-route`, `modify --remove-route`);
1016
+ the Runtime reads rosters on every live attempt.
1052
1017
 
1053
1018
  ## `.kxm/routes.yaml` (`kxm.routes.v2`)
1054
1019
 
@@ -1723,7 +1688,7 @@ Everything under `.kxm/` at the project root falls into one of three groups.
1723
1688
  | Path | Group | Written by |
1724
1689
  |---|---|---|
1725
1690
  | `project.yaml`, `repo/`, `project/env.yaml`, `agents/`, `models/*.yaml` (except `inventory.yaml`), `workflows/`, `gates.yaml`, `template-provenance.yaml` | Tracked configuration (the bundle) | You and `kxm init` |
1726
- | `roles/`, `routes.yaml`, `prices.yaml`, `modes.yaml`, `role-hosts.yaml`, `roster.yaml` | Tracked configuration outside the bundle | You and their commands |
1691
+ | `roles/`, `routes.yaml`, `prices.yaml`, `modes.yaml`, `roster.yaml` | Tracked configuration outside the bundle | You and their commands |
1727
1692
  | `config.yaml` | Tracked if the project wants shared preferences; otherwise ignore it | `kxm config set` |
1728
1693
  | `memory/`, `skills/`, `candidates/`, `goals/` | Tracked durable records | Their commands |
1729
1694
  | `models/inventory.yaml` | Generated; track it if you want a reviewed snapshot | `kxm models inventory-refresh` |
@@ -1775,7 +1740,7 @@ and `$XDG_STATE_HOME/kxm` (default `~/.local/state/kxm`) on Linux.
1775
1740
 
1776
1741
  The **user configuration directory** is `KXM_USER_CONFIG_DIR`, default
1777
1742
  `~/.config/kxm`. It holds `config.yaml`, global `roles/` and `workflows/`,
1778
- `role-hosts.yaml`, `session.token`, and shell completion scripts. Global
1743
+ `session.token`, and shell completion scripts. Global
1779
1744
  workflows are listed by `kxm workflow definitions` but never loaded by
1780
1745
  `kxm run`.
1781
1746
 
@@ -133,21 +133,28 @@ The harness and the selector have to agree. Pi refuses `provider: xai` and `open
133
133
 
134
134
  ### Role roster entries
135
135
 
136
- A role file lists the selectors a role may run. For example, `.kxm/roles/writer.yaml`:
136
+ A `kxm.role.v2` role file lists route ids, not selectors. Each `roster[].route` is resolved through `.kxm/models/<route-id>.yaml`, which carries `harness`, `model`, `vendor`, `status`, and `permissions`. The entry itself may set `effort` (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`) and `mode` (`headless`, `interactive`, or `either`). This checkout's writer role is `.kxm/roles/writer.yaml`:
137
137
 
138
138
  ```yaml
139
- schema: kxm.role.v1
139
+ schema: kxm.role.v2
140
140
  id: writer
141
+ purpose: writer
142
+ permission: edit
143
+ description: Primary implementation agent.
144
+ # Rotation priority = order. Effort default: medium for implementation.
141
145
  roster:
142
- - model: xai/grok-4.6
143
- enabled: true
144
- - model: openrouter/qwen/qwen3-coder-plus
145
- enabled: true
146
+ - route: grok-native
147
+ effort: medium
148
+ - route: qwen-openrouter-pi
149
+ effort: medium
150
+ - route: gemini-agy
146
151
  ```
147
152
 
148
- Every roster entry is a selector. When the engine checks the roster, it reads only `model`. The role schema accepts a `harness:` key on an entry, but dispatch never reads it: the harness still comes from the agent that runs the step. `kxm role list` prints the first entry as `(harness:xai/grok-4.6)`; there, "harness" is a placeholder label, not a harness.
153
+ `grok-native` resolves to `.kxm/models/grok-native.yaml`: harness `grok`, model `grok-4.7`, vendor `xai`, status `admitted`, permission `edit`. `qwen-openrouter-pi` is harness `pi`, model `openrouter/qwen/qwen3-coder-plus`, vendor `alibaba`. `gemini-agy` is harness `agy`, model `gemini-3.8-flash-high`, vendor `google`. `kxm role list` prints the first route id as the primary, for example `(grok-native)`.
149
154
 
150
- The consequence matters when a roster mixes routes, as this one does. If the implementer agent declares `harness: grok`, only `xai/grok-4.6` can run under it. Under that agent, a Pi selector fails closed with `grok_not_authenticated: grok harness not detected (harness_unhosted_model)`. To run a Pi selector, create a separate agent with `harness:` omitted. The engine does not walk the roster to fail over on its own.
155
+ The model that runs a step still comes from the agent file. The harness is the agent's `harness:`, or Pi when the agent omits it. A route file records which harness can host that model; it does not replace the agent. If the implementer agent declares `harness: grok`, only the Grok selector can run under it. A Pi selector under that agent fails closed with `grok_not_authenticated: grok harness not detected (harness_unhosted_model)`. To run a Pi selector, create a separate agent with `harness:` omitted. The engine does not walk the roster to fail over on its own.
156
+
157
+ Dispatch still reads `.kxm/roster.yaml` until P2. Role files are what `kxm role` and the Runtime membership check use.
151
158
 
152
159
  ### Route ids in `.kxm/routes.yaml`
153
160
 
@@ -1,6 +1,10 @@
1
- schema: kxm.model.v1
2
- provider: anthropic
1
+ schema: kxm.model.v2
2
+ harness: claude
3
3
  model: claude-fable-5-1
4
+ vendor: anthropic
5
+ status: admitted
6
+ permissions:
7
+ - read-only
4
8
  tags:
5
9
  - critic
6
10
  capabilities:
@@ -1,6 +1,10 @@
1
- schema: kxm.model.v1
2
- provider: google
1
+ schema: kxm.model.v2
2
+ harness: agy
3
3
  model: gemini-3.1-pro-preview
4
+ vendor: google
5
+ status: admitted
6
+ permissions:
7
+ - read-only
4
8
  tags:
5
9
  - critic
6
10
  capabilities:
@@ -1,6 +1,10 @@
1
- schema: kxm.model.v1
2
- provider: xai
1
+ schema: kxm.model.v2
2
+ harness: grok
3
3
  model: grok-4.6
4
+ vendor: xai
5
+ status: admitted
6
+ permissions:
7
+ - read-only
4
8
  thinking: high
5
9
  tags:
6
10
  - critic
@@ -1,6 +1,10 @@
1
- schema: kxm.model.v1
2
- provider: anthropic
1
+ schema: kxm.model.v2
2
+ harness: claude
3
3
  model: claude-sonnet-4-6
4
+ vendor: anthropic
5
+ status: admitted
6
+ permissions:
7
+ - read-only
4
8
  thinking: high
5
9
  tags:
6
10
  - implementation