@kontextmind/kxm 0.7.126 → 0.7.128

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 (56) 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/architecture/access.md +2 -4
  8. package/docs/contracts/validation.md +1 -1
  9. package/docs/contributing/harness-routing-internals.md +1 -1
  10. package/docs/contributing/learnings.md +21 -0
  11. package/docs/contributing/test-matrix.md +1 -1
  12. package/docs/glossary.md +1 -1
  13. package/docs/guides/agent-skills.md +1 -1
  14. package/docs/operations/backup-and-restore.md +2 -2
  15. package/docs/reference/cli-reference.md +31 -74
  16. package/docs/reference/config-reference.md +83 -118
  17. package/docs/reference/harness-routing.md +15 -8
  18. package/examples/project/.kxm/models/critic-claude.yaml +6 -2
  19. package/examples/project/.kxm/models/critic-gemini.yaml +6 -2
  20. package/examples/project/.kxm/models/critic-grok.yaml +6 -2
  21. package/examples/project/.kxm/models/implementation.yaml +6 -2
  22. package/examples/project/.kxm/models/primary.yaml +6 -2
  23. package/examples/project/.kxm/roles/planner.yaml +8 -0
  24. package/examples/project/.kxm/roles/reviewer-arch.yaml +8 -0
  25. package/examples/project/.kxm/roles/reviewer-cli.yaml +8 -0
  26. package/examples/project/.kxm/roles/writer.yaml +8 -0
  27. package/package.json +1 -1
  28. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  29. package/plugins/kxm/dist/cli.js +1759 -1306
  30. package/plugins/kxm/dist/mcp-server.js +1 -1
  31. package/plugins/kxm/dist/runtime-supervisor.js +994 -245
  32. package/plugins/kxm/dist/runtime.js +1018 -269
  33. package/plugins/kxm/dist/server.js +55 -1
  34. package/plugins/kxm/package.json +1 -1
  35. package/plugins/kxm/skills/kxm/SKILL.md +1 -1
  36. package/plugins/kxm/skills/kxm-definitions/SKILL.md +3 -7
  37. package/plugins/kxm/src/autocomplete.ts +1 -1
  38. package/plugins/kxm/src/cli/project.ts +7 -1
  39. package/plugins/kxm/src/cli/roles.ts +46 -150
  40. package/plugins/kxm/src/cli.ts +8 -22
  41. package/plugins/kxm/src/engine.ts +24 -1
  42. package/plugins/kxm/src/init-guide-setup.ts +38 -4
  43. package/plugins/kxm/src/mcp-server.ts +1 -1
  44. package/plugins/kxm/src/permission.ts +11 -0
  45. package/plugins/kxm/src/policy-draft.mjs +56 -16
  46. package/plugins/kxm/src/project-config.ts +121 -10
  47. package/plugins/kxm/src/repair.ts +1 -0
  48. package/plugins/kxm/src/role.ts +40 -440
  49. package/plugins/kxm/src/routes.ts +28 -5
  50. package/plugins/kxm/src/template.ts +34 -1
  51. package/schemas/README.md +2 -1
  52. package/schemas/model.schema.json +111 -13
  53. package/schemas/role.schema.json +61 -31
  54. package/schemas/policy-draft/README.md +0 -17
  55. package/schemas/policy-draft/model.v2.schema.json +0 -140
  56. package/schemas/policy-draft/role.v2.schema.json +0 -91
@@ -11,7 +11,7 @@
11
11
  "name": "kxm",
12
12
  "source": "./plugins/kxm",
13
13
  "description": "Durable workflows, peer agents, and kxm tui",
14
- "version": "0.7.126",
14
+ "version": "0.7.128",
15
15
  "category": "development",
16
16
  "tags": ["kxm", "multi-agent", "workflows", "mcp"]
17
17
  }
@@ -1,10 +1,9 @@
1
- schema: kxm.role.v1
1
+ schema: kxm.role.v2
2
2
  id: planner
3
- # fable primary (proven planning); qwen3.8-max failover (admitted, planning-unproven)
3
+ purpose: planner
4
+ permission: read-only
5
+ description: Plans the change before implementation.
6
+ # fable primary (proven planning).
4
7
  roster:
5
- - model: anthropic/fable
8
+ - route: fable-claude
6
9
  effort: medium
7
- enabled: true
8
- - model: qwen-token-plan/qwen3.8-max
9
- effort: medium
10
- enabled: true
@@ -1,10 +1,9 @@
1
- schema: kxm.role.v1
1
+ schema: kxm.role.v2
2
2
  id: reviewer-arch
