@kontextmind/kxm 0.7.126 → 0.7.128

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/roles/planner.yaml +6 -7
  3. package/.kxm/roles/reviewer-arch.yaml +6 -7
  4. package/.kxm/roles/reviewer-cli.yaml +6 -7
  5. package/.kxm/roles/writer.yaml +8 -17
  6. package/CHANGELOG.md +17 -0
  7. package/docs/architecture/access.md +2 -4
  8. package/docs/contracts/validation.md +1 -1
  9. package/docs/contributing/harness-routing-internals.md +1 -1
  10. package/docs/contributing/learnings.md +21 -0
  11. package/docs/contributing/test-matrix.md +1 -1
  12. package/docs/glossary.md +1 -1
  13. package/docs/guides/agent-skills.md +1 -1
  14. package/docs/operations/backup-and-restore.md +2 -2
  15. package/docs/reference/cli-reference.md +31 -74
  16. package/docs/reference/config-reference.md +83 -118
  17. package/docs/reference/harness-routing.md +15 -8
  18. package/examples/project/.kxm/models/critic-claude.yaml +6 -2
  19. package/examples/project/.kxm/models/critic-gemini.yaml +6 -2
  20. package/examples/project/.kxm/models/critic-grok.yaml +6 -2
  21. package/examples/project/.kxm/models/implementation.yaml +6 -2
  22. package/examples/project/.kxm/models/primary.yaml +6 -2
  23. package/examples/project/.kxm/roles/planner.yaml +8 -0
  24. package/examples/project/.kxm/roles/reviewer-arch.yaml +8 -0
  25. package/examples/project/.kxm/roles/reviewer-cli.yaml +8 -0
  26. package/examples/project/.kxm/roles/writer.yaml +8 -0
  27. package/package.json +1 -1
  28. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  29. package/plugins/kxm/dist/cli.js +1759 -1306
  30. package/plugins/kxm/dist/mcp-server.js +1 -1
  31. package/plugins/kxm/dist/runtime-supervisor.js +994 -245
  32. package/plugins/kxm/dist/runtime.js +1018 -269
  33. package/plugins/kxm/dist/server.js +55 -1
  34. package/plugins/kxm/package.json +1 -1
  35. package/plugins/kxm/skills/kxm/SKILL.md +1 -1
  36. package/plugins/kxm/skills/kxm-definitions/SKILL.md +3 -7
  37. package/plugins/kxm/src/autocomplete.ts +1 -1
  38. package/plugins/kxm/src/cli/project.ts +7 -1
  39. package/plugins/kxm/src/cli/roles.ts +46 -150
  40. package/plugins/kxm/src/cli.ts +8 -22
  41. package/plugins/kxm/src/engine.ts +24 -1
  42. package/plugins/kxm/src/init-guide-setup.ts +38 -4
  43. package/plugins/kxm/src/mcp-server.ts +1 -1
  44. package/plugins/kxm/src/permission.ts +11 -0
  45. package/plugins/kxm/src/policy-draft.mjs +56 -16
  46. package/plugins/kxm/src/project-config.ts +121 -10
  47. package/plugins/kxm/src/repair.ts +1 -0
  48. package/plugins/kxm/src/role.ts +40 -440
  49. package/plugins/kxm/src/routes.ts +28 -5
  50. package/plugins/kxm/src/template.ts +34 -1
  51. package/schemas/README.md +2 -1
  52. package/schemas/model.schema.json +111 -13
  53. package/schemas/role.schema.json +61 -31
  54. package/schemas/policy-draft/README.md +0 -17
  55. package/schemas/policy-draft/model.v2.schema.json +0 -140
  56. package/schemas/policy-draft/role.v2.schema.json +0 -91
@@ -24,14 +24,17 @@ const MODEL_KEYS = Object.freeze([
24
24
  "thinking", "tags", "capabilities", "priority", "fallbacks", "limits",
25
25
  ]);
26
26
  const ROLE_KEYS = Object.freeze([
27
- "schema", "id", "purpose", "permission", "description", "roster",
27
+ "schema", "id", "purpose", "permission", "description", "extends", "roster",
28
28
  "skills", "tools", "produces", "consumes", "policy",
29
29
  ]);
30
30
  const ORIGIN_KEYS = Object.freeze(["source", "sha256"]);
31
31
  const LIMIT_KEYS = Object.freeze(["contextTokens", "outputTokens", "timeoutMs"]);
32
32
  const TOOL_KEYS = Object.freeze(["preset", "allow", "deny"]);
