@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.kxm/workflows/default.yaml +1 -1
- package/CHANGELOG.md +212 -0
- package/README.md +3 -0
- package/docs/README.md +3 -0
- package/docs/agent-skills.md +123 -60
- package/docs/architecture.md +5 -2
- package/docs/cli-reference.md +3527 -0
- package/docs/config-reference.md +1943 -0
- package/docs/configuration.md +30 -4
- package/docs/continuous-improvement.md +122 -10
- package/docs/contracts/routing.md +95 -11
- package/docs/harness-routing.md +616 -0
- package/docs/kxm-handbook.md +106 -19
- package/docs/templates/README.md +1 -1
- package/docs/test-matrix.md +12 -6
- package/docs/troubleshooting.md +2 -2
- package/examples/project/.kxm/workflows/fix.yaml +1 -1
- package/examples/project/.kxm/workflows/improve.yaml +1 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +9 -10
- package/plugins/kxm/README.md +238 -56
- package/plugins/kxm/dist/claude-hook.js +10083 -0
- package/plugins/kxm/dist/cli.js +2487 -1848
- package/plugins/kxm/dist/client.js +64 -0
- package/plugins/kxm/dist/core.js +102 -9
- package/plugins/kxm/dist/extension.js +210 -68
- package/plugins/kxm/dist/mcp-server.js +217 -40
- package/plugins/kxm/dist/runtime-supervisor.js +1628 -157
- package/plugins/kxm/dist/runtime.js +1874 -298
- package/plugins/kxm/dist/server.js +416 -82
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/hints.json +1 -1
- package/plugins/kxm/skills/kxm/SKILL.md +48 -24
- package/plugins/kxm/skills/kxm/references/protocol.md +3 -3
- package/plugins/kxm/skills/kxm-context-memory/SKILL.md +67 -21
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +9 -0
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +82 -16
- package/plugins/kxm/skills/kxm-harvest/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +55 -27
- package/plugins/kxm/skills/kxm-insights/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-mind/SKILL.md +2 -2
- package/plugins/kxm/skills/{kxm-setup → kxm-mind-setup}/SKILL.md +4 -4
- package/plugins/kxm/skills/kxm-peer/SKILL.md +68 -93
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +156 -23
- package/plugins/kxm/skills/kxm-projects/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-protocol/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-query/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +74 -15
- package/plugins/kxm/skills/kxm-runs/SKILL.md +46 -17
- package/plugins/kxm/skills/kxm-session/SKILL.md +64 -36
- package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +44 -15
- package/plugins/kxm/skills/kxm-tasks/SKILL.md +16 -4
- package/plugins/kxm/skills/kxm-triage/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-work/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +60 -19
- package/plugins/kxm/src/arbiter.ts +67 -22
- package/plugins/kxm/src/autocomplete.ts +1 -1
- package/plugins/kxm/src/claude-hook.ts +192 -0
- package/plugins/kxm/src/cli/project.ts +11 -5
- package/plugins/kxm/src/cli/system.ts +85 -13
- package/plugins/kxm/src/cli/types.ts +4 -1
- package/plugins/kxm/src/cli/workflows.ts +18 -16
- package/plugins/kxm/src/cli.ts +23 -13
- package/plugins/kxm/src/client.ts +15 -4
- package/plugins/kxm/src/commands.ts +19 -9
- package/plugins/kxm/src/config.ts +42 -7
- package/plugins/kxm/src/context-packet.ts +14 -2
- package/plugins/kxm/src/context.ts +16 -5
- package/plugins/kxm/src/dispatch-context.ts +286 -0
- package/plugins/kxm/src/engine-plan.ts +40 -0
- package/plugins/kxm/src/engine.ts +138 -6
- package/plugins/kxm/src/hub-env.ts +17 -1
- package/plugins/kxm/src/hub.ts +92 -29
- package/plugins/kxm/src/improve-sources.ts +228 -0
- package/plugins/kxm/src/improve.ts +325 -140
- package/plugins/kxm/src/local-snapshot.ts +101 -42
- package/plugins/kxm/src/mcp-server.ts +129 -30
- package/plugins/kxm/src/memory.ts +43 -20
- package/plugins/kxm/src/project-config.ts +25 -0
- package/plugins/kxm/src/protocol.ts +11 -0
- package/plugins/kxm/src/relevance.ts +138 -0
- package/plugins/kxm/src/retrospective.ts +16 -10
- package/plugins/kxm/src/runtime-service.ts +8 -1
- package/plugins/kxm/src/runtime-supervisor.ts +16 -2
- package/plugins/kxm/src/session-token-hint.ts +17 -0
- package/plugins/kxm/src/suggest.ts +7 -7
- package/plugins/kxm/src/workflow-manager.ts +80 -78
- package/plugins/kxm/src/workflow.ts +202 -12
- package/scripts/build-runtime.mjs +7 -1
- package/scripts/check-generated.mjs +1 -0
- 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.
|