3
- # fable is the required arch critic; glm-5.3 failover (admitted, review-unproven)
3
+ purpose: reviewer-arch
4
+ permission: read-only
5
+ description: Independent architecture critic.
6
+ # fable is the required arch critic.
4
7
  roster:
5
- - model: anthropic/fable
8
+ - route: fable-claude
6
9
  effort: medium
7
- enabled: true
8
- - model: zai-coding-cn/glm-5.3
9
- effort: medium
10
- enabled: true
@@ -1,10 +1,9 @@
1
- schema: kxm.role.v1
1
+ schema: kxm.role.v2
2
2
  id: reviewer-cli
3
- # sol is the required cli critic; glm-5.3-flash failover (fast tier, review-unproven)
3
+ purpose: reviewer-cli
4
+ permission: read-only
5
+ description: Independent CLI and docs critic.
6
+ # sol is the required cli critic.
4
7
  roster:
5
- - model: openai/gpt-5.6-sol
8
+ - route: sol-codex
6
9
  effort: low
7
- enabled: true
8
- - model: zai-coding-cn/glm-5.3-flash
9
- effort: low
10
- enabled: true
@@ -1,21 +1,12 @@
1
- schema: kxm.role.v1
1
+ schema: kxm.role.v2
2
2
  id: writer
3
- # Rotation priority = order. Primaries first; subscription/unmetered models
4
- # act as failover (verified edit probes 2026-09-16; see routes.yaml).
5
- # Effort default: medium for implementation (AGENTS.md). agy ids bake in the
6
- # effort tier, so those entries carry no effort field.
3
+ purpose: writer
4
+ permission: edit
5
+ description: Primary implementation agent.
6
+ # Rotation priority = order. Effort default: medium for implementation.
7
7
  roster:
8
- - model: xai/grok-4.7
8
+ - route: grok-native
9
9
  effort: medium
10
- enabled: true
11
- - model: openrouter/qwen/qwen3-coder-plus
10
+ - route: qwen-openrouter-pi
12
11
  effort: medium