33
33
  const TEMPLATE_KEYS = Object.freeze(["template", "schema"]);
34
- const POLICY_KEYS = Object.freeze(["vendorIndependenceRequired", "maxTransitions", "requiresGateVerification"]);
34
+ const POLICY_KEYS = Object.freeze(["vendorIndependenceRequired", "maxTransitions", "requiresGateVerification", "fallback"]);
35
+ const FALLBACK_KEYS = Object.freeze(["onError", "maxSwitches", "revert"]);
36
+ const FALLBACK_ERRORS = Object.freeze(["rate_limit", "transport", "provider_unavailable"]);
37
+ const FALLBACK_REVERT = Object.freeze(["next_run", "never"]);
35
38
  const ROSTER_ENTRY_KEYS = Object.freeze(["route", "effort", "mode"]);
36
39
  const CRITIC_PURPOSES = Object.freeze(["reviewer-arch", "reviewer-cli"]);
37
40
 
@@ -318,6 +321,9 @@ function validateRoleShape(document, id, label, issues) {
318
321
  if (document.id !== undefined && document.id !== id) {
319
322
  issues.push(issue("schema", "identity_mismatch", label, `declared id ${String(document.id)} does not match ${id}`));
320
323
  }
324
+ if (document.extends !== undefined && !identifier(document.extends)) {
325
+ issues.push(issue("schema", "schema_pattern", label, "extends must be a role identifier"));
326
+ }
321
327
  if (document.purpose !== undefined && !POLICY_DRAFT_PURPOSES.includes(document.purpose)) {
322
328
  issues.push(issue("schema", "unsupported_purpose", label, "purpose must be a runner role label"));
323
329
  }
@@ -382,6 +388,23 @@ function validateRoleShape(document, id, label, issues) {
382
388
  if (document.policy.maxTransitions !== undefined && (!Number.isInteger(document.policy.maxTransitions) || document.policy.maxTransitions < 1)) {
383
389
  issues.push(issue("schema", "schema_type", `${label}.policy`, "maxTransitions must be a positive integer"));
384
390
  }
391
+ if (document.policy.fallback !== undefined) {
392
+ const fallback = document.policy.fallback;
393
+ const path = `${label}.policy.fallback`;
394
+ if (!closedObject(fallback, FALLBACK_KEYS, path, issues)) return;
395
+ if (fallback.onError !== undefined) {
396
+ if (!Array.isArray(fallback.onError) || new Set(fallback.onError).size !== fallback.onError.length
397
+ || fallback.onError.some((item) => !FALLBACK_ERRORS.includes(item))) {
398
+ issues.push(issue("schema", "schema_enum", path, "onError must be unique rate_limit, transport, or provider_unavailable values"));
399
+ }
400
+ }
401
+ if (fallback.maxSwitches !== undefined && (!Number.isInteger(fallback.maxSwitches) || fallback.maxSwitches < 0)) {
402
+ issues.push(issue("schema", "schema_type", path, "maxSwitches must be an integer >= 0"));
403
+ }
404
+ if (fallback.revert !== undefined && !FALLBACK_REVERT.includes(fallback.revert)) {
405
+ issues.push(issue("schema", "schema_enum", path, "revert must be next_run or never"));
406
+ }
407
+ }
385
408
  }
386
409
  }
387
410
 
@@ -431,20 +454,15 @@ function validateModelSemantics(model, label, options, evidence, issues) {
431
454
  issues.push(issue("semantic", "pi_native_vendor_forbidden", label, "native vendor cannot use Pi"));
432
455
  }
433
456
  }
434
- const origin = model.origin;
435
- if (isObject(origin) && typeof origin.source === "string" && typeof origin.sha256 === "string") {
436
- if (!own(evidence, origin.source)) {
437
- issues.push(issue("semantic", "origin_evidence_missing", label, `missing evidence bytes for ${origin.source}`));
438
- } else if (sha256Bytes(evidence[origin.source]) !== origin.sha256) {
439
- issues.push(issue("semantic", "origin_hash_mismatch", label, "origin evidence hash does not match supplied bytes"));
440
- }
441
- }
442
- } else {
443
- if (vendor !== ceiling.provider || (typeof model.model === "string" && model.model.includes("/"))) {
444
- issues.push(issue("semantic", "native_vendor_mismatch", label, "native route vendor or model does not match the harness ceiling"));
445
- }
446
- if (own(model, "origin")) {
447
- issues.push(issue("semantic", "origin_unexpected", label, "origin is only valid for Pi routes"));
457
+ } else if (vendor !== ceiling.provider || (typeof model.model === "string" && model.model.includes("/"))) {
458
+ issues.push(issue("semantic", "native_vendor_mismatch", label, "native route vendor or model does not match the harness ceiling"));
459
+ }
460
+ const origin = model.origin;
461
+ if (isObject(origin) && typeof origin.source === "string" && typeof origin.sha256 === "string") {
462
+ if (!own(evidence, origin.source)) {
463
+ issues.push(issue("semantic", "origin_evidence_missing", label, `missing evidence bytes for ${origin.source}`));
464
+ } else if (sha256Bytes(evidence[origin.source]) !== origin.sha256) {
465
+ issues.push(issue("semantic", "origin_hash_mismatch", label, "origin evidence hash does not match supplied bytes"));
448
466
  }
449
467
  }
450
468
  }
