@kontextmind/kxm 0.7.128 → 0.7.131
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/README.md +4 -1
- package/.kxm/agents/coordinator.yaml +1 -0
- package/.kxm/agents/critic-arch.yaml +1 -4
- package/.kxm/agents/critic-cli.yaml +1 -4
- package/.kxm/agents/implementer.yaml +1 -4
- package/.kxm/workflows/implement-only.yaml +1 -1
- package/.kxm/workflows/review-arch-only.yaml +1 -1
- package/.kxm/workflows/review-cli-only.yaml +1 -1
- package/CHANGELOG.md +42 -1
- package/docs/architecture/inventory.md +1 -1
- package/docs/concepts/architecture.md +1 -1
- package/docs/concepts/data-and-storage.md +1 -1
- package/docs/contracts/README.md +1 -1
- package/docs/contracts/migration.md +12 -8
- package/docs/contracts/routing.md +3 -3
- package/docs/contributing/assignment-runner.md +14 -15
- package/docs/contributing/development.md +22 -6
- package/docs/contributing/harness-routing-internals.md +8 -8
- package/docs/glossary.md +1 -1
- package/docs/guides/agent-skills.md +1 -1
- package/docs/operations/backup-and-restore.md +7 -7
- package/docs/operations/upgrade.md +2 -2
- package/docs/reference/cli-reference.md +37 -14
- package/docs/reference/config-reference.md +81 -155
- package/docs/reference/harness-routing.md +29 -21
- package/docs/reference/workflow-catalog.md +4 -4
- package/docs/start/first-workflow.md +1 -1
- package/examples/project/.kxm/agents/coordinator.yaml +1 -2
- package/examples/project/.kxm/agents/critic-1.yaml +1 -3
- package/examples/project/.kxm/agents/critic-2.yaml +1 -2
- package/examples/project/.kxm/agents/critic-3.yaml +2 -3
- package/examples/project/.kxm/agents/implementer.yaml +1 -2
- package/examples/project/.kxm/agents/planner.yaml +1 -2
- package/examples/project/.kxm/agents/reproducer.yaml +1 -2
- package/examples/project/.kxm/agents/reviewer.yaml +1 -2
- package/examples/project/.kxm/workflows/fix.yaml +0 -12
- package/package.json +8 -8
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +2565 -465
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +683 -348
- package/plugins/kxm/dist/runtime.js +694 -358
- package/plugins/kxm/dist/server.js +17 -14
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm/SKILL.md +1 -1
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +8 -0
- package/plugins/kxm/src/cli/lanes.ts +109 -4
- package/plugins/kxm/src/cli/project.ts +6 -6
- package/plugins/kxm/src/database.ts +25 -19
- package/plugins/kxm/src/engine.ts +168 -163
- package/plugins/kxm/src/init-guide-setup.ts +75 -15
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/oneshot-producer.ts +3 -15
- package/plugins/kxm/src/project-config.ts +83 -70
- package/plugins/kxm/src/routes.ts +26 -18
- package/plugins/kxm/src/runtime-service.ts +1 -0
- package/plugins/kxm/src/runtime-store.ts +390 -77
- package/plugins/kxm/src/runtime-supervisor.ts +87 -21
- package/plugins/kxm/src/suggest.ts +2 -2
- package/plugins/kxm/src/template.ts +2 -14
- package/schemas/agent.schema.json +8 -0
- package/scripts/roster-policy.d.mts +2 -1
- package/scripts/roster-policy.mjs +119 -22
- package/scripts/run-bounded.mjs +53 -0
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
export interface KxmOneShotProducerOptions {
|
|
30
30
|
projectRoot?: string | undefined;
|
|
31
31
|
evidenceRoot?: string | undefined;
|
|
32
|
-
defaultHarness?: string | undefined;
|
|
33
32
|
defaultModel?: string | undefined;
|
|
34
33
|
defaultProvider?: string | undefined;
|
|
35
34
|
inventory?: HarnessInventory | undefined;
|
|
@@ -39,9 +38,7 @@ export interface KxmOneShotProducerOptions {
|
|
|
39
38
|
timeoutMs?: number | undefined;
|
|
40
39
|
spawnProcess?: KxmOneShotSpawn | undefined;
|
|
41
40
|
probeHarness?: ((options: Omit<HarnessAssignmentProbeOptions, "runCommand"> & { signal?: AbortSignal | undefined }) => HarnessStatus | Promise<HarnessStatus>) | undefined;
|
|
42
|
-
resolveHarness?: ((agentId: string, runId: string) => string | undefined) | undefined;
|
|
43
41
|
resolveModel?: ((agentId: string, runId: string) => {
|
|
44
|
-
harness?: string | undefined;
|
|
45
42
|
provider?: string | undefined;
|
|
46
43
|
model?: string | undefined;
|
|
47
44
|
thinking?: string | undefined;
|
|
@@ -66,21 +63,12 @@ function determineOutcome(text: string, allowedOutcomes: readonly string[]): str
|
|
|
66
63
|
}
|
|
67
64
|
|
|
68
65
|
export function createKxmOneShotProducer(options: KxmOneShotProducerOptions = {}): KxmOneShotProducer {
|
|
69
|
-
const defaultHarness = options.defaultHarness ?? "claude";
|
|
70
66
|
const running = new Map<AbortController, Promise<KxmProducerResult>>();
|
|
71
67
|
let closed = false;
|
|
72
68
|
|
|
73
|
-
function
|
|
69
|
+
function requireHarness(request: KxmProducerRequest): string {
|
|
74
70
|
if (request.harness) return request.harness;
|
|
75
|
-
|
|
76
|
-
const resolved = options.resolveHarness(request.agentId, request.runId);
|
|
77
|
-
if (resolved) return resolved;
|
|
78
|
-
}
|
|
79
|
-
if (options.resolveModel) {
|
|
80
|
-
const resolved = options.resolveModel(request.agentId, request.runId);
|
|
81
|
-
if (resolved?.harness) return resolved.harness;
|
|
82
|
-
}
|
|
83
|
-
return defaultHarness;
|
|
71
|
+
throw new Error("producer_harness_required");
|
|
84
72
|
}
|
|
85
73
|
|
|
86
74
|
function parseModelString(spec: string, harness: string): { provider: string; model: string } {
|
|
@@ -155,7 +143,7 @@ export function createKxmOneShotProducer(options: KxmOneShotProducerOptions = {}
|
|
|
155
143
|
|
|
156
144
|
async function executeRequest(request: KxmProducerRequest): Promise<KxmProducerResult> {
|
|
157
145
|
const startTime = Date.now();
|
|
158
|
-
const harness =
|
|
146
|
+
const harness = requireHarness(request);
|
|
159
147
|
const resolved = resolveModelForRequest(request, harness);
|
|
160
148
|
|
|
161
149
|
if (closed) throw new Error("oneshot_producer_closed");
|
|
@@ -13,7 +13,7 @@ import { pathToFileURL } from "node:url";
|
|
|
13
13
|
import { validatePolicyDraft } from "./policy-draft.mjs";
|
|
14
14
|
import { resolveKxmTemplateBaseline } from "./template.ts";
|
|
15
15
|
import { findKxmRepoRoot } from "./repo-root.ts";
|
|
16
|
-
import { BUILTIN_HARNESS_IDS, DEFAULT_HARNESS
|
|
16
|
+
import { BUILTIN_HARNESS_IDS, DEFAULT_HARNESS } from "./harness.ts";
|
|
17
17
|
|
|
18
18
|
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
|
19
19
|
export type JsonObject = { [key: string]: JsonValue };
|
|
@@ -522,6 +522,7 @@ function listNamedResources(
|
|
|
522
522
|
logicalDirectory: string,
|
|
523
523
|
kind: "agent" | "model" | "role" | "workflow",
|
|
524
524
|
replacedId?: string,
|
|
525
|
+
collected?: KxmConfigIssue[],
|
|
525
526
|
): Map<string, KxmResource> {
|
|
526
527
|
const resources = new Map<string, KxmResource>();
|
|
527
528
|
if (!existsSync(directory)) return resources;
|
|
@@ -561,7 +562,10 @@ function listNamedResources(
|
|
|
561
562
|
else throw error;
|
|
562
563
|
}
|
|
563
564
|
}
|
|
564
|
-
if (issues.length > 0)
|
|
565
|
+
if (issues.length > 0) {
|
|
566
|
+
if (collected) collected.push(...issues);
|
|
567
|
+
else throw new KxmConfigError(issues);
|
|
568
|
+
}
|
|
565
569
|
return resources;
|
|
566
570
|
}
|
|
567
571
|
|
|
@@ -925,6 +929,9 @@ function validateWorkflow(
|
|
|
925
929
|
for (const repositoryId of Object.keys(objectValue(step.repositories) ?? {})) {
|
|
926
930
|
if (!repositories.has(repositoryId)) issues.push(issue("reference", "repository_unknown", file, `${stepId} references unknown repository ${repositoryId}`));
|
|
927
931
|
}
|
|
932
|
+
if ((kind === "agent" || kind === "moa") && step.model !== undefined) {
|
|
933
|
+
issues.push(issue("semantic", "producer_route_unsupported", file, `${stepId} model is not honored; remove model from the step`));
|
|
934
|
+
}
|
|
928
935
|
selectorCandidates(step.model, models, file, `${stepId}.model`, issues);
|
|
929
936
|
|
|
930
937
|
const assignment = objectValue(step.assignments);
|
|
@@ -1128,7 +1135,7 @@ function validateBundle(
|
|
|
1128
1135
|
options: KxmConfigOptions,
|
|
1129
1136
|
gateRegistry?: KxmResource,
|
|
1130
1137
|
projectRoot?: string,
|
|
1131
|
-
|
|
1138
|
+
roles: ReadonlyMap<string, KxmResource> = new Map(),
|
|
1132
1139
|
): KxmConfigIssue[] {
|
|
1133
1140
|
const issues: KxmConfigIssue[] = [];
|
|
1134
1141
|
const entries = valuesOf(project.value, "repositories").map((candidate) => objectValue(candidate)).filter((candidate): candidate is JsonObject => Boolean(candidate));
|
|
@@ -1173,28 +1180,17 @@ function validateBundle(
|
|
|
1173
1180
|
if (!harnesses.has(defaultHarness)) issues.push(issue("reference", "harness_unknown", project.logicalPath, `defaultHarness ${defaultHarness} is not registered`));
|
|
1174
1181
|
|
|
1175
1182
|
for (const agent of agents.values()) {
|
|
1183
|
+
if (agent.value.model !== undefined || agent.value.harness !== undefined) {
|
|
1184
|
+
issues.push(issue("semantic", "retired_agent_routing_fields", agent.logicalPath, "routing resolves from role; remove model and harness"));
|
|
1185
|
+
}
|
|
1176
1186
|
validateToolPolicy(agent, objectValue(agent.value.tools), "tools", issues);
|
|
1177
1187
|
const executor = stringValue(agent.value.executor);
|
|
1178
1188
|
if (executor && !executors.has(executor)) issues.push(issue("reference", "executor_unknown", agent.logicalPath, `executor ${executor} is not registered`));
|
|
1179
|
-
const harness = stringValue(agent.value.harness) ?? defaultHarness;
|
|
1180
|
-
if (!harnesses.has(harness)) issues.push(issue("reference", "harness_unknown", agent.logicalPath, `harness ${harness} is not registered`));
|
|
1181
1189
|
const preset = stringValue(objectValue(agent.value.tools)?.preset);
|
|
1182
1190
|
if (preset && !presets.has(preset)) issues.push(issue("reference", "tool_preset_unknown", agent.logicalPath, `tool preset ${preset} is not registered`));
|
|
1183
1191
|
for (const repositoryId of Object.keys(objectValue(agent.value.repositories) ?? {})) {
|
|
1184
1192
|
if (!repositoryIds.has(repositoryId)) issues.push(issue("reference", "repository_unknown", agent.logicalPath, `references unknown repository ${repositoryId}`));
|
|
1185
1193
|
}
|
|
1186
|
-
const candidates = selectorCandidates(agent.value.model, models, agent.logicalPath, "model", issues);
|
|
1187
|
-
const declaredHarness = stringValue(agent.value.harness);
|
|
1188
|
-
if (declaredHarness && harnesses.has(declaredHarness)) {
|
|
1189
|
-
for (const candidate of candidates) {
|
|
1190
|
-
const candidateProvider = stringValue(candidate.value.provider);
|
|
1191
|
-
const candidateModel = stringValue(candidate.value.model);
|
|
1192
|
-
const validation = validateHarnessModelPair(declaredHarness, { provider: candidateProvider, model: candidateModel });
|
|
1193
|
-
if (!validation.valid) {
|
|
1194
|
-
issues.push(issue("semantic", validation.issue ?? "harness_unhosted_model", agent.logicalPath, validation.message ?? `harness ${declaredHarness} cannot host model ${candidateModel ?? candidate.logicalPath}`));
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
1194
|
}
|
|
1199
1195
|
validateModelReferences(models, issues);
|
|
1200
1196
|
|
|
@@ -1202,62 +1198,73 @@ function validateBundle(
|
|
|
1202
1198
|
if (!workflows.has(defaultWorkflow)) issues.push(issue("reference", "default_workflow_unknown", project.logicalPath, `default workflow ${defaultWorkflow} does not exist`));
|
|
1203
1199
|
for (const workflow of workflows.values()) validateWorkflow(workflow, agents, models, repositoryIds, gates, issues);
|
|
1204
1200
|
|
|
1201
|
+
// Developer policy is the role files under .kxm/roles/ (with the model
|
|
1202
|
+
// files they name). scripts/harness-run.mjs only supplies ceilings.
|
|
1203
|
+
if (projectRoot && existsSync(join(projectRoot, ".kxm", "roster.yaml"))) {
|
|
1204
|
+
issues.push(issue("semantic", "retired_roster_file", ".kxm/roster.yaml", "create the role and model files and delete roster.yaml"));
|
|
1205
|
+
}
|
|
1206
|
+
if (projectRoot && existsSync(join(projectRoot, ".kxm", "roles"))) {
|
|
1207
|
+
issues.push(...developerRolePolicyIssues(projectRoot));
|
|
1208
|
+
}
|
|
1205
1209
|
if (projectRoot) {
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1210
|
+
issues.push(...rosterModelIssues(projectRoot, roles));
|
|
1211
|
+
issues.push(...originHashIssues(projectRoot, models));
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
return sortIssues(issues);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function rosterModelIssues(projectRoot: string, roles: ReadonlyMap<string, KxmResource>): KxmConfigIssue[] {
|
|
1218
|
+
const issues: KxmConfigIssue[] = [];
|
|
1219
|
+
for (const role of roles.values()) {
|
|
1220
|
+
for (const entry of valuesOf(role.value, "roster")) {
|
|
1221
|
+
const route = stringValue(objectValue(entry)?.route);
|
|
1222
|
+
if (!route) continue;
|
|
1223
|
+
const logical = `.kxm/models/${route}.yaml`;
|
|
1224
|
+
const file = join(projectRoot, ".kxm", "models", `${route}.yaml`);
|
|
1225
|
+
let harness: string | undefined;
|
|
1209
1226
|
try {
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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
|
-
});
|
|
1235
|
-
|
|
1236
|
-
const agentModelObj = objectValue(implementerAgent.value.model);
|
|
1237
|
-
const agentModelStr = stringValue(implementerAgent.value.model);
|
|
1238
|
-
const agentProvider = agentModelObj ? stringValue(agentModelObj.provider) : undefined;
|
|
1239
|
-
const agentModel = agentModelObj ? stringValue(agentModelObj.model) : agentModelStr;
|
|
1240
|
-
const canonicalAgentModel = agentProvider && agentModel ? `${agentProvider}/${agentModel}` : agentModel;
|
|
1241
|
-
|
|
1242
|
-
if (enabledRosterModels.length > 0 && canonicalAgentModel) {
|
|
1243
|
-
const matches = enabledRosterModels.some((rm) => rm === canonicalAgentModel || rm === agentModel || rm.endsWith(`/${agentModel}`));
|
|
1244
|
-
if (!matches) {
|
|
1245
|
-
issues.push(issue("semantic", "role_roster_conflicts_with_agent", ".kxm/roles/writer.yaml", `role roster in .kxm/roles/writer.yaml does not include agent model ${canonicalAgentModel} from ${implementerAgent.logicalPath}`));
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
} catch (error) {
|
|
1249
|
-
if (error instanceof KxmConfigError) {
|
|
1250
|
-
issues.push(...error.issues);
|
|
1251
|
-
}
|
|
1227
|
+
if (existsSync(file)) harness = stringValue(parseRestrictedYaml(readFileSync(file, "utf8"), logical).harness);
|
|
1228
|
+
} catch {
|
|
1229
|
+
harness = undefined;
|
|
1230
|
+
}
|
|
1231
|
+
if (!harness) {
|
|
1232
|
+
issues.push(issue("semantic", "roster_model_unreadable", role.logicalPath, `roster route ${route} does not resolve to a readable ${logical} carrying harness`));
|
|
1252
1233
|
}
|
|
1253
1234
|
}
|
|
1254
1235
|
}
|
|
1236
|
+
return issues;
|
|
1237
|
+
}
|
|
1255
1238
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1239
|
+
function originHashIssues(projectRoot: string, models: ReadonlyMap<string, KxmResource>): KxmConfigIssue[] {
|
|
1240
|
+
const issues: KxmConfigIssue[] = [];
|
|
1241
|
+
for (const model of models.values()) {
|
|
1242
|
+
const origin = objectValue(model.value.origin);
|
|
1243
|
+
const source = origin ? stringValue(origin.source) : undefined;
|
|
1244
|
+
const digest = origin ? stringValue(origin.sha256) : undefined;
|
|
1245
|
+
if (!source || !digest) continue;
|
|
1246
|
+
const file = resolve(projectRoot, source);
|
|
1247
|
+
const escaped = relative(projectRoot, file);
|
|
1248
|
+
if (isAbsolute(source) || escaped.startsWith("..") || isAbsolute(escaped)) {
|
|
1249
|
+
issues.push(issue("semantic", "origin_evidence_missing", model.logicalPath, `missing evidence bytes for ${source}`));
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
let actual: string;
|
|
1253
|
+
try {
|
|
1254
|
+
actual = createHash("sha256").update(readFileSync(file, "utf8"), "utf8").digest("hex");
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
1257
|
+
if (code === "ENOENT" || code === "EACCES" || code === "EPERM" || code === "EISDIR") {
|
|
1258
|
+
issues.push(issue("semantic", "origin_evidence_missing", model.logicalPath, `missing evidence bytes for ${source}`));
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
throw error;
|
|
1262
|
+
}
|
|
1263
|
+
if (actual !== digest) {
|
|
1264
|
+
issues.push(issue("semantic", "origin_hash_mismatch", model.logicalPath, "origin evidence hash does not match supplied bytes"));
|
|
1265
|
+
}
|
|
1258
1266
|
}
|
|
1259
|
-
|
|
1260
|
-
return sortIssues(issues);
|
|
1267
|
+
return issues;
|
|
1261
1268
|
}
|
|
1262
1269
|
|
|
1263
1270
|
function developerCeilings(): {
|
|
@@ -1294,6 +1301,10 @@ function developerCeilings(): {
|
|
|
1294
1301
|
}
|
|
1295
1302
|
|
|
1296
1303
|
function developerRolePolicyIssues(projectRoot: string): KxmConfigIssue[] {
|
|
1304
|
+
// Product checkouts also have .kxm/roles/ for dispatch. The ceiling check
|
|
1305
|
+
// is this package's developer policy, so it runs when the loaded project
|
|
1306
|
+
// is that package.
|
|
1307
|
+
if (resolve(projectRoot) !== findKxmRepoRoot(import.meta.url)) return [];
|
|
1297
1308
|
const rolesDir = join(projectRoot, ".kxm", "roles");
|
|
1298
1309
|
const modelsDir = join(projectRoot, ".kxm", "models");
|
|
1299
1310
|
const roles: Record<string, JsonObject> = {};
|
|
@@ -1314,6 +1325,7 @@ function developerRolePolicyIssues(projectRoot: string): KxmConfigIssue[] {
|
|
|
1314
1325
|
parseIssues.push(...error.issues);
|
|
1315
1326
|
continue;
|
|
1316
1327
|
}
|
|
1328
|
+
throw error;
|
|
1317
1329
|
}
|
|
1318
1330
|
}
|
|
1319
1331
|
};
|
|
@@ -1328,7 +1340,7 @@ function developerRolePolicyIssues(projectRoot: string): KxmConfigIssue[] {
|
|
|
1328
1340
|
}
|
|
1329
1341
|
const ceilings = developerCeilings();
|
|
1330
1342
|
if (!ceilings.ok) {
|
|
1331
|
-
return [...parseIssues, issue("semantic", "developer_ceilings_unavailable", ".kxm/
|
|
1343
|
+
return [...parseIssues, issue("semantic", "developer_ceilings_unavailable", ".kxm/models", `developer ceilings could not be loaded: ${ceilings.detail}`)];
|
|
1332
1344
|
}
|
|
1333
1345
|
const result = validatePolicyDraft({ models, roles, evidence }, {
|
|
1334
1346
|
ceilings: ceilings.ROUTES,
|
|
@@ -1489,7 +1501,8 @@ function loadProjectBundle(
|
|
|
1489
1501
|
if (earlyIssues.length > 0) throw new KxmConfigError(earlyIssues);
|
|
1490
1502
|
|
|
1491
1503
|
const agents = listNamedResources(registry, root, join(root, ".kxm", "agents"), ".kxm/agents", "agent");
|
|
1492
|
-
const
|
|
1504
|
+
const modelIssues: KxmConfigIssue[] = [];
|
|
1505
|
+
const models = listNamedResources(registry, root, join(root, ".kxm", "models"), ".kxm/models", "model", undefined, modelIssues);
|
|
1493
1506
|
const roles = listNamedResources(registry, root, join(root, ".kxm", "roles"), ".kxm/roles", "role", writerRole === undefined ? undefined : "writer");
|
|
1494
1507
|
if (writerRole !== undefined) {
|
|
1495
1508
|
const logicalPath = ".kxm/roles/writer.yaml";
|
|
@@ -1620,9 +1633,9 @@ function loadProjectBundle(
|
|
|
1620
1633
|
}
|
|
1621
1634
|
}
|
|
1622
1635
|
}
|
|
1623
|
-
if (loadIssues.length > 0) throw new KxmConfigError(loadIssues);
|
|
1636
|
+
if (loadIssues.length > 0) throw new KxmConfigError([...loadIssues, ...modelIssues]);
|
|
1624
1637
|
|
|
1625
|
-
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root,
|
|
1638
|
+
const issues = [...modelIssues, ...validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root, roles)];
|
|
1626
1639
|
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
1627
1640
|
const resources = [project, ...repositories.values(), ...agents.values(), ...models.values(), ...roles.values(), ...workflows.values(), ...environments, ...(gateRegistry ? [gateRegistry] : [])]
|
|
1628
1641
|
.sort((left, right) => compareCodeUnits(left.logicalPath, right.logicalPath));
|
|
@@ -5,15 +5,15 @@ import { rolePurposeForId } from "./role.ts";
|
|
|
5
5
|
|
|
6
6
|
const RETIRED_POLICY = ".kxm/producers.yaml";
|
|
7
7
|
|
|
8
|
-
export interface RoutePolicy { schema: "kxm.routes.v2"; updatedAt: string; admitted: string[]; disabled: string[];
|
|
9
|
-
const empty = (): RoutePolicy => ({ schema: "kxm.routes.v2", updatedAt: new Date().toISOString(), admitted: [], disabled: []
|
|
8
|
+
export interface RoutePolicy { schema: "kxm.routes.v2"; updatedAt: string; admitted: string[]; disabled: string[]; }
|
|
9
|
+
const empty = (): RoutePolicy => ({ schema: "kxm.routes.v2", updatedAt: new Date().toISOString(), admitted: [], disabled: [] });
|
|
10
10
|
export function loadRoutePolicy(root: string): RoutePolicy {
|
|
11
11
|
if (existsSync(join(root, RETIRED_POLICY))) throw new Error(`retired ${RETIRED_POLICY} present; use .kxm/routes.yaml (kxm.routes.v2)`);
|
|
12
12
|
const path = join(root, ".kxm", "routes.yaml");
|
|
13
13
|
if (!existsSync(path)) return empty();
|
|
14
14
|
const value = parse(readFileSync(path, "utf8")) as Partial<RoutePolicy>;
|
|
15
15
|
if (value?.schema !== "kxm.routes.v2" || !Array.isArray(value.admitted)) throw new Error("invalid .kxm/routes.yaml");
|
|
16
|
-
return { schema: "kxm.routes.v2", updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : new Date().toISOString(), admitted: value.admitted.filter((x): x is string => typeof x === "string"), disabled: Array.isArray(value.disabled) ? value.disabled.filter((x): x is string => typeof x === "string") : []
|
|
16
|
+
return { schema: "kxm.routes.v2", updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : new Date().toISOString(), admitted: value.admitted.filter((x): x is string => typeof x === "string"), disabled: Array.isArray(value.disabled) ? value.disabled.filter((x): x is string => typeof x === "string") : [] };
|
|
17
17
|
}
|
|
18
18
|
export function updateRouteState(root: string, model: string, state: "admitted" | "disabled"): RoutePolicy {
|
|
19
19
|
return setRouteState(root, model, state);
|
|
@@ -33,21 +33,7 @@ export function setRouteState(root: string, model: string, state: "admitted" | "
|
|
|
33
33
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) roleFile = parsed as Record<string, unknown>;
|
|
34
34
|
}
|
|
35
35
|
const roster = Array.isArray(roleFile.roster) ? roleFile.roster.filter((entry): entry is Record<string, unknown> => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)) : [];
|
|
36
|
-
const
|
|
37
|
-
let routeId: string | undefined;
|
|
38
|
-
if (existsSync(modelsDir)) {
|
|
39
|
-
for (const name of readdirSync(modelsDir)) {
|
|
40
|
-
if (!name.endsWith(".yaml") || name === "inventory.yaml") continue;
|
|
41
|
-
const parsedModel = parse(readFileSync(join(modelsDir, name), "utf8")) as { vendor?: unknown; model?: unknown; schema?: unknown };
|
|
42
|
-
if (parsedModel?.schema !== "kxm.model.v2") continue;
|
|
43
|
-
const vendor = typeof parsedModel.vendor === "string" ? parsedModel.vendor : "";
|
|
44
|
-
const named = typeof parsedModel.model === "string" ? parsedModel.model : "";
|
|
45
|
-
if (named === model || (vendor && `${vendor}/${named}` === model)) {
|
|
46
|
-
routeId = name.slice(0, -5);
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
36
|
+
const routeId = routeIdForInventoryModel(root, model);
|
|
51
37
|
if (!routeId) throw new Error(`unknown route: no v2 model file matches '${model}'`);
|
|
52
38
|
const existing = roster.findIndex((entry) => entry.route === routeId || entry.model === model);
|
|
53
39
|
if (removeRole) { if (existing >= 0) roster.splice(existing, 1); }
|
|
@@ -68,6 +54,28 @@ export function setRouteState(root: string, model: string, state: "admitted" | "
|
|
|
68
54
|
return policy;
|
|
69
55
|
}
|
|
70
56
|
|
|
57
|
+
/** Route-file id whose vendor/model matches an inventory selector, if one exists. */
|
|
58
|
+
export function routeIdForInventoryModel(root: string, model: string): string | undefined {
|
|
59
|
+
const modelsDir = join(root, ".kxm", "models");
|
|
60
|
+
if (!existsSync(modelsDir)) return undefined;
|
|
61
|
+
for (const name of readdirSync(modelsDir)) {
|
|
62
|
+
if (!name.endsWith(".yaml") || name === "inventory.yaml") continue;
|
|
63
|
+
const parsedModel = parse(readFileSync(join(modelsDir, name), "utf8")) as { vendor?: unknown; model?: unknown; schema?: unknown };
|
|
64
|
+
if (parsedModel?.schema !== "kxm.model.v2") continue;
|
|
65
|
+
const vendor = typeof parsedModel.vendor === "string" ? parsedModel.vendor : "";
|
|
66
|
+
const named = typeof parsedModel.model === "string" ? parsedModel.model : "";
|
|
67
|
+
if (named === model || (vendor && `${vendor}/${named}` === model)) return name.slice(0, -5);
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Role ids whose roster names the model file that matches this inventory selector. */
|
|
73
|
+
export function rolesForInventoryModel(root: string, inventoryId: string): string[] {
|
|
74
|
+
const routeId = routeIdForInventoryModel(root, inventoryId);
|
|
75
|
+
if (!routeId) return [];
|
|
76
|
+
return Object.entries(listRoleBindings(root)).filter(([, ids]) => ids.includes(routeId)).map(([role]) => role);
|
|
77
|
+
}
|
|
78
|
+
|
|
71
79
|
export function listRoleBindings(root: string): Record<string, string[]> {
|
|
72
80
|
const dir = join(root, ".kxm", "roles"); const result: Record<string, string[]> = {};
|
|
73
81
|
if (!existsSync(dir)) return result;
|
|
@@ -315,6 +315,7 @@ export function openKxmRuntimeContext(
|
|
|
315
315
|
homeRuntimeId: options.homeRuntimeId,
|
|
316
316
|
configRevision: bundle.configRevision,
|
|
317
317
|
now: options.now ?? new Date().toISOString(),
|
|
318
|
+
...(options.logger ? { logger: options.logger } : {}),
|
|
318
319
|
});
|
|
319
320
|
const eventStore = new KxmRunEventStore(join(paths.projectsDir, registration.projectKey, "run-events.db"));
|
|
320
321
|
registerKxmRuntimeHandle(eventStore.path);
|