@kontextmind/kxm 0.7.125 → 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/README.md +2 -0
- package/docs/architecture/access.md +2 -4
- package/docs/architecture/inventory.md +4 -0
- package/docs/contracts/validation.md +1 -1
- package/docs/contributing/harness-routing-internals.md +1 -1
- package/docs/contributing/learnings.md +105 -0
- package/docs/contributing/operating-rules.md +107 -0
- 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
|
@@ -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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
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.
|
|
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
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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
|
}
|