13
- enabled: true
14
- - model: zai-coding-cn/glm-5.3-flash
15
- effort: medium
16
- enabled: true
17
- - model: qwen-token-plan/qwen3.8-flash
18
- effort: medium
19
- enabled: true
20
- - model: google/gemini-3.8-flash-high
21
- enabled: true
12
+ - route: gemini-agy
package/CHANGELOG.md CHANGED
@@ -138,6 +138,23 @@ All notable user-facing changes are documented here. The project follows [Semant
138
138
 
139
139
  ### Changed
140
140
 
141
+ - **Role and model files are live `kxm.role.v2` and `kxm.model.v2`.**
142
+ `schemas/role.schema.json` and `schemas/model.schema.json` are the files
143
+ `kxm config` validates. Each admitted roster route is a
144
+ `.kxm/models/<route-id>.yaml` (`harness`, `model`, `vendor`, `status`,
145
+ `permissions`, and `origin` when the route records one). Native routes may now carry an `origin` block; the validator's `origin_unexpected` refusal was removed. `kxm role modify`
146
+ takes `--add-route <route-id>` and `--remove-route <route-id>`. `kxm role add`
147
+ takes repeatable `--route <route-id>` (the first id is primary) instead of
148
+ `--harness` and `--model`. Adding a
149
+ route exits 1 when `.kxm/models/<route-id>.yaml` is missing
150
+ (`kxm: route '<route-id>' is not a file under .kxm/models/`).
151
+ The writer roster again includes `google/gemini-3.8-flash-high` as
152
+ `gemini-agy`. These selectors stayed off the v2 rosters because nothing
153
+ in the developer ceilings or the harness inventory can dispatch them:
154
+ `zai-coding-cn/glm-5.3-flash`, `qwen-token-plan/qwen3.8-flash`,
155
+ `qwen-token-plan/qwen3.8-max`, and `zai-coding-cn/glm-5.3`.
156
+ Dispatch still reads `.kxm/roster.yaml` until P2.
157
+
141
158
  - **Usage errors under `--json` print a `usage_error` envelope and exit 2.**
142
159
  A missing required option, unknown command, or other Commander usage error
143
160
  writes `kxm.cli-result.v1` to stdout with `command`, `error`, and `detail`.
@@ -5,12 +5,10 @@ Read from `.kxm/roles/writer.yaml`, `.kxm/roles/planner.yaml`,
5
5
  `plugins/kxm/src/role.ts`, `plugins/kxm/src/commands.ts`,
6
6
  `plugins/kxm/src/harness.ts`, and `plugins/kxm/src/project-config.ts`.
7
7
 
8
- The four principals in this checkout are the role files under `.kxm/roles/`.
8
+ The four principals in this checkout are the v2 role files under `.kxm/roles/`:
9
+ `writer.yaml`, `planner.yaml`, `reviewer-arch.yaml`, and `reviewer-cli.yaml`.
9
10
  None of those files sets `tools`. `listRoles` in `plugins/kxm/src/role.ts`
10
11
  sets `toolsCount` from `tools.allow.length` and does not apply the list.
11
- `DEFAULT_ROLES` in the same file carries preset names such as `author` and
12
- `read_only`. Those defaults are not the files in `.kxm/roles/`, and those
13
- preset strings are not the `BUILTIN_TOOL_PRESETS` list.
14
12
 
15
13
  `isToolAllowed` in `plugins/kxm/src/commands.ts` enforces `preset: read-only`
16
14
  by denying mutating `kxm_*` tools. `oneShotReadOnlyArgs` and
@@ -29,7 +29,7 @@ messages.
29
29
  `schemas/policy-draft` (`kxm.model.v2`, `kxm.role.v2`) and
30
30
  `validatePolicyDraft` are non-authoritative scaffolding. They are not live
31
31
  registry identities, operator settings, or admission. Live model files remain
32
- `kxm.model.v1` under `schemas`.
32
+ `kxm.model.v2` under `schemas`.
33
33
 
34
34
  ## Validation pipeline
35
35
 
@@ -10,7 +10,7 @@ This page records how the KXM repository applies [harness routing](../reference/
10
10
  `node scripts/kxm.mjs role get writer`:
11
11
 
12
12
  ```text
13
- schema: kxm.role.v1
13
+ schema: kxm.role.v2
14
14
  id: writer
15
15
  description: ""
16
16
  skills: []
@@ -44,6 +44,24 @@ entry whose fix has landed is deleted, not archived.
44
44
 
45
45
  ## Engine and runtime
46
46
 
47
+ - **A lane that changes `.kxm` schemas cannot be observed or re-driven by
48
+ the installed runtime until that change ships.** The runtime revalidates
49
+ the lane's project config on every request with the installed code, so
50
+ once the P1 writer rewrote the role files to v2, `kxm runs status`,
51
+ `cancel`, and a second `kxm lane run` all refused with
52
+ `schema_identity_mismatch`. Watch such a writer by process and tree, and
53
+ dispatch its repair through the harness runner. Evidence: omp-align-p1,
54
+ 2026-09-26. Applies to: any schema or config-identity cutover.
55
+
56
+ - **One control root per project id in the Runtime registry.** A lane
57
+ worktree carries the same `.kxm/project.yaml` id as the main checkout, so
58
+ the second root that posts a run is refused with `project_home_conflict`,
59
+ and a lane that was removed leaves a dead row behind. Until backlog S24
60
+ lands: stop the runtime, delete the dead row from `registry.db` under the
61
+ user state root, restart, redispatch. Never delete a row whose root still
62
+ exists. Evidence: omp-align-p1 dispatch, 2026-09-26. Applies to:
63
+ `kxm lane run` and `kxm run --lane`.
64
+
47
65
  - **The improvement loop is blind while writers bypass `kxm run`.** Every
48
66
  writer this week ran through the harness runner (`just impl-bg` and the
49
67
  critic recipes), which records no engine events, so `kxm improve report`
@@ -53,6 +71,9 @@ entry whose fix has landed is deleted, not archived.
53
71
  workflows land, dispatch through `kxm lane run --workflow implement-only`
54
72
  so attempts land in the run-events store; until then the sixth-tick
55
73
  improvement loop reports nothing by design.
74
+ The reports read the store of the control root they run in, so a lane's
75
+ attempts show up only when the report runs inside that lane, or once the
76
+ lane is registered under the project (backlog S24).
56
77
 
57
78
  - **A live agent step had a hard 120 second timeout with no configuration
58
79
  path.** The supervisor built the one-shot producer without a timeout.
@@ -67,7 +67,7 @@ to run one file is in [Develop KXM](development.md#run-one-file-or-one-test).
67
67
  | Explicit local YAML validates with runner schema/transitions; webhook environment sources retain JSON/secret checks | `gate-validation.test.ts` |
68
68
  | `kxm workflow add` writes a local workflow only where the loader reads it and only if the project still loads; outside a project it refuses | `role-and-workflow-manager.test.ts` ("workflow add writes only what the project loader accepts, at the project root, and loadKxmProject still loads") |
69
69
  | `kxm workflow add --pick <global-id>` copies the global definition into the project, not the scaffold, and the loader check refuses one that does not fit | `role-and-workflow-manager.test.ts` ("workflow add --pick <global-id> copies that global definition into the project, and refuses one the project loader rejects") |
70
- | `kxm role add --pick <global-id>` copies the global role into the project, not an empty role, with `--description`, `--skills` and `--model` applied over it | `role-and-workflow-manager.test.ts` ("role add --pick <global-id> copies that global role into the project, with --description, --skills and --model applied over it") |
70
+ | `kxm role add --pick <global-id>` copies the global role into the project, not an empty role, with `--description`, `--skills` and `--route` applied over it | `role-and-workflow-manager.test.ts` ("role add --pick <global-id> copies that global role into the project, with --description, --skills and --route applied over it") |
71
71
  | `kxm role add` writes a local role only at the project root and only if the project still loads, so a `writer` roster without the implementer's model is refused; outside a project it refuses | `role-and-workflow-manager.test.ts` ("role add writes a local role only at the project root, and only if the project loader accepts it") |
72
72
  | `default.yaml` and the 13-step `fix.yaml` compile deterministically; back edges need budgets | `engine-compile.test.ts` |
73
73
  | Artifact gate: non-empty regular files pass; missing, empty, non-file and escaping paths fail | `artifacts-exist.test.ts` |
package/docs/glossary.md CHANGED
@@ -419,7 +419,7 @@ A [trust diff](#trust-diff) change that widens what agents may do, such as a new
419
419
 
420
420
  ### Role
421
421
 
422
- A `kxm.role.v1` definition in `.kxm/roles/`, managed with `kxm role`, that describes a seat such as `planner`, `writer` or `verifier` and its model roster. When a role file exists, a Runtime step that uses the role may run only routes in its roster.
422
+ A `kxm.role.v2` definition in `.kxm/roles/`, managed with `kxm role`, that describes a seat such as `planner`, `writer` or `verifier` and its model roster. When a role file exists, a Runtime step that uses the role may run only routes in its roster.
423
423
 
424
424
  ### Roster
425
425
 
@@ -57,7 +57,7 @@ Every top-level `kxm` command is owned by exactly one skill. A skill can own sev
57
57
  | `kxm-session` | `session`, `dash`, `studio` | Read session and hub status and open dashboard or studio screens |
58
58
  | `kxm-peer` | `peer` | Delegate to, fan out to, await and answer other agents |
59
59
  | `kxm-workflow` | `workflow`, `gate` | Record journal entries, pass checkpoints and wait on signed callbacks |
60
- | `kxm-definitions` | `role` | Inspect or edit roles, role hosts and model rosters without granting writer admission |
60
+ | `kxm-definitions` | `role` | Inspect or edit roles and model rosters without granting writer admission |
61
61
  | `kxm-runs` | `run`, `runs`, `lane`, `assign` | Create, drive, inspect and cancel runs, manage worktree lanes, smoke-test a workflow, or call the assignment runner |
62
62
  | `kxm-context-memory` | `context`, `memory`, `explain` | Recall what the project knows, explain a context footprint, record memory candidates |
63
63
  | `kxm-skill-lifecycle` | `skills` | Turn a repeated practice into a governed skill candidate |
@@ -15,7 +15,7 @@ KXM spreads state over six roots. Some of them move with environment variables,
15
15
  ```mermaid
16
16
  flowchart TB
17
17
  subgraph R["$R checkout root: the Git checkout, never moves"]
18
- R1["Project definition: .kxm/project.yaml, config.yaml, agents/, models/, workflows/, gates.yaml, roles/, role-hosts.yaml, routes.yaml, roster.yaml, prices.yaml, repo/, project/env.yaml"]
18
+ R1["Project definition: .kxm/project.yaml, config.yaml, agents/, models/, workflows/, gates.yaml, roles/, roles, routes.yaml, roster.yaml, prices.yaml, repo/, project/env.yaml"]
19
19
  R2["Durable records: .kxm/memory/, skills/, goals/, tasks/, candidates/"]
20
20
  end
21
21
  subgraph D["$D workspace: KXM_WORKSPACE_DIR or --workspace, default $R/.kxm"]
@@ -31,7 +31,7 @@ flowchart TB
31
31
  S2["hub-env.json, hub-binding.json, update.yaml, projects/HASH/repository-bindings.json"]
32
32
  end
33
33
  subgraph C["$C user config: KXM_USER_CONFIG_DIR, default ~/.config/kxm"]
34
- C1["config.yaml, roles/, workflows/, role-hosts.yaml, session.token"]
34
+ C1["config.yaml, roles/, workflows/, roles, session.token"]
35
35
  end
36
36
  subgraph T["$T federated telemetry: XDG_CONFIG_HOME/kxm/telemetry"]
37
37
  T1["model-metrics.jsonl, not written by any command today"]
@@ -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`