@kontextmind/kxm 0.7.91 → 0.7.93

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 (92) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/workflows/default.yaml +1 -1
  3. package/CHANGELOG.md +212 -0
  4. package/README.md +3 -0
  5. package/docs/README.md +3 -0
  6. package/docs/agent-skills.md +123 -60
  7. package/docs/architecture.md +5 -2
  8. package/docs/cli-reference.md +3527 -0
  9. package/docs/config-reference.md +1943 -0
  10. package/docs/configuration.md +30 -4
  11. package/docs/continuous-improvement.md +122 -10
  12. package/docs/contracts/routing.md +95 -11
  13. package/docs/harness-routing.md +616 -0
  14. package/docs/kxm-handbook.md +106 -19
  15. package/docs/templates/README.md +1 -1
  16. package/docs/test-matrix.md +12 -6
  17. package/docs/troubleshooting.md +2 -2
  18. package/examples/project/.kxm/workflows/fix.yaml +1 -1
  19. package/examples/project/.kxm/workflows/improve.yaml +1 -1
  20. package/package.json +1 -1
  21. package/plugins/kxm/.claude-plugin/plugin.json +9 -10
  22. package/plugins/kxm/README.md +238 -56
  23. package/plugins/kxm/dist/claude-hook.js +10083 -0
  24. package/plugins/kxm/dist/cli.js +2487 -1848
  25. package/plugins/kxm/dist/client.js +64 -0
  26. package/plugins/kxm/dist/core.js +102 -9
  27. package/plugins/kxm/dist/extension.js +210 -68
  28. package/plugins/kxm/dist/mcp-server.js +217 -40
  29. package/plugins/kxm/dist/runtime-supervisor.js +1628 -157
  30. package/plugins/kxm/dist/runtime.js +1874 -298
  31. package/plugins/kxm/dist/server.js +416 -82
  32. package/plugins/kxm/package.json +1 -1
  33. package/plugins/kxm/skills/hints.json +1 -1
  34. package/plugins/kxm/skills/kxm/SKILL.md +48 -24
  35. package/plugins/kxm/skills/kxm/references/protocol.md +3 -3
  36. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +67 -21
  37. package/plugins/kxm/skills/kxm-definitions/SKILL.md +9 -0
  38. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +82 -16
  39. package/plugins/kxm/skills/kxm-harvest/SKILL.md +1 -1
  40. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +55 -27
  41. package/plugins/kxm/skills/kxm-insights/SKILL.md +1 -1
  42. package/plugins/kxm/skills/kxm-mind/SKILL.md +2 -2
  43. package/plugins/kxm/skills/{kxm-setup → kxm-mind-setup}/SKILL.md +4 -4
  44. package/plugins/kxm/skills/kxm-peer/SKILL.md +68 -93
  45. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +156 -23
  46. package/plugins/kxm/skills/kxm-projects/SKILL.md +1 -1
  47. package/plugins/kxm/skills/kxm-protocol/SKILL.md +1 -1
  48. package/plugins/kxm/skills/kxm-query/SKILL.md +1 -1
  49. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +74 -15
  50. package/plugins/kxm/skills/kxm-runs/SKILL.md +46 -17
  51. package/plugins/kxm/skills/kxm-session/SKILL.md +64 -36
  52. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +44 -15
  53. package/plugins/kxm/skills/kxm-tasks/SKILL.md +16 -4
  54. package/plugins/kxm/skills/kxm-triage/SKILL.md +1 -1
  55. package/plugins/kxm/skills/kxm-work/SKILL.md +1 -1
  56. package/plugins/kxm/skills/kxm-workflow/SKILL.md +60 -19
  57. package/plugins/kxm/src/arbiter.ts +67 -22
  58. package/plugins/kxm/src/autocomplete.ts +1 -1
  59. package/plugins/kxm/src/claude-hook.ts +192 -0
  60. package/plugins/kxm/src/cli/project.ts +11 -5
  61. package/plugins/kxm/src/cli/system.ts +85 -13
  62. package/plugins/kxm/src/cli/types.ts +4 -1
  63. package/plugins/kxm/src/cli/workflows.ts +18 -16
  64. package/plugins/kxm/src/cli.ts +23 -13
  65. package/plugins/kxm/src/client.ts +15 -4
  66. package/plugins/kxm/src/commands.ts +19 -9
  67. package/plugins/kxm/src/config.ts +42 -7
  68. package/plugins/kxm/src/context-packet.ts +14 -2
  69. package/plugins/kxm/src/context.ts +16 -5
  70. package/plugins/kxm/src/dispatch-context.ts +286 -0
  71. package/plugins/kxm/src/engine-plan.ts +40 -0
  72. package/plugins/kxm/src/engine.ts +138 -6
  73. package/plugins/kxm/src/hub-env.ts +17 -1
  74. package/plugins/kxm/src/hub.ts +92 -29
  75. package/plugins/kxm/src/improve-sources.ts +228 -0
  76. package/plugins/kxm/src/improve.ts +325 -140
  77. package/plugins/kxm/src/local-snapshot.ts +101 -42
  78. package/plugins/kxm/src/mcp-server.ts +129 -30
  79. package/plugins/kxm/src/memory.ts +43 -20
  80. package/plugins/kxm/src/project-config.ts +25 -0
  81. package/plugins/kxm/src/protocol.ts +11 -0
  82. package/plugins/kxm/src/relevance.ts +138 -0
  83. package/plugins/kxm/src/retrospective.ts +16 -10
  84. package/plugins/kxm/src/runtime-service.ts +8 -1
  85. package/plugins/kxm/src/runtime-supervisor.ts +16 -2
  86. package/plugins/kxm/src/session-token-hint.ts +17 -0
  87. package/plugins/kxm/src/suggest.ts +7 -7
  88. package/plugins/kxm/src/workflow-manager.ts +80 -78
  89. package/plugins/kxm/src/workflow.ts +202 -12
  90. package/scripts/build-runtime.mjs +7 -1
  91. package/scripts/check-generated.mjs +1 -0
  92. package/scripts/emit-codex-artifacts.mjs +1 -1