@@ -501,6 +519,27 @@ function validateRoleSemantics(role, id, label, models, options, issues) {
501
519
  }
502
520
  }
503
521
 
522
+ function validateExtends(roles, issues) {
523
+ for (const [id, role] of roles) {
524
+ if (role.extends === undefined) continue;
525
+ const label = `roles/${id}`;
526
+ if (!roles.has(role.extends)) {
527
+ issues.push(issue("reference", "role_extends_unknown", label, `extends names unknown role ${role.extends}`));
528
+ continue;
529
+ }
530
+ const seen = new Set([id]);
531
+ let current = role.extends;
532
+ while (typeof current === "string" && current.length > 0) {
533
+ if (seen.has(current)) {
534
+ issues.push(issue("semantic", "role_extends_cycle", label, `extends cycle: ${[...seen, current].join(" -> ")}`));
535
+ break;
536
+ }
537
+ seen.add(current);
538
+ current = roles.get(current)?.extends;
539
+ }
540
+ }
541
+ }
542
+
504
543
  function validateCriticVendors(roles, models, options, issues) {
505
544
  const aliases = options.vendorAliases ?? {};
506
545
  const critics = [];
@@ -569,6 +608,7 @@ export function validatePolicyDraft(input, options) {
569
608
  normalized.id = entry.id;
570
609
  roles.set(entry.id, normalized);
571
610
  }
611
+ validateExtends(roles, issues);
572
612
  validateCriticVendors(roles, models, options, issues);
573
613
  if (issues.length > 0) return { ok: false, issues: sortIssues(issues) };
574
614
 
@@ -9,6 +9,8 @@ import {
9
9
  parseRestrictedYaml as parseRestrictedYamlShared,
10
10
  type KxmYamlLimits,
11
11
  } from "./restricted-yaml.mjs";
12
+ import { pathToFileURL } from "node:url";
13
+ import { validatePolicyDraft } from "./policy-draft.mjs";
12
14
  import { resolveKxmTemplateBaseline } from "./template.ts";
13
15
  import { findKxmRepoRoot } from "./repo-root.ts";
14
16
  import { BUILTIN_HARNESS_IDS, DEFAULT_HARNESS, validateHarnessModelPair } from "./harness.ts";
@@ -38,7 +40,7 @@ export class KxmConfigError extends Error {
38
40
  }
39
41
  }
40
42
 
41
- export type KxmResourceKind = "project" | "repository" | "agent" | "model" | "environment" | "workflow" | "gate-registry";
43
+ export type KxmResourceKind = "project" | "repository" | "agent" | "model" | "role" | "environment" | "workflow" | "gate-registry";
42
44
 
