@kontextmind/kxm 0.7.126 → 0.7.127
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.kxm/roles/planner.yaml +6 -7
- package/.kxm/roles/reviewer-arch.yaml +6 -7
- package/.kxm/roles/reviewer-cli.yaml +6 -7
- package/.kxm/roles/writer.yaml +8 -17
- package/CHANGELOG.md +17 -0
- package/docs/architecture/access.md +2 -4
- package/docs/contracts/validation.md +1 -1
- package/docs/contributing/harness-routing-internals.md +1 -1
- package/docs/contributing/test-matrix.md +1 -1
- package/docs/glossary.md +1 -1
- package/docs/guides/agent-skills.md +1 -1
- package/docs/operations/backup-and-restore.md +2 -2
- package/docs/reference/cli-reference.md +31 -74
- package/docs/reference/config-reference.md +83 -118
- package/docs/reference/harness-routing.md +15 -8
- package/examples/project/.kxm/models/critic-claude.yaml +6 -2
- package/examples/project/.kxm/models/critic-gemini.yaml +6 -2
- package/examples/project/.kxm/models/critic-grok.yaml +6 -2
- package/examples/project/.kxm/models/implementation.yaml +6 -2
- package/examples/project/.kxm/models/primary.yaml +6 -2
- package/examples/project/.kxm/roles/planner.yaml +8 -0
- package/examples/project/.kxm/roles/reviewer-arch.yaml +8 -0
- package/examples/project/.kxm/roles/reviewer-cli.yaml +8 -0
- package/examples/project/.kxm/roles/writer.yaml +8 -0
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +1759 -1306
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +994 -245
- package/plugins/kxm/dist/runtime.js +1018 -269
- package/plugins/kxm/dist/server.js +55 -1
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +3 -7
- package/plugins/kxm/src/autocomplete.ts +1 -1
- package/plugins/kxm/src/cli/project.ts +7 -1
- package/plugins/kxm/src/cli/roles.ts +46 -150
- package/plugins/kxm/src/cli.ts +8 -22
- package/plugins/kxm/src/engine.ts +24 -1
- package/plugins/kxm/src/init-guide-setup.ts +38 -4
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/permission.ts +11 -0
- package/plugins/kxm/src/policy-draft.mjs +56 -16
- package/plugins/kxm/src/project-config.ts +121 -10
- package/plugins/kxm/src/repair.ts +1 -0
- package/plugins/kxm/src/role.ts +40 -440
- package/plugins/kxm/src/routes.ts +28 -5
- package/plugins/kxm/src/template.ts +34 -1
- package/schemas/README.md +2 -1
- package/schemas/model.schema.json +111 -13
- package/schemas/role.schema.json +61 -31
- package/schemas/policy-draft/README.md +0 -17
- package/schemas/policy-draft/model.v2.schema.json +0 -140
- package/schemas/policy-draft/role.v2.schema.json +0 -91
|
@@ -14943,6 +14943,59 @@ var KXM_YAML_LIMITS = Object.freeze({
|
|
|
14943
14943
|
maxKeys: 8192
|
|
14944
14944
|
});
|
|
14945
14945
|
|
|
14946
|
+
// plugins/kxm/src/policy-draft.mjs
|
|
14947
|
+
var POLICY_DRAFT_PURPOSES = Object.freeze([
|
|
14948
|
+
"writer",
|
|
14949
|
+
"planner",
|
|
14950
|
+
"reviewer-arch",
|
|
14951
|
+
"reviewer-cli",
|
|
14952
|
+
"experiment"
|
|
14953
|
+
]);
|
|
14954
|
+
var POLICY_DRAFT_PERMISSIONS = Object.freeze(["edit", "read-only"]);
|
|
14955
|
+
var POLICY_DRAFT_STATUSES = Object.freeze(["admitted", "candidate", "retired"]);
|
|
14956
|
+
var EFFORTS = Object.freeze(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
14957
|
+
var MODES = Object.freeze(["headless", "interactive", "either"]);
|
|
14958
|
+
var MODEL_KEYS = Object.freeze([
|
|
14959
|
+
"schema",
|
|
14960
|
+
"id",
|
|
14961
|
+
"harness",
|
|
14962
|
+
"model",
|
|
14963
|
+
"vendor",
|
|
14964
|
+
"status",
|
|
14965
|
+
"permissions",
|
|
14966
|
+
"origin",
|
|
14967
|
+
"thinking",
|
|
14968
|
+
"tags",
|
|
14969
|
+
"capabilities",
|
|
14970
|
+
"priority",
|
|
14971
|
+
"fallbacks",
|
|
14972
|
+
"limits"
|
|
14973
|
+
]);
|
|
14974
|
+
var ROLE_KEYS = Object.freeze([
|
|
14975
|
+
"schema",
|
|
14976
|
+
"id",
|
|
14977
|
+
"purpose",
|
|
14978
|
+
"permission",
|
|
14979
|
+
"description",
|
|
14980
|
+
"extends",
|
|
14981
|
+
"roster",
|
|
14982
|
+
"skills",
|
|
14983
|
+
"tools",
|
|
14984
|
+
"produces",
|
|
14985
|
+
"consumes",
|
|
14986
|
+
"policy"
|
|
14987
|
+
]);
|
|
14988
|
+
var ORIGIN_KEYS = Object.freeze(["source", "sha256"]);
|
|
14989
|
+
var LIMIT_KEYS = Object.freeze(["contextTokens", "outputTokens", "timeoutMs"]);
|
|
14990
|
+
var TOOL_KEYS = Object.freeze(["preset", "allow", "deny"]);
|
|
14991
|
+
var TEMPLATE_KEYS = Object.freeze(["template", "schema"]);
|
|
14992
|
+
var POLICY_KEYS = Object.freeze(["vendorIndependenceRequired", "maxTransitions", "requiresGateVerification", "fallback"]);
|
|
14993
|
+
var FALLBACK_KEYS = Object.freeze(["onError", "maxSwitches", "revert"]);
|
|
14994
|
+
var FALLBACK_ERRORS = Object.freeze(["rate_limit", "transport", "provider_unavailable"]);
|
|
14995
|
+
var FALLBACK_REVERT = Object.freeze(["next_run", "never"]);
|
|
14996
|
+
var ROSTER_ENTRY_KEYS = Object.freeze(["route", "effort", "mode"]);
|
|
14997
|
+
var CRITIC_PURPOSES = Object.freeze(["reviewer-arch", "reviewer-cli"]);
|
|
14998
|
+
|
|
14946
14999
|
// plugins/kxm/src/template.ts
|
|
14947
15000
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
14948
15001
|
|
|
@@ -15439,7 +15492,8 @@ var RESOURCE_SCHEMA = Object.freeze({
|
|
|
15439
15492
|
project: { identity: "kxm.project.v1", file: "project.schema.json" },
|
|
15440
15493
|
repository: { identity: "kxm.repository.v1", file: "repository.schema.json" },
|
|
15441
15494
|
agent: { identity: "kxm.agent.v1", file: "agent.schema.json" },
|
|
15442
|
-
model: { identity: "kxm.model.
|
|
15495
|
+
model: { identity: "kxm.model.v2", file: "model.schema.json" },
|
|
15496
|
+
role: { identity: "kxm.role.v2", file: "role.schema.json" },
|
|
15443
15497
|
environment: { identity: "kxm.environment.v1", file: "environment.schema.json" },
|
|
15444
15498
|
workflow: { identity: "kxm.workflow.v1", file: "workflow.schema.json" },
|
|
15445
15499
|
"gate-registry": { identity: "kxm.gate-registry.v1", file: "gate-registry.schema.json" }
|
package/plugins/kxm/package.json
CHANGED
|
@@ -19,7 +19,7 @@ writers, or replace `.kxm/roster.yaml` trusted policy.
|
|
|
19
19
|
| Session status, dashboard screens, studio layout | `kxm-session` | `session`, `dash`, `studio` |
|
|
20
20
|
| Delegate to or answer another agent | `kxm-peer` | `peer` (`peer await` is capped at 60 seconds) |
|
|
21
21
|
| Work inside a durable workflow run, gates, signed callbacks | `kxm-workflow` | `workflow`, `gate` |
|
|
22
|
-
| Role definitions
|
|
22
|
+
| Role definitions and model rosters | `kxm-definitions` | `role` |
|
|
23
23
|
| Create, drive, inspect, or cancel a run, use a worktree lane, land a branch, run the developer assignment loop, or build and serve the docs site | `kxm-runs` | `run`, `runs`, `lane`, `land`, `assign`, `docs` |
|
|
24
24
|
| What we know or decided, recall, context footprint, Git memory | `kxm-context-memory` | `context`, `memory`, `explain` |
|
|
25
25
|
| Turn a repeated practice into a governed skill | `kxm-skill-lifecycle` | `skills` |
|
|
@@ -15,7 +15,6 @@ or permission to execute an assignment.
|
|
|
15
15
|
```bash
|
|
16
16
|
kxm role list --scope all --json
|
|
17
17
|
kxm role get writer --scope local --json
|
|
18
|
-
kxm role hosts --json
|
|
19
18
|
kxm role --help
|
|
20
19
|
```
|
|
21
20
|
|
|
@@ -29,19 +28,16 @@ changes every project or the trusted developer runner.
|
|
|
29
28
|
|---|---|---|
|
|
30
29
|
| `kxm role list` | List configured roles | `--scope all\|global\|local` |
|
|
31
30
|
| `kxm role get <roleId>` | Read role YAML and details | `--scope all\|global\|local` |
|
|
32
|
-
| `kxm role add [roleId]` | Add a YAML definition or construct a role | `--file`, `--description`, `--skills`, `--
|
|
33
|
-
| `kxm role modify [roleId]` | Change description, skill references, or
|
|
31
|
+
| `kxm role add [roleId]` | Add a YAML definition or construct a role | `--file`, `--description`, `--skills`, `--route` (repeatable; the first id is primary and must name `.kxm/models/<route-id>.yaml`), `--scope`, `--overwrite`, `--pick` |
|
|
32
|
+
| `kxm role modify [roleId]` | Change description, skill references, or the route roster | `--description`, `--add-skill`, `--remove-skill`, `--add-route <route-id>`, `--remove-route <route-id>`, `--scope`, `--pick` |
|
|
34
33
|
| `kxm role remove [roleId]` | Remove a definition | `--scope`, `--pick` |
|
|
35
|
-
| `kxm role hosts` | List role seats and the host, model, and effort each resolves to, with the source of that decision | `--scope all\|global\|local` |
|
|
36
|
-
| `kxm role set-host <seatId> <host>` | Bind a role seat to a host in `.kxm/role-hosts.yaml` | `--model`, `--effort low\|medium\|high\|xhigh`, `--scope global\|local` |
|
|
37
34
|
| `kxm role resume <runId> [ruling]` | Resume an audit-escalated run with an operator directive | `[ruling]` free text |
|
|
38
35
|
|
|
39
36
|
Use `--json` for structured output. Inspect command-specific `--help` before
|
|
40
37
|
constructing a mutation; do not invent `create`, `update`, `delete`, `validate`,
|
|
41
38
|
or `apply` subcommands under `kxm role`.
|
|
42
39
|
|
|
43
|
-
`kxm role
|
|
44
|
-
(or the global file); show the user the resulting diff. `kxm role resume`
|
|
40
|
+
`kxm role resume`
|
|
45
41
|
records the operator's ruling on an audit escalation: run it only with the
|
|
46
42
|
ruling the user gave you, and preview a KXM run with `--dry-run` first.
|
|
47
43
|
|
|
@@ -57,7 +57,7 @@ const SUBCOMMANDS: Record<string, string[]> = {
|
|
|
57
57
|
goal: ["create", "list", "get"],
|
|
58
58
|
task: ["create", "list", "get", "run", "sync"],
|
|
59
59
|
studio: ["layout", "serve"],
|
|
60
|
-
role: ["list", "get", "add", "remove", "modify", "
|
|
60
|
+
role: ["list", "get", "add", "remove", "modify", "resume"],
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
export function generateShellCompletion(shell: SupportedShell): string {
|
|
@@ -897,7 +897,13 @@ export async function cmdModelsScreen(runtime: Runtime): Promise<number> {
|
|
|
897
897
|
if (!Number.isInteger(index) || !models[index]) continue;
|
|
898
898
|
const model = models[index];
|
|
899
899
|
if (command === "a" || command === "d") setRouteState(runtime.dirs.workdir, model, command === "a" ? "admitted" : "disabled");
|
|
900
|
-
else if (command === "r" || command === "x")
|
|
900
|
+
else if (command === "r" || command === "x") {
|
|
901
|
+
try {
|
|
902
|
+
setRouteState(runtime.dirs.workdir, model, "admitted", (await ask("role: ")).trim(), command === "x");
|
|
903
|
+
} catch (error) {
|
|
904
|
+
runtime.io.stderr(`${error instanceof Error ? error.message : "unknown route"}\n`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
901
907
|
}
|
|
902
908
|
} finally { rl.close(); }
|
|
903
909
|
}
|
|
@@ -5,17 +5,13 @@ import { createInterface } from "node:readline";
|
|
|
5
5
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
6
6
|
import { DatabaseSync, openReadOnlyDatabase } from "../sqlite.ts";
|
|
7
7
|
import {
|
|
8
|
-
DEFAULT_ROLES,
|
|
9
|
-
DEFAULT_ROLE_SEATS,
|
|
10
8
|
listRoles,
|
|
11
9
|
getRole,
|
|
12
10
|
addRole,
|
|
13
11
|
removeRole,
|
|
14
12
|
modifyRole,
|
|
15
|
-
loadRoleHostsConfig,
|
|
16
|
-
setRoleSeatHost,
|
|
17
|
-
resolveRoleSeat,
|
|
18
13
|
parseRoleFile,
|
|
14
|
+
rolePurposeForId,
|
|
19
15
|
type KxmRoleDefinition,
|
|
20
16
|
} from "../role.ts";
|
|
21
17
|
import { discoverKxmProjectRoot, kxmRoleWriteIssues } from "../project-config.ts";
|
|
@@ -112,7 +108,7 @@ export async function cmdRoleList(
|
|
|
112
108
|
const lines: string[] = ["ROLES:"];
|
|
113
109
|
for (const r of roles) {
|
|
114
110
|
const scopeTag = r.scope === "overridden" ? "[local override]" : `[${r.scope}]`;
|
|
115
|
-
const modelTag = r.
|
|
111
|
+
const modelTag = r.primaryRoute ? `(${r.primaryRoute})` : "";
|
|
116
112
|
lines.push(` ${r.id.padEnd(16)} ${scopeTag.padEnd(16)} ${modelTag.padEnd(28)} ${r.description}`);
|
|
117
113
|
}
|
|
118
114
|
print(runtime.io, false, {}, `${lines.join("\n")}\n`);
|
|
@@ -149,8 +145,7 @@ export async function cmdRoleAdd(
|
|
|
149
145
|
file?: string | undefined;
|
|
150
146
|
description?: string | undefined;
|
|
151
147
|
skills?: string | undefined;
|
|
152
|
-
|
|
153
|
-
model?: string | undefined;
|
|
148
|
+
route?: string[] | undefined;
|
|
154
149
|
scope?: "global" | "local" | undefined;
|
|
155
150
|
overwrite?: boolean | undefined;
|
|
156
151
|
pick?: string | boolean | undefined;
|
|
@@ -159,12 +154,7 @@ export async function cmdRoleAdd(
|
|
|
159
154
|
const scope = options.scope ?? "local";
|
|
160
155
|
let base: KxmRoleDefinition | undefined;
|
|
161
156
|
if (!roleId || options.pick) {
|
|
162
|
-
const candidates: PickCandidate[] =
|
|
163
|
-
id: r.id,
|
|
164
|
-
description: r.description,
|
|
165
|
-
label: "template",
|
|
166
|
-
payload: r,
|
|
167
|
-
}));
|
|
157
|
+
const candidates: PickCandidate[] = [];
|
|
168
158
|
if (scope === "local") {
|
|
169
159
|
const globalRoles = listRoles({ scope: "global", userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
170
160
|
for (const gr of globalRoles) {
|
|
@@ -175,7 +165,7 @@ export async function cmdRoleAdd(
|
|
|
175
165
|
}
|
|
176
166
|
}
|
|
177
167
|
}
|
|
178
|
-
const picked = await resolvePickItem(runtime.io, `Select a role
|
|
168
|
+
const picked = await resolvePickItem(runtime.io, `Select a global role to add (${scope})`, candidates, options.pick, runtime.env);
|
|
179
169
|
if (!picked) {
|
|
180
170
|
if (!roleId) {
|
|
181
171
|
runtime.io.stderr("role add failed: missing roleId or pick selection\n");
|
|
@@ -188,24 +178,50 @@ export async function cmdRoleAdd(
|
|
|
188
178
|
}
|
|
189
179
|
|
|
190
180
|
const skills = options.skills ? options.skills.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
|
191
|
-
const
|
|
181
|
+
const routes = (options.route ?? []).filter((route) => route.length > 0);
|
|
182
|
+
const roster = routes.length > 0 ? routes.map((route) => ({ route })) : undefined;
|
|
183
|
+
// A constructed or copied role takes --route, and the first id is the primary.
|
|
184
|
+
// A missing model file is the same refusal as modify --add-route. --file is checked
|
|
185
|
+
// after its roster is parsed, for both local and global scope.
|
|
186
|
+
const projectRoot = discoverKxmProjectRoot(runtime.cwd) ?? runtime.cwd;
|
|
187
|
+
const refuseMissingRoute = (routeId: string): boolean => {
|
|
188
|
+
const modelFile = join(projectRoot, ".kxm", "models", `${routeId}.yaml`);
|
|
189
|
+
if (existsSync(modelFile)) return false;
|
|
190
|
+
runtime.io.stderr(`kxm: route '${routeId}' is not a file under .kxm/models/\n`);
|
|
191
|
+
return true;
|
|
192
|
+
};
|
|
193
|
+
if (roster && !options.file) {
|
|
194
|
+
for (const entry of roster) {
|
|
195
|
+
if (refuseMissingRoute(entry.route)) return 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const purpose = rolePurposeForId(roleId ?? "experiment");
|
|
192
199
|
let roleDef: KxmRoleDefinition;
|
|
193
200
|
if (options.file) {
|
|
194
201
|
const filePath = resolve(runtime.cwd, options.file);
|
|
195
202
|
const content = readFileSync(filePath, "utf8");
|
|
196
203
|
roleDef = parseYaml(content) as KxmRoleDefinition;
|
|
197
204
|
roleDef.id = roleId!;
|
|
205
|
+
roleDef.schema = "kxm.role.v2";
|
|
206
|
+
roleDef.purpose ??= rolePurposeForId(roleDef.id);
|
|
207
|
+
roleDef.permission ??= roleDef.purpose === "writer" ? "edit" : "read-only";
|
|
208
|
+
for (const entry of roleDef.roster ?? []) {
|
|
209
|
+
if (typeof entry?.route === "string" && entry.route.length > 0 && refuseMissingRoute(entry.route)) return 1;
|
|
210
|
+
}
|
|
198
211
|
} else if (base) {
|
|
199
212
|
roleDef = {
|
|
200
213
|
...base,
|
|
214
|
+
schema: "kxm.role.v2",
|
|
201
215
|
description: options.description || base.description,
|
|
202
216
|
skills: skills ?? base.skills,
|
|
203
217
|
roster: roster ?? base.roster,
|
|
204
218
|
};
|
|
205
219
|
} else {
|
|
206
220
|
roleDef = {
|
|
207
|
-
schema: "kxm.role.
|
|
221
|
+
schema: "kxm.role.v2",
|
|
208
222
|
id: roleId!,
|
|
223
|
+
purpose,
|
|
224
|
+
permission: purpose === "writer" ? "edit" : "read-only",
|
|
209
225
|
description: options.description || `Role ${roleId}`,
|
|
210
226
|
skills: skills ?? [],
|
|
211
227
|
roster: roster ?? [],
|
|
@@ -327,8 +343,8 @@ export async function cmdRoleModify(
|
|
|
327
343
|
description?: string | undefined;
|
|
328
344
|
addSkill?: string | undefined;
|
|
329
345
|
removeSkill?: string | undefined;
|
|
330
|
-
|
|
331
|
-
|
|
346
|
+
addRoute?: string | undefined;
|
|
347
|
+
removeRoute?: string | undefined;
|
|
332
348
|
scope?: "global" | "local" | undefined;
|
|
333
349
|
pick?: string | boolean | undefined;
|
|
334
350
|
},
|
|
@@ -379,14 +395,18 @@ export async function cmdRoleModify(
|
|
|
379
395
|
}
|
|
380
396
|
|
|
381
397
|
let roster = [...(role.roster ?? [])];
|
|
382
|
-
if (options.
|
|
383
|
-
const
|
|
384
|
-
const
|
|
385
|
-
const
|
|
386
|
-
|
|
398
|
+
if (options.addRoute) {
|
|
399
|
+
const routeId = options.addRoute;
|
|
400
|
+
const projectRoot = discoverKxmProjectRoot(runtime.cwd) ?? runtime.cwd;
|
|
401
|
+
const modelFile = join(projectRoot, ".kxm", "models", `${routeId}.yaml`);
|
|
402
|
+
if (!existsSync(modelFile)) {
|
|
403
|
+
runtime.io.stderr(`kxm: route '${routeId}' is not a file under .kxm/models/\n`);
|
|
404
|
+
return 1;
|
|
405
|
+
}
|
|
406
|
+
if (!roster.some((entry) => entry.route === routeId)) roster.push({ route: routeId });
|
|
387
407
|
}
|
|
388
|
-
if (options.
|
|
389
|
-
roster = roster.filter((entry) => entry.
|
|
408
|
+
if (options.removeRoute) {
|
|
409
|
+
roster = roster.filter((entry) => entry.route !== options.removeRoute);
|
|
390
410
|
}
|
|
391
411
|
|
|
392
412
|
const updates: Partial<KxmRoleDefinition> = {
|
|
@@ -419,130 +439,6 @@ export async function cmdRoleModify(
|
|
|
419
439
|
}
|
|
420
440
|
}
|
|
421
441
|
|
|
422
|
-
export async function cmdRoleHosts(
|
|
423
|
-
runtime: Runtime,
|
|
424
|
-
options: { scope?: "all" | "global" | "local" | undefined } = {},
|
|
425
|
-
): Promise<number> {
|
|
426
|
-
const hostsConfig = loadRoleHostsConfig({
|
|
427
|
-
scope: options.scope,
|
|
428
|
-
repoRoot: runtime.cwd,
|
|
429
|
-
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
const seatIds = Array.from(
|
|
433
|
-
new Set([
|
|
434
|
-
...Object.keys(DEFAULT_ROLE_SEATS),
|
|
435
|
-
...Object.keys(hostsConfig.config.seats ?? {}),
|
|
436
|
-
]),
|
|
437
|
-
).sort();
|
|
438
|
-
|
|
439
|
-
const seats = seatIds.map((seatId) => {
|
|
440
|
-
const resolved = resolveRoleSeat(seatId, {
|
|
441
|
-
repoRoot: runtime.cwd,
|
|
442
|
-
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
443
|
-
});
|
|
444
|
-
return {
|
|
445
|
-
seatId,
|
|
446
|
-
host: resolved.host,
|
|
447
|
-
model: resolved.model,
|
|
448
|
-
provider: resolved.provider,
|
|
449
|
-
effort: resolved.effort,
|
|
450
|
-
source: resolved.source,
|
|
451
|
-
configuredHost: hostsConfig.config.seats?.[seatId]?.host,
|
|
452
|
-
configuredModel: hostsConfig.config.seats?.[seatId]?.model,
|
|
453
|
-
};
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
if (runtime.json) {
|
|
457
|
-
print(
|
|
458
|
-
runtime.io,
|
|
459
|
-
true,
|
|
460
|
-
{
|
|
461
|
-
ok: true,
|
|
462
|
-
command: "role hosts",
|
|
463
|
-
scope: hostsConfig.scope,
|
|
464
|
-
filePath: hostsConfig.filePath,
|
|
465
|
-
seats,
|
|
466
|
-
hostProviders: hostsConfig.config.hostProviders ?? {},
|
|
467
|
-
},
|
|
468
|
-
"",
|
|
469
|
-
);
|
|
470
|
-
return 0;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const lines: string[] = [
|
|
474
|
-
`ROLE SEATS (${hostsConfig.scope}${hostsConfig.filePath ? ` at ${hostsConfig.filePath}` : ""}):`,
|
|
475
|
-
];
|
|
476
|
-
for (const s of seats) {
|
|
477
|
-
const modelStr = s.model ? ` [${s.model}]` : "";
|
|
478
|
-
const sourceTag = `(via ${s.source})`;
|
|
479
|
-
lines.push(` ${s.seatId.padEnd(16)} -> host: ${s.host.padEnd(12)} ${modelStr.padEnd(30)} ${sourceTag}`);
|
|
480
|
-
}
|
|
481
|
-
if (hostsConfig.config.hostProviders && Object.keys(hostsConfig.config.hostProviders).length > 0) {
|
|
482
|
-
lines.push("\nHOST PROVIDERS:");
|
|
483
|
-
for (const [h, p] of Object.entries(hostsConfig.config.hostProviders)) {
|
|
484
|
-
lines.push(` ${h.padEnd(16)} -> provider: ${p}`);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
print(runtime.io, false, {}, `${lines.join("\n")}\n`);
|
|
488
|
-
return 0;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
export async function cmdRoleSetHost(
|
|
492
|
-
runtime: Runtime,
|
|
493
|
-
seatId: string,
|
|
494
|
-
host: string,
|
|
495
|
-
options: {
|
|
496
|
-
model?: string | undefined;
|
|
497
|
-
effort?: "low" | "medium" | "high" | "xhigh" | undefined;
|
|
498
|
-
scope?: "global" | "local" | undefined;
|
|
499
|
-
} = {},
|
|
500
|
-
): Promise<number> {
|
|
501
|
-
if (!seatId || !host) {
|
|
502
|
-
runtime.io.stderr("kxm role set-host requires <seatId> and <host>\n");
|
|
503
|
-
return 1;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
try {
|
|
507
|
-
const result = setRoleSeatHost(seatId, host, {
|
|
508
|
-
model: options.model,
|
|
509
|
-
effort: options.effort,
|
|
510
|
-
scope: options.scope ?? "local",
|
|
511
|
-
repoRoot: runtime.cwd,
|
|
512
|
-
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
513
|
-
dryRun: runtime.dryRun,
|
|
514
|
-
});
|
|
515
|
-
if (runtime.dryRun) {
|
|
516
|
-
printPlan(
|
|
517
|
-
runtime,
|
|
518
|
-
{ command: "role set-host", seatId, host, binding: result.binding, filePath: result.filePath, scope: result.scope },
|
|
519
|
-
[{ action: "write", target: result.filePath }],
|
|
520
|
-
`bind seat '${seatId}' to host '${host}'`,
|
|
521
|
-
);
|
|
522
|
-
return 0;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
print(
|
|
526
|
-
runtime.io,
|
|
527
|
-
runtime.json,
|
|
528
|
-
{
|
|
529
|
-
ok: true,
|
|
530
|
-
command: "role set-host",
|
|
531
|
-
seatId,
|
|
532
|
-
host,
|
|
533
|
-
binding: result.binding,
|
|
534
|
-
filePath: result.filePath,
|
|
535
|
-
scope: result.scope,
|
|
536
|
-
},
|
|
537
|
-
`Bound seat '${seatId}' to host '${host}' in ${result.filePath}\n`,
|
|
538
|
-
);
|
|
539
|
-
return 0;
|
|
540
|
-
} catch (err: unknown) {
|
|
541
|
-
runtime.io.stderr(`role set-host failed: ${(err as Error).message}\n`);
|
|
542
|
-
return 1;
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
442
|
export async function cmdRoleResume(
|
|
547
443
|
runtime: Runtime,
|
|
548
444
|
runId: string,
|
package/plugins/kxm/src/cli.ts
CHANGED
|
@@ -48,8 +48,7 @@ import {
|
|
|
48
48
|
cmdRoleAdd,
|
|
49
49
|
cmdRoleRemove,
|
|
50
50
|
cmdRoleModify,
|
|
51
|
-
|
|
52
|
-
cmdRoleSetHost,
|
|
51
|
+
|
|
53
52
|
cmdRoleResume,
|
|
54
53
|
} from "./cli/roles.ts";
|
|
55
54
|
|
|
@@ -215,7 +214,7 @@ const DRY_RUN_COMMANDS: ReadonlySet<string> = new Set([
|
|
|
215
214
|
"peer list", "peer send", "peer get", "peer await", "peer cancel", "peer fanout", "peer inbox", "peer reply",
|
|
216
215
|
"workflow list", "workflow get", "workflow checkpoint", "workflow record", "workflow wait", "workflow signal",
|
|
217
216
|
"workflow start", "workflow export", "workflow definitions", "workflow add", "workflow remove", "workflow modify",
|
|
218
|
-
"role list", "role get", "role add", "role remove", "role modify", "role
|
|
217
|
+
"role list", "role get", "role add", "role remove", "role modify", "role resume",
|
|
219
218
|
"gate validate", "gate artifacts-exist", "gate degrade", "gate signal", "gate github watch",
|
|
220
219
|
"improve report",
|
|
221
220
|
"context get", "context recall", "context state", "context episode", "context promote", "context explain",
|
|
@@ -1026,12 +1025,11 @@ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly
|
|
|
1026
1025
|
.option("--file <path>", "Path to YAML role definition file")
|
|
1027
1026
|
.option("--description <text>", "Role description")
|
|
1028
1027
|
.option("--skills <skills>", "Comma-separated skills list")
|
|
1029
|
-
.option("--
|
|
1030
|
-
.option("--model <model>", "Primary model identifier (e.g. grok-4.6, fable, gemini-3.8-flash-high)")
|
|
1028
|
+
.option("--route <route-id>", "Route id under .kxm/models/. Repeat to set the roster; the first id is primary", (value: string, previous: string[] | undefined) => (previous ?? []).concat(value))
|
|
1031
1029
|
.option("--scope <scope>", "Configuration scope: global or local (default: local)", "local")
|
|
1032
1030
|
.option("--overwrite", "Overwrite existing role definition if present")
|
|
1033
|
-
.option("--pick [selection]", "Pick
|
|
1034
|
-
.action(async function roleAddAction(this: Command, roleId?: string, options?: { file?: string; description?: string; skills?: string;
|
|
1031
|
+
.option("--pick [selection]", "Pick a global role to copy (index or id)")
|
|
1032
|
+
.action(async function roleAddAction(this: Command, roleId?: string, options?: { file?: string; description?: string; skills?: string; route?: string[]; scope?: "global" | "local"; overwrite?: boolean; pick?: string | boolean }) {
|
|
1035
1033
|
result.code = await cmdRoleAdd(runtimeFrom(ctx, this), roleId, options ?? {});
|
|
1036
1034
|
});
|
|
1037
1035
|
addGlobalOptions(role.command("remove [roleId]").description("Remove a role definition"))
|
|
@@ -1044,25 +1042,13 @@ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly
|
|
|
1044
1042
|
.option("--description <text>", "Updated description")
|
|
1045
1043
|
.option("--add-skill <skill>", "Skill to add")
|
|
1046
1044
|
.option("--remove-skill <skill>", "Skill to remove")
|
|
1047
|
-
.option("--add-
|
|
1048
|
-
.option("--remove-
|
|
1045
|
+
.option("--add-route <route-id>", "Route id to add to the roster. The id must name a file under .kxm/models/")
|
|
1046
|
+
.option("--remove-route <route-id>", "Route id to remove from the roster")
|
|
1049
1047
|
.option("--scope <scope>", "Configuration scope: global or local")
|
|
1050
1048
|
.option("--pick [selection]", "Pick a role to modify (index or id)")
|
|
1051
|
-
.action(async function roleModifyAction(this: Command, roleId?: string, options?: { description?: string; addSkill?: string; removeSkill?: string;
|
|
1049
|
+
.action(async function roleModifyAction(this: Command, roleId?: string, options?: { description?: string; addSkill?: string; removeSkill?: string; addRoute?: string; removeRoute?: string; scope?: "global" | "local"; pick?: string | boolean }) {
|
|
1052
1050
|
result.code = await cmdRoleModify(runtimeFrom(ctx, this), roleId, options ?? {});
|
|
1053
1051
|
});
|
|
1054
|
-
addGlobalOptions(role.command("hosts").description("List role seats and resolved execution hosts from .kxm/role-hosts.yaml"))
|
|
1055
|
-
.option("--scope <scope>", "Filter by scope: all, global, or local", "all")
|
|
1056
|
-
.action(async function roleHostsAction(this: Command, options: { scope?: "all" | "global" | "local" }) {
|
|
1057
|
-
result.code = await cmdRoleHosts(runtimeFrom(ctx, this), options);
|
|
1058
|
-
});
|
|
1059
|
-
addGlobalOptions(role.command("set-host <seatId> <host>").description("Bind a role seat to a host in .kxm/role-hosts.yaml"))
|
|
1060
|
-
.option("--model <model>", "Model identifier for this seat")
|
|
1061
|
-
.option("--effort <effort>", "Effort level: low, medium, high, xhigh")
|
|
1062
|
-
.option("--scope <scope>", "Configuration scope: global or local (default: local)", "local")
|
|
1063
|
-
.action(async function roleSetHostAction(this: Command, seatId: string, host: string, options: { model?: string; effort?: "low" | "medium" | "high" | "xhigh"; scope?: "global" | "local" }) {
|
|
1064
|
-
result.code = await cmdRoleSetHost(runtimeFrom(ctx, this), seatId, host, options);
|
|
1065
|
-
});
|
|
1066
1052
|
addGlobalOptions(role.command("resume <runId> [ruling]").description("Resume an audit-escalated role run with an operator directive"))
|
|
1067
1053
|
.action(async function roleResumeAction(this: Command, runId: string, ruling?: string) {
|
|
1068
1054
|
result.code = await cmdRoleResume(runtimeFrom(ctx, this), runId, ruling);
|
|
@@ -1342,6 +1342,28 @@ export interface KxmPanelDispatchSeams {
|
|
|
1342
1342
|
|
|
1343
1343
|
export const kxmPanelDispatchSeams: KxmPanelDispatchSeams = {};
|
|
1344
1344
|
|
|
1345
|
+
/** Selectors named by v2 route files: the model id, `vendor/model`, and `harness/model`. */
|
|
1346
|
+
function selectorsNamedByRoutes(projectRoot: string, routeIds: readonly string[]): string[] {
|
|
1347
|
+
const selectors = new Set<string>();
|
|
1348
|
+
for (const routeId of routeIds) {
|
|
1349
|
+
const file = join(projectRoot, ".kxm", "models", `${routeId}.yaml`);
|
|
1350
|
+
if (!existsSync(file)) continue;
|
|
1351
|
+
let parsed: { harness?: unknown; model?: unknown; vendor?: unknown };
|
|
1352
|
+
try {
|
|
1353
|
+
parsed = parse(readFileSync(file, "utf8")) as { harness?: unknown; model?: unknown; vendor?: unknown };
|
|
1354
|
+
} catch {
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
const model = typeof parsed.model === "string" ? parsed.model : "";
|
|
1358
|
+
const vendor = typeof parsed.vendor === "string" ? parsed.vendor : "";
|
|
1359
|
+
const harness = typeof parsed.harness === "string" ? parsed.harness : "";
|
|
1360
|
+
if (model) selectors.add(model);
|
|
1361
|
+
if (vendor && model) selectors.add(`${vendor}/${model}`);
|
|
1362
|
+
if (harness && model) selectors.add(`${harness}/${model}`);
|
|
1363
|
+
}
|
|
1364
|
+
return [...selectors];
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1345
1367
|
function unreconciledPanelAttemptId(state: KxmRunState): string | undefined {
|
|
1346
1368
|
const current = state.currentStep;
|
|
1347
1369
|
if (!current) return undefined;
|
|
@@ -1438,7 +1460,8 @@ function resolveProducerRoute(
|
|
|
1438
1460
|
const role = agentId === "implementer" ? "writer" : agentId;
|
|
1439
1461
|
const roleBindings = listRoleBindings(projectRoot);
|
|
1440
1462
|
const roster = roleBindings[role];
|
|
1441
|
-
|
|
1463
|
+
const rosterSelectors = roster ? selectorsNamedByRoutes(projectRoot, roster) : undefined;
|
|
1464
|
+
if (rosterSelectors && !rosterSelectors.includes(selector)) {
|
|
1442
1465
|
return {
|
|
1443
1466
|
error: {
|
|
1444
1467
|
reason: "step_unsupported",
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
* This module writes only current KXM project resources:
|
|
11
11
|
* - `.kxm/agents/<role-slug>.yaml` (kxm.agent.v1)
|
|
12
12
|
* - `.kxm/workflows/<slug>.yaml` (kxm.workflow.v1)
|
|
13
|
+
* - `.kxm/roles/<role-slug>.yaml` (kxm.role.v2)
|
|
14
|
+
* - `.kxm/models/<route-id>.yaml` (kxm.model.v2)
|
|
13
15
|
* - admitted selectors appended to `.kxm/routes.yaml`
|
|
14
16
|
* It never writes retired legacy authority (`.kxm/config`, retired
|
|
15
|
-
* `.kxm/roster.json`) or the trusted `.kxm/roster.yaml` policy
|
|
16
|
-
* and does not use the kxm.role.v1 subsystem.
|
|
17
|
+
* `.kxm/roster.json`) or the trusted `.kxm/roster.yaml` policy.
|
|
17
18
|
*
|
|
18
19
|
* Guide research ids are not dispatch ids. Only the admitted map below is
|
|
19
20
|
* written, and only when that harness is authenticated. Unmapped ids are skipped.
|
|
@@ -27,7 +28,8 @@
|
|
|
27
28
|
* admission decision says so.
|
|
28
29
|
*/
|
|
29
30
|
|
|
30
|
-
import {
|
|
31
|
+
import { createHash } from "node:crypto";
|
|
32
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
31
33
|
import { dirname, join } from "node:path";
|
|
32
34
|
import { stringify } from "yaml";
|
|
33
35
|
import { type HarnessInventory } from "./harness.ts";
|
|
@@ -485,9 +487,14 @@ function workflowDocument(workflow: GuideWorkflow): Record<string, unknown> {
|
|
|
485
487
|
|
|
486
488
|
/**
|
|
487
489
|
* Render the planned KXM resource files (`.kxm/agents/*.yaml`,
|
|
488
|
-
* `.kxm/workflows/*.yaml`).
|
|
490
|
+
* `.kxm/workflows/*.yaml`). A Pi model origin hashes `.kxm/project.yaml`
|
|
491
|
+
* when that file exists, and is omitted when it does not.
|
|
489
492
|
*/
|
|
490
493
|
export function renderGuideSetupFiles(projectRoot: string, plan: GuideSetupPlan): GuideSetupFile[] {
|
|
494
|
+
const projectYaml = join(projectRoot, ".kxm", "project.yaml");
|
|
495
|
+
const projectOrigin = existsSync(projectYaml)
|
|
496
|
+
? { source: ".kxm/project.yaml", sha256: createHash("sha256").update(readFileSync(projectYaml)).digest("hex") }
|
|
497
|
+
: undefined;
|
|
491
498
|
const files: GuideSetupFile[] = [];
|
|
492
499
|
const roleStages = new Map<string, GuideStage>();
|
|
493
500
|
for (const workflow of plan.workflows) {
|
|
@@ -496,10 +503,37 @@ export function renderGuideSetupFiles(projectRoot: string, plan: GuideSetupPlan)
|
|
|
496
503
|
for (const [role, binding] of [...plan.agents.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
497
504
|
const stage = roleStages.get(role);
|
|
498
505
|
if (!stage) continue;
|
|
506
|
+
const routeId = role.replaceAll("_", "-");
|
|
507
|
+
const writer = isWriterRole(role);
|
|
508
|
+
const modelDocument: Record<string, unknown> = {
|
|
509
|
+
schema: "kxm.model.v2",
|
|
510
|
+
id: routeId,
|
|
511
|
+
harness: binding.harness,
|
|
512
|
+
model: binding.model,
|
|
513
|
+
vendor: binding.provider,
|
|
514
|
+
status: "admitted",
|
|
515
|
+
permissions: [writer ? "edit" : "read-only"],
|
|
516
|
+
};
|
|
517
|
+
if (binding.harness === "pi" && projectOrigin) modelDocument.origin = projectOrigin;
|
|
499
518
|
files.push({
|
|
500
519
|
path: join(projectRoot, ".kxm", "agents", `${role}.yaml`),
|
|
501
520
|
content: stringify(agentDocument(role, stage, binding)),
|
|
502
521
|
});
|
|
522
|
+
files.push({
|
|
523
|
+
path: join(projectRoot, ".kxm", "models", `${routeId}.yaml`),
|
|
524
|
+
content: stringify(modelDocument),
|
|
525
|
+
});
|
|
526
|
+
files.push({
|
|
527
|
+
path: join(projectRoot, ".kxm", "roles", `${role}.yaml`),
|
|
528
|
+
content: stringify({
|
|
529
|
+
schema: "kxm.role.v2",
|
|
530
|
+
id: role,
|
|
531
|
+
purpose: writer ? "writer" : "experiment",
|
|
532
|
+
permission: writer ? "edit" : "read-only",
|
|
533
|
+
description: stage.domain.slice(0, 2000),
|
|
534
|
+
roster: [{ route: routeId }],
|
|
535
|
+
}),
|
|
536
|
+
});
|
|
503
537
|
}
|
|
504
538
|
for (const workflow of plan.workflows) {
|
|
505
539
|
files.push({
|
|
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
|
|
|
11
11
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
12
12
|
import { sessionTokenFixHint } from "./session-token-hint.ts";
|
|
13
13
|
|
|
14
|
-
const VERSION = "0.7.
|
|
14
|
+
const VERSION = "0.7.127";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -56,6 +56,7 @@ const PROSE_FIELDS: Readonly<Record<KxmResourceKind, ReadonlySet<string>>> = {
|
|
|
56
56
|
repository: new Set(["description"]),
|
|
57
57
|
agent: new Set(["purpose", "instructions"]),
|
|
58
58
|
model: new Set([]),
|
|
59
|
+
role: new Set(["description"]),
|
|
59
60
|
environment: new Set([]),
|
|
60
61
|
workflow: new Set(["description"]),
|
|
61
62
|
"gate-registry": new Set([]),
|
|
@@ -193,6 +194,16 @@ export function kxmAuthorityEntries(resource: { kind: KxmResourceKind; id?: stri
|
|
|
193
194
|
}
|
|
194
195
|
break;
|
|
195
196
|
}
|
|
197
|
+
case "role": {
|
|
198
|
+
push("/schema", "resource-shape", value.schema);
|
|
199
|
+
push("/purpose", "resource-shape", value.purpose);
|
|
200
|
+
push("/permission", "resource-shape", value.permission);
|
|
201
|
+
push("/extends", "resource-shape", value.extends);
|
|
202
|
+
push("/roster", "resource-shape", value.roster);
|
|
203
|
+
push("/policy", "resource-shape", value.policy);
|
|
204
|
+
push("/tools", "tools", value.tools);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
196
207
|
case "gate-registry": {
|
|
197
208
|
push("/schema", "resource-shape", value.schema);
|
|
198
209
|
const gates = asObject(value.gates) ?? {};
|