@@ -0,0 +1,1943 @@
1
+ # KXM configuration file reference
2
+
3
+ This page describes every file a KXM project or operator configures: where it
4
+ lives, which parser reads it, every field that parser accepts, the error codes
5
+ it reports, and which commands read or write it. It was written against the
6
+ parsers in `plugins/kxm/src` and `scripts/` for KXM 0.7.1, and every example on
7
+ this page was validated with the commands named next to it. Where a field is
8
+ accepted but nothing acts on it yet, the tables say so.
9
+
10
+ Related pages:
11
+
12
+ - [Configuration](configuration.md) lists the environment variables for the hub,
13
+ workers, and agents.
14
+ - [Harness routing](harness-routing.md) explains when to run a model through its
15
+ native harness and when to reach the same model through OpenRouter on Pi. This
16
+ page documents the fields; that guide covers the decision.
17
+ - [Operations](operations.md) covers backup and restore of every path below.
18
+
19
+ ## At a glance
20
+
21
+ | File | Schema id | Purpose | Who writes it | Tracked in Git? |
22
+ |---|---|---|---|---|
23
+ | `.kxm/project.yaml` | `kxm.project.v1` | Project identity, repositories, defaults, run limits | You; `kxm init` creates it | Yes |
24
+ | `.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 |
25
+ | `.kxm/project/env.yaml`, `.kxm/repo/env.yaml` | `kxm.environment.v1` | Portable, non-secret environment | You | Yes |
26
+ | `.kxm/agents/<id>.yaml` | `kxm.agent.v1` | Agent harness, model, and permission ceilings | You; `kxm init` creates two | Yes |
27
+ | `.kxm/models/<id>.yaml` | `kxm.model.v1` | Named model profiles for selectors | You | Yes |
28
+ | `.kxm/workflows/<id>.yaml` | `kxm.workflow.v1` | Ordered steps and typed transitions | You; `kxm init` creates `default` | Yes |
29
+ | `.kxm/gates.yaml` | `kxm.gate-registry.v1` | The executable gate registry | You; `kxm init` creates it | Yes |
30
+ | `.kxm/roles/<role>.yaml` | `kxm.role.v1` | Model rosters per role | You, `kxm role`, `kxm models` | Yes |
31
+ | `.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/routes.yaml` | `kxm.routes.v2` | Admitted and disabled model routes | You, `kxm routes`, `kxm models` | Yes |
33
+ | `.kxm/roster.yaml` | `kxm.developer-roster.v1` | Developer assignment roster for the KXM source repository | Maintainers | Yes, and it must be committed |
34
+ | `.kxm/prices.yaml` | `kxm.prices.v1` | Dated, hash-pinned list prices | You | Yes |
35
+ | `.kxm/models/inventory.yaml` | `kxm.model-inventory.v1` | Discovered model catalog | `kxm models inventory-refresh` only | Your choice (generated) |
36
+ | `.kxm/config.yaml`, `~/.config/kxm/config.yaml` | `kxm.config.v1` | Personalization and hub auto-start | `kxm config set` | Project file: yes, unless ignored |
37
+ | `.kxm/modes.yaml` | `kxm.modes.v1` | Modes for `kxm explain` | You | Yes |
38
+ | `.kxm/template-provenance.yaml` | `kxm.template-provenance.v1` | Hashes of the built-in template | `kxm init` only | Yes |
39
+ | `.kxm/tasks/<id>.yaml`, `.kxm/goals/<id>.yaml` | `kxm.task.v1`, `kxm.goal.v1` | Work records | `kxm task`, `kxm goal` | Your choice |
40
+ | `.kxm/memory/*.md`, `.kxm/memory/candidates/*.md` | `kxm.memory.v1` | Project memory facts | You; `kxm memory note` writes candidates | Yes |
41
+ | `.kxm/skills/`, `.kxm/candidates/` | `kxm.skill-candidate.v1`, `kxm.candidate.v1` | Governed skills and improvement candidates | `kxm skills`, `kxm improve` | Yes |
42
+ | Webhook definitions (JSON file or variable) | none (JSON array) | Signed webhook workflows for the hub | You | Yes if stored in the repository, never with secrets |
43
+ | Claude Code plugin `userConfig` | Claude plugin manifest | Hub URL, token, agent identity for Claude Code | Claude Code, per user | No |
44
+ | `<state root>/update.yaml` | `kxm.update.v1` | Updater settings | You | No (host-local) |
45
+
46
+ `kxm init` does not write a `.gitignore`. See
47
+ [Workspace layout](#workspace-layout-tracked-ignored-and-state) for the entries
48
+ to add.
49
+
50
+ ## How the files fit together
51
+
52
+ ```text
53
+ .kxm/project.yaml ── repositories[] ──> .kxm/repo/repo.yaml (+ env.yaml) in each repository
54
+ │ defaultWorkflow, defaultHarness, limits
55
+ ▼
56
+ .kxm/workflows/<id>.yaml ── coordinator ──> .kxm/agents/coordinator.yaml
57
+ │ steps[]
58
+ ├─ kind agent | moa | approval | wait ──> .kxm/agents/<id>.yaml
59
+ │ ├─ harness ──> pi | claude | codex | grok | agy | kimi | deepseek
60
+ │ ├─ model ────> {provider, model} | {profile} | {tag}
61
+ │ │ └─> .kxm/models/<id>.yaml
62
+ │ └─ tools, repositories, network: permission ceilings
63
+ └─ kind gate ──> .kxm/gates.yaml: command | artifacts-exist | reserved
64
+
65
+ Live dispatch admission (checked by the Runtime for every attempt):
66
+ agent model "provider/model" ──> .kxm/routes.yaml: admitted and not disabled
67
+ └─> .kxm/roles/<role>.yaml roster, if that file exists
68
+ (role = agent id; "writer" for agent "implementer")
69
+ developer assignments (scripts/assignment-run.mjs) ──> .kxm/roster.yaml routes + lineup
70
+
71
+ Cost accounting:
72
+ producer token usage ──> .kxm/prices.yaml (dated today, hash verified) ──> list estimate
73
+ workflow limits.maxModelCost ──> sums only attempts recorded with costBasis "metered"
74
+ ```
75
+
76
+ ### What validates what
77
+
78
+ | Check | Files it covers | Where it runs |
79
+ |---|---|---|
80
+ | Project bundle load: restricted YAML, JSON Schema, cross-file semantics | `project.yaml`, every `repo.yaml` and `env.yaml`, `agents/`, `models/`, `workflows/`, `gates.yaml`, `template-provenance.yaml`, plus the `roles/writer.yaml` cross-check | `kxm init` (validate mode), `kxm run` and `kxm run --dry-run`, `kxm trust`, every Runtime request |
81
+ | Workflow compile | `workflows/` | `kxm run` when it creates a run |
82
+ | Runtime acceptance | Workflow steps, gate definitions, agent models, `routes.yaml`, `roles/` | `kxm runs drive` and live drives, per step |
83
+ | Permission diff | The bundle only | `kxm trust diff`, `kxm trust check` |
84
+ | 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` |
85
+
86
+ Nothing validates `routes.yaml`, `roles/` (other than `writer.yaml`),
87
+ `role-hosts.yaml`, `roster.yaml`, `prices.yaml`, `inventory.yaml`,
88
+ `config.yaml`, `modes.yaml`, memory, tasks, goals, or webhook JSON during
89
+ `kxm init`. Their own readers report problems when they run. None of them are
90
+ part of `configRevision`, and `kxm trust check` does not see them: a change to
91
+ route admission or a role roster is not flagged as a permission expansion.
92
+
93
+ ### Rules shared by the project bundle
94
+
95
+ These rules apply to the bundle files listed above.
96
+
97
+ - **Restricted YAML.** UTF-8, at most 256 KiB per file, nesting depth 32,
98
+ 64 KiB per scalar, 4,096 items per collection, 16,384 nodes, and 8,192
99
+ mapping keys. Anchors (`anchor_forbidden`), aliases (`alias_forbidden`),
100
+ custom tags (`tag_forbidden`), non-string keys (`non_string_key`), and
101
+ duplicate keys (`invalid_yaml`) are rejected; YAML warnings are errors
102
+ (`yaml_warning`); the root must be a mapping (`root_not_object`). Other
103
+ files on this page use the general YAML parser, which accepts anchors.
104
+ - **Identity comes from the filename.** `.kxm/agents/reviewer.yaml` defines
105
+ agent `reviewer`. Identifiers match `^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$`, are
106
+ at most 64 characters, and cannot be a Windows device name such as `con` or
107
+ `nul` (`resource_id_invalid`). The extension must be exactly `.yaml`; a
108
+ `.yml` file is rejected (`resource_filename_invalid`). Subdirectories are
109
+ rejected (`nested_resource_directory`), names that differ only by case
110
+ collide (`resource_id_collision`), and symbolic links are rejected
111
+ (`resource_symlink`, `resource_parent_symlink`).
112
+ - **Unknown fields fail closed** with `schema_additionalProperties`.
113
+ - **Schema errors** use the code `schema_<keyword>`, for example
114
+ `schema_required`, `schema_enum`, `schema_const`, `schema_pattern`,
115
+ `schema_type`, `schema_if`, `schema_not`, and `schema_oneOf`. A mistake inside
116
+ a `oneOf` (gate definitions, transitions, model selectors) produces several
117
+ errors at once; read them together.
118
+ - **Issues** are reported as `file: code: message` with a phase of
119
+ `discovery`, `parse`, `schema`, `path`, `reference`, or `semantic`.
120
+ `kxm init --json` returns them in `issues[]`.
121
+ - **Legacy JSON is refused.** `.kxm/config/agents.json`,
122
+ `.kxm/config/gates.json`, or any `.kxm/config/workflows/*.json` makes the
123
+ project unloadable (`legacy_state_unsupported`). If the bundle fails to load
124
+ for any reason while a hub database exists at `.kxm/state/kxm.db`, `kxm init`
125
+ reports mode `legacy` and adds `legacy_state_unsupported` to the real issues.
126
+ - **Where commands look.** `kxm init`, `kxm run`, and `kxm trust` find the
127
+ project at the nearest Git root. `kxm routes`, `kxm role`, `kxm config`,
128
+ `kxm memory`, `kxm task`, `kxm goal`, and `kxm explain` use the current
129
+ directory (or `KXM_WORKDIR`). Run those from the project root; from a
130
+ subdirectory they read or create a stray `.kxm/` there.
131
+ - **Commands rewrite whole files.** `kxm config set`, `kxm routes admit`, and
132
+ the `kxm role` commands write the file back through a YAML serializer, so
133
+ comments are dropped.
134
+
135
+ ## `.kxm/project.yaml` (`kxm.project.v1`)
136
+
137
+ The authoritative project definition. Its presence at the Git root is what
138
+ makes a directory a KXM project. Parser: `loadKxmProject` in
139
+ `plugins/kxm/src/project-config.ts`; schema: `schemas/project.schema.json`.
140
+
141
+ | Field | Type and allowed values | Required, default | What reads it |
142
+ |---|---|---|---|
143
+ | `schema` | `kxm.project.v1` | Required | Loader |
144
+ | `id` | Opaque ID matching `^[a-z][a-z0-9]{1,15}_[A-Za-z0-9][A-Za-z0-9_-]{5,127}$` | Required | Loader, Runtime. `kxm init` generates `prj_` plus 32 hex characters; `--project-id` must start with `prj_`. The Runtime binds one ID to one control root per state root, so a second checkout with the same ID is refused with `project_home_conflict`. |
145
+ | `name` | String, 1 to 120 characters | Required | Display only |
146
+ | `description` | String, at most 2,000 characters | Optional | Display only |
147
+ | `defaultWorkflow` | Identifier | Optional, `default` | Loader only: the named workflow must exist (`default_workflow_unknown`). `kxm run` always takes an explicit workflow. |
148
+ | `defaultExecutor` | `local`, `ssh`, or `exe-dev` | Optional | Loader (`executor_unknown`); recorded in the run's executor-policy revision |
149
+ | `defaultHarness` | `pi`, `claude`, `codex`, `grok`, `agy`, `kimi`, or `deepseek` | Optional, `pi` | Loader (`harness_unknown`); harness for agents without `harness`; fallback harness for live dispatch |
150
+ | `repositories` | Array of 1 to 64 entries | Required | Loader, Runtime |
151
+ | `repositories[].id` | Identifier | Required | Must be unique after case folding (`repository_id_collision`) |
152
+ | `repositories[].role` | `control` or `member` | Required | Exactly one `control` (`control_repository_count`) |
153
+ | `repositories[].required` | Boolean | Optional, `true` | A missing optional member is skipped |
154
+ | `repositories[].remoteIdentity` | String, at most 2,048 characters | Optional | Not read by any code path yet; diffed by `kxm trust` |
155
+ | `repositories[].defaultBranch` | String, at most 255 characters | Optional | Not read by any code path yet; diffed by `kxm trust` |
156
+ | `repositories[].pathHint` | Portable forward-slash relative path | Optional | The control repository must use `.`; a member path must stay inside the project root |
157
+ | `workspace.dirtySnapshot.untracked` | `bounded`, `tracked-only`, or `ask` | Optional | Not read by any code path yet; diffed as snapshot policy |
158
+ | `workspace.dirtySnapshot.maxUntrackedFileBytes` | Integer, 1 to 1,073,741,824 | Required when `untracked: bounded` | Not read by any code path yet |
159
+ | `workspace.dirtySnapshot.maxUntrackedTotalBytes` | Integer, 1 to 10,737,418,240 | Required when `untracked: bounded` | Not read by any code path yet |
160
+ | `workspace.dirtySnapshot.dirtySubmodules` | `fail` | Optional | Not read by any code path yet |
161
+ | `sync.prompts`, `sync.results`, `sync.evidence`, `sync.artifacts`, `sync.fileChanges` | Exactly `title-only`, `bounded-summary`, `references`, `metadata`, `paths-only` | Optional | Not read by any code path yet; diffed as sync policy |
162
+ | `sync.rawLogs`, `sync.diffs`, `sync.environmentValues` | Exactly `false` | Optional | Not read by any code path yet |
163
+ | `limits.maxConcurrentRuns` | Integer, 1 to 128 | Optional, `1` | Runtime admission. A changed bound is refused (`scheduler_policy_conflict`) while admitted or queued runs still use the previous one. |
164
+ | `limits.maxRunDurationMs` | Integer, 0 to 31,536,000,000 | Optional | Runtime. Combined with the workflow's own value; the smaller one wins. |
165
+ | `limits.maxAgentTimeMs` | Integer, 0 to 31,536,000,000 | Optional | Runtime refuses to drive any run while it is set (`limit_unsupported`); leave it out |
166
+
167
+ Example (validated with `kxm init --json`, including a nested member checkout
168
+ at `repositories/api`):
169
+
170
+ ```yaml
171
+ # .kxm/project.yaml — one per control repository.
172
+ schema: kxm.project.v1
173
+ id: prj_01JEXAMPLE0000000000000000 # opaque, stable; never reuse across projects
174
+ name: Payments Platform
175
+ description: Control project with one optional member repository.
176
+ defaultWorkflow: review # must name a file in .kxm/workflows/
177
+ defaultExecutor: local # local | ssh | exe-dev
178
+ defaultHarness: pi # pi | claude | codex | grok | agy | kimi | deepseek
179
+ repositories:
180
+ - id: control # exactly one control repository
181
+ role: control
182
+ required: true
183
+ pathHint: . # control must use "."
184
+ - id: api
185
+ role: member
186
+ required: false # optional: a missing checkout is skipped
187
+ remoteIdentity: ssh://git.example.test/payments/api.git
188
+ defaultBranch: main
189
+ pathHint: repositories/api # portable forward-slash path under the project root
190
+ workspace:
191
+ dirtySnapshot:
192
+ untracked: bounded # bounded | tracked-only | ask
193
+ maxUntrackedFileBytes: 26214400 # required when untracked is bounded
194
+ maxUntrackedTotalBytes: 262144000
195
+ dirtySubmodules: fail # the only accepted value
196
+ sync:
197
+ prompts: title-only
198
+ results: bounded-summary
199
+ evidence: references
200
+ artifacts: metadata
201
+ fileChanges: paths-only
202
+ rawLogs: false
203
+ diffs: false
204
+ environmentValues: false
205
+ limits:
206
+ maxConcurrentRuns: 2
207
+ maxRunDurationMs: 14400000
208
+ ```
209
+
210
+ ### Repository binding rules and error codes
211
+
212
+ The loader resolves each repository to a directory: the project root for the
213
+ control repository, a host-local binding from `kxm init --repository
214
+ <id>=<absolute path>` when one exists, or `pathHint` otherwise.
215
+
216
+ | Code | Meaning |
217
+ |---|---|
218
+ | `control_repository_count` | Not exactly one repository has `role: control` |
219
+ | `control_repository_path_invalid` | The control repository's `pathHint` is not `.` |
220
+ | `control_repository_binding_invalid` | A host-local binding for the control repository points somewhere other than the project root |
221
+ | `repository_id_collision` | Two repository IDs differ only by case |
222
+ | `portable_path_invalid` | `pathHint` is absolute, uses `\`, contains `.` or `..` segments, or names a Windows device |
223
+ | `repository_binding_path_link` | `pathHint` traverses a symbolic link or junction |
224
+ | `repository_binding_outside_project` | `pathHint` resolves outside the project root |
225
+ | `repository_binding_missing` | A required member has neither `pathHint` nor a host-local binding |
226
+ | `repository_binding_unavailable` | A required or explicitly bound repository directory does not exist |
227
+ | `repository_binding_invalid` | The binding is a link or not a directory |
228
+ | `repository_git_root_invalid` | A member binding is not the root of its own Git worktree |
229
+ | `repository_binding_collision` | Two repository IDs resolve to the same directory |
230
+ | `repository_binding_id_invalid`, `repository_binding_unknown`, `repository_binding_not_absolute` | A `--repository` binding names an invalid or undeclared ID, or a relative path |
231
+ | `default_workflow_unknown`, `executor_unknown`, `harness_unknown` | A default names something that does not exist or is not registered |
232
+
233
+ `kxm trust` can only diff a member that is a Git submodule (gitlink) at
234
+ `pathHint` or a host-local binding. A nested, ignored member checkout makes
235
+ `kxm trust` fail with `trust_scope_unsupported`.
236
+
237
+ Commands: `kxm init` creates, validates, repairs, and binds; `kxm run`,
238
+ `kxm trust diff|check`, `kxm tenant status`, and every Runtime request load it.
239
+
240
+ ## `.kxm/repo/repo.yaml` (`kxm.repository.v1`)
241
+
242
+ One file per bound repository, stored in that repository: the control
243
+ repository's is `<project root>/.kxm/repo/repo.yaml`, and a member's is
244
+ `<member checkout>/.kxm/repo/repo.yaml`. Issues for it are reported under the
245
+ logical path `.kxm/repositories/<id>/repo.yaml`. It is required for every
246
+ required or explicitly bound repository (`repository_definition_missing`).
247
+
248
+ | Field | Type and allowed values | Required, default | What reads it |
249
+ |---|---|---|---|
250
+ | `schema` | `kxm.repository.v1` | Required | Loader |
251
+ | `projectId` | Opaque ID | Required | Must equal `project.yaml` `id` (`repository_project_mismatch`) |
252
+ | `repositoryId` | Identifier | Required | Must be declared in `project.yaml` (`repository_definition_unknown`) and match the binding it was found under (`repository_binding_identity_mismatch`) |
253
+ | `description` | String, at most 2,000 characters | Optional | Display only |
254
+ | `ecosystems` | Unique identifiers, at most 16 | Optional | Not read by any code path yet; diffed by `kxm trust` |
255
+ | `classification` | `public`, `internal`, or `sensitive` | Optional | Not read by any code path yet; diffed by `kxm trust` |
256
+ | `defaultAccess` | `none`, `read`, or `write` | Optional | Not read by any code path yet; diffed by `kxm trust`. Agent access ceilings come from agent files. |
257
+
258
+ ```yaml
259
+ # .kxm/repo/repo.yaml — lives in the repository it describes.
260
+ schema: kxm.repository.v1
261
+ projectId: prj_01JEXAMPLE0000000000000000 # must equal project.yaml id
262
+ repositoryId: control # must match the project.yaml repositories[].id
263
+ description: Authoritative project configuration and application code.
264
+ ecosystems:
265
+ - node
266
+ classification: internal # public | internal | sensitive
267
+ defaultAccess: write # none | read | write
268
+ ```
269
+
270
+ Commands: the same as `project.yaml`.
271
+
272
+ ## Environment files (`kxm.environment.v1`)
273
+
274
+ Portable, non-secret environment declarations. The project file is
275
+ `.kxm/project/env.yaml`. Any bound repository, the control repository
276
+ included, may also carry `.kxm/repo/env.yaml`, which requires a valid
277
+ `repo.yaml` in the same repository
278
+ (`repository_environment_without_definition`).
279
+
280
+ | Field | Type and allowed values | Required, default | Notes |
281
+ |---|---|---|---|
282
+ | `schema` | `kxm.environment.v1` | Required | |
283
+ | `values` | Map of name to string (at most 4,096 characters), number, or boolean; at most 256 entries | Optional | Names match `^[A-Z_][A-Z0-9_]*$` and may not contain `SECRET`, `TOKEN`, `PASSWORD`, `PASSWD`, `PRIVATE_KEY`, `API_KEY`, `CREDENTIAL`, `DATABASE_URL`, or `CONNECTION_STRING` (`schema_not`) |
284
+ | `secrets[].name` | Environment name | Required per entry | Unique (`secret_name_duplicate`); may not also appear in `values` (`environment_name_conflict`) |
285
+ | `secrets[].ref` | Identifier | Required per entry | A secret reference resolved outside Git |
286
+ | `secrets[].required` | Boolean | Optional, `true` | |
287
+ | `path.prepend`, `path.append` | Unique portable relative paths, at most 32 each | Optional | `portable_path_invalid` otherwise |
288
+
289
+ A value that looks like a credential (a private-key header, a `ghp_` or
290
+ `github_pat_` token, an `sk-` key, a Slack `xox` token, an AWS `AKIA` key, or a
291
+ JWT) is rejected with `probable_secret_value`.
292
+
293
+ No Runtime path applies these values to a process yet. They are validated,
294
+ pinned in `configRevision`, and diffed by `kxm trust` (values redacted).
295
+
296
+ ```yaml
297
+ # .kxm/project/env.yaml — portable, non-secret environment for the project.
298
+ schema: kxm.environment.v1
299
+ values:
300
+ CI: false
301
+ NODE_OPTIONS: --max-old-space-size=4096
302
+ secrets:
303
+ - name: TEST_DATABASE_URL # variable name the process sees
304
+ ref: test-database-url # secret reference, resolved outside Git
305
+ required: false
306
+ path:
307
+ prepend:
308
+ - tools/bin
309
+ ```
310
+
311
+ ## `.kxm/agents/<id>.yaml` (`kxm.agent.v1`)
312
+
313
+ One file per agent; the filename is the agent ID that workflow steps
314
+ reference. Schema: `schemas/agent.schema.json`; semantic checks in
315
+ `validateBundle` in `plugins/kxm/src/project-config.ts`.
316
+
317
+ | Field | Type and allowed values | Required, default | What reads it |
318
+ |---|---|---|---|
319
+ | `schema` | `kxm.agent.v1` | Required | Loader |
320
+ | `purpose` | String, 1 to 2,000 characters | Required | Display; neutral in `kxm trust` |
321
+ | `instructions` | String, at most 16,000 characters | Optional | Not read by any code path yet. Step `instructions` are what reach the prompt. |
322
+ | `harness` | `pi`, `claude`, `codex`, `grok`, `agy`, `kimi`, or `deepseek` | Optional, the project's `defaultHarness` | Loader (`harness_unknown`, harness and model pairing); live dispatch launches this harness |
323
+ | `model` | One selector: `{provider, model}`, `{profile}`, or `{tag, capabilities}` | Optional | See [Model selectors](#model-selectors) |
324
+ | `executor` | `local`, `ssh`, or `exe-dev` | Optional | Loader (`executor_unknown`); recorded in the executor-policy revision; no dispatch path selects an executor from it yet |
325
+ | `tools.preset` | `coordinator`, `read-only`, `workspace-writer`, or `tests-writer` | Optional | Loader (`tool_preset_unknown`); pinned in the tool-policy revision |
326
+ | `tools.allow`, `tools.deny` | Unique identifiers, at most 128 each | Optional | A tool in both lists is `tool_policy_contradiction`; steps may only narrow the ceiling |
327
+ | `defaultRepositoryAccess` | `none`, `read`, or `write` | Optional; the ceiling is `none` when absent | Loader: access ceiling for repositories not listed in `repositories` |
328
+ | `repositories` | Map of repository ID to `none`, `read`, or `write`; at most 64 | Optional | Loader: per-repository access ceiling; IDs must be declared (`repository_unknown`) |
329
+ | `secrets[].ref` | Identifier | Required per entry | Loader: steps may only request refs granted here |
330
+ | `secrets[].as` | Environment name, `^[A-Z_][A-Z0-9_]*$` | Optional | Not read by any code path yet |
331
+ | `secrets[].required` | Boolean | Optional, `true` | Not read by any code path yet |
332
+ | `network` | `none`, `provider-only`, `restricted`, or `host` (ranked in that order) | Optional | Not enforced yet; `kxm trust` reports a move up the ranking as an expansion |
333
+ | `resultSchema` | String, at most 512 characters | Optional | Not read by any code path yet; diffed by `kxm trust` |
334
+ | `session.reuse` | `compatible-run-scope` | Optional | Not read by any code path yet |
335
+ | `session.maxIdleMs` | Integer, 0 to 31,536,000,000 | Optional | Not read by any code path yet |
336
+
337
+ Tool presets are names checked against a registered list. The live one-shot
338
+ producer launches every harness with a fixed read-only argument set
339
+ (`READ_ONLY_ONESHOT_ARGS` in `plugins/kxm/src/harness.ts`); it does not
340
+ translate `tools` into harness flags. The Runtime refuses live steps that
341
+ request `write` access until the writer sandbox is in place, so a step that
342
+ writes a repository runs today only under the simulated producer.
343
+
344
+ ### Model selectors
345
+
346
+ A selector has exactly one of three shapes (`common.schema.json#/$defs/modelSelector`):
347
+
348
+ | Shape | Fields | Resolves to |
349
+ |---|---|---|
350
+ | Direct | `provider` (identifier), `model` (1 to 200 characters) | That provider and model. The route string is `provider/model`, for example `openrouter/qwen/qwen3-coder-plus`. |
351
+ | Profile | `profile` (identifier) | `.kxm/models/<profile>.yaml` (`model_profile_unknown` if missing) |
352
+ | Tag | `tag` (identifier), optional `capabilities` (identifiers) | Every profile carrying the tag and all listed capabilities (`model_tag_unresolved` if none) |
353
+
354
+ For live dispatch, only the direct shape works. The Runtime reads the agent's
355
+ `model.provider` and `model.model` and joins them into the route string; a
356
+ profile or tag selector validates at load time but live dispatch refuses the
357
+ step with `producer_route_unsupported: invalid model declaration`. An agent
358
+ with no model is refused too, except that an agent named `implementer`
359
+ without a model falls back to `xai/grok-4.6`.
360
+
361
+ ### Harness and model pairing
362
+
363
+ The loader checks that the agent's harness can host its model, using
364
+ `validateHarnessModelPair` in `plugins/kxm/src/harness.ts`. It applies this
365
+ check only to models reached through a profile or tag selector. A direct
366
+ `{provider, model}` selector is not checked at load time; the live producer
367
+ checks it when it probes the harness before dispatch.
368
+
369
+ | Harness | Accepts |
370
+ |---|---|
371
+ | `claude` | Provider `anthropic`; rejects model IDs starting with `gpt-`, `o1-`, `o3-`, `grok-`, `gemini-`, `kimi-`, `moonshot-`, `deepseek-`, or `qwen-` |
372
+ | `codex` | Provider `openai`; rejects `claude-`, `fable-`, `grok-`, `gemini-`, `kimi-`, `moonshot-`, `deepseek-`, and `qwen-` models |
373
+ | `grok` | Provider `xai` and `grok-` models |
374
+ | `agy` | Provider `google` and `gemini-` models |
375
+ | `kimi` | Provider `moonshot` and `kimi`, `moonshot`, or `kimi-for-coding` models |
376
+ | `deepseek` | Provider `deepseek` and `deepseek-` models |
377
+ | `pi` | Any provider except `anthropic`, `openai`, `xai`, `moonshot`, `google`, and `deepseek` (`pi_native_impersonation_blocked`); use the native harness for those |
378
+
379
+ A mismatch is reported as `harness_unhosted_model`. Which route to choose for a
380
+ model that more than one harness can reach is covered in
381
+ [Harness routing](harness-routing.md).
382
+
383
+ ### Live dispatch requirements
384
+
385
+ Before a live attempt, the Runtime (`resolveProducerRoute` in
386
+ `plugins/kxm/src/engine.ts`) requires all of the following. A failure hands the
387
+ run off with `step_unsupported` and a `producer_route_unsupported` detail.
388
+
389
+ 1. The agent declares a direct `{provider, model}` selector (see above).
390
+ 2. `provider/model` is listed in `.kxm/routes.yaml` `admitted` and not in
391
+ `disabled`.
392
+ 3. If `.kxm/roles/<role>.yaml` exists, its roster contains exactly
393
+ `provider/model`. The role is the agent ID, except that agent
394
+ `implementer` maps to role `writer`.
395
+
396
+ Example (validated with `kxm init --json`):
397
+
398
+ ```yaml
399
+ # .kxm/agents/implementer.yaml — the filename is the agent id.
400
+ schema: kxm.agent.v1
401
+ purpose: Implement the approved change within the declared repository scope.
402
+ instructions: Keep changes inside the files named by the approved plan.
403
+ harness: grok # pi | claude | codex | grok | agy | kimi | deepseek
404
+ model: # direct selector: provider + model
405
+ provider: xai
406
+ model: grok-4.6
407
+ executor: local # local | ssh | exe-dev
408
+ tools:
409
+ preset: workspace-writer # coordinator | read-only | workspace-writer | tests-writer
410
+ allow: [read, edit, write, bash]
411
+ deny: [web_fetch]
412
+ defaultRepositoryAccess: none # ceiling for repositories not listed below
413
+ repositories:
414
+ control: write
415
+ api: write
416
+ secrets:
417
+ - ref: npm-token # secret reference name
418
+ as: NPM_TOKEN # environment variable name inside the attempt
419
+ required: false
420
+ network: provider-only # none | provider-only | restricted | host
421
+ resultSchema: kxm.assignment-result.v1
422
+ session:
423
+ reuse: compatible-run-scope
424
+ maxIdleMs: 1800000
425
+ ```
426
+
427
+ A critic that uses a profile selector:
428
+
429
+ ```yaml
430
+ schema: kxm.agent.v1
431
+ purpose: Architecture critic for the approved change.
432
+ harness: claude
433
+ model:
434
+ profile: critic-claude # profile selector: .kxm/models/critic-claude.yaml
435
+ tools:
436
+ preset: read-only
437
+ defaultRepositoryAccess: read
438
+ network: provider-only
439
+ resultSchema: kxm.assignment-result.v1
440
+ ```
441
+
442
+ Error codes: `executor_unknown`, `harness_unknown`, `tool_preset_unknown`,
443
+ `tool_policy_contradiction`, `repository_unknown`, `model_profile_unknown`,
444
+ `model_tag_unresolved`, `harness_unhosted_model`,
445
+ `pi_native_impersonation_blocked`, the path codes under
446
+ [Rules shared by the project bundle](#rules-shared-by-the-project-bundle), and
447
+ `role_roster_conflicts_with_agent` (see [Roles](#kxmrolesroleyaml-kxmrolev1)).
448
+
449
+ Commands: `kxm init` creates `coordinator` and `implementer`; an interactive
450
+ `kxm init` can add workflow-guide agents for authenticated harnesses; `kxm run`
451
+ and the Runtime read them; `kxm trust` diffs them.
452
+
453
+ ## `.kxm/models/<id>.yaml` (`kxm.model.v1`)
454
+
455
+ Named model profiles that agent and step selectors can reference by `profile`
456
+ or `tag`. The filename is the profile ID; `inventory.yaml` in the same
457
+ directory is reserved for the generated inventory and is skipped by this
458
+ loader.
459
+
460
+ | Field | Type and allowed values | Required, default | What reads it |
461
+ |---|---|---|---|
462
+ | `schema` | `kxm.model.v1` | Required | Loader |
463
+ | `provider` | Identifier | Required | Loader: selector resolution, harness pairing, provider diversity |
464
+ | `model` | String, 1 to 200 characters | Required | Loader: same |
465
+ | `thinking` | String, 1 to 64 characters | Optional | Not read by any code path yet |
466
+ | `tags` | Unique identifiers, at most 32 | Optional | Loader: `tag` selectors |
467
+ | `capabilities` | Unique identifiers, at most 32 | Optional | Loader: `tag` selectors with `capabilities` |
468
+ | `priority` | Integer, -10,000 to 10,000 | Optional | Not read by any code path yet |
469
+ | `fallbacks` | Up to 8 selectors | Optional | Loader checks references (`model_profile_unknown`, `model_tag_unresolved`) and cycles (`model_fallback_cycle`); nothing fails over yet |
470
+ | `limits.contextTokens`, `limits.outputTokens` | Integer, at least 1 | Optional | Not read by any code path yet |
471
+ | `limits.timeoutMs` | Integer, 0 to 31,536,000,000 | Optional | Not read by any code path yet |
472
+
473
+ Profiles are load-time data only. The Runtime's live route resolution reads
474
+ the agent file directly and does not consult profiles.
475
+
476
+ ```yaml
477
+ # .kxm/models/critic-claude.yaml — the filename is the profile id.
478
+ schema: kxm.model.v1
479
+ provider: anthropic
480
+ model: fable
481
+ thinking: high
482
+ tags: [critic]
483
+ capabilities: [tools, structured-output]
484
+ priority: 100
485
+ fallbacks:
486
+ - profile: critic-sol
487
+ limits:
488
+ contextTokens: 200000
489
+ outputTokens: 32000
490
+ timeoutMs: 1800000
491
+ ```
492
+
493
+ Commands: the loader in `kxm init`, `kxm run`, and `kxm trust`.
494
+
495
+ ## `.kxm/workflows/<id>.yaml` (`kxm.workflow.v1`)
496
+
497
+ An ordered list of steps with typed transitions. The filename is the workflow
498
+ ID used by `kxm run <workflow>`. Three layers check it:
499
+
500
+ 1. The loader (`schemas/workflow.schema.json` plus `validateWorkflow` in
501
+ `plugins/kxm/src/project-config.ts`) on every project load.
502
+ 2. The compiler (`compileKxmWorkflow` in `plugins/kxm/src/engine-compile.ts`)
503
+ when `kxm run` creates a run. It pins the compiled plan on the run.
504
+ 3. Runtime acceptance (`plugins/kxm/src/engine.ts`) before each step executes.
505
+ A step the current Runtime cannot execute hands the run off instead of
506
+ running it.
507
+
508
+ ### Top-level fields
509
+
510
+ | Field | Type and allowed values | Required, default | Notes |
511
+ |---|---|---|---|
512
+ | `schema` | `kxm.workflow.v1` | Required | |
513
+ | `description` | String, at most 4,000 characters | Optional | Prose; neutral in `kxm trust` |
514
+ | `coordinator` | Agent ID | Optional, `coordinator` | Must exist (`coordinator_unknown`); pinned on the compiled plan. Approval and wait steps without `assignments.allowedAgents` are dispatched to the agent whose ID is literally `coordinator`, not to this field's value. |
515
+ | `limits.maxTransitions` | Integer, 1 to 1,000 | Required once any back-edge exists (`workflow_cycle_unbounded`) | Run-wide transition budget; defaults to the number of steps. Exceeding it fails the run with `budget_transitions`. |
516
+ | `limits.maxRunDurationMs` | Integer, 0 to 31,536,000,000 | Optional | The Runtime cancels the run with `budget_run_duration`; the smaller of this and the project limit applies |
517
+ | `limits.maxAgentTimeMs` | Integer, 0 to 31,536,000,000 | Optional | The Runtime refuses to drive a run that declares it (`limit_unsupported`; the CLI reports `run_handoff_required`). The built-in template sets it, so the template's `default` workflow cannot be driven as generated. |
518
+ | `limits.maxModelCost` | Number greater than 0 | Optional | Fails the run with `budget_model_cost` once attempts recorded with cost basis `metered` reach it. See [Cost basis](#cost-basis-and-staleness). |
519
+ | `limits.currency` | Three uppercase letters | Optional | Pinned on the plan; not otherwise read |
520
+ | `planHash` | `{stageId, evidenceKey}` | Optional | When `stageId` passes, the hash of that evidence is captured. The step must declare that evidence key (`oracle_evidence_unknown`, `oracle_stage_unknown`). |
521
+ | `reproOracle` | `{stageId, evidenceKey}` | Optional | Same shape as `planHash`, for an immutable reproduction |
522
+ | `requirePlanHash` | Unique step IDs, at most 128 | Optional | Requires `planHash` (`plan_hash_missing`); every step that writes a repository after the `planHash` stage must be listed (`mutation_missing_plan_hash`) |
523
+ | `steps` | 1 to 128 steps | Required | The first step is the entry point |
524
+
525
+ ### Step fields
526
+
527
+ | Field | Type and allowed values | Required, default | Notes |
528
+ |---|---|---|---|
529
+ | `id` | Identifier | Required | Unique (`step_id_duplicate`) |
530
+ | `kind` | `agent`, `moa`, `gate`, `approval`, or `wait` | Required | `workflow` is reserved and rejected |
531
+ | `description` | String, at most 2,000 characters | Optional | Prose |
532
+ | `instructions` | String, at most 16,000 characters | Optional | Prepended to the generated prompt for the step |
533
+ | `agent` | Agent ID | Required for `agent` and `moa` | `agent_unknown`. When `assignments.allowedAgents` is set, it must include this agent (`primary_agent_ineligible`). |
534
+ | `gate` | Gate ID from `.kxm/gates.yaml` | Required for `gate` | `gate_unknown`; `gate_registry_missing` when there is no `gates.yaml` |
535
+ | `expect` | `pass` or `fail` | Optional, `pass`; gate steps only | `gate_expect_invalid` on other kinds |
536
+ | `signal` | Identifier | Required for `wait` | Compiled and diffed; the Runtime does not match signals to wait steps yet |
537
+ | `model` | Model selector | Optional | Intersected with each allowed agent's own model ceiling; an empty intersection is `model_selector_incompatible`. Live route resolution ignores it. The Runtime refuses it on gate steps. |
538
+ | `maxAttempts` | Integer, 1 to 20 | Optional, `1` | Entering the step again after this many attempts fails the run (`budget_step_attempts`) |
539
+ | `timeoutMs` | Integer, 0 to 31,536,000,000 | Optional | Gate steps: refused on `artifacts-exist` gates and when shorter than the command gate's own `timeoutMs`. Other kinds: pinned but not passed to the producer yet (the live one-shot producer uses its own 120-second process timeout). `0` is refused. |
540
+ | `repositories` | Map of repository ID to `none`, `read`, or `write` | Optional | IDs must be declared (`repository_unknown`); may not exceed the agent's ceiling (`repository_scope_expansion`) |
541
+ | `tools` | `{preset, allow, deny}` | Optional | Must keep the agent's preset and denials and allow only tools the agent allows (`tool_scope_expansion`). The Runtime refuses steps that declare `tools`. |
542
+ | `secrets` | `[{ref, as, required}]` | Optional | Only refs the agent grants (`secret_scope_expansion`). The Runtime refuses steps that declare `secrets`. |
543
+ | `assignments` | See the next table | Optional | |
544
+ | `join` | See the next table | Optional, `{strategy: all}` | |
545
+ | `requiredEvidence` | Up to 64 requirements | Optional | See [Evidence](#evidence-requirements) |
546
+ | `safeSpeculation` | Boolean | Optional, `false` | Required `true` with `join.strategy: first-success`. The Runtime refuses it. |
547
+ | `on` | Map of outcome to transition, 1 to 32 entries | Required (`step_transitions_missing` when absent) | See [Transitions](#transitions-and-outcomes) |
548
+
549
+ Fields such as `role`, `area`, or `outcomes` are not part of this schema and
550
+ fail with `schema_additionalProperties`. `area` belongs to
551
+ [webhook workflow stages](#webhook-workflow-definitions); the compiled plan
552
+ derives its outcome list from the keys of `on`.
553
+
554
+ ### Assignments and join
555
+
556
+ | Field | Type and allowed values | Default | Notes |
557
+ |---|---|---|---|
558
+ | `assignments.allowedAgents` | 1 to 32 unique agent IDs | `[agent]`, or none on steps without `agent` | `assignment_agent_unknown`; every listed agent's ceilings are checked |
559
+ | `assignments.minimum` | Integer, 1 to 64 | `1` | `minimum <= target <= maximum` (`assignment_bounds_invalid`) |
560
+ | `assignments.target` | Integer, 1 to 64 | `minimum` | |
561
+ | `assignments.maximum` | Integer, 1 to 64 | `target` | For `moa`, at most the number of allowed agents (`assignment_pool_too_small`) |
562
+ | `assignments.maxParallel` | Integer, 1 to 64 | `maximum` | At most `maximum` (`assignment_parallelism_invalid`) |
563
+ | `assignments.maxAttemptsPerAssignment` | Integer, 1 to 20 | `1` | The Runtime executes at most 2 |
564
+ | `assignments.maxWriteRepositories` | Integer, 1 to 64 | none | At most the number of `write` repositories on the step (`write_repository_bound_invalid`); the Runtime executes at most 1 |
565
+ | `assignments.distinctBy` | Any of `provider`, `model`, `profile` | `[]` | The resolved candidates must offer `target` distinct values (`model_diversity_impossible`); the Runtime executes only `provider` |
566
+ | `join.strategy` | `all`, `all-settled`, `quorum`, or `first-success` | `all` | The Runtime executes `all` and `all-settled` |
567
+ | `join.minimumPassed` | Integer, 1 to 64 | none | Required for `quorum`; at most `maximum` (`join_impossible`); the Runtime accepts it only with `all-settled` |
568
+ | `join.cancelRemaining` | Boolean | none | The Runtime refuses it |
569
+
570
+ ### Evidence requirements
571
+
572
+ | Field | Type and allowed values | Default | Notes |
573
+ |---|---|---|---|
574
+ | `key` | Identifier | Required | Unique per step (`evidence_key_duplicate`) |
575
+ | `kind` | `assignment-result`, `gate`, `receipt`, `approval`, or `artifact` | Required | |
576
+ | `minimum` | Integer, 1 to 16 | `1` | |
577
+ | `reusableAcrossAttempts` | Boolean | `false` | |
578
+ | `producerPolicy.minimumProducers` | Integer, 1 to 16 | Required in a policy | At most the step's `target` and the eligible count (`producer_minimum_impossible`) |
579
+ | `producerPolicy.eligibleAgents` | 1 to 32 agent IDs | Required in a policy | Must exist (`producer_agent_unknown`) and be in `allowedAgents` (`producer_agent_ineligible`) |
580
+ | `producerPolicy.acceptedStatuses` | Exactly `[passed]` | Required in a policy | |
581
+ | `producerPolicy.degradation.minimumProducers` | Integer, 1 to 15 | Optional | Must be lower than `minimumProducers` (`producer_degradation_invalid`) |
582
+
583
+ A `producerPolicy` is allowed only on `kind: assignment-result`.
584
+
585
+ ### Transitions and outcomes
586
+
587
+ Each key of `on` is an outcome identifier. Each value is either a step ID
588
+ (shorthand) or an object:
589
+
590
+ | Field | Type and allowed values | Notes |
591
+ |---|---|---|
592
+ | `target` | Step ID or `$terminal` | `transition_target_unknown` if the step does not exist |
593
+ | `maxTransitions` | Integer, 1 to 100 | Required on a back-edge, meaning a target at or before the current step (`back_edge_unbounded`). Exceeding it fails the run with `budget_edge`. |
594
+ | `terminalStatus` | `completed`, `failed`, or `cancelled` | Required when `target` is `$terminal` and forbidden otherwise |
595
+
596
+ Graph rules checked by the loader:
597
+
598
+ - Every step must be reachable from the first step (`step_unreachable`).
599
+ - Every reachable step must have a path to a terminal transition
600
+ (`step_cannot_terminate`).
601
+ - No path that ends in `terminalStatus: completed` may skip a `gate` or
602
+ `approval` step (`required_step_bypass`).
603
+ - If steps named `verify` and `ready` both exist, `verify` must transition to
604
+ `ready` and no other step may (`verify_must_precede_ready`).
605
+ - On gate steps, `implementation_failure` and `repro_missing` are misspellings
606
+ of `implementation-failure` and `repro-missing` (`gate_outcome_renamed`).
607
+ - A gate step settles only on `passed` or `implementation-failure` when
608
+ `expect` is `pass`, and only on `passed` or `repro-missing` when `expect` is
609
+ `fail`. A gate step is refused when it declares an outcome it never
610
+ produces (such as `failed`) and also leaves an outcome it does produce
611
+ undeclared (`gate_outcome_impossible`); the message names the outcomes to
612
+ declare. An extra outcome next to every produced one is accepted, which is
613
+ why the `verify` step `kxm init` writes, with `failed` beside
614
+ `implementation-failure`, still loads.
615
+
616
+ Outcomes the Runtime produces:
617
+
618
+ | Step | Outcome |
619
+ |---|---|
620
+ | Command gate, `expect: pass` | Exit code 0 gives `passed`; any other exit gives `implementation-failure` |
621
+ | Command gate, `expect: fail` | Exit code 0 gives `repro-missing`; any other exit gives `passed` |
622
+ | `artifacts-exist` gate | All paths present gives `passed`; otherwise `implementation-failure` (`expect: fail` is refused) |
623
+ | Agent step | The producer asks the model for a JSON object whose `outcome` is one of the step's declared outcomes; anything else becomes `failed` |
624
+
625
+ Declare `passed` and `implementation-failure` on every `expect: pass` gate
626
+ step, and `passed` and `repro-missing` on every `expect: fail` gate step. A
627
+ gate step that routes a failure on `failed` instead of `implementation-failure`
628
+ is refused when the project loads (`gate_outcome_impossible`), so `kxm init`,
629
+ `kxm run`, and `kxm run --dry-run` report it before a run exists. Without that
630
+ check, a failing gate attempt could not settle: the Runtime records the
631
+ produced outcome, finds no transition for it, hands the run off with
632
+ `attempt_unsettled`, leaves it `running`, and holds later gate steps in the
633
+ same project with `gate_recovery_pending`.
634
+
635
+ The Runtime's drive-time pre-flight check is separate. It hands off
636
+ (`gate_outcome_undeclared`) an `expect: pass` gate step without `passed`, an
637
+ `expect: fail` gate step without `repro-missing`, and any gate step that
638
+ declares no failure outcome, accepting either `implementation-failure` or
639
+ `failed` as that outcome. An `expect: fail` gate step therefore also needs
640
+ `implementation-failure` or `failed` declared to be driven, although it never
641
+ produces either. Declare `failed` on every agent step, because the producer
642
+ falls back to it.
643
+
644
+ ### Steps the Runtime does not execute yet
645
+
646
+ Validation accepts more than the Runtime executes. When a drive reaches one of
647
+ these, the run is handed off (`step_unsupported`, `gate_unsupported`, or
648
+ `limit_unsupported`) instead of executing:
649
+
650
+ - `limits.maxAgentTimeMs` in the workflow or project.
651
+ - `tools`, `secrets`, or `safeSpeculation: true` on any step.
652
+ - `join.strategy` other than `all` or `all-settled`, `join.minimumPassed` with
653
+ `all`, and `join.cancelRemaining`.
654
+ - `assignments.maxAttemptsPerAssignment` above 2,
655
+ `assignments.maxWriteRepositories` above 1, and `distinctBy` other than
656
+ `provider`.
657
+ - A live (non-simulated) step with `write` access to any repository.
658
+ - Gate steps with `assignments.allowedAgents`, any assignment count or
659
+ `maxAttemptsPerAssignment` other than 1, `distinctBy`,
660
+ `maxWriteRepositories`, a join other than plain `all`, a `model`, no
661
+ `repositories`, or the role of `planHash` or `reproOracle` stage; a command
662
+ gate with no `write` repository; an `artifacts-exist` gate without `read` or
663
+ `write` access to `control` or with access to any other repository; a
664
+ `reserved` gate.
665
+
666
+ The Runtime also caps each run at 100 attempts whose cost basis is `unmetered`
667
+ or `unknown` (`budget_unmetered_attempts`).
668
+
669
+ Example (validated with `kxm init --json` and compiled with
670
+ `compileKxmWorkflow`). Besides the `implementer` and `critic-arch` agents and
671
+ the `critic-claude` profile shown above, it needs a `coordinator` agent, a
672
+ `planner` agent (harness `claude`, model `anthropic/fable`, read access), a
673
+ `critic-cli` agent (harness `codex`, model `openai/gpt-5.6-sol`, read access),
674
+ and a second profile tagged `critic`, `critic-sol` (`openai/gpt-5.6-sol`).
675
+ The two providers among the `critic` candidates satisfy
676
+ `distinctBy: [provider]` with `target: 2`.
677
+
678
+ ```yaml
679
+ # .kxm/workflows/review.yaml — the filename is the workflow id.
680
+ schema: kxm.workflow.v1
681
+ description: Plan, implement, review with two critics, verify, approve, and wait for CI.
682
+ coordinator: coordinator # agent id; defaults to "coordinator"
683
+ limits:
684
+ maxTransitions: 12 # required once any back-edge exists
685
+ maxRunDurationMs: 14400000
686
+ maxModelCost: 25
687
+ currency: USD
688
+ planHash: # capture the approved plan when "plan" passes
689
+ stageId: plan
690
+ evidenceKey: plan
691
+ requirePlanHash: # writing steps after "plan" must be listed here
692
+ - implement
693
+ - verify
694
+ steps:
695
+ - id: plan
696
+ kind: agent
697
+ agent: planner
698
+ description: Produce an implementation plan.
699
+ instructions: Write the plan as a numbered list of file-level changes.
700
+ maxAttempts: 2
701
+ timeoutMs: 1200000
702
+ repositories:
703
+ control: read
704
+ requiredEvidence:
705
+ - key: plan
706
+ kind: artifact
707
+ on:
708
+ passed: implement # shorthand transition: a step id
709
+ blocked:
710
+ target: $terminal
711
+ terminalStatus: failed
712
+
713
+ - id: implement
714
+ kind: agent
715
+ agent: implementer
716
+ maxAttempts: 3
717
+ repositories:
718
+ control: write
719
+ assignments:
720
+ allowedAgents: [implementer]
721
+ minimum: 1
722
+ target: 1
723
+ maximum: 1
724
+ maxParallel: 1
725
+ maxAttemptsPerAssignment: 2
726
+ maxWriteRepositories: 1
727
+ requiredEvidence:
728
+ - key: diff
729
+ kind: artifact
730
+ on:
731
+ passed: review
732
+ failed:
733
+ target: $terminal
734
+ terminalStatus: failed
735
+
736
+ - id: review
737
+ kind: moa # panel of agents
738
+ agent: critic-arch # primary agent; must be in allowedAgents
739
+ model:
740
+ tag: critic # intersected with each agent's own model ceiling
741
+ repositories:
742
+ control: read
743
+ assignments:
744
+ allowedAgents: [critic-arch, critic-cli]
745
+ minimum: 2
746
+ target: 2
747
+ maximum: 2
748
+ maxParallel: 2
749
+ distinctBy: [provider]
750
+ join:
751
+ strategy: all-settled
752
+ minimumPassed: 2
753
+ requiredEvidence:
754
+ - key: review
755
+ kind: assignment-result
756
+ producerPolicy:
757
+ minimumProducers: 2
758
+ eligibleAgents: [critic-arch, critic-cli]
759
+ acceptedStatuses: [passed]
760
+ degradation:
761
+ minimumProducers: 1
762
+ on:
763
+ passed: verify
764
+ changes-requested: # back-edge: needs maxTransitions
765
+ target: implement
766
+ maxTransitions: 2
767
+
768
+ - id: verify
769
+ kind: gate
770
+ gate: test # key in .kxm/gates.yaml
771
+ expect: pass # pass | fail (gate steps only)
772
+ maxAttempts: 2
773
+ repositories:
774
+ control: write
775
+ requiredEvidence:
776
+ - key: tests
777
+ kind: gate
778
+ on:
779
+ passed: approve
780
+ implementation-failure:
781
+ target: implement
782
+ maxTransitions: 2
783
+
784
+ - id: approve
785
+ kind: approval
786
+ requiredEvidence:
787
+ - key: signoff
788
+ kind: approval
789
+ on:
790
+ passed: ci
791
+ rejected:
792
+ target: $terminal
793
+ terminalStatus: cancelled
794
+
795
+ - id: ci
796
+ kind: wait
797
+ signal: ci-checks
798
+ timeoutMs: 86400000
799
+ requiredEvidence:
800
+ - key: ci
801
+ kind: receipt
802
+ on:
803
+ passed:
804
+ target: $terminal
805
+ terminalStatus: completed
806
+ failed:
807
+ target: $terminal
808
+ terminalStatus: failed
809
+ ```
810
+
811
+ This example validates, but a live drive would be handed off at `implement`
812
+ (write access), at `review` (`critic-arch` uses a profile selector), and at
813
+ `approve` and `ci` (dispatched to `coordinator`, which declares no model). Use
814
+ it as a field reference; the
815
+ [worked example](#worked-example-a-minimal-two-step-project) is one that runs.
816
+
817
+ Commands: `kxm init` validates; `kxm run <id> [prompt]` compiles and creates a
818
+ run (`--dry-run` only loads the bundle); `kxm runs drive <runId> --simulated`
819
+ drives without models; `kxm runs status|list|cancel`; `kxm trust` diffs.
820
+ `kxm workflow add <id> --template <name>` writes a built-in template
821
+ (`implement-and-verify`, `dual-critic-review`, or `spec-and-plan`), and
822
+ `kxm workflow add <id>` a one-step scaffold, under `.kxm/workflows/` (or
823
+ `~/.config/kxm/workflows/` with `--scope global`, which the loader never
824
+ reads). Both are valid `kxm.workflow.v1` definitions that use only what
825
+ `kxm init` creates: the `coordinator` and `implementer` agents, the `control`
826
+ repository, and the `test` gate. The templates route gate failures on
827
+ `implementation-failure`. Review the new file with `kxm trust check` before
828
+ committing it.
829
+
830
+ ## `.kxm/gates.yaml` (`kxm.gate-registry.v1`)
831
+
832
+ The only place a gate ID becomes executable. A workflow gate step names a key
833
+ of `gates`. Schema: `schemas/gate-registry.schema.json`; the executable check is
834
+ in `validateBundle`; execution is in `plugins/kxm/src/engine-command.ts` and
835
+ `plugins/kxm/src/engine-artifacts.ts`.
836
+
837
+ | Field | Type and allowed values | Required, default | Notes |
838
+ |---|---|---|---|
839
+ | `schema` | `kxm.gate-registry.v1` | Required | |
840
+ | `gates` | Map of gate ID (identifier) to a definition, 1 to 64 entries | Required | |
841
+ | `gates.<id>.kind` | `command`, `artifacts-exist`, or `reserved` | Required | Each kind accepts only its own fields |
842
+ | `argv` (`command`) | 1 to 64 strings, each 1 to 4,096 characters, no NUL | Required | `argv[0]` must be a bare executable name or an absolute POSIX path (`gate_executable_invalid`). No shell is involved. |
843
+ | `timeoutMs` (`command`) | Integer, 1 to 2,147,483,647 | Required | The process is stopped when it expires |
844
+ | `cwd` (`command`) | `control` | Optional | The command always runs in the control repository root |
845
+ | `paths` (`artifacts-exist`) | 1 to 64 unique portable relative paths, not `.` | Required | Relative to `<control root>/.kxm/assets`; each must be a non-empty regular file that does not escape that directory |
846
+ | (`reserved`) | no other fields | | Declared but never executed; a step that reaches it is handed off with `gate_unsupported` |
847
+
848
+ A command gate runs `argv` with the Runtime supervisor's environment. Its exit
849
+ code decides the outcome (see
850
+ [Transitions and outcomes](#transitions-and-outcomes)); the Runtime records
851
+ hashes and byte counts of stdout and stderr, not their text.
852
+
853
+ ```yaml
854
+ # .kxm/gates.yaml — the only place a gate id becomes executable.
855
+ schema: kxm.gate-registry.v1
856
+ gates:
857
+ test:
858
+ kind: command
859
+ argv: [npm, test] # argv[0]: bare executable or absolute POSIX path; no shell
860
+ timeoutMs: 1800000 # 1..2147483647
861
+ cwd: control # optional; the only accepted value
862
+ release-notes:
863
+ kind: artifacts-exist
864
+ paths: # relative to <control root>/.kxm/assets
865
+ - release/NOTES.md
866
+ scm-delivery:
867
+ kind: reserved # declared but never executed
868
+ ```
869
+
870
+ Error codes: `gate_executable_invalid`; `schema_oneOf` with the per-branch
871
+ `schema_*` errors for a malformed definition; in workflows, `gate_unknown` and
872
+ `gate_registry_missing`.
873
+
874
+ Commands: `kxm init` creates a `test` gate (`npm test`, one hour); the loader
875
+ validates it; the Runtime executes it; `kxm trust` diffs it, reporting a
876
+ `timeoutMs` change as a budget change. The gate registry is part of the tool
877
+ policy revision pinned on each run. `kxm gate artifacts-exist --path <file>` is a
878
+ separate CLI check against the workspace assets directory (`KXM_ASSETS_DIR`),
879
+ not this registry.
880
+
881
+ ## `.kxm/roles/<role>.yaml` (`kxm.role.v1`)
882
+
883
+ Role files hold model rosters. Three different readers use them, and they
884
+ read different fields:
885
+
886
+ | Reader | File | Fields it reads | Effect |
887
+ |---|---|---|---|
888
+ | Project loader (`validateBundle`) | `.kxm/roles/writer.yaml` only | `roster[].model`, `roster[].enabled` | If the agent `implementer` (or else `writer`) declares a model and the roster has at least one enabled entry, one enabled entry must equal `provider/model`, equal the bare model, or end with `/<model>`; otherwise `role_roster_conflicts_with_agent`. Parsed as restricted YAML. |
889
+ | 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. |
890
+ | `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. |
891
+
892
+ Write roster models as the full `provider/model` string. `kxm role add --model
893
+ grok-4.6` writes a bare model ID, which satisfies the loader check but not the
894
+ Runtime route check. `kxm role modify <role> --add-model grok:xai/grok-4.6`
895
+ writes the full form.
896
+
897
+ | Field | Type | Required, default | What reads it |
898
+ |---|---|---|---|
899
+ | `schema` | `kxm.role.v1` | Required by `kxm role` | `kxm role` commands |
900
+ | `id` | String | Optional, the filename | `kxm role` commands; the loader and Runtime use the filename |
901
+ | `description` | String | Optional | `kxm role` commands |
902
+ | `roster[].model` | String, `provider/model` | Required per entry | Loader (writer only), Runtime route check |
903
+ | `roster[].enabled` | Boolean | Optional, `true` | Loader writer check only |
904
+ | `roster[].harness` | String | Optional | `kxm role list` and `kxm role hosts` display |
905
+ | `roster[].provider` | String | Optional | `kxm role hosts` display |
906
+ | `roster[].effort` | `low`, `medium`, `high`, or `xhigh` | Optional | `kxm role hosts` display; not passed to any producer |
907
+ | `roster[].mode` | `headless`, `interactive`, or `either` | Optional | Not read by any code path yet |
908
+ | `skills`, `tools`, `produces`, `consumes`, `policy` | See `schemas/role.schema.json` | Optional | Stored and shown by `kxm role`; not read by any code path yet |
909
+
910
+ Roster order is priority by convention, and `kxm role list` shows the first
911
+ entry as the primary. No code path fails over along the roster yet: the
912
+ Runtime only checks membership, and the model comes from the agent file.
913
+
914
+ `schemas/role.schema.json` describes a stricter shape (required
915
+ `description`, required `harness` per entry, `model` as a selector object, no
916
+ `enabled`). No loader enforces it, and the role files the Runtime reads do not
917
+ follow it.
918
+
919
+ ```yaml
920
+ # .kxm/roles/writer.yaml — the filename is the role id the Runtime looks up.
921
+ schema: kxm.role.v1
922
+ id: writer
923
+ description: Primary implementation role.
924
+ roster: # order is priority
925
+ - model: xai/grok-4.6 # full provider/model string
926
+ effort: medium
927
+ enabled: true
928
+ - model: openrouter/qwen/qwen3-coder-plus
929
+ effort: medium
930
+ enabled: true
931
+ ```
932
+
933
+ Validated with `kxm init --json` (writer cross-check), `kxm role list --json`,
934
+ and the Runtime's `listRoleBindings`.
935
+
936
+ Commands: `kxm role list|get|add|remove|modify` (`--scope global|local`);
937
+ `kxm models` (interactive) adds or removes `{model, enabled: true}` entries
938
+ while admitting a route; the Runtime reads rosters on every live attempt.
939
+
940
+ ## `.kxm/role-hosts.yaml` (`kxm.role-hosts.v1`)
941
+
942
+ Seat-to-host bindings for display. Read and written only by `kxm role hosts`
943
+ and `kxm role set-host`; no dispatch path reads it. Location:
944
+ `.kxm/role-hosts.yaml` (or `.yml`, or `role-hosts.json`) locally and
945
+ `~/.config/kxm/role-hosts.yaml` globally; local seats override global ones.
946
+
947
+ | Field | Type | Notes |
948
+ |---|---|---|
949
+ | `schema` | `kxm.role-hosts.v1` | A file with a different `schema` is ignored |
950
+ | `seats.<seat>.host` | String | Harness shown for the seat |
951
+ | `seats.<seat>.model` | String | Model shown for the seat |
952
+ | `seats.<seat>.effort` | `low`, `medium`, `high`, or `xhigh` | |
953
+ | `hostProviders.<host>` | String | Provider shown for a host |
954
+
955
+ Seats without a binding fall back to built-in defaults (`planner`, `writer`,
956
+ `critic-arch`, `critic-cli`, `verifier`), then to the role roster's first entry.
957
+
958
+ ```yaml
959
+ schema: kxm.role-hosts.v1
960
+ seats:
961
+ writer:
962
+ host: grok
963
+ model: xai/grok-4.6
964
+ effort: medium
965
+ ```
966
+
967
+ Written by `kxm role set-host writer grok --model xai/grok-4.6 --effort medium`.
968
+
969
+ ## `.kxm/routes.yaml` (`kxm.routes.v2`)
970
+
971
+ The route admission list the Runtime checks before every live attempt.
972
+ Parser: `loadRoutePolicy` in `plugins/kxm/src/routes.ts` (general YAML parser;
973
+ unknown keys are ignored).
974
+
975
+ | Field | Type | Required, default | What reads it |
976
+ |---|---|---|---|
977
+ | `schema` | `kxm.routes.v2` | Required | Anything else fails with `invalid .kxm/routes.yaml` |
978
+ | `admitted` | Array of route strings | Required | Runtime route check; `kxm routes list`, `kxm routes count`; `kxm models` |
979
+ | `disabled` | Array of route strings | Optional, `[]` | Runtime: a disabled route is refused even if admitted |
980
+ | `roles` | Map of name to route strings | Optional, `{}` | Not read by any code path yet; shown by `kxm routes list` and preserved on rewrite. Role rosters live in `.kxm/roles/`. |
981
+ | `updatedAt` | ISO timestamp string | Optional | Rewritten by every CLI change |
982
+
983
+ A route string is exactly the agent's `model.provider`, a slash, and
984
+ `model.model`: `xai/grok-4.6`, `anthropic/fable`,
985
+ `openrouter/qwen/qwen3-coder-plus`. When the file is missing, nothing is
986
+ admitted and every live attempt is refused (`producer_route_unsupported`, or
987
+ `producer_route_not_admitted` from the live producer). A leftover
988
+ `.kxm/producers.yaml` makes every reader fail with `retired
989
+ .kxm/producers.yaml present; use .kxm/routes.yaml (kxm.routes.v2)`.
990
+
991
+ `kxm routes admit|disable --model <id>` only accepts an ID that appears in
992
+ `.kxm/models/inventory.yaml`; otherwise it exits 2 with
993
+ `model_selection_required`. Inventory IDs come in several shapes (bare
994
+ `grok-4.6` from `grok models`, `vendor/model` from OpenRouter,
995
+ `provider/model` from `pi --list-models`), so a route such as
996
+ `openrouter/qwen/qwen3-coder-plus` or `anthropic/fable` usually has to be
997
+ added by editing the file.
998
+
999
+ ```yaml
1000
+ schema: kxm.routes.v2
1001
+ updatedAt: '2026-09-23T00:00:00.000Z'
1002
+ admitted:
1003
+ - anthropic/fable
1004
+ - openai/gpt-5.6-sol
1005
+ - xai/grok-4.6
1006
+ - openrouter/qwen/qwen3-coder-plus
1007
+ disabled: []
1008
+ roles:
1009
+ implementer:
1010
+ - xai/grok-4.6
1011
+ ```
1012
+
1013
+ Validated with `kxm routes list --json` and `kxm routes count --json`.
1014
+
1015
+ Commands: `kxm routes list|count|admit|disable` (`--dry-run` supported for
1016
+ changes), `kxm models` (interactive), the Runtime, and the live producer.
1017
+ Route changes are not part of `configRevision` and `kxm trust check` does not
1018
+ report them; review them in the pull request diff.
1019
+
1020
+ ## `.kxm/roster.yaml` (`kxm.developer-roster.v1`)
1021
+
1022
+ The developer roster for `scripts/assignment-run.mjs` (the `just` assignment
1023
+ recipes; see [Assignment runner](assignment-runner.md)). It applies to the KXM
1024
+ source repository itself: the loader in `scripts/roster-policy.mjs` reads the
1025
+ copy committed at `HEAD` of the repository that contains the script, and
1026
+ refuses unless the worktree is clean, `HEAD` is an ancestor of
1027
+ `origin/main`, and the working file is byte-identical to the committed one.
1028
+ It has no dispatch authority in the project Runtime.
1029
+
1030
+ | Field | Type and allowed values | Notes |
1031
+ |---|---|---|
1032
+ | `schema` | `kxm.developer-roster.v1` | The five top-level keys are all required and no others are allowed |
1033
+ | `routes.<id>` | Route ID matching `^[a-z0-9]+(?:-[a-z0-9]+)*$` | At least one route |
1034
+ | `routes.<id>.harness` | `grok`, `agy`, `claude`, `codex`, or `pi` | Other harnesses are refused (`unsupported harness`) |
1035
+ | `routes.<id>.model` | Token without whitespace | Native harnesses: a bare model ID. Pi: `openrouter/<vendor>/<model>`, `nous-portal/<vendor>/<model>`, or `antigravity/gemini-<id>` |
1036
+ | `routes.<id>.vendor` | Token | The model vendor, not the billing provider. Aliases: `x-ai` is `xai`, `moonshotai` is `moonshot`, `google-ai` is `google`, `qwen` is `alibaba`. |
1037
+ | `routes.<id>.roles` | Unique subset of the harness's roles | grok: `writer`; agy: `writer`, `experiment`; claude: `planner`, `reviewer-arch`; codex: `reviewer-cli`; pi: all five |
1038
+ | `routes.<id>.permissions` | Unique subset of the harness's permissions | grok and agy: `edit`; claude and codex: `read-only`; pi: `read-only`, `edit` |
1039
+ | `routes.<id>.status` | `admitted` or `retired` | |
1040
+ | `lineup.<role>` | Unique route IDs | Roles: `writer`, `planner`, `reviewer-arch`, `reviewer-cli`, `experiment`. The first four are required. Every listed route must be admitted for that role. |
1041
+ | `required_critics.review-arch`, `required_critics.review-cli` | Route IDs | Exactly these two keys; each must be in the matching reviewer lineup, `read-only`, and the two vendors must differ |
1042
+ | `model_origins.<model>` | `{vendor, evidence}` | Required for every Pi route model |
1043
+ | `model_origins.<model>.evidence` | `{source, sha256}` or `{source, commit, sha256}` | `source` is a repository-relative file; its bytes at `commit` (or at `HEAD`) must hash to `sha256`, and `commit` must be in trusted history |
1044
+
1045
+ Other refusals, all prefixed `Roster policy refused:`: `native vendor cannot
1046
+ use Pi` (a Pi route to anthropic, openai, xai, moonshot, google, or deepseek),
1047
+ `native route vendor/model mismatch`, `Pi writer requires edit permission
1048
+ only`, `Pi critic/planner cannot edit`, `writer and critics must have
1049
+ independent vendors`, and `retired .kxm/roster.json present`.
1050
+
1051
+ ```yaml
1052
+ # .kxm/roster.yaml — developer roster policy for scripts/assignment-run.mjs.
1053
+ schema: kxm.developer-roster.v1
1054
+ routes:
1055
+ grok-native: # route id: lowercase words joined by "-"
1056
+ harness: grok
1057
+ model: grok-4.6 # native harnesses take a bare model id
1058
+ vendor: xai
1059
+ roles: [writer]
1060
+ permissions: [edit]
1061
+ status: admitted # admitted | retired
1062
+ qwen-openrouter-pi:
1063
+ harness: pi
1064
+ model: openrouter/qwen/qwen3-coder-plus # Pi: allowed provider prefix + vendor/model
1065
+ vendor: alibaba
1066
+ roles: [writer]
1067
+ permissions: [edit]
1068
+ status: admitted
1069
+ fable-claude:
1070
+ harness: claude
1071
+ model: fable
1072
+ vendor: anthropic
1073
+ roles: [planner, reviewer-arch]
1074
+ permissions: [read-only]
1075
+ status: admitted
1076
+ sol-codex:
1077
+ harness: codex
1078
+ model: gpt-5.6-sol
1079
+ vendor: openai
1080
+ roles: [reviewer-cli]
1081
+ permissions: [read-only]
1082
+ status: admitted
1083
+ lineup: # routes admitted for each role
1084
+ writer: [grok-native, qwen-openrouter-pi]
1085
+ planner: [fable-claude]
1086
+ reviewer-arch: [fable-claude]
1087
+ reviewer-cli: [sol-codex]
1088
+ required_critics:
1089
+ review-arch: fable-claude
1090
+ review-cli: sol-codex
1091
+ model_origins: # required for every Pi route model
1092
+ openrouter/qwen/qwen3-coder-plus:
1093
+ vendor: alibaba
1094
+ evidence:
1095
+ source: docs/workflow-guide.md
1096
+ commit: 69341ca200c31b98b5ba2371437398f0ce501089
1097
+ sha256: 358d436dbf1b6f3904252afba167e643d33d597d16148ac1286b1c21b81448a4
1098
+ ```
1099
+
1100
+ Validated with `validateRosterDocument` from `scripts/roster-policy.mjs`,
1101
+ reading evidence from this repository's history. The lineup order is not a
1102
+ selection order: the assignment manifest names the harness and model, and the
1103
+ runner checks that the pair is admitted in the lineup.
1104
+
1105
+ ## `.kxm/prices.yaml` (`kxm.prices.v1`)
1106
+
1107
+ A dated, hash-pinned snapshot of list prices in USD per million tokens.
1108
+ Parser: `parsePriceCatalog` in `plugins/kxm/src/prices.ts`; cost math in
1109
+ `plugins/kxm/src/price-calc.ts`. The loader looks for `<root>/.kxm/prices.yaml`,
1110
+ then `<root>/prices.yaml`.
1111
+
1112
+ | Field | Type and allowed values | Required, default | Notes |
1113
+ |---|---|---|---|
1114
+ | `schema` | `kxm.prices.v1` | Required | |
1115
+ | `date` | `YYYY-MM-DD`, a real calendar date | Required | Compared with today's UTC date; see below |
1116
+ | `sha256` | 64 lowercase hex characters, optionally prefixed `sha256:` | Required | Must equal the canonical digest (`price catalog hash mismatch`) |
1117
+ | `currency` | `USD` | Optional, `USD` | Any other value is refused |
1118
+ | `models` | Non-empty array | Required | |
1119
+ | `models[].id` | Non-empty string | Required | Unique (`duplicate price catalog model id`); usually `provider/model` |
1120
+ | `models[].provider` | Non-empty string | Required | A lookup that names a provider only matches rows with that provider |
1121
+ | `models[].model` | Non-empty string | Required | |
1122
+ | `models[].aliases` | Array of strings | Optional | Also matched, case-insensitively, against the requested model |
1123
+ | `models[].tiers` | Non-empty array | Required | |
1124
+ | `tiers[].upToContextTokens` | Positive integer, or `null`/absent for unbounded | Optional | Bounds must strictly increase; the unbounded tier must be last |
1125
+ | `tiers[].inputPerMillion` | Finite number, at least 0 | Required | The parser also accepts the short name `input` |
1126
+ | `tiers[].outputPerMillion` | Finite number, at least 0 | Required | Short name `output` |
1127
+ | `tiers[].cacheReadPerMillion` | Finite number, at least 0, or `null` | Optional | Short name `cacheRead` |
1128
+ | `tiers[].cacheWritePerMillion` | Finite number, at least 0, or `null` | Optional | Short name `cacheWrite` |
1129
+
1130
+ The parser ignores unknown fields; `schemas/prices.schema.json` forbids them
1131
+ but is not enforced. Use the long field names: the short names parse, but the
1132
+ hash recipe below only works on the long ones.
1133
+
1134
+ ### How `sha256` is computed
1135
+
1136
+ The digest is SHA-256, in hex, over `JSON.stringify` of this object (key order
1137
+ as shown):
1138
+
1139
+ 1. `schema`, `date`, and `currency` (`USD` when absent).
1140
+ 2. `models`, sorted by `id` (locale compare). Each model is `id`, `provider`,
1141
+ `model`, `aliases` (sorted; `[]` when absent), and `tiers`.
1142
+ 3. Each tier is `upToContextTokens`, `inputPerMillion`, `outputPerMillion`,
1143
+ `cacheReadPerMillion`, and `cacheWritePerMillion`, with `null` for any
1144
+ absent optional value.
1145
+
1146
+ Comments, YAML formatting, and the order of fields in the file do not change
1147
+ the digest. To compute it from a source checkout, run this from the repository
1148
+ root and paste the output into `sha256`:
1149
+
1150
+ ```bash
1151
+ node --disable-warning=ExperimentalWarning --experimental-strip-types --input-type=module -e '
1152
+ import { readFileSync } from "node:fs";
1153
+ import { parse } from "yaml";
1154
+ import { hashPriceCatalog } from "./plugins/kxm/src/prices.ts";
1155
+ const { sha256, ...body } = parse(readFileSync(process.argv[1], "utf8"));
1156
+ console.log(hashPriceCatalog(body));
1157
+ ' /path/to/project/.kxm/prices.yaml
1158
+ ```
1159
+
1160
+ ### Cost basis and staleness
1161
+
1162
+ - The producers only use a catalog whose `date` is today's UTC date. An older
1163
+ snapshot is treated as stale (`priceCatalogStale: true` in the attempt's
1164
+ provider metadata) and contributes nothing. A catalog that fails to parse or
1165
+ verify is marked `priceCatalogUnavailable: true`. A missing file contributes
1166
+ nothing and sets no flag.
1167
+ - A current catalog adds a list-price estimate to the attempt's metadata
1168
+ (`listCostUsd`, `listPriceRef` of the form `<date>#<id>`, `listPriceSha256`).
1169
+ It needs all four token counts, and one-shot harnesses produce one only for
1170
+ `claude` with a single unbounded tier. Without a context measurement only a
1171
+ single unbounded tier can be priced; with one, the first tier whose bound is at
1172
+ least the context size applies. Cache tokens against a `null` rate, or a
1173
+ missing tier, yield no estimate rather than zero.
1174
+ - The built-in producers always record `costBasis: unknown` (Pi, and one-shot
1175
+ harnesses on API credentials) or `unmetered` (one-shot harnesses signed in
1176
+ with a subscription, and the simulated producer), with `costUsd: null`. A list
1177
+ estimate never becomes a metered cost.
1178
+ - `limits.maxModelCost` sums only `metered` attempts, so with the built-in
1179
+ producers it does not trip. The Runtime caps `unmetered` and `unknown`
1180
+ attempts at 100 per run instead.
1181
+
1182
+ Example (validated by `parsePriceCatalog`; the rates are illustrative):
1183
+
1184
+ ```yaml
1185
+ # .kxm/prices.yaml — dated list-price snapshot, USD per million tokens.
1186
+ schema: kxm.prices.v1
1187
+ date: "2026-09-23" # UTC calendar date of the snapshot
1188
+ sha256: 628004d20c0487e0196fcdea53235d085282929260ba479258a8a6999a4dff6e
1189
+ currency: USD # only USD is accepted
1190
+ models:
1191
+ - id: anthropic/fable # unique; matched against the requested model
1192
+ provider: anthropic
1193
+ model: fable
1194
+ aliases:
1195
+ - claude-fable-5
1196
+ tiers:
1197
+ - inputPerMillion: 10 # a single unbounded tier (no upToContextTokens)
1198
+ outputPerMillion: 50
1199
+ cacheReadPerMillion: 0.25
1200
+ cacheWritePerMillion: 12.5
1201
+ - id: example/tiered-model
1202
+ provider: example
1203
+ model: tiered-model
1204
+ tiers:
1205
+ - upToContextTokens: 200000 # applies while context <= 200000 tokens
1206
+ inputPerMillion: 1.25
1207
+ outputPerMillion: 10
1208
+ cacheReadPerMillion: 0.125
1209
+ cacheWritePerMillion: null # null = no published rate
1210
+ - inputPerMillion: 2.5 # unbounded tier must be last
1211
+ outputPerMillion: 15
1212
+ cacheReadPerMillion: 0.25
1213
+ ```
1214
+
1215
+ Commands: the Pi and one-shot producers read it on every attempt;
1216
+ `kxm explain` reports `catalogStatus` (`verified`, `stale`, `corrupt`, or
1217
+ `missing`); `kxm routing report --list-prices [--prices <file>]` reads it
1218
+ (default `<workspace>/prices.yaml`, normally `.kxm/prices.yaml`) and ignores a
1219
+ bad file.
1220
+
1221
+ ## `.kxm/models/inventory.yaml` (`kxm.model-inventory.v1`)
1222
+
1223
+ A generated catalog of models the machine can see, with public list prices.
1224
+ Never hand-edit it: `kxm models inventory-refresh` (alias `refresh`) rewrites
1225
+ the whole file. No parser validates it; `kxm routes admit|disable` and the
1226
+ interactive `kxm models` screen read only `models[].id`.
1227
+
1228
+ The refresh runs `pi --list-models`, `grok models`, and `agy models`, and
1229
+ fetches `https://openrouter.ai/api/v1/models` (override with
1230
+ `KXM_OPENROUTER_MODELS_URL`, authenticated with `OPENROUTER_API_KEY` when set)
1231
+ and `https://inference-api.nousresearch.com/v1/models` (override with
1232
+ `KXM_NOUS_MODELS_URL`, `NOUS_API_KEY`). It makes network requests, writes the
1233
+ file even when a source fails, and exits 1 if any source failed.
1234
+ `--dry-run` writes nothing.
1235
+
1236
+ | Field | Contents |
1237
+ |---|---|
1238
+ | `schema` | `kxm.model-inventory.v1` |
1239
+ | `fetchedAt` | ISO timestamp of the refresh |
1240
+ | `currency` | `USD` |
1241
+ | `sources.<name>` | `url` (a command line or URL), `ok`, and `error` on failure, for `pi`, `grok`, `agy`, `openrouter`, and `nous` |
1242
+ | `models[].id` | Model ID as the source reported it, sorted |
1243
+ | `models[].name`, `models[].contextLength` | From the first source that reported them |
1244
+ | `models[].capabilities.thinking` | `supported` (true or null), optional `levels` and `default`, and `source` |
1245
+ | `models[].capabilities.speed` | `fast` (true or null), optional `tiers`, and `source` |
1246
+ | `models[].sources` | Which sources listed the model |
1247
+ | `models[].standard` | OpenRouter per-million rates: `inputPerMillion`, `outputPerMillion`, `cacheReadPerMillion`, `cacheWritePerMillion` |
1248
+ | `models[].discount` | Nous per-million rates, same fields |
1249
+
1250
+ The inventory is discovery data, not admission. Admitting a route is a
1251
+ separate, reviewed change to `.kxm/routes.yaml`.
1252
+
1253
+ ## Personalization settings (`kxm.config.v1`)
1254
+
1255
+ Personal and workflow preferences, merged in three layers: built-in defaults
1256
+ in `plugins/kxm/src/config.ts`, then the user file
1257
+ `$KXM_USER_CONFIG_DIR/config.yaml` (default `~/.config/kxm/config.yaml`), then
1258
+ the project file `.kxm/config.yaml` in the current directory. Later layers win
1259
+ key by key; arrays are replaced, not merged. The files need no `schema` key.
1260
+ Nothing validates keys or values except `hub.autoStart` and the `improvement.*`
1261
+ keys, which fall back to their defaults field by field (see the table), but a file
1262
+ that is not valid YAML makes every `kxm config` command fail. `kxm improve` loads
1263
+ the project file from its project root (the current directory's Git root when it
1264
+ holds `.kxm/project.yaml`) rather than from the current directory.
1265
+
1266
+ Two groups of keys change behavior today: `hub.autoStart`, and the `improvement.*`
1267
+ keys that shape the report `kxm improve` prints. The **Read by** column lists every
1268
+ reader found in `plugins/kxm/src`, `scripts/`, and `packages/`; the `kxm config`
1269
+ commands themselves are not counted.
1270
+
1271
+ | Key | Type and allowed values | Default | Read by |
1272
+ |---|---|---|---|
1273
+ | `hub.autoStart` | `background` or `off`; any other value falls back to `background` | `background` | The Pi extension on load (`plugins/kxm/src/extension.ts`, `hub-autostart.ts`) |
1274
+ | `user.name`, `user.email` | String | none | Not read by any code path yet |
1275
+ | `user.preferredHarness`, `user.preferredModel` | String | none | Not read by any code path yet |
1276
+ | `user.preferredCritics` | Array of strings | `[reviewer-arch, reviewer-cli]` | Not read by any code path yet |
1277
+ | `user.theme` | `dark`, `light`, or `minimal` | `dark` | Not read by any code path yet |
1278
+ | `user.tokenBudget` | Number | `16000` | Not read by any code path yet (`kxm context get --budget` takes its own value) |
1279
+ | `defaults.project`, `defaults.model` | String | none | Not read by any code path yet |
1280
+ | `defaults.workflow` | String | `software-engineering/feature-implementation` | Not read by any code path yet |
1281
+ | `defaults.harness` | String | `pi` | Not read by any code path yet; the harness default that takes effect is `defaultHarness` in `.kxm/project.yaml` |
1282
+ | `dash.defaultScreen` | `agents`, `tasks`, `workflows`, `plans`, `inbox`, `procs`, or `spend` | `agents` | Not read by any code path yet |
1283
+ | `dash.refreshIntervalMs` | Number | `1000` | Not read by any code path yet |
1284
+ | `dash.autoOpen` | Boolean | `false` | Not read by any code path yet |
1285
+ | `sync.defaultTracker` | `github`, `jira`, or `none` | `none` | Not read by any code path yet |
1286
+ | `sync.github.owner`, `.repo`, `.syncLabels`, `.autoComment` | String or boolean | none | Not read by any code path yet |
1287
+ | `sync.jira.host`, `.projectKey`, `.issueType`, `.autoTransition` | String or boolean | none | Not read by any code path yet |
1288
+ | `improvement.promotionPolicy` | `manual_pr`, `critic_quorum`, or `auto_threshold`; any other value falls back to `manual_pr` | `manual_pr` | `kxm improve` (`cmdImprove` in `plugins/kxm/src/cli/system.ts`, then `evaluatePromotionPolicy` in `improve.ts`): selects the review-readiness rule reported per candidate. No value authorizes or activates anything |
1289
+ | `improvement.telemetryHalfLifeDays` | Number greater than 0 and at most 3650; otherwise `14` | `14` | `kxm improve`: the half-life of each record's weight in `weightedRecurrence`, which orders report rows and never decides candidacy |
1290
+ | `improvement.autoThreshold.minRuns`, `.minPassRate`, `.minCostSavings` | `minRuns` an integer from 1 to 1,000,000, `minPassRate` from 0 to 1, `minCostSavings` at least 0; otherwise the default | `10`, `0.95`, `0.5` | `kxm improve`, only under `auto_threshold`: distinct runs, accepted share, and mean recorded cost per attempt a candidate needs to report ready for review. A group with no recorded cost is never ready |
1291
+ | `routing.shadowExecution.enabled` | Boolean | `false` | Not read by any code path yet |
1292
+ | `routing.shadowExecution.sampleRate` | Number | `0.05` | Not read by any code path yet |
1293
+ | `routing.shadowExecution.candidateModels` | Array of strings | `[]` | Not read by any code path yet |
1294
+ | `routing.circuitBreaker.mode` | `soft_demotion` or `quarantine` | `soft_demotion` | Not read by any code path yet (`evaluateCircuitBreaker` in `routing.ts` has no production caller) |
1295
+ | `routing.circuitBreaker.failureThreshold`, `.windowSeconds`, `.cooldownSeconds`, `.penaltyMultiplier` | Number | `3`, `3600`, `1800`, `5` | Not read by any code path yet |
1296
+ | `telemetry.federated` | Boolean | `true` | Not read by any code path yet (`exportFederatedTelemetry` has no production caller) |
1297
+ | `telemetry.anonymize` | Boolean | `true` | Not read by any code path yet |
1298
+ | `telemetry.userTelemetryDir` | Path | none | Not read by any code path yet |
1299
+
1300
+ ```yaml
1301
+ # .kxm/config.yaml (project scope) or ~/.config/kxm/config.yaml (user scope).
1302
+ # Only hub.autoStart and improvement.* change behavior today.
1303
+ hub:
1304
+ autoStart: background # background | off
1305
+ improvement:
1306
+ promotionPolicy: manual_pr # manual_pr | critic_quorum | auto_threshold (readiness only)
1307
+ telemetryHalfLifeDays: 14
1308
+ user:
1309
+ preferredHarness: grok
1310
+ theme: dark # dark | light | minimal
1311
+ defaults:
1312
+ workflow: review
1313
+ harness: pi
1314
+ routing:
1315
+ circuitBreaker:
1316
+ mode: soft_demotion # soft_demotion | quarantine
1317
+ failureThreshold: 3
1318
+ telemetry:
1319
+ federated: true
1320
+ anonymize: true
1321
+ ```
1322
+
1323
+ Validated with `kxm config list --json` and `kxm config get`.
1324
+
1325
+ Commands:
1326
+
1327
+ - `kxm config get <key>` prints one merged value. Top-level sections other
1328
+ than the ones in the table are dropped, so `kxm config get no.such.key`
1329
+ prints `(undefined)` even after `kxm config set` wrote it.
1330
+ - `kxm config set <key> <value> [--scope user|project]` writes one file
1331
+ (project by default). The value is parsed as JSON when it can be (`true`,
1332
+ `5`, `["a"]`), otherwise stored as a string. Keys are not validated.
1333
+ - `kxm config list` prints `user`, `defaults`, `dash`, `sync`, and
1334
+ `loadedFrom`; `--json` prints every section. An empty `loadedFrom` means no
1335
+ file was found.
1336
+
1337
+ There is no `kxm config unset`. Delete the key from the file to fall back to
1338
+ the next layer; setting `null` stores `null`.
1339
+
1340
+ ## `.kxm/modes.yaml` (`kxm.modes.v1`)
1341
+
1342
+ Modes for `kxm explain`, which estimates the prompt footprint and token cost of
1343
+ a mode before you run it. Parsed with the general YAML parser; a file that fails
1344
+ to parse, lacks `schema: kxm.modes.v1`, or lacks `majorModes` is silently
1345
+ replaced by the built-in modes (`coder`, `planner`, `auditor`, `browser` and
1346
+ domains `git`, `k8s`, `database`, `browser`). Your modes and domains are merged
1347
+ over the built-in ones by name.
1348
+
1349
+ | Field | Type | Notes |
1350
+ |---|---|---|
1351
+ | `schema` | `kxm.modes.v1` | Required |
1352
+ | `majorModes.<name>.baseTools` | Array of strings | Required per mode |
1353
+ | `majorModes.<name>.description` | String | |
1354
+ | `majorModes.<name>.contextFiles` | Array of repository-relative paths | Counted into the footprint |
1355
+ | `majorModes.<name>.thinkingLevel` | `low`, `medium`, `high`, or `xhigh` | |
1356
+ | `majorModes.<name>.model` | String | Default model for the estimate; `kxm explain --model` overrides it |
1357
+ | `domains.<name>.tools` | Array of strings | Required per domain |
1358
+ | `domains.<name>.description`, `.contextFiles`, `.promptSnippet` | String, array, string | |
1359
+
1360
+ ```yaml
1361
+ schema: kxm.modes.v1
1362
+ majorModes:
1363
+ reviewer:
1364
+ description: Read-only review of a diff
1365
+ baseTools: [read, grep]
1366
+ contextFiles: [AGENTS.md]
1367
+ thinkingLevel: high
1368
+ model: claude/fable
1369
+ domains:
1370
+ payments:
1371
+ description: Payments domain rules
1372
+ tools: [sqlite_query]
1373
+ contextFiles: [docs/payments.md]
1374
+ promptSnippet: Amounts are integer cents; never use floats.
1375
+ ```
1376
+
1377
+ Validated with `kxm explain --mode reviewer --domains payments,git --json`.
1378
+
1379
+ ## `.kxm/template-provenance.yaml` (`kxm.template-provenance.v1`)
1380
+
1381
+ Written only by `kxm init`. It records exact-byte hashes and authority hashes
1382
+ of the files the built-in template created, so later `kxm init` runs can apply
1383
+ conflict-free template updates. Never edit it. Deleting it keeps the project
1384
+ valid but makes template repair planning-only.
1385
+
1386
+ | Field | Type | Notes |
1387
+ |---|---|---|
1388
+ | `schema` | `kxm.template-provenance.v1` | |
1389
+ | `templateId` | `builtin-minimal` | |
1390
+ | `templateRevision` | `sha256:<hex>` | Hash of the canonical `files` list |
1391
+ | `inputs.projectId` | Opaque ID | Must equal `project.yaml` `id` (`template_provenance_project_mismatch`) |
1392
+ | `inputs.projectName` | String, 1 to 120 characters, one line | The name used when the template was rendered |
1393
+ | `files[].path` | `.kxm/...` portable path | Strict code-unit order (`template_provenance_order_invalid`), unique after case folding (`template_provenance_path_collision`), never the provenance file itself (`template_provenance_path_invalid`) |
1394
+ | `files[].sha256` | `sha256:<hex>` | Exact bytes as written |
1395
+ | `files[].authoritySha256` | `sha256:<hex>` | Canonical JSON with prose fields removed: project `name`, repository `description`, agent `purpose`, workflow `description` |
1396
+ | `files[].bytes` | Integer, 1 to 262,144 | Total at most 8 MiB (`template_provenance_bounds_exceeded`) |
1397
+
1398
+ The whole file must equal what one of the built-in template variants (`v1`,
1399
+ `v2`, `v3-policy`, `v4-registry`) would produce for its `inputs`; otherwise
1400
+ `template_provenance_revision_invalid`. It must be a regular file under
1401
+ regular directories (`resource_not_file`, `resource_parent_symlink`).
1402
+
1403
+ Commands: `kxm init` creates it last in a create transaction and reads it in
1404
+ validate and repair modes. The transaction directory `.kxm-init-transaction/`
1405
+ at the Git root holds interrupted create or repair state; do not commit it.
1406
+
1407
+ ## Tasks and goals (`kxm.task.v1`, `kxm.goal.v1`)
1408
+
1409
+ Work records under `.kxm/tasks/<id>.yaml` and `.kxm/goals/<id>.yaml`, parsed
1410
+ with the general YAML parser by `plugins/kxm/src/task-manager.ts`. A file that
1411
+ does not parse or lacks the right `schema` is skipped silently. The commands
1412
+ create them; editing by hand is supported.
1413
+
1414
+ | Task field | Type and allowed values | Notes |
1415
+ |---|---|---|
1416
+ | `schema` | `kxm.task.v1` | |
1417
+ | `id` | `task_` plus 12 hex characters | Also the filename |
1418
+ | `goalId` | Goal ID | Optional; not checked |
1419
+ | `title`, `objective` | String | `kxm task run` passes `objective` as the run prompt |
1420
+ | `acceptanceCriteria` | `[{id, description, required}]` | No command writes it; edit by hand |
1421
+ | `status` | `todo`, `in_progress`, `blocked`, `in_review`, or `done` | |
1422
+ | `assignedWorkflow` | Workflow ID | Not checked at creation; `kxm task run` uses it, or `default` |
1423
+ | `workflowRunId` | Run ID | Optional |
1424
+ | `trackerSync` | `{tracker, issueKey, syncStatus, lastSyncedAt}` | `tracker` is `github`, `jira`, `gitlab`, or `none`; `syncStatus` is `synced`, `pending`, or `failed` |
1425
+ | `createdAt`, `updatedAt` | ISO timestamps | |
1426
+
1427
+ | Goal field | Type and allowed values | Notes |
1428
+ |---|---|---|
1429
+ | `schema` | `kxm.goal.v1` | |
1430
+ | `id` | `goal_` plus 12 hex characters | Also the filename |
1431
+ | `title` | String | |
1432
+ | `area` | String | Default `software-engineering` |
1433
+ | `status` | `active`, `achieved`, or `abandoned` | |
1434
+ | `successMetrics` | Array of strings | |
1435
+ | `targetDate` | String | Optional |
1436
+ | `createdAt`, `updatedAt` | ISO timestamps | |
1437
+
1438
+ ```yaml
1439
+ schema: kxm.task.v1
1440
+ id: task_5644e7e51277
1441
+ title: Add refund endpoint
1442
+ objective: Expose POST /refunds with idempotency keys
1443
+ acceptanceCriteria: []
1444
+ status: todo
1445
+ assignedWorkflow: review
1446
+ trackerSync:
1447
+ tracker: github
1448
+ issueKey: "42"
1449
+ syncStatus: pending
1450
+ createdAt: 2026-09-23T14:02:04.574Z
1451
+ updatedAt: 2026-09-23T14:02:04.574Z
1452
+ ```
1453
+
1454
+ Written by `kxm task create "Add refund endpoint" --objective "…" --workflow
1455
+ review --tracker github --issue 42`.
1456
+
1457
+ Commands: `kxm goal create|list`; `kxm task create|list|get|run|sync`.
1458
+ `kxm task run` calls `kxm run <assignedWorkflow or default> <objective>` and
1459
+ sets `status: in_progress` on success. `kxm task sync` marks the record
1460
+ `synced` locally; it makes no tracker API call.
1461
+
1462
+ ## Memory records (`kxm.memory.v1`)
1463
+
1464
+ Harness-agnostic project memory as Markdown files with YAML front matter.
1465
+ Parser: `parseMemoryRecord` in `plugins/kxm/src/memory.ts`.
1466
+
1467
+ - `.kxm/memory/*.md` (top level only) are authored facts. Only
1468
+ `lifecycle: active` facts appear in `kxm memory brief` and in the projection
1469
+ that `kxm memory sync` writes into `AGENTS.md`, `CLAUDE.md`, and `GEMINI.md`.
1470
+ - `.kxm/memory/candidates/*.md` are candidates written by `kxm memory note`.
1471
+ Promote one by moving it into `.kxm/memory/` in a reviewed change.
1472
+ - Every file under `.kxm/memory/` except `candidates/` is hashed into the
1473
+ memory revision pinned on each run.
1474
+
1475
+ | Field | Type and allowed values | Required |
1476
+ |---|---|---|
1477
+ | `schema` | `kxm.memory.v1` | Yes |
1478
+ | `id` | Identifier starting with a letter, with single `-` or `_` between letter and digit runs; case-insensitive | Yes |
1479
+ | `scope` | `agent`, `project`, `run`, or `operator` | Yes |
1480
+ | `kind` | Non-empty string, for example `decision`, `architecture`, `convention`, `policy`, `learning` | Yes |
1481
+ | `summary` | Non-empty string; secrets are redacted | Yes |
1482
+ | `provenance.sourceType` | Non-empty string | Yes |
1483
+ | `provenance.sourceRef`, `provenance.runId`, `provenance.timestamp` | String | No |
1484
+ | `authority` | `instruction`, `evidence`, or `promoted` | Yes |
1485
+ | `confidence` | `verified`, `probable`, or `uncertain` | Yes |
1486
+ | `lifecycle` | `active`, `deprecated`, or `superseded` | Yes |
1487
+ | `evidenceRefs` | Array (values are converted to strings) | No |
1488
+
1489
+ Front matter may not contain control-plane fields: `permissions`, `tools`,
1490
+ `allow`, `deny`, `grants`, `approval`, `policy`, `scopes`, `credentials`,
1491
+ `secrets`, `token`, `apiKey`, or `password`. The text after the front matter is
1492
+ the body. One malformed authored file makes `kxm memory brief` and
1493
+ `kxm memory sync` fail. The Claude Code plugin's session-start hook still
1494
+ runs; it leaves out the memory brief and keeps the status sections.
1495
+
1496
+ ```markdown
1497
+ ---
1498
+ schema: kxm.memory.v1
1499
+ id: gate-before-review
1500
+ scope: project # agent | project | run | operator
1501
+ kind: convention
1502
+ summary: Run the test gate before asking critics to review a diff.
1503
+ provenance:
1504
+ sourceType: review
1505
+ sourceRef: docs/workflow-guide.md
1506
+ timestamp: 2026-09-20T12:00:00Z
1507
+ authority: promoted # instruction | evidence | promoted
1508
+ confidence: verified # verified | probable | uncertain
1509
+ lifecycle: active # active | deprecated | superseded
1510
+ evidenceRefs:
1511
+ - run_01JEXAMPLE000000
1512
+ ---
1513
+
1514
+ Critics reviewed several diffs that later failed `npm test`. Gating first saves a review round.
1515
+ ```
1516
+
1517
+ Validated with `kxm memory brief --json`.
1518
+
1519
+ ## Skills and improvement candidates
1520
+
1521
+ Both directories are written by commands, not configured by hand.
1522
+
1523
+ - `.kxm/skills/` holds the governed skill lifecycle:
1524
+ `candidates/<id>/`, `promoted/<id>/`, `quarantined/<id>/`, and
1525
+ `rejected/<id>/`, each with `SKILL.md` and `metadata.json`
1526
+ (`kxm.skill-candidate.v1`), plus `history/<id>.jsonl`. Written by
1527
+ `kxm skills create|evaluate|promote|reject`; `kxm skills verify` detects
1528
+ out-of-band edits to promoted skills. Promoted skills are hashed into the
1529
+ memory revision pinned on each run. See [Skills](skills.md).
1530
+ - `.kxm/candidates/` holds improvement candidates (`<id>.json`,
1531
+ `kxm.candidate.v1`, with a proposed diff file) written by `kxm improve`
1532
+ (`--out-dir` relocates them; `--dry-run` writes none). The report itself goes to
1533
+ `<workspace>/assets/improvements/`. A candidate is a proposal: its diff has
1534
+ placeholder hunks, nothing applies it, and its promotion readiness never
1535
+ authorizes. See [Continuous improvement](continuous-improvement.md#coded-repeats-kxm-improve).
1536
+
1537
+ ## Webhook workflow definitions
1538
+
1539
+ The hub's signed-webhook workflows are a JSON array, not YAML and not a
1540
+ `kxm.workflow.v1` file. Supply it through exactly one of
1541
+ `KXM_WEBHOOK_WORKFLOWS` (inline JSON) or `KXM_WEBHOOK_WORKFLOWS_FILE` (a path).
1542
+ Parser: `parseWorkflowDefinitions` in `plugins/kxm/src/workflow.ts`; the hub
1543
+ reads it at startup (`plugins/kxm/src/server.ts`).
1544
+
1545
+ Do not store the file under `.kxm/config/workflows/`: any `*.json` there is
1546
+ treated as legacy configuration and makes the whole project unloadable
1547
+ (`legacy_state_unsupported`). Do not point it at `.kxm/workflows/*.yaml`
1548
+ either; those are `kxm.workflow.v1` files and fail to parse as JSON. A path
1549
+ such as `.kxm/assets/webhooks/workflows.json` works.
1550
+
1551
+ | Field | Type and allowed values | Required, default |
1552
+ |---|---|---|
1553
+ | `id` | String, at most 64 characters, unique | Required; appears in `/v1/webhooks/<id>` |
1554
+ | `source` | `jira`, `github`, or `generic` | `generic` |
1555
+ | `project` | Hub project name, at most 128 characters | Required |
1556
+ | `target` | Coordinator name or durable agent ID, at most 80 characters | Required |
1557
+ | `secretEnv` or `secret` | Variable name, or the literal secret (at least 16 characters); exactly one | Required; prefer `secretEnv` |
1558
+ | `signalSecretEnv` or `signalSecret` | Same rules, for result callbacks | Optional; callbacks fall back to the start secret |
1559
+ | `event` | String, at most 128 characters | Optional provider event filter |
1560
+ | `filter.path`, `filter.equals` | Dotted JSON path and exact string | Optional |
1561
+ | `delivery` | `followUp` or `steer` | `followUp` |
1562
+ | `ttlMs` | Integer, 1,000 to 604,800,000 | Optional |
1563
+ | `promptTemplate` | String, at most 20,000 characters, with `{{payload.path}}` substitutions | Required |
1564
+ | `maxTransitions` | Integer, 1 to 200 | Required when any stage has a back-edge |
1565
+ | `planHash`, `reproOracle` | `{stageId, evidenceKey}` | Optional |
1566
+ | `requirePlanHash` | Stage IDs | Optional |
1567
+ | `stages` | 1 to 32 stages | Required |
1568
+ | `stages[].id` | String, at most 64 characters, unique | Required |
1569
+ | `stages[].label` | String, at most 128 characters | The stage ID |
1570
+ | `stages[].instructions` | String, at most 4,000 characters | Required |
1571
+ | `stages[].requiredEvidence` | Up to 32 strings, unique after trimming, collapsing whitespace, and lowercasing | `[]` |
1572
+ | `stages[].maxAttempts` | Integer, 1 to 20 | `3` |
1573
+ | `stages[].autoResumeLimit` | Integer, 1 to 20 | Optional |
1574
+ | `stages[].area` | `harness`, `gates`, `implementation`, `workflow`, `documentation`, `security`, or `other` | Optional |
1575
+ | `stages[].on` | Map of outcome to a stage ID, `$terminal`, or `{target, maxTransitions}` | Optional |
1576
+ | `stages[].maxTransitions` | Integer, 1 to 100 | Optional |
1577
+ | `stages[].evidencePolicies.<requirement>` | `{kind: peer-reply, minProducers (1 to 8), eligibleAgents (1 to 16), acceptedStatuses: [replied], degradation: {minProducers}}` | Optional; see [Peer provenance and quorum gates](provenance-gates.md) |
1578
+
1579
+ Rules that differ from `kxm.workflow.v1`: a forward transition may only target
1580
+ the next stage; `$terminal` takes no `terminalStatus`; an evidence policy key
1581
+ must match a `requiredEvidence` entry, may not list the workflow `target` among
1582
+ its eligible agents, and its degradation minimum must be lower than
1583
+ `minProducers` (a degraded minimum below 2 is a warning). Unknown fields are
1584
+ ignored rather than rejected, and errors are plain messages, not codes. The
1585
+ secret variables must be set when the file is parsed.
1586
+
1587
+ ```json
1588
+ [
1589
+ {
1590
+ "id": "jira-development",
1591
+ "source": "jira",
1592
+ "project": "payments",
1593
+ "target": "coordinator",
1594
+ "secretEnv": "JIRA_WEBHOOK_SECRET",
1595
+ "signalSecretEnv": "WORKFLOW_SIGNAL_SECRET",
1596
+ "event": "jira:issue_updated",
1597
+ "filter": { "path": "issue.fields.status.name", "equals": "In Progress" },
1598
+ "delivery": "followUp",
1599
+ "ttlMs": 86400000,
1600
+ "maxTransitions": 6,
1601
+ "planHash": { "stageId": "plan", "evidenceKey": "approved plan" },
1602
+ "requirePlanHash": ["implement"],
1603
+ "promptTemplate": "Deliver {{issue.key}}: {{issue.fields.summary}}",
1604
+ "stages": [
1605
+ {
1606
+ "id": "plan",
1607
+ "label": "Plan and review",
1608
+ "instructions": "Produce a plan and collect two independent peer reviews.",
1609
+ "requiredEvidence": ["approved plan", "peer reviews"],
1610
+ "evidencePolicies": {
1611
+ "peer reviews": {
1612
+ "kind": "peer-reply",
1613
+ "minProducers": 2,
1614
+ "eligibleAgents": ["reviewer-claude", "reviewer-grok"],
1615
+ "acceptedStatuses": ["replied"],
1616
+ "degradation": { "minProducers": 1 }
1617
+ }
1618
+ },
1619
+ "maxAttempts": 3,
1620
+ "area": "workflow",
1621
+ "on": { "passed": "implement" }
1622
+ },
1623
+ {
1624
+ "id": "implement",
1625
+ "label": "Implement",
1626
+ "instructions": "Implement the approved plan.",
1627
+ "requiredEvidence": ["diff"],
1628
+ "maxAttempts": 3,
1629
+ "autoResumeLimit": 2,
1630
+ "area": "implementation",
1631
+ "on": { "passed": "ci" }
1632
+ },
1633
+ {
1634
+ "id": "ci",
1635
+ "label": "Wait for CI",
1636
+ "instructions": "Start kxm_workflow_wait and let kxm gate github watch report the checks.",
1637
+ "requiredEvidence": ["github.check:ci"],
1638
+ "maxAttempts": 3,
1639
+ "area": "gates",
1640
+ "on": {
1641
+ "passed": "$terminal",
1642
+ "failed": { "target": "implement", "maxTransitions": 2 }
1643
+ },
1644
+ "maxTransitions": 2
1645
+ }
1646
+ ]
1647
+ }
1648
+ ]
1649
+ ```
1650
+
1651
+ Validated with `kxm gate validate --file .kxm/assets/webhooks/workflows.json`
1652
+ with both secret variables set (one warning, for the degraded minimum of 1).
1653
+
1654
+ Commands: `kxm gate validate [--file <path>]` parses the active source without
1655
+ printing secrets (exit 2 when no source or both variables are set); the hub
1656
+ loads it on `kxm hub start`; `kxm workflow start`, `kxm gate signal`, and
1657
+ `kxm gate github watch` resolve each definition's secret variables. See
1658
+ [Webhook workflows](webhook-workflows.md).
1659
+
1660
+ ## Claude Code plugin settings
1661
+
1662
+ The Claude Code plugin manifest `plugins/kxm/.claude-plugin/plugin.json`
1663
+ declares `userConfig` fields that Claude Code asks each user for. The plugin's
1664
+ `.mcp.json` passes them to the KXM MCP server as environment variables.
1665
+
1666
+ | `userConfig` field | Environment variable | Required, default | Notes |
1667
+ |---|---|---|---|
1668
+ | `server_url` | `KXM_SERVER_URL` | Required, `http://127.0.0.1:7331` | The MCP server also falls back to that URL when empty |
1669
+ | `auth_token` | `KXM_AUTH_TOKEN` | Optional; marked sensitive | Use the project token, never the admin token. When empty, the MCP server uses only this project's saved project token from the hub credential file (`hub-env.json`) and never falls back to the admin token; with neither, tool calls fail with a message naming the fix |
1670
+ | `agent_name` | `KXM_AGENT_NAME` | Required, `claude` | When empty, `claude-<pid>`. If another live session already holds the name, the server registers once more as `<name>-<pid>` |
1671
+ | `agent_purpose` | `KXM_AGENT_PURPOSE` | Required, `Claude Code implementation and review agent` | |
1672
+ | `project` | `KXM_PROJECT` | Optional | When empty, the `name` in `package.json` at the project directory, else the directory name |
1673
+ | (not a user field) | `KXM_PROJECT_DIR` | Set from `${CLAUDE_PROJECT_DIR}` | Used to derive the default project |
1674
+
1675
+ The manifest also registers one `SessionStart` hook,
1676
+ `node ${CLAUDE_PLUGIN_ROOT}/dist/claude-hook.js session-start`, with a 5-second
1677
+ timeout. It runs only inside a KXM project, reads this project's state only,
1678
+ never mints a token or writes a file, and always exits 0. See the
1679
+ [plugin README](../plugins/kxm/README.md) for what it adds to the session.
1680
+
1681
+ ## Updater settings (`kxm.update.v1`)
1682
+
1683
+ `update.yaml` under the user state root (see the next section) controls
1684
+ `kxm update`. A project `.kxm/update.yaml` is ignored with a warning. Parser:
1685
+ `loadKxmUpdateConfig` in `plugins/kxm/src/kxm-update-config.ts`.
1686
+
1687
+ | Field | Type and allowed values | Required, default |
1688
+ |---|---|---|
1689
+ | `schema` | `kxm.update.v1` | Required |
1690
+ | `auto` | Boolean | Required |
1691
+ | `source` | `github` or `npm` | Optional, `github` |
1692
+
1693
+ Unknown fields are refused (`update.yaml unknown field <name>`).
1694
+
1695
+ ```yaml
1696
+ schema: kxm.update.v1
1697
+ auto: false # required boolean
1698
+ source: github # github (default) | npm
1699
+ ```
1700
+
1701
+ ## Workspace layout: tracked, ignored, and state
1702
+
1703
+ Everything under `.kxm/` at the project root falls into one of three groups.
1704
+ `kxm init` writes none of the ignore rules, so add them yourself.
1705
+
1706
+ | Path | Group | Written by |
1707
+ |---|---|---|
1708
+ | `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` |
1709
+ | `roles/`, `routes.yaml`, `prices.yaml`, `modes.yaml`, `role-hosts.yaml`, `roster.yaml` | Tracked configuration outside the bundle | You and their commands |
1710
+ | `config.yaml` | Tracked if the project wants shared preferences; otherwise ignore it | `kxm config set` |
1711
+ | `memory/`, `skills/`, `candidates/`, `goals/` | Tracked durable records | Their commands |
1712
+ | `models/inventory.yaml` | Generated; track it if you want a reviewed snapshot | `kxm models inventory-refresh` |
1713
+ | `assets/` | Tracked intentionally; `assets/generated/` is ignored | Workflows, `kxm improve`, `kxm session start` |
1714
+ | `tasks/` | Ignored in the KXM repository; your choice | `kxm task` |
1715
+ | `logs/` | Ignored runtime logs | The hub and workers |
1716
+ | `state/` | Ignored restart state: the hub database `kxm.db`, Pi sessions, worker manifests | The hub and workers |
1717
+ | `run/` | Ignored sockets (`run/ssh-sockets/`) | `kxm ssh` |
1718
+ | `config/` | Legacy: its JSON files make the project unloadable | Nothing current |
1719
+ | `.kxm-init-transaction/` (sibling of `.kxm/` at the Git root) | Ignored; interrupted `kxm init` state | `kxm init` |
1720
+
1721
+ The ignore rules the KXM repository itself uses, adapted for a project:
1722
+
1723
+ ```text
1724
+ .kxm/logs/*
1725
+ .kxm/state/*
1726
+ .kxm/tasks/
1727
+ .kxm/run/
1728
+ .kxm/assets/generated/
1729
+ .kxm-init-transaction/
1730
+ *.db
1731
+ *.db-shm
1732
+ *.db-wal
1733
+ ```
1734
+
1735
+ `KXM_WORKSPACE_DIR`, `KXM_LOGS_DIR`, `KXM_ASSETS_DIR`, `KXM_STATE_DIR`, and
1736
+ related variables move `logs/`, `assets/`, and `state/`. They do not move the
1737
+ configuration files, which always live under `<project root>/.kxm/`. See
1738
+ [Configuration](configuration.md) and [Operations](operations.md).
1739
+
1740
+ ### State outside the project
1741
+
1742
+ The **user state root** is `KXM_STATE_HOME` when set (it must be absolute;
1743
+ a relative value fails with `local_state_root_not_absolute`). Otherwise it is
1744
+ `~/Library/Application Support/KXM` on macOS, `%LOCALAPPDATA%\KXM` on Windows,
1745
+ and `$XDG_STATE_HOME/kxm` (default `~/.local/state/kxm`) on Linux.
1746
+
1747
+ | Path under the state root | Contents |
1748
+ |---|---|
1749
+ | `runtime/registry.db` | Runtime registry: projects, their control roots, and the supervisor claim |
1750
+ | `runtime/projects/<key>/run-events.db` | Event-sourced runs for one project; `<key>` is the first 24 hex characters of the SHA-256 of the canonical project root path |
1751
+ | `runtime/supervisor.token`, `runtime/logs/kxm-runtime.jsonl` | Supervisor credential and log |
1752
+ | `projects/<hash>/repository-bindings.json` | Host-local member bindings from `kxm init --repository` (`kxm.local-repository-bindings.v1`) |
1753
+ | `hub-env.json`, `hub-binding.json` | Persisted hub credentials and the machine's hub binding |
1754
+ | `update.yaml` | Updater settings |
1755
+
1756
+ The **user configuration directory** is `KXM_USER_CONFIG_DIR`, default
1757
+ `~/.config/kxm`. It holds `config.yaml`, global `roles/` and `workflows/`,
1758
+ `role-hosts.yaml`, `session.token`, and shell completion scripts. Global
1759
+ workflows are listed by `kxm workflow definitions` but never loaded by
1760
+ `kxm run`.
1761
+
1762
+ ## Worked example: a minimal two-step project
1763
+
1764
+ A project with one agent step and one command gate. Every file below passed
1765
+ `kxm init --json`, `kxm run --dry-run`, `kxm trust check`, and a simulated
1766
+ drive that finished `completed`.
1767
+
1768
+ Directory layout (inside a Git repository):
1769
+
1770
+ ```text
1771
+ .
1772
+ ├── package.json
1773
+ └── .kxm/
1774
+ ├── project.yaml
1775
+ ├── repo/repo.yaml
1776
+ ├── agents/coordinator.yaml
1777
+ ├── agents/implementer.yaml
1778
+ ├── gates.yaml
1779
+ ├── routes.yaml
1780
+ └── workflows/default.yaml
1781
+ ```
1782
+
1783
+ `.kxm/project.yaml`:
1784
+
1785
+ ```yaml
1786
+ schema: kxm.project.v1
1787
+ id: prj_01JMINIMAL000000000000000
1788
+ name: Minimal Example
1789
+ defaultWorkflow: default
1790
+ defaultExecutor: local
1791
+ defaultHarness: pi
1792
+ repositories:
1793
+ - id: control
1794
+ role: control
1795
+ required: true
1796
+ pathHint: .
1797
+ ```
1798
+
1799
+ `.kxm/repo/repo.yaml`:
1800
+
1801
+ ```yaml
1802
+ schema: kxm.repository.v1
1803
+ projectId: prj_01JMINIMAL000000000000000
1804
+ repositoryId: control
1805
+ defaultAccess: write
1806
+ ```
1807
+
1808
+ `.kxm/agents/coordinator.yaml` (every workflow needs its coordinator agent):
1809
+
1810
+ ```yaml
1811
+ schema: kxm.agent.v1
1812
+ purpose: Coordinate the pinned workflow.
1813
+ tools:
1814
+ preset: coordinator
1815
+ defaultRepositoryAccess: read
1816
+ repositories:
1817
+ control: read
1818
+ network: provider-only
1819
+ resultSchema: kxm.assignment-result.v1
1820
+ ```
1821
+
1822
+ `.kxm/agents/implementer.yaml`:
1823
+
1824
+ ```yaml
1825
+ schema: kxm.agent.v1
1826
+ purpose: Implement the requested change in the control repository.
1827
+ harness: grok
1828
+ model:
1829
+ provider: xai
1830
+ model: grok-4.6
1831
+ tools:
1832
+ preset: workspace-writer
1833
+ defaultRepositoryAccess: none
1834
+ repositories:
1835
+ control: write
1836
+ network: provider-only
1837
+ resultSchema: kxm.assignment-result.v1
1838
+ ```
1839
+
1840
+ `.kxm/gates.yaml`:
1841
+
1842
+ ```yaml
1843
+ schema: kxm.gate-registry.v1
1844
+ gates:
1845
+ test:
1846
+ kind: command
1847
+ argv: [npm, test]
1848
+ timeoutMs: 600000
1849
+ ```
1850
+
1851
+ `.kxm/workflows/default.yaml`:
1852
+
1853
+ ```yaml
1854
+ schema: kxm.workflow.v1
1855
+ description: Implement a change, then run the test suite.
1856
+ coordinator: coordinator
1857
+ limits:
1858
+ maxTransitions: 4
1859
+ steps:
1860
+ - id: implement
1861
+ kind: agent
1862
+ agent: implementer
1863
+ maxAttempts: 2
1864
+ repositories:
1865
+ control: write
1866
+ requiredEvidence:
1867
+ - key: diff
1868
+ kind: artifact
1869
+ on:
1870
+ passed: verify
1871
+ failed:
1872
+ target: $terminal
1873
+ terminalStatus: failed
1874
+
1875
+ - id: verify
1876
+ kind: gate
1877
+ gate: test
1878
+ maxAttempts: 2
1879
+ repositories:
1880
+ control: write
1881
+ requiredEvidence:
1882
+ - key: tests
1883
+ kind: gate
1884
+ on:
1885
+ passed:
1886
+ target: $terminal
1887
+ terminalStatus: completed
1888
+ implementation-failure:
1889
+ target: implement
1890
+ maxTransitions: 2
1891
+ ```
1892
+
1893
+ `.kxm/routes.yaml` (needed only for live drives):
1894
+
1895
+ ```yaml
1896
+ schema: kxm.routes.v2
1897
+ updatedAt: '2026-09-23T00:00:00.000Z'
1898
+ admitted:
1899
+ - xai/grok-4.6
1900
+ disabled: []
1901
+ roles: {}
1902
+ ```
1903
+
1904
+ Why each piece is there:
1905
+
1906
+ - `implement` routes a failure straight to a terminal `failed` status and
1907
+ declares `failed`, the outcome the producer falls back to.
1908
+ - `verify` declares `implementation-failure`, the outcome a failing command
1909
+ produces. Routing that edge on `failed` instead is refused when the project
1910
+ loads (`gate_outcome_impossible`). The edge back to `implement` makes it a
1911
+ back-edge, so it carries `maxTransitions: 2` and the workflow carries
1912
+ `limits.maxTransitions`.
1913
+ - The gate step lists `control: write`; the Runtime refuses command gate steps
1914
+ without a writable repository.
1915
+ - The workflow omits `limits.maxAgentTimeMs`; with it, the Runtime refuses to
1916
+ drive the run.
1917
+ - There is no `.kxm/roles/writer.yaml`. If you add one, it must list
1918
+ `xai/grok-4.6`, or the loader reports `role_roster_conflicts_with_agent`.
1919
+
1920
+ Validate and run it:
1921
+
1922
+ ```bash
1923
+ kxm init --json # "action": "validated", "issues": []
1924
+ kxm run default "Add a health endpoint" --dry-run --json
1925
+ git add .kxm package.json && git commit -m "Add KXM project"
1926
+ kxm trust check # exit 0: no permission expansion
1927
+ kxm run default "Add a health endpoint" # starts the Runtime and prints a run ID
1928
+ kxm runs drive <runId> --simulated --wait # model-free drive; runs npm test for real
1929
+ ```
1930
+
1931
+ The simulated drive records `passed` for the agent step, runs `npm test` in the
1932
+ project root, and settles `completed` when it exits 0. When `npm test` fails,
1933
+ the run returns to `implement` and fails with `budget_step_attempts` once
1934
+ `implement` has used its two attempts. A live drive of this workflow is refused
1935
+ today, because the `implement` step has write access (see
1936
+ [Steps the Runtime does not execute yet](#steps-the-runtime-does-not-execute-yet)).
1937
+ Stop the Runtime afterwards with `kxm runtime stop`.
1938
+
1939
+ In a project that `kxm init` created,
1940
+ `kxm workflow add <id> --template implement-and-verify` writes a workflow of the
1941
+ same shape (without the `requiredEvidence` entries), and `kxm run` prints the
1942
+ matching `kxm runs drive <runId> --simulated --wait` command for each run it
1943
+ creates.