43
45
  export interface KxmResource {
44
46
  kind: KxmResourceKind;
@@ -54,6 +56,7 @@ export interface KxmProjectBundle {
54
56
  repositories: ReadonlyMap<string, KxmResource>;
55
57
  agents: ReadonlyMap<string, KxmResource>;
56
58
  models: ReadonlyMap<string, KxmResource>;
59
+ roles: ReadonlyMap<string, KxmResource>;
57
60
  workflows: ReadonlyMap<string, KxmResource>;
58
61
  environments: readonly KxmResource[];
59
62
  gateRegistry?: KxmResource;
@@ -91,7 +94,8 @@ const RESOURCE_SCHEMA: Readonly<Record<KxmResourceKind, { identity: string; file
91
94
  project: { identity: "kxm.project.v1", file: "project.schema.json" },
92
95
  repository: { identity: "kxm.repository.v1", file: "repository.schema.json" },
93
96
  agent: { identity: "kxm.agent.v1", file: "agent.schema.json" },
94
- model: { identity: "kxm.model.v1", file: "model.schema.json" },
97
+ model: { identity: "kxm.model.v2", file: "model.schema.json" },
98
+ role: { identity: "kxm.role.v2", file: "role.schema.json" },
95
99
  environment: { identity: "kxm.environment.v1", file: "environment.schema.json" },
96
100
  workflow: { identity: "kxm.workflow.v1", file: "workflow.schema.json" },
97
101
  "gate-registry": { identity: "kxm.gate-registry.v1", file: "gate-registry.schema.json" },
@@ -516,7 +520,7 @@ function listNamedResources(
516
520
  root: string,
517
521
  directory: string,
518
522
  logicalDirectory: string,
519
- kind: "agent" | "model" | "workflow",
523
+ kind: "agent" | "model" | "role" | "workflow",
520
524
  replacedId?: string,
521
525
  ): Map<string, KxmResource> {
522
526
  const resources = new Map<string, KxmResource>();
@@ -658,7 +662,7 @@ function modelCandidates(selectorValue: JsonValue | undefined, models: ReadonlyM
658
662
  return [{
659
663
  key: `profile:${profile}`,
660
664
  profile,
661
- ...(stringValue(resource.value.provider) ? { provider: stringValue(resource.value.provider)! } : {}),
665
+ ...(stringValue(resource.value.provider) || stringValue(resource.value.vendor) ? { provider: (stringValue(resource.value.provider) ?? stringValue(resource.value.vendor))! } : {}),
662
666
  ...(stringValue(resource.value.model) ? { model: stringValue(resource.value.model)! } : {}),
663
667
  }];
664
668
  }
@@ -671,7 +675,7 @@ function modelCandidates(selectorValue: JsonValue | undefined, models: ReadonlyM
671
675
  .map((resource) => ({
672
676
  key: `profile:${resource.id ?? "unknown"}`,
673
677
  ...(resource.id ? { profile: resource.id } : {}),
674
- ...(stringValue(resource.value.provider) ? { provider: stringValue(resource.value.provider)! } : {}),
678
+ ...(stringValue(resource.value.provider) || stringValue(resource.value.vendor) ? { provider: (stringValue(resource.value.provider) ?? stringValue(resource.value.vendor))! } : {}),
675
679
  ...(stringValue(resource.value.model) ? { model: stringValue(resource.value.model)! } : {}),
676
680
  }));
677
681
  }
@@ -1208,10 +1212,26 @@ function validateBundle(
1208
1212
  const rosterEntries = valuesOf(roleObj ?? {}, "roster")
1209
1213
  .map((candidate) => objectValue(candidate))
1210
1214
  .filter((entry): entry is JsonObject => Boolean(entry));
1211
- const enabledRosterModels = rosterEntries
1212
- .filter((entry) => entry.enabled !== false)
1213
- .map((entry) => stringValue(entry.model))
1214
- .filter((m): m is string => Boolean(m));
1215
+ const enabledRosterModels = rosterEntries.flatMap((entry) => {
1216
+ const direct = stringValue(entry.model);
1217
+ const route = stringValue(entry.route);
1218
+ const named: string[] = direct ? [direct] : [];
1219
+ if (route) {
1220
+ const modelFile = join(projectRoot, ".kxm", "models", `${route}.yaml`);
1221
+ if (existsSync(modelFile)) {
1222
+ try {
1223
+ const modelDoc = objectValue(parseRestrictedYaml(readFileSync(modelFile, "utf8"), modelFile));
1224
+ const model = modelDoc ? stringValue(modelDoc.model) : undefined;
1225
+ const vendor = modelDoc ? stringValue(modelDoc.vendor) : undefined;
1226
+ const harness = modelDoc ? stringValue(modelDoc.harness) : undefined;
1227
+ if (model) named.push(model);
1228
+ if (vendor && model) named.push(`${vendor}/${model}`);
1229
+ if (harness && model) named.push(`${harness}/${model}`);
1230
+ } catch { /* schema issues are reported by resource validation */ }
1231
+ }
1232
+ }
1233
+ return named;
1234
+ });
1215
1235
 
1216
1236
  const agentModelObj = objectValue(implementerAgent.value.model);
1217
1237
  const agentModelStr = stringValue(implementerAgent.value.model);
@@ -1233,9 +1253,94 @@ function validateBundle(
1233
1253
  }
1234
1254
  }
1235
1255
 
1256
+ if (projectRoot && existsSync(join(projectRoot, ".kxm", "roster.yaml"))) {
1257
+ issues.push(...developerRolePolicyIssues(projectRoot));
1258
+ }
1259
+
1236
1260
  return sortIssues(issues);
1237
1261
  }
1238
1262
 
1263
+ function developerCeilings(): {
1264
+ ok: true;
1265
+ ROUTES: Record<string, { provider: string; permissions: readonly string[]; models?: readonly string[]; roles?: readonly string[]; efforts?: readonly string[] }>;
1266
+ NATIVE_PI_BRAKE_PROVIDERS: string[];
1267
+ PI_ALLOWED_PROVIDERS: string[];
1268
+ PI_NATIVE_VENDOR_PROVIDERS: Record<string, string>;
1269
+ } | { ok: false; detail: string } {
1270
+ const script = join(findKxmRepoRoot(import.meta.url), "scripts", "harness-run.mjs");
1271
+ let loaded;
1272
+ try {
1273
+ loaded = spawnSync(process.execPath, ["--input-type=module", "-e", `const m = await import(${JSON.stringify(pathToFileURL(script).href)}); process.stdout.write(JSON.stringify({ROUTES:m.ROUTES,NATIVE_PI_BRAKE_PROVIDERS:m.NATIVE_PI_BRAKE_PROVIDERS,PI_ALLOWED_PROVIDERS:m.PI_ALLOWED_PROVIDERS,PI_NATIVE_VENDOR_PROVIDERS:m.PI_NATIVE_VENDOR_PROVIDERS}))`], {
1274
+ encoding: "utf8",
1275
+ timeout: 15000,
1276
+ });
1277
+ } catch (error) {
1278
+ return { ok: false, detail: error instanceof Error ? error.message : String(error) };
1279
+ }
1280
+ if (loaded.error) {
1281
+ const code = (loaded.error as NodeJS.ErrnoException).code;
1282
+ const detail = code === "ETIMEDOUT" || loaded.signal ? `timed out after 15s (${loaded.error.message})` : loaded.error.message;
1283
+ return { ok: false, detail };
1284
+ }
1285
+ if (loaded.status !== 0 || !loaded.stdout) {
1286
+ const stderr = (loaded.stderr ?? "").trim();
1287
+ return { ok: false, detail: stderr || `scripts/harness-run.mjs exited ${loaded.status}` };
1288
+ }
1289
+ try {
1290
+ return { ok: true, ...JSON.parse(loaded.stdout) as { ROUTES: Record<string, { provider: string; permissions: readonly string[]; models?: readonly string[]; roles?: readonly string[]; efforts?: readonly string[] }>; NATIVE_PI_BRAKE_PROVIDERS: string[]; PI_ALLOWED_PROVIDERS: string[]; PI_NATIVE_VENDOR_PROVIDERS: Record<string, string> } };
1291
+ } catch (error) {
1292
+ return { ok: false, detail: `ceilings JSON parse failed: ${error instanceof Error ? error.message : String(error)}` };
1293
+ }
1294
+ }
1295
+
1296
+ function developerRolePolicyIssues(projectRoot: string): KxmConfigIssue[] {
1297
+ const rolesDir = join(projectRoot, ".kxm", "roles");
1298
+ const modelsDir = join(projectRoot, ".kxm", "models");
1299
+ const roles: Record<string, JsonObject> = {};
1300
+ const models: Record<string, JsonObject> = {};
1301
+ const evidence: Record<string, string> = {};
1302
+ const parseIssues: KxmConfigIssue[] = [];
1303
+ const readMap = (dir: string, into: Record<string, JsonObject>, skipInventory: boolean) => {
1304
+ if (!existsSync(dir)) return;
1305
+ for (const name of readdirSync(dir)) {
1306
+ if (!name.endsWith(".yaml")) continue;
1307
+ const id = basename(name, ".yaml");
1308
+ if (skipInventory && id === "inventory") continue;
1309
+ try {
1310
+ const value = parseRestrictedYaml(readFileSync(join(dir, name), "utf8"), `${dir}/${name}`);
1311
+ into[id] = value;
1312
+ } catch (error) {
1313
+ if (error instanceof KxmConfigError) {
1314
+ parseIssues.push(...error.issues);
1315
+ continue;
1316
+ }
1317
+ }
1318
+ }
1319
+ };
1320
+ readMap(rolesDir, roles, false);
1321
+ readMap(modelsDir, models, true);
1322
+ for (const model of Object.values(models)) {
1323
+ const origin = objectValue(model.origin);
1324
+ const source = origin ? stringValue(origin.source) : undefined;
1325
+ if (!source || evidence[source] !== undefined) continue;
1326
+ const evidenceFile = isAbsolute(source) ? source : join(projectRoot, source);
1327
+ if (existsSync(evidenceFile)) evidence[source] = readFileSync(evidenceFile, "utf8");
1328
+ }
1329
+ const ceilings = developerCeilings();
1330
+ if (!ceilings.ok) {
1331
+ return [...parseIssues, issue("semantic", "developer_ceilings_unavailable", ".kxm/roster.yaml", `developer ceilings could not be loaded: ${ceilings.detail}`)];
1332
+ }
1333
+ const result = validatePolicyDraft({ models, roles, evidence }, {
1334
+ ceilings: ceilings.ROUTES,
1335
+ nativePiBrakeProviders: ceilings.NATIVE_PI_BRAKE_PROVIDERS,
1336
+ piAllowedProviders: ceilings.PI_ALLOWED_PROVIDERS,
1337
+ piNativeVendorProviders: ceilings.PI_NATIVE_VENDOR_PROVIDERS,
1338
+ vendorAliases: { "x-ai": "xai", moonshotai: "moonshot", "google-ai": "google", qwen: "alibaba" },
1339
+ });
1340
+ if (result.ok) return parseIssues;
1341
+ return [...parseIssues, ...result.issues.map((entry) => issue(entry.phase, entry.code, entry.file, entry.message))];
1342
+ }
1343
+
1239
1344
  export function kxmCanonicalJson(value: JsonValue): string {
1240
1345
  if (value === null || typeof value !== "object") return JSON.stringify(value);
1241
1346
  if (Array.isArray(value)) return `[${value.map((candidate) => kxmCanonicalJson(candidate)).join(",")}]`;
@@ -1385,6 +1490,11 @@ function loadProjectBundle(
1385
1490
 
1386
1491
  const agents = listNamedResources(registry, root, join(root, ".kxm", "agents"), ".kxm/agents", "agent");
1387
1492
  const models = listNamedResources(registry, root, join(root, ".kxm", "models"), ".kxm/models", "model");
1493
+ const roles = listNamedResources(registry, root, join(root, ".kxm", "roles"), ".kxm/roles", "role", writerRole === undefined ? undefined : "writer");
1494
+ if (writerRole !== undefined) {
1495
+ const logicalPath = ".kxm/roles/writer.yaml";
1496
+ roles.set("writer", { kind: "role", id: "writer", file: join(root, logicalPath), logicalPath, value: resourceValue(registry, writerRole, logicalPath, "role") });
1497
+ }
1388
1498
  const workflows = listNamedResources(registry, root, join(root, ".kxm", "workflows"), ".kxm/workflows", "workflow", workflowCandidate?.id);
1389
1499
  if (workflowCandidate) {
1390
1500
  const { id, document } = workflowCandidate;
@@ -1514,7 +1624,7 @@ function loadProjectBundle(
1514
1624
 
1515
1625
  const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root, writerRole);
1516
1626
  if (issues.length > 0) throw new KxmConfigError(issues);
1517
- const resources = [project, ...repositories.values(), ...agents.values(), ...models.values(), ...workflows.values(), ...environments, ...(gateRegistry ? [gateRegistry] : [])]
1627
+ const resources = [project, ...repositories.values(), ...agents.values(), ...models.values(), ...roles.values(), ...workflows.values(), ...environments, ...(gateRegistry ? [gateRegistry] : [])]
1518
1628
  .sort((left, right) => compareCodeUnits(left.logicalPath, right.logicalPath));
1519
1629
  return {
1520
1630
  projectRoot: root,
@@ -1522,6 +1632,7 @@ function loadProjectBundle(
1522
1632
  repositories,
1523
1633
  agents,
1524
1634
  models,
1635
+ roles,
1525
1636
  workflows,
1526
1637
  environments,
1527
1638
  ...(gateRegistry ? { gateRegistry } : {}),
@@ -48,6 +48,7 @@ function resourceKindForTemplatePath(path: string): KxmResourceKind {
48
48
  if (path.endsWith("repo.yaml")) return "repository";
49
49
  if (path.startsWith(".kxm/agents/")) return "agent";
50
50
  if (path.startsWith(".kxm/models/")) return "model";
51
+ if (path.startsWith(".kxm/roles/")) return "role";
51
52
  if (path.startsWith(".kxm/workflows/")) return "workflow";
52
53
  return "environment";
53
54
  }