@kontextmind/kxm 0.7.130 → 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 +22 -1
- package/docs/concepts/architecture.md +1 -1
- package/docs/contracts/routing.md +3 -3
- package/docs/contributing/assignment-runner.md +14 -15
- package/docs/contributing/development.md +19 -5
- 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 +4 -4
- package/docs/reference/cli-reference.md +26 -8
- package/docs/reference/config-reference.md +76 -153
- 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 +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +287 -249
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime-supervisor.js +222 -242
- package/plugins/kxm/dist/runtime.js +224 -244
- 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/project.ts +4 -4
- 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-supervisor.ts +0 -18
- 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
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
* - `.kxm/workflows/<slug>.yaml` (kxm.workflow.v1)
|
|
13
13
|
* - `.kxm/roles/<role-slug>.yaml` (kxm.role.v2)
|
|
14
14
|
* - `.kxm/models/<route-id>.yaml` (kxm.model.v2)
|
|
15
|
+
* - `plans/evidence/route-guide-qwen-pi.md` when a Pi binding matches that note
|
|
15
16
|
* - admitted selectors appended to `.kxm/routes.yaml`
|
|
16
|
-
* It never writes retired legacy authority (`.kxm/config
|
|
17
|
-
* `.kxm/roster.json`)
|
|
17
|
+
* It never writes retired legacy authority (`.kxm/config` or
|
|
18
|
+
* `.kxm/roster.json`). Role and model files are the dispatch authority.
|
|
18
19
|
*
|
|
19
20
|
* Guide research ids are not dispatch ids. Only the admitted map below is
|
|
20
21
|
* written, and only when that harness is authenticated. Unmapped ids are skipped.
|
|
@@ -28,8 +29,7 @@
|
|
|
28
29
|
* admission decision says so.
|
|
29
30
|
*/
|
|
30
31
|
|
|
31
|
-
import {
|
|
32
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
32
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
33
33
|
import { dirname, join } from "node:path";
|
|
34
34
|
import { stringify } from "yaml";
|
|
35
35
|
import { type HarnessInventory } from "./harness.ts";
|
|
@@ -446,13 +446,12 @@ function isWriterRole(roleSlug: string): boolean {
|
|
|
446
446
|
return WRITER_ROLE_PATTERN.test(roleSlug);
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
function agentDocument(roleSlug: string, stage: GuideStage
|
|
449
|
+
function agentDocument(roleSlug: string, stage: GuideStage): Record<string, unknown> {
|
|
450
450
|
const writer = isWriterRole(roleSlug);
|
|
451
451
|
return {
|
|
452
452
|
schema: "kxm.agent.v1",
|
|
453
453
|
purpose: `${stage.domain} (workflow-guide ${stage.title}; candidate verification is the operator's responsibility)`,
|
|
454
|
-
|
|
455
|
-
model: { provider: binding.provider, model: binding.model },
|
|
454
|
+
role: roleSlug,
|
|
456
455
|
tools: { preset: writer ? "workspace-writer" : "read-only" },
|
|
457
456
|
defaultRepositoryAccess: writer ? "none" : "read",
|
|
458
457
|
repositories: { control: writer ? "write" : "read" },
|
|
@@ -485,26 +484,78 @@ function workflowDocument(workflow: GuideWorkflow): Record<string, unknown> {
|
|
|
485
484
|
};
|
|
486
485
|
}
|
|
487
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Evidence written into the user's project for the one admitted guided Pi model.
|
|
489
|
+
* The hash is of these exact bytes. A Pi binding that is not this vendor, model
|
|
490
|
+
* id, and harness gets no model file and no origin.
|
|
491
|
+
*/
|
|
492
|
+
const GUIDE_PI_EVIDENCE_SOURCE = "plans/evidence/route-guide-qwen-pi.md";
|
|
493
|
+
const GUIDE_PI_EVIDENCE_SHA256 = "109f39728b251dd0565e275ae689a6e1d9b889a488d996b157d500c538115759";
|
|
494
|
+
const GUIDE_PI_NOTE = {
|
|
495
|
+
harness: "pi",
|
|
496
|
+
vendor: "openrouter",
|
|
497
|
+
model: "qwen/qwen3-coder-plus",
|
|
498
|
+
reason: "Qwen has no supported native harness on this runner, and the operator admitted that exact model for guided setup.",
|
|
499
|
+
provingCommand: "pi auth check --provider openrouter",
|
|
500
|
+
} as const;
|
|
501
|
+
|
|
502
|
+
const GUIDE_PI_EVIDENCE_NOTE = `---
|
|
503
|
+
schema: "kxm.doc.v1"
|
|
504
|
+
id: "RES-GUIDE-QWEN-PI"
|
|
505
|
+
type: "research"
|
|
506
|
+
title: "Research: guided setup Pi Qwen origin"
|
|
507
|
+
project: "kxm"
|
|
508
|
+
status: "approved"
|
|
509
|
+
owner: "@operator"
|
|
510
|
+
created: "2026-09-26"
|
|
511
|
+
updated: "2026-09-26"
|
|
512
|
+
authority: "evidence"
|
|
513
|
+
confidence: "verified"
|
|
514
|
+
summary: "Guided setup pins every Pi model origin to this note so a later project.yaml edit cannot change the hash."
|
|
515
|
+
tags: ["routing", "qwen", "pi", "init-guide"]
|
|
516
|
+
related: []
|
|
517
|
+
details:
|
|
518
|
+
research_status: "complete"
|
|
519
|
+
target_decision_date: "2026-09-26"
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
# route-guide-qwen-pi
|
|
523
|
+
|
|
524
|
+
Dated 2026-09-26. Route ids are the guided role slugs. Harness \`${GUIDE_PI_NOTE.harness}\`. Model id \`${GUIDE_PI_NOTE.model}\`. Vendor \`${GUIDE_PI_NOTE.vendor}\`. Admitted because ${GUIDE_PI_NOTE.reason} Auth was proved with \`${GUIDE_PI_NOTE.provingCommand}\`.
|
|
525
|
+
`;
|
|
526
|
+
|
|
527
|
+
function guidePiNoteMatches(binding: AgentBinding): boolean {
|
|
528
|
+
return binding.harness === GUIDE_PI_NOTE.harness
|
|
529
|
+
&& binding.provider === GUIDE_PI_NOTE.vendor
|
|
530
|
+
&& binding.model === GUIDE_PI_NOTE.model;
|
|
531
|
+
}
|
|
532
|
+
|
|
488
533
|
/**
|
|
489
534
|
* Render the planned KXM resource files (`.kxm/agents/*.yaml`,
|
|
490
|
-
* `.kxm/workflows/*.yaml
|
|
491
|
-
*
|
|
535
|
+
* `.kxm/workflows/*.yaml`, and the Pi evidence note when a binding matches it).
|
|
536
|
+
* The Pi origin is the hash of `plans/evidence/route-guide-qwen-pi.md` written
|
|
537
|
+
* into the project, whether or not `.kxm/project.yaml` exists.
|
|
492
538
|
*/
|
|
493
539
|
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;
|
|
498
540
|
const files: GuideSetupFile[] = [];
|
|
499
541
|
const roleStages = new Map<string, GuideStage>();
|
|
500
542
|
for (const workflow of plan.workflows) {
|
|
501
543
|
for (const stage of workflow.stages) roleStages.set(stage.role, stage);
|
|
502
544
|
}
|
|
545
|
+
let evidenceQueued = false;
|
|
503
546
|
for (const [role, binding] of [...plan.agents.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
504
547
|
const stage = roleStages.get(role);
|
|
505
548
|
if (!stage) continue;
|
|
506
549
|
const routeId = role.replaceAll("_", "-");
|
|
507
550
|
const writer = isWriterRole(role);
|
|
551
|
+
const piMatch = binding.harness === "pi" && guidePiNoteMatches(binding);
|
|
552
|
+
if (binding.harness === "pi" && !piMatch) {
|
|
553
|
+
files.push({
|
|
554
|
+
path: join(projectRoot, ".kxm", "agents", `${role}.yaml`),
|
|
555
|
+
content: stringify(agentDocument(role, stage)),
|
|
556
|
+
});
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
508
559
|
const modelDocument: Record<string, unknown> = {
|
|
509
560
|
schema: "kxm.model.v2",
|
|
510
561
|
id: routeId,
|
|
@@ -514,10 +565,19 @@ export function renderGuideSetupFiles(projectRoot: string, plan: GuideSetupPlan)
|
|
|
514
565
|
status: "admitted",
|
|
515
566
|
permissions: [writer ? "edit" : "read-only"],
|
|
516
567
|
};
|
|
517
|
-
if (
|
|
568
|
+
if (piMatch) {
|
|
569
|
+
modelDocument.origin = { source: GUIDE_PI_EVIDENCE_SOURCE, sha256: GUIDE_PI_EVIDENCE_SHA256 };
|
|
570
|
+
if (!evidenceQueued) {
|
|
571
|
+
files.push({
|
|
572
|
+
path: join(projectRoot, GUIDE_PI_EVIDENCE_SOURCE),
|
|
573
|
+
content: GUIDE_PI_EVIDENCE_NOTE,
|
|
574
|
+
});
|
|
575
|
+
evidenceQueued = true;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
518
578
|
files.push({
|
|
519
579
|
path: join(projectRoot, ".kxm", "agents", `${role}.yaml`),
|
|
520
|
-
content: stringify(agentDocument(role, stage
|
|
580
|
+
content: stringify(agentDocument(role, stage)),
|
|
521
581
|
});
|
|
522
582
|
files.push({
|
|
523
583
|
path: join(projectRoot, ".kxm", "models", `${routeId}.yaml`),
|
|
@@ -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.131";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -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;
|
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
type KxmRuntimeContext,
|
|
29
29
|
} from "./runtime-service.ts";
|
|
30
30
|
import { createKxmOneShotProducer } from "./oneshot-producer.ts";
|
|
31
|
-
import { isRouteAdmitted } from "./routes.ts";
|
|
32
31
|
import { KxmRunScheduler, createKxmSimulatedProducer, recordDriveReceipt, recoverKxmRun, kxmDrivePollProjection } from "./engine.ts";
|
|
33
32
|
import { kxmDriveSession, kxmOpenDriveSessions } from "./runtime-owner.ts";
|
|
34
33
|
import { RuntimeHubClient, HubHttpError, type SyncPushResponse } from "./client.ts";
|
|
@@ -919,23 +918,6 @@ async function startKxmRuntimeSupervisorInner(
|
|
|
919
918
|
? createKxmOneShotProducer({
|
|
920
919
|
projectRoot,
|
|
921
920
|
timeoutMs: kxmProjectAdmissionLimits(bundle).agentStepTimeoutMs,
|
|
922
|
-
defaultHarness: String(bundle.project.value.defaultHarness ?? "pi"),
|
|
923
|
-
resolveHarness: (agentId) => {
|
|
924
|
-
const agent = bundle.agents.get(agentId);
|
|
925
|
-
return typeof agent?.value.harness === "string" ? agent.value.harness : undefined;
|
|
926
|
-
},
|
|
927
|
-
resolveModel: (agentId) => {
|
|
928
|
-
const agent = bundle.agents.get(agentId);
|
|
929
|
-
const model = agent?.value.model;
|
|
930
|
-
if (!model || typeof model !== "object" || Array.isArray(model)) return undefined;
|
|
931
|
-
const value = model as Record<string, unknown>;
|
|
932
|
-
const provider = typeof value.provider === "string" ? value.provider : undefined;
|
|
933
|
-
const modelName = typeof value.model === "string" ? value.model : undefined;
|
|
934
|
-
if (!provider || !modelName || !isRouteAdmitted(projectRoot, `${provider}/${modelName}`)) {
|
|
935
|
-
throw new Error("producer_route_not_admitted");
|
|
936
|
-
}
|
|
937
|
-
return { provider, model: modelName };
|
|
938
|
-
},
|
|
939
921
|
})
|
|
940
922
|
: createKxmSimulatedProducer(async () => {
|
|
941
923
|
if (delayMs > 0) {
|
|
@@ -191,10 +191,10 @@ export function suggestWorkflowAndRoles(
|
|
|
191
191
|
supported: true,
|
|
192
192
|
prerequisites: [
|
|
193
193
|
"Run kxm init in the target repository if needed, then install the exact template with the suggested command. If that workflow ID already exists, stop and review it; the commands below apply only to a newly installed template, not an existing definition with potentially different agents or permissions.",
|
|
194
|
-
...roles.map((role) => `Set
|
|
194
|
+
...roles.map((role) => `Set role on .kxm/agents/${role.agent}.yaml to a role whose roster names an admitted route for harness ${role.harness}. Admit that route's selector in .kxm/routes.yaml and ensure it is not disabled; this recommendation does not choose a model or change routing.`),
|
|
195
195
|
...(writeStep ? [
|
|
196
196
|
`Keep each writer step at assignments.maximum: 1 and set limits.maxConcurrentRuns: 1 in .kxm/project.yaml; the live ${selected.id} writer profile requires a lone writer in the checkout.`,
|
|
197
|
-
`
|
|
197
|
+
`The writer role roster must name a route whose harness is ${selected.id}, whose status is admitted, and whose permissions contain edit.`,
|
|
198
198
|
"Configure the test gate in .kxm/gates.yaml with kind: command and argv for this repository's actual verification command. A scaffold/example command or simulation is not evidence that the implementation works.",
|
|
199
199
|
] : []),
|
|
200
200
|
`Validate the installed workflow with kxm gate validate --file .kxm/workflows/${bestPattern.id}.yaml and the project configuration with kxm init --dry-run before creating a run.`,
|
|
@@ -175,21 +175,9 @@ function coreTemplate(projectId: string, projectName: string, variant: KxmTempla
|
|
|
175
175
|
]);
|
|
176
176
|
if (variant === "v4-registry") {
|
|
177
177
|
const coordinator = files.get(".kxm/agents/coordinator.yaml");
|
|
178
|
-
if (coordinator) {
|
|
179
|
-
files.set(".kxm/agents/coordinator.yaml", {
|
|
180
|
-
...coordinator,
|
|
181
|
-
harness: "claude",
|
|
182
|
-
model: { provider: "anthropic", model: "fable" },
|
|
183
|
-
});
|
|
184
|
-
}
|
|
178
|
+
if (coordinator) files.set(".kxm/agents/coordinator.yaml", { ...coordinator, role: "planner" });
|
|
185
179
|
const implementer = files.get(".kxm/agents/implementer.yaml");
|
|
186
|
-
if (implementer) {
|
|
187
|
-
files.set(".kxm/agents/implementer.yaml", {
|
|
188
|
-
...implementer,
|
|
189
|
-
harness: "grok",
|
|
190
|
-
model: { provider: "xai", model: "grok-4.6" },
|
|
191
|
-
});
|
|
192
|
-
}
|
|
180
|
+
if (implementer) files.set(".kxm/agents/implementer.yaml", { ...implementer, role: "writer" });
|
|
193
181
|
const workflow = files.get(".kxm/workflows/default.yaml");
|
|
194
182
|
if (workflow) {
|
|
195
183
|
const limits = { ...(workflow.limits as JsonObject) };
|
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
"model": { "$ref": "common.schema.json#/$defs/modelSelector" },
|
|
13
13
|
"executor": { "$ref": "common.schema.json#/$defs/identifier" },
|
|
14
14
|
"harness": { "$ref": "common.schema.json#/$defs/identifier" },
|
|
15
|
+
"role": { "$ref": "common.schema.json#/$defs/identifier" },
|
|
16
|
+
"effort": { "enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max"] },
|
|
17
|
+
"skills": {
|
|
18
|
+
"type": "array",
|
|
19
|
+
"items": { "$ref": "common.schema.json#/$defs/identifier" },
|
|
20
|
+
"maxItems": 64,
|
|
21
|
+
"uniqueItems": true
|
|
22
|
+
},
|
|
15
23
|
"tools": { "$ref": "common.schema.json#/$defs/toolPolicy" },
|
|
16
24
|
"defaultRepositoryAccess": { "$ref": "common.schema.json#/$defs/repositoryAccess" },
|
|
17
25
|
"repositories": {
|
|
@@ -7,7 +7,6 @@ export interface RosterRoute {
|
|
|
7
7
|
readonly status: 'admitted' | 'retired';
|
|
8
8
|
}
|
|
9
9
|
export interface RosterPolicy {
|
|
10
|
-
readonly schema: 'kxm.developer-roster.v1';
|
|
11
10
|
readonly routes: Readonly<Record<string, RosterRoute>>;
|
|
12
11
|
readonly lineup: Readonly<Record<string, readonly string[]>>;
|
|
13
12
|
readonly required_critics: Readonly<Record<'review-arch' | 'review-cli', string>>;
|
|
@@ -15,9 +14,11 @@ export interface RosterPolicy {
|
|
|
15
14
|
}
|
|
16
15
|
export interface PolicyIdentity { readonly commit: string; readonly blob: string; readonly sha256: string }
|
|
17
16
|
export interface LoadedPolicy { readonly identity: PolicyIdentity; readonly policy: RosterPolicy }
|
|
17
|
+
export function refuseRetiredRosterFile(root: string): void;
|
|
18
18
|
export function loadTrustedRosterPolicy(): LoadedPolicy;
|
|
19
19
|
export function resolveBoundPolicy(identity: PolicyIdentity): LoadedPolicy;
|
|
20
20
|
export function canonicalVendor(value: string): string;
|
|
21
|
+
export function assembleRosterPolicy(models: readonly Record<string, unknown>[], roles: readonly Record<string, unknown>[]): RosterPolicy;
|
|
21
22
|
export function validateRosterDocument(
|
|
22
23
|
policy: RosterPolicy,
|
|
23
24
|
readBlob: (source: string) => string | Uint8Array | null | undefined,
|