@kontextmind/kxm 0.7.130 → 0.7.132

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/README.md +4 -1
  3. package/.kxm/agents/coordinator.yaml +1 -0
  4. package/.kxm/agents/critic-arch.yaml +1 -4
  5. package/.kxm/agents/critic-cli.yaml +1 -4
  6. package/.kxm/agents/implementer.yaml +1 -4
  7. package/.kxm/workflows/implement-only.yaml +1 -1
  8. package/.kxm/workflows/review-arch-only.yaml +1 -1
  9. package/.kxm/workflows/review-cli-only.yaml +1 -1
  10. package/CHANGELOG.md +22 -1
  11. package/docs/concepts/architecture.md +1 -1
  12. package/docs/contracts/routing.md +3 -3
  13. package/docs/contributing/assignment-runner.md +14 -15
  14. package/docs/contributing/development.md +19 -5
  15. package/docs/contributing/harness-routing-internals.md +8 -8
  16. package/docs/glossary.md +1 -1
  17. package/docs/guides/agent-skills.md +1 -1
  18. package/docs/operations/backup-and-restore.md +4 -4
  19. package/docs/reference/cli-reference.md +26 -8
  20. package/docs/reference/config-reference.md +76 -153
  21. package/docs/reference/harness-routing.md +29 -21
  22. package/docs/reference/workflow-catalog.md +4 -4
  23. package/docs/start/first-workflow.md +1 -1
  24. package/examples/project/.kxm/agents/coordinator.yaml +1 -2
  25. package/examples/project/.kxm/agents/critic-1.yaml +1 -3
  26. package/examples/project/.kxm/agents/critic-2.yaml +1 -2
  27. package/examples/project/.kxm/agents/critic-3.yaml +2 -3
  28. package/examples/project/.kxm/agents/implementer.yaml +1 -2
  29. package/examples/project/.kxm/agents/planner.yaml +1 -2
  30. package/examples/project/.kxm/agents/reproducer.yaml +1 -2
  31. package/examples/project/.kxm/agents/reviewer.yaml +1 -2
  32. package/examples/project/.kxm/workflows/fix.yaml +0 -12
  33. package/package.json +1 -1
  34. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  35. package/plugins/kxm/dist/cli.js +287 -249
  36. package/plugins/kxm/dist/mcp-server.js +1 -1
  37. package/plugins/kxm/dist/runtime-supervisor.js +222 -242
  38. package/plugins/kxm/dist/runtime.js +224 -244
  39. package/plugins/kxm/package.json +1 -1
  40. package/plugins/kxm/skills/kxm/SKILL.md +1 -1
  41. package/plugins/kxm/skills/kxm-definitions/SKILL.md +8 -0
  42. package/plugins/kxm/src/cli/project.ts +4 -4
  43. package/plugins/kxm/src/engine.ts +168 -163
  44. package/plugins/kxm/src/init-guide-setup.ts +75 -15
  45. package/plugins/kxm/src/mcp-server.ts +1 -1
  46. package/plugins/kxm/src/oneshot-producer.ts +3 -15
  47. package/plugins/kxm/src/project-config.ts +83 -70
  48. package/plugins/kxm/src/routes.ts +26 -18
  49. package/plugins/kxm/src/runtime-supervisor.ts +0 -18
  50. package/plugins/kxm/src/suggest.ts +2 -2
  51. package/plugins/kxm/src/template.ts +2 -14
  52. package/schemas/agent.schema.json +8 -0
  53. package/scripts/roster-policy.d.mts +2 -1
  54. package/scripts/roster-policy.mjs +119 -22
@@ -17362,21 +17362,9 @@ function coreTemplate(projectId, projectName, variant) {
17362
17362
  ]);
17363
17363
  if (variant === "v4-registry") {
17364
17364
  const coordinator = files.get(".kxm/agents/coordinator.yaml");
17365
- if (coordinator) {
17366
- files.set(".kxm/agents/coordinator.yaml", {
17367
- ...coordinator,
17368
- harness: "claude",
17369
- model: { provider: "anthropic", model: "fable" }
17370
- });
17371
- }
17365
+ if (coordinator) files.set(".kxm/agents/coordinator.yaml", { ...coordinator, role: "planner" });
17372
17366
  const implementer = files.get(".kxm/agents/implementer.yaml");
17373
- if (implementer) {
17374
- files.set(".kxm/agents/implementer.yaml", {
17375
- ...implementer,
17376
- harness: "grok",
17377
- model: { provider: "xai", model: "grok-4.6" }
17378
- });
17379
- }
17367
+ if (implementer) files.set(".kxm/agents/implementer.yaml", { ...implementer, role: "writer" });
17380
17368
  const workflow = files.get(".kxm/workflows/default.yaml");
17381
17369
  if (workflow) {
17382
17370
  const limits = { ...workflow.limits };
@@ -18910,7 +18898,7 @@ function readTemplateProvenance(registry, root) {
18910
18898
  }
18911
18899
  return value;
18912
18900
  }
18913
- function listNamedResources(registry, root, directory, logicalDirectory, kind, replacedId) {
18901
+ function listNamedResources(registry, root, directory, logicalDirectory, kind, replacedId, collected) {
18914
18902
  const resources = /* @__PURE__ */ new Map();
18915
18903
  if (!existsSync6(directory)) return resources;
18916
18904
  const stat = lstatSync(directory);
@@ -18949,7 +18937,10 @@ function listNamedResources(registry, root, directory, logicalDirectory, kind, r
18949
18937
  else throw error;
18950
18938
  }
18951
18939
  }
18952
- if (issues.length > 0) throw new KxmConfigError(issues);
18940
+ if (issues.length > 0) {
18941
+ if (collected) collected.push(...issues);
18942
+ else throw new KxmConfigError(issues);
18943
+ }
18953
18944
  return resources;
18954
18945
  }
18955
18946
  function valuesOf(object2, field) {
@@ -19209,6 +19200,9 @@ function validateWorkflow(workflow, agents, models, repositories, gates, issues)
19209
19200
  for (const repositoryId of Object.keys(objectValue(step.repositories) ?? {})) {
19210
19201
  if (!repositories.has(repositoryId)) issues.push(issue3("reference", "repository_unknown", file, `${stepId} references unknown repository ${repositoryId}`));
19211
19202
  }
19203
+ if ((kind === "agent" || kind === "moa") && step.model !== void 0) {
19204
+ issues.push(issue3("semantic", "producer_route_unsupported", file, `${stepId} model is not honored; remove model from the step`));
19205
+ }
19212
19206
  selectorCandidates(step.model, models, file, `${stepId}.model`, issues);
19213
19207
  const assignment = objectValue(step.assignments);
19214
19208
  const declaredAgents = assignment ? names(assignment.allowedAgents) : [];
@@ -19391,7 +19385,7 @@ function validateModelReferences(models, issues) {
19391
19385
  };
19392
19386
  for (const id of models.keys()) walk(id, []);
19393
19387
  }
19394
- function validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, projectRoot, writerRole) {
19388
+ function validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, projectRoot, roles = /* @__PURE__ */ new Map()) {
19395
19389
  const issues = [];
19396
19390
  const entries = valuesOf(project.value, "repositories").map((candidate) => objectValue(candidate)).filter((candidate) => Boolean(candidate));
19397
19391
  const repositoryIds = /* @__PURE__ */ new Set();
@@ -19432,84 +19426,84 @@ function validateBundle(project, repositories, agents, models, workflows, enviro
19432
19426
  const defaultHarness = stringValue(project.value.defaultHarness) ?? DEFAULT_HARNESS;
19433
19427
  if (!harnesses.has(defaultHarness)) issues.push(issue3("reference", "harness_unknown", project.logicalPath, `defaultHarness ${defaultHarness} is not registered`));
19434
19428
  for (const agent of agents.values()) {
19429
+ if (agent.value.model !== void 0 || agent.value.harness !== void 0) {
19430
+ issues.push(issue3("semantic", "retired_agent_routing_fields", agent.logicalPath, "routing resolves from role; remove model and harness"));
19431
+ }
19435
19432
  validateToolPolicy(agent, objectValue(agent.value.tools), "tools", issues);
19436
19433
  const executor = stringValue(agent.value.executor);
19437
19434
  if (executor && !executors.has(executor)) issues.push(issue3("reference", "executor_unknown", agent.logicalPath, `executor ${executor} is not registered`));
19438
- const harness = stringValue(agent.value.harness) ?? defaultHarness;
19439
- if (!harnesses.has(harness)) issues.push(issue3("reference", "harness_unknown", agent.logicalPath, `harness ${harness} is not registered`));
19440
19435
  const preset = stringValue(objectValue(agent.value.tools)?.preset);
19441
19436
  if (preset && !presets.has(preset)) issues.push(issue3("reference", "tool_preset_unknown", agent.logicalPath, `tool preset ${preset} is not registered`));
19442
19437
  for (const repositoryId of Object.keys(objectValue(agent.value.repositories) ?? {})) {
19443
19438
  if (!repositoryIds.has(repositoryId)) issues.push(issue3("reference", "repository_unknown", agent.logicalPath, `references unknown repository ${repositoryId}`));
19444
19439
  }
19445
- const candidates = selectorCandidates(agent.value.model, models, agent.logicalPath, "model", issues);
19446
- const declaredHarness = stringValue(agent.value.harness);
19447
- if (declaredHarness && harnesses.has(declaredHarness)) {
19448
- for (const candidate of candidates) {
19449
- const candidateProvider = stringValue(candidate.value.provider);
19450
- const candidateModel = stringValue(candidate.value.model);
19451
- const validation = validateHarnessModelPair(declaredHarness, { provider: candidateProvider, model: candidateModel });
19452
- if (!validation.valid) {
19453
- issues.push(issue3("semantic", validation.issue ?? "harness_unhosted_model", agent.logicalPath, validation.message ?? `harness ${declaredHarness} cannot host model ${candidateModel ?? candidate.logicalPath}`));
19454
- }
19455
- }
19456
- }
19457
19440
  }
19458
19441
  validateModelReferences(models, issues);
19459
19442
  const defaultWorkflow = stringValue(project.value.defaultWorkflow) ?? "default";
19460
19443
  if (!workflows.has(defaultWorkflow)) issues.push(issue3("reference", "default_workflow_unknown", project.logicalPath, `default workflow ${defaultWorkflow} does not exist`));
19461
19444
  for (const workflow of workflows.values()) validateWorkflow(workflow, agents, models, repositoryIds, gates, issues);
19445
+ if (projectRoot && existsSync6(join6(projectRoot, ".kxm", "roster.yaml"))) {
19446
+ issues.push(issue3("semantic", "retired_roster_file", ".kxm/roster.yaml", "create the role and model files and delete roster.yaml"));
19447
+ }
19448
+ if (projectRoot && existsSync6(join6(projectRoot, ".kxm", "roles"))) {
19449
+ issues.push(...developerRolePolicyIssues(projectRoot));
19450
+ }
19462
19451
  if (projectRoot) {
19463
- const writerRolePath = join6(projectRoot, ".kxm", "roles", "writer.yaml");
19464
- const implementerAgent = agents.get("implementer") ?? agents.get("writer");
19465
- if ((writerRole !== void 0 || existsSync6(writerRolePath)) && implementerAgent) {
19452
+ issues.push(...rosterModelIssues(projectRoot, roles));
19453
+ issues.push(...originHashIssues(projectRoot, models));
19454
+ }
19455
+ return sortIssues3(issues);
19456
+ }
19457
+ function rosterModelIssues(projectRoot, roles) {
19458
+ const issues = [];
19459
+ for (const role of roles.values()) {
19460
+ for (const entry of valuesOf(role.value, "roster")) {
19461
+ const route = stringValue(objectValue(entry)?.route);
19462
+ if (!route) continue;
19463
+ const logical = `.kxm/models/${route}.yaml`;
19464
+ const file = join6(projectRoot, ".kxm", "models", `${route}.yaml`);
19465
+ let harness;
19466
19466
  try {
19467
- const rawRole = parseRestrictedYaml2(writerRole ?? readFileSync5(writerRolePath, "utf8"));
19468
- const roleObj = objectValue(rawRole);
19469
- const rosterEntries = valuesOf(roleObj ?? {}, "roster").map((candidate) => objectValue(candidate)).filter((entry) => Boolean(entry));
19470
- const enabledRosterModels = rosterEntries.flatMap((entry) => {
19471
- const direct = stringValue(entry.model);
19472
- const route = stringValue(entry.route);
19473
- const named = direct ? [direct] : [];
19474
- if (route) {
19475
- const modelFile = join6(projectRoot, ".kxm", "models", `${route}.yaml`);
19476
- if (existsSync6(modelFile)) {
19477
- try {
19478
- const modelDoc = objectValue(parseRestrictedYaml2(readFileSync5(modelFile, "utf8"), modelFile));
19479
- const model = modelDoc ? stringValue(modelDoc.model) : void 0;
19480
- const vendor = modelDoc ? stringValue(modelDoc.vendor) : void 0;
19481
- const harness = modelDoc ? stringValue(modelDoc.harness) : void 0;
19482
- if (model) named.push(model);
19483
- if (vendor && model) named.push(`${vendor}/${model}`);
19484
- if (harness && model) named.push(`${harness}/${model}`);
19485
- } catch {
19486
- }
19487
- }
19488
- }
19489
- return named;
19490
- });
19491
- const agentModelObj = objectValue(implementerAgent.value.model);
19492
- const agentModelStr = stringValue(implementerAgent.value.model);
19493
- const agentProvider = agentModelObj ? stringValue(agentModelObj.provider) : void 0;
19494
- const agentModel = agentModelObj ? stringValue(agentModelObj.model) : agentModelStr;
19495
- const canonicalAgentModel = agentProvider && agentModel ? `${agentProvider}/${agentModel}` : agentModel;
19496
- if (enabledRosterModels.length > 0 && canonicalAgentModel) {
19497
- const matches = enabledRosterModels.some((rm) => rm === canonicalAgentModel || rm === agentModel || rm.endsWith(`/${agentModel}`));
19498
- if (!matches) {
19499
- issues.push(issue3("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}`));
19500
- }
19501
- }
19502
- } catch (error) {
19503
- if (error instanceof KxmConfigError) {
19504
- issues.push(...error.issues);
19505
- }
19467
+ if (existsSync6(file)) harness = stringValue(parseRestrictedYaml2(readFileSync5(file, "utf8"), logical).harness);
19468
+ } catch {
19469
+ harness = void 0;
19470
+ }
19471
+ if (!harness) {
19472
+ issues.push(issue3("semantic", "roster_model_unreadable", role.logicalPath, `roster route ${route} does not resolve to a readable ${logical} carrying harness`));
19506
19473
  }
19507
19474
  }
19508
19475
  }
19509
- if (projectRoot && existsSync6(join6(projectRoot, ".kxm", "roster.yaml"))) {
19510
- issues.push(...developerRolePolicyIssues(projectRoot));
19476
+ return issues;
19477
+ }
19478
+ function originHashIssues(projectRoot, models) {
19479
+ const issues = [];
19480
+ for (const model of models.values()) {
19481
+ const origin = objectValue(model.value.origin);
19482
+ const source = origin ? stringValue(origin.source) : void 0;
19483
+ const digest = origin ? stringValue(origin.sha256) : void 0;
19484
+ if (!source || !digest) continue;
19485
+ const file = resolve3(projectRoot, source);
19486
+ const escaped = relative(projectRoot, file);
19487
+ if (isAbsolute2(source) || escaped.startsWith("..") || isAbsolute2(escaped)) {
19488
+ issues.push(issue3("semantic", "origin_evidence_missing", model.logicalPath, `missing evidence bytes for ${source}`));
19489
+ continue;
19490
+ }
19491
+ let actual;
19492
+ try {
19493
+ actual = createHash7("sha256").update(readFileSync5(file, "utf8"), "utf8").digest("hex");
19494
+ } catch (error) {
19495
+ const code = error.code;
19496
+ if (code === "ENOENT" || code === "EACCES" || code === "EPERM" || code === "EISDIR") {
19497
+ issues.push(issue3("semantic", "origin_evidence_missing", model.logicalPath, `missing evidence bytes for ${source}`));
19498
+ continue;
19499
+ }
19500
+ throw error;
19501
+ }
19502
+ if (actual !== digest) {
19503
+ issues.push(issue3("semantic", "origin_hash_mismatch", model.logicalPath, "origin evidence hash does not match supplied bytes"));
19504
+ }
19511
19505
  }
19512
- return sortIssues3(issues);
19506
+ return issues;
19513
19507
  }
19514
19508
  function developerCeilings() {
19515
19509
  const script = join6(findKxmRepoRoot(import.meta.url), "scripts", "harness-run.mjs");
@@ -19538,6 +19532,7 @@ function developerCeilings() {
19538
19532
  }
19539
19533
  }
19540
19534
  function developerRolePolicyIssues(projectRoot) {
19535
+ if (resolve3(projectRoot) !== findKxmRepoRoot(import.meta.url)) return [];
19541
19536
  const rolesDir = join6(projectRoot, ".kxm", "roles");
19542
19537
  const modelsDir = join6(projectRoot, ".kxm", "models");
19543
19538
  const roles = {};
@@ -19558,6 +19553,7 @@ function developerRolePolicyIssues(projectRoot) {
19558
19553
  parseIssues.push(...error.issues);
19559
19554
  continue;
19560
19555
  }
19556
+ throw error;
19561
19557
  }
19562
19558
  }
19563
19559
  };
@@ -19572,7 +19568,7 @@ function developerRolePolicyIssues(projectRoot) {
19572
19568
  }
19573
19569
  const ceilings = developerCeilings();
19574
19570
  if (!ceilings.ok) {
19575
- return [...parseIssues, issue3("semantic", "developer_ceilings_unavailable", ".kxm/roster.yaml", `developer ceilings could not be loaded: ${ceilings.detail}`)];
19571
+ return [...parseIssues, issue3("semantic", "developer_ceilings_unavailable", ".kxm/models", `developer ceilings could not be loaded: ${ceilings.detail}`)];
19576
19572
  }
19577
19573
  const result = validatePolicyDraft({ models, roles, evidence }, {
19578
19574
  ceilings: ceilings.ROUTES,
@@ -19680,7 +19676,8 @@ function loadProjectBundle(projectRoot, options, candidate) {
19680
19676
  }
19681
19677
  if (earlyIssues.length > 0) throw new KxmConfigError(earlyIssues);
19682
19678
  const agents = listNamedResources(registry, root, join6(root, ".kxm", "agents"), ".kxm/agents", "agent");
19683
- const models = listNamedResources(registry, root, join6(root, ".kxm", "models"), ".kxm/models", "model");
19679
+ const modelIssues = [];
19680
+ const models = listNamedResources(registry, root, join6(root, ".kxm", "models"), ".kxm/models", "model", void 0, modelIssues);
19684
19681
  const roles = listNamedResources(registry, root, join6(root, ".kxm", "roles"), ".kxm/roles", "role", writerRole === void 0 ? void 0 : "writer");
19685
19682
  if (writerRole !== void 0) {
19686
19683
  const logicalPath = ".kxm/roles/writer.yaml";
@@ -19807,8 +19804,8 @@ function loadProjectBundle(projectRoot, options, candidate) {
19807
19804
  }
19808
19805
  }
19809
19806
  }
19810
- if (loadIssues.length > 0) throw new KxmConfigError(loadIssues);
19811
- const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root, writerRole);
19807
+ if (loadIssues.length > 0) throw new KxmConfigError([...loadIssues, ...modelIssues]);
19808
+ const issues = [...modelIssues, ...validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root, roles)];
19812
19809
  if (issues.length > 0) throw new KxmConfigError(issues);
19813
19810
  const resources = [project, ...repositories.values(), ...agents.values(), ...models.values(), ...roles.values(), ...workflows.values(), ...environments, ...gateRegistry ? [gateRegistry] : []].sort((left, right) => compareCodeUnits4(left.logicalPath, right.logicalPath));
19814
19811
  return {
@@ -24724,7 +24721,7 @@ function loadRoutePolicy(root) {
24724
24721
  if (!existsSync14(path4)) return empty();
24725
24722
  const value = (0, import_yaml6.parse)(readFileSync12(path4, "utf8"));
24726
24723
  if (value?.schema !== "kxm.routes.v2" || !Array.isArray(value.admitted)) throw new Error("invalid .kxm/routes.yaml");
24727
- return { schema: "kxm.routes.v2", updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : (/* @__PURE__ */ new Date()).toISOString(), admitted: value.admitted.filter((x2) => typeof x2 === "string"), disabled: Array.isArray(value.disabled) ? value.disabled.filter((x2) => typeof x2 === "string") : [], roles: value.roles && typeof value.roles === "object" ? Object.fromEntries(Object.entries(value.roles).filter(([, v2]) => Array.isArray(v2)).map(([k2, v2]) => [k2, v2.filter((x2) => typeof x2 === "string")])) : {} };
24724
+ return { schema: "kxm.routes.v2", updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : (/* @__PURE__ */ new Date()).toISOString(), admitted: value.admitted.filter((x2) => typeof x2 === "string"), disabled: Array.isArray(value.disabled) ? value.disabled.filter((x2) => typeof x2 === "string") : [] };
24728
24725
  }
24729
24726
  function updateRouteState(root, model, state) {
24730
24727
  return setRouteState(root, model, state);
@@ -24744,21 +24741,7 @@ function setRouteState(root, model, state, role, removeRole2 = false) {
24744
24741
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) roleFile = parsed;
24745
24742
  }
24746
24743
  const roster = Array.isArray(roleFile.roster) ? roleFile.roster.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)) : [];
24747
- const modelsDir = join14(root, ".kxm", "models");
24748
- let routeId2;
24749
- if (existsSync14(modelsDir)) {
24750
- for (const name of readdirSync4(modelsDir)) {
24751
- if (!name.endsWith(".yaml") || name === "inventory.yaml") continue;
24752
- const parsedModel = (0, import_yaml6.parse)(readFileSync12(join14(modelsDir, name), "utf8"));
24753
- if (parsedModel?.schema !== "kxm.model.v2") continue;
24754
- const vendor = typeof parsedModel.vendor === "string" ? parsedModel.vendor : "";
24755
- const named = typeof parsedModel.model === "string" ? parsedModel.model : "";
24756
- if (named === model || vendor && `${vendor}/${named}` === model) {
24757
- routeId2 = name.slice(0, -5);
24758
- break;
24759
- }
24760
- }
24761
- }
24744
+ const routeId2 = routeIdForInventoryModel(root, model);
24762
24745
  if (!routeId2) throw new Error(`unknown route: no v2 model file matches '${model}'`);
24763
24746
  const existing = roster.findIndex((entry) => entry.route === routeId2 || entry.model === model);
24764
24747
  if (removeRole2) {
@@ -24779,6 +24762,24 @@ function setRouteState(root, model, state, role, removeRole2 = false) {
24779
24762
  writeFileSync10(join14(root, ".kxm", "routes.yaml"), (0, import_yaml6.stringify)(policy), "utf8");
24780
24763
  return policy;
24781
24764
  }
24765
+ function routeIdForInventoryModel(root, model) {
24766
+ const modelsDir = join14(root, ".kxm", "models");
24767
+ if (!existsSync14(modelsDir)) return void 0;
24768
+ for (const name of readdirSync4(modelsDir)) {
24769
+ if (!name.endsWith(".yaml") || name === "inventory.yaml") continue;
24770
+ const parsedModel = (0, import_yaml6.parse)(readFileSync12(join14(modelsDir, name), "utf8"));
24771
+ if (parsedModel?.schema !== "kxm.model.v2") continue;
24772
+ const vendor = typeof parsedModel.vendor === "string" ? parsedModel.vendor : "";
24773
+ const named = typeof parsedModel.model === "string" ? parsedModel.model : "";
24774
+ if (named === model || vendor && `${vendor}/${named}` === model) return name.slice(0, -5);
24775
+ }
24776
+ return void 0;
24777
+ }
24778
+ function rolesForInventoryModel(root, inventoryId) {
24779
+ const routeId2 = routeIdForInventoryModel(root, inventoryId);
24780
+ if (!routeId2) return [];
24781
+ return Object.entries(listRoleBindings(root)).filter(([, ids]) => ids.includes(routeId2)).map(([role]) => role);
24782
+ }
24782
24783
  function listRoleBindings(root) {
24783
24784
  const dir = join14(root, ".kxm", "roles");
24784
24785
  const result = {};
@@ -24790,10 +24791,6 @@ function listRoleBindings(root) {
24790
24791
  }
24791
24792
  return result;
24792
24793
  }
24793
- function isRouteAdmitted(root, modelId2) {
24794
- const policy = loadRoutePolicy(root);
24795
- return policy.admitted.includes(modelId2) && !policy.disabled.includes(modelId2);
24796
- }
24797
24794
  function listInventoryModels(root) {
24798
24795
  const path4 = join14(root, ".kxm", "models", "inventory.yaml");
24799
24796
  if (!existsSync14(path4)) return [];
@@ -24807,7 +24804,7 @@ var init_routes = __esm({
24807
24804
  import_yaml6 = __toESM(require_dist(), 1);
24808
24805
  init_role();
24809
24806
  RETIRED_POLICY = ".kxm/producers.yaml";
24810
- empty = () => ({ schema: "kxm.routes.v2", updatedAt: (/* @__PURE__ */ new Date()).toISOString(), admitted: [], disabled: [], roles: {} });
24807
+ empty = () => ({ schema: "kxm.routes.v2", updatedAt: (/* @__PURE__ */ new Date()).toISOString(), admitted: [], disabled: [] });
24811
24808
  }
24812
24809
  });
24813
24810
 
@@ -26235,103 +26232,132 @@ var init_routing = __esm({
26235
26232
  // plugins/kxm/src/engine.ts
26236
26233
  import { existsSync as existsSync17, readFileSync as readFileSync15 } from "node:fs";
26237
26234
  import { join as join17 } from "node:path";
26238
- function selectorsNamedByRoutes(projectRoot, routeIds) {
26239
- const selectors = /* @__PURE__ */ new Set();
26240
- for (const routeId2 of routeIds) {
26241
- const file = join17(projectRoot, ".kxm", "models", `${routeId2}.yaml`);
26242
- if (!existsSync17(file)) continue;
26243
- let parsed;
26244
- try {
26245
- parsed = (0, import_yaml9.parse)(readFileSync15(file, "utf8"));
26246
- } catch {
26247
- continue;
26248
- }
26249
- const model = typeof parsed.model === "string" ? parsed.model : "";
26250
- const vendor = typeof parsed.vendor === "string" ? parsed.vendor : "";
26251
- const harness = typeof parsed.harness === "string" ? parsed.harness : "";
26252
- if (model) selectors.add(model);
26253
- if (vendor && model) selectors.add(`${vendor}/${model}`);
26254
- if (harness && model) selectors.add(`${harness}/${model}`);
26235
+ function readYamlFile(file) {
26236
+ return readModelDocument(file);
26237
+ }
26238
+ function readModelDocument(file) {
26239
+ if (!existsSync17(file)) return void 0;
26240
+ try {
26241
+ const parsed = (0, import_yaml9.parse)(readFileSync15(file, "utf8"));
26242
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
26243
+ return parsed;
26244
+ } catch {
26245
+ return void 0;
26255
26246
  }
26256
- return [...selectors];
26257
26247
  }
26258
26248
  function resolveProducerRoute(projectRoot, step, agentId) {
26259
- let agentModel;
26260
- const agentFile = join17(projectRoot, ".kxm", "agents", `${agentId}.yaml`);
26261
- if (existsSync17(agentFile)) {
26262
- try {
26263
- const parsed = (0, import_yaml9.parse)(readFileSync15(agentFile, "utf8"));
26264
- if (typeof parsed?.model === "string") {
26265
- agentModel = parsed.model;
26266
- } else if (parsed?.model && typeof parsed.model === "object" && !Array.isArray(parsed.model)) {
26267
- const declared = parsed.model;
26268
- const providerOk = typeof declared.provider === "string" && declared.provider.length > 0 && !declared.provider.includes("/");
26269
- const modelOk = typeof declared.model === "string" && declared.model.length > 0 && !declared.model.startsWith("/") && !declared.model.endsWith("/");
26270
- if (providerOk && modelOk) {
26271
- agentModel = `${declared.provider}/${declared.model}`;
26272
- } else {
26273
- return {
26274
- error: {
26275
- reason: "step_unsupported",
26276
- field: "model",
26277
- detail: "producer_route_unsupported: invalid model declaration"
26278
- }
26279
- };
26280
- }
26281
- }
26282
- } catch {
26283
- }
26284
- }
26285
- let selector = typeof step.model === "string" ? step.model : agentModel;
26286
- if (!selector) {
26287
- if (agentId === "implementer") {
26288
- selector = "xai/grok-4.6";
26289
- }
26290
- }
26291
- if (!selector) {
26249
+ if ((step.kind === "agent" || step.kind === "moa") && step.model !== void 0) {
26292
26250
  return {
26293
26251
  error: {
26294
26252
  reason: "step_unsupported",
26295
26253
  field: "model",
26296
- detail: "producer_route_unsupported: agent or step has no model declared"
26254
+ detail: "producer_route_unsupported: agent step model is not honored"
26297
26255
  }
26298
26256
  };
26299
26257
  }
26300
- const slash = selector.indexOf("/");
26301
- if (slash <= 0 || slash === selector.length - 1) {
26258
+ const agent = readYamlFile(join17(projectRoot, ".kxm", "agents", `${agentId}.yaml`));
26259
+ const role = typeof agent?.role === "string" ? agent.role : "";
26260
+ if (!role) {
26302
26261
  return {
26303
26262
  error: {
26304
26263
  reason: "step_unsupported",
26305
26264
  field: "model",
26306
- detail: `producer_route_unsupported: invalid selector '${selector}'`
26265
+ detail: "producer_route_unsupported: agent has no role"
26307
26266
  }
26308
26267
  };
26309
26268
  }
26310
- if (!isRouteAdmitted(projectRoot, selector)) {
26269
+ const roleDoc = readYamlFile(join17(projectRoot, ".kxm", "roles", `${role}.yaml`));
26270
+ const roster = Array.isArray(roleDoc?.roster) ? roleDoc.roster : void 0;
26271
+ if (!roster) {
26311
26272
  return {
26312
26273
  error: {
26313
26274
  reason: "step_unsupported",
26314
26275
  field: "model",
26315
- detail: `producer_route_unsupported: model '${selector}' is not admitted`
26276
+ detail: `producer_route_unsupported: role '${role}' has no roster`
26316
26277
  }
26317
26278
  };
26318
26279
  }
26319
- const role = agentId === "implementer" ? "writer" : agentId;
26320
- const roleBindings = listRoleBindings(projectRoot);
26321
- const roster = roleBindings[role];
26322
- const rosterSelectors = roster ? selectorsNamedByRoutes(projectRoot, roster) : void 0;
26323
- if (rosterSelectors && !rosterSelectors.includes(selector)) {
26280
+ const policy = loadRoutePolicy(projectRoot);
26281
+ for (const entry of roster) {
26282
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
26283
+ const record = entry;
26284
+ if (typeof record.route !== "string" || record.route.length === 0) continue;
26285
+ const modelPath = join17(projectRoot, ".kxm", "models", `${record.route}.yaml`);
26286
+ const doc = readModelDocument(modelPath);
26287
+ if (!doc) {
26288
+ return {
26289
+ error: {
26290
+ reason: "step_unsupported",
26291
+ field: "model",
26292
+ detail: `producer_route_unsupported: model file '.kxm/models/${record.route}.yaml' is missing or unreadable`
26293
+ }
26294
+ };
26295
+ }
26296
+ if (doc.status !== "admitted") continue;
26297
+ const modelName = typeof doc.model === "string" ? doc.model : "";
26298
+ const vendor = typeof doc.vendor === "string" ? doc.vendor : "";
26299
+ const harness = typeof doc.harness === "string" ? doc.harness : "";
26300
+ if (!modelName || !vendor || !harness) continue;
26301
+ const candidates = [modelName, `${vendor}/${modelName}`, `${harness}/${modelName}`];
26302
+ const admittedSelector = candidates.find((candidate) => policy.admitted.includes(candidate));
26303
+ const disabledSelector = candidates.find((candidate) => policy.disabled.includes(candidate));
26304
+ if (!admittedSelector && disabledSelector) {
26305
+ return {
26306
+ error: {
26307
+ reason: "step_unsupported",
26308
+ field: "model",
26309
+ detail: `producer_route_unsupported: first admitted route '${record.route}' for role '${role}' is disabled`
26310
+ }
26311
+ };
26312
+ }
26313
+ const selector = admittedSelector ?? (modelName.includes("/") ? modelName : `${vendor}/${modelName}`);
26314
+ if (policy.disabled.includes(selector)) {
26315
+ return {
26316
+ error: {
26317
+ reason: "step_unsupported",
26318
+ field: "model",
26319
+ detail: `producer_route_unsupported: first admitted route '${record.route}' for role '${role}' is disabled`
26320
+ }
26321
+ };
26322
+ }
26323
+ if (!policy.admitted.includes(selector)) {
26324
+ return {
26325
+ error: {
26326
+ reason: "step_unsupported",
26327
+ field: "model",
26328
+ detail: `producer_route_unsupported: model '${selector}' is not admitted`
26329
+ }
26330
+ };
26331
+ }
26332
+ const slash = selector.indexOf("/");
26333
+ if (slash <= 0 || slash === selector.length - 1) {
26334
+ return {
26335
+ error: {
26336
+ reason: "step_unsupported",
26337
+ field: "model",
26338
+ detail: `producer_route_unsupported: invalid selector '${selector}'`
26339
+ }
26340
+ };
26341
+ }
26342
+ const permissions = Array.isArray(doc.permissions) ? doc.permissions.filter((item) => typeof item === "string") : [];
26324
26343
  return {
26325
- error: {
26326
- reason: "step_unsupported",
26327
- field: "model",
26328
- detail: `producer_route_unsupported: model '${selector}' not in role '${role}' roster`
26329
- }
26344
+ provider: selector.slice(0, slash),
26345
+ model: selector.slice(slash + 1),
26346
+ selector,
26347
+ harness,
26348
+ routeId: record.route,
26349
+ role,
26350
+ permissions,
26351
+ ...typeof record.effort === "string" ? { effort: record.effort } : {}
26330
26352
  };
26331
26353
  }
26332
- const provider = selector.slice(0, slash);
26333
- const model = selector.slice(slash + 1);
26334
- return { provider, model, selector };
26354
+ return {
26355
+ error: {
26356
+ reason: "step_unsupported",
26357
+ field: "model",
26358
+ detail: `producer_route_unsupported: role '${role}' has no admitted route`
26359
+ }
26360
+ };
26335
26361
  }
26336
26362
  function unsupportedAgentStepTimeout(step, limitMs) {
26337
26363
  if (step.kind !== "agent" && step.kind !== "moa") return void 0;
@@ -26369,26 +26395,26 @@ function kxmLiveRunPrerequisites(bundle, workflowId, projectRoot) {
26369
26395
  if (step.kind !== "agent" && step.kind !== "moa") continue;
26370
26396
  const agentIds = step.assignments.allowedAgents.length > 0 ? step.assignments.allowedAgents : [step.agent];
26371
26397
  for (const agentId of agentIds) {
26372
- const agent = bundle.agents.get(agentId);
26373
- const harness = String(agent?.value.harness ?? bundle.project.value.defaultHarness ?? "pi");
26398
+ const route = resolveProducerRoute(projectRoot, step, agentId);
26399
+ if ("error" in route) {
26400
+ const detail = route.error.detail === "producer_route_unsupported: agent step model is not honored" ? `${agentId}: agent step model is not honored; remove model from the step` : `${agentId}: ${route.error.detail}; set role in .kxm/agents/${agentId}.yaml and admit the route model with kxm routes admit --model <provider/model>`;
26401
+ prerequisites.push({ ...route.error, stepId, detail });
26402
+ continue;
26403
+ }
26404
+ const harness = route.harness;
26374
26405
  const permission = Object.values(step.repositories).includes("write") ? "edit" : "read-only";
26375
26406
  if (!BUILTIN_HARNESSES.some((entry) => entry.id === harness && entry.oneShot) || !oneShotPermissionArgs(harness, permission)) {
26376
26407
  prerequisites.push({ reason: "step_unsupported", stepId, field: "harness", detail: `${agentId}: ${harness} has no audited ${permission} one-shot profile; use a supported workflow or execute this work directly in ${harness}, without substituting another harness` });
26377
26408
  continue;
26378
26409
  }
26379
- const route = resolveProducerRoute(projectRoot, step, agentId);
26380
- if ("error" in route) {
26381
- prerequisites.push({ ...route.error, stepId, detail: `${agentId}: ${route.error.detail}; configure its model in .kxm/agents/${agentId}.yaml and admit the installed model with kxm routes admit --model <provider/model>` });
26382
- continue;
26383
- }
26384
- const writeRefusal = unsupportedLiveWrite(projectRoot, step, agentId, route.selector, envelope2.projectLimits.maxConcurrentRuns);
26410
+ const writeRefusal = unsupportedLiveWrite(projectRoot, step, route, envelope2.projectLimits.maxConcurrentRuns);
26385
26411
  if (writeRefusal) {
26386
26412
  prerequisites.push({ ...writeRefusal, stepId });
26387
26413
  continue;
26388
26414
  }
26389
26415
  const compatible = validateHarnessModelPair(harness, route);
26390
26416
  if (!compatible.valid) {
26391
- prerequisites.push({ reason: "step_unsupported", stepId, field: "model", detail: `${agentId}: ${compatible.message ?? `${harness} cannot run ${route.selector}`}; configure a model supported by ${harness} in .kxm/agents/${agentId}.yaml` });
26417
+ prerequisites.push({ reason: "step_unsupported", stepId, field: "model", detail: `${agentId}: ${compatible.message ?? `${harness} cannot run ${route.selector}`}; configure a model supported by ${harness} on the role roster` });
26392
26418
  }
26393
26419
  }
26394
26420
  }
@@ -26461,25 +26487,7 @@ function unsupportedStep(plan, step, producerId) {
26461
26487
  }
26462
26488
  return void 0;
26463
26489
  }
26464
- function readYamlRecord(path4) {
26465
- if (!existsSync17(path4)) return void 0;
26466
- try {
26467
- const parsed = (0, import_yaml9.parse)(readFileSync15(path4, "utf8"));
26468
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
26469
- } catch {
26470
- return void 0;
26471
- }
26472
- return void 0;
26473
- }
26474
- function agentHarness(projectRoot, agentId) {
26475
- const harness = readYamlRecord(join17(projectRoot, ".kxm", "agents", `${agentId}.yaml`))?.harness;
26476
- return typeof harness === "string" && harness.length > 0 ? harness : void 0;
26477
- }
26478
- function projectDefaultHarness(projectRoot) {
26479
- const harness = readYamlRecord(join17(projectRoot, ".kxm", "project.yaml"))?.defaultHarness;
26480
- return typeof harness === "string" && harness.length > 0 ? harness : "pi";
26481
- }
26482
- function unsupportedLiveWrite(projectRoot, step, agentId, selector, maxConcurrentRuns) {
26490
+ function unsupportedLiveWrite(projectRoot, step, route, maxConcurrentRuns) {
26483
26491
  if (!Object.values(step.repositories).some((access) => access === "write")) return void 0;
26484
26492
  if (step.assignments.maximum !== 1) {
26485
26493
  return {
@@ -26495,42 +26503,23 @@ function unsupportedLiveWrite(projectRoot, step, agentId, selector, maxConcurren
26495
26503
  detail: "live write steps require limits.maxConcurrentRuns of 1; concurrent runs share one checkout"
26496
26504
  };
26497
26505
  }
26498
- const harness = agentHarness(projectRoot, agentId) ?? projectDefaultHarness(projectRoot);
26499
- if (!oneShotWriterArgs(harness)) {
26506
+ if (!oneShotWriterArgs(route.harness)) {
26500
26507
  return {
26501
26508
  reason: "step_unsupported",
26502
26509
  field: "repositories",
26503
- detail: `live write steps require an audited writer profile; ${harness} has none`
26504
- };
26505
- }
26506
- const rosterPath = join17(projectRoot, ".kxm", "roster.yaml");
26507
- if (!existsSync17(rosterPath)) return void 0;
26508
- const roster = readYamlRecord(rosterPath);
26509
- if (!roster || roster.schema !== "kxm.developer-roster.v1") {
26510
- return { reason: "step_unsupported", field: "model", detail: "live write steps require a readable kxm.developer-roster.v1" };
26511
- }
26512
- const routes = roster.routes;
26513
- const lineup = roster.lineup;
26514
- const writerIds = lineup && typeof lineup === "object" && !Array.isArray(lineup) ? lineup.writer : void 0;
26515
- if (!routes || typeof routes !== "object" || Array.isArray(routes) || !Array.isArray(writerIds)) {
26516
- return { reason: "step_unsupported", field: "model", detail: "developer roster has no writer lineup" };
26517
- }
26518
- const allowed = writerIds.some((id) => {
26519
- if (typeof id !== "string") return false;
26520
- const route = routes[id];
26521
- if (!route || typeof route !== "object" || Array.isArray(route)) return false;
26522
- const record = route;
26523
- if (record.harness !== harness || record.status !== "admitted") return false;
26524
- if (!Array.isArray(record.permissions) || !record.permissions.includes("edit")) return false;
26525
- const model = typeof record.model === "string" ? record.model : "";
26526
- const vendor = typeof record.vendor === "string" ? record.vendor : "";
26527
- return model === selector || vendor.length > 0 && `${vendor}/${model}` === selector;
26528
- });
26529
- if (!allowed) {
26510
+ detail: `live write steps require an audited writer profile; ${route.harness} has none`
26511
+ };
26512
+ }
26513
+ const writerIds = listRoleBindings(projectRoot).writer;
26514
+ if (!Array.isArray(writerIds) || writerIds.length === 0) {
26515
+ return { reason: "step_unsupported", field: "model", detail: "writer roster has no admitted edit route" };
26516
+ }
26517
+ const named = writerIds.includes(route.routeId);
26518
+ if (!named || !route.permissions.includes("edit")) {
26530
26519
  return {
26531
26520
  reason: "step_unsupported",
26532
26521
  field: "model",
26533
- detail: `live write route ${selector} on ${harness} is not on the developer roster writer lineup`
26522
+ detail: `live write route ${route.selector} on ${route.harness} is not an admitted edit route on the writer roster`
26534
26523
  };
26535
26524
  }
26536
26525
  return void 0;
@@ -27063,7 +27052,6 @@ var init_runtime_supervisor = __esm({
27063
27052
  init_sqlite();
27064
27053
  init_runtime_service();
27065
27054
  init_oneshot_producer();
27066
- init_routes();
27067
27055
  init_engine();
27068
27056
  init_runtime_owner();
27069
27057
  init_client();
@@ -30445,8 +30433,7 @@ async function cmdModelsScreen(runtime) {
30445
30433
  try {
30446
30434
  while (true) {
30447
30435
  const policy = loadRoutePolicy(runtime.dirs.workdir);
30448
- const roleBindings = listRoleBindings(runtime.dirs.workdir);
30449
- runtime.io.stdout(models.map((m2, i) => `${i + 1}. ${m2} [${policy.admitted.includes(m2) ? "admitted" : policy.disabled.includes(m2) ? "disabled" : "unset"}] roles:${Object.entries(roleBindings).filter(([, xs]) => xs.includes(m2)).map(([r]) => r).join(",") || "-"}`).join("\n") + "\n");
30436
+ runtime.io.stdout(models.map((m2, i) => `${i + 1}. ${m2} [${policy.admitted.includes(m2) ? "admitted" : policy.disabled.includes(m2) ? "disabled" : "unset"}] roles:${rolesForInventoryModel(runtime.dirs.workdir, m2).join(",") || "-"}`).join("\n") + "\n");
30450
30437
  const command = (await ask("[a]dmit [d]isable [r]ole-add [x]ole-remove [q]uit: ")).trim().toLowerCase();
30451
30438
  if (command === "q" || command === "quit") return 0;
30452
30439
  const index = Number.parseInt((await ask("model number: ")).trim(), 10) - 1;
@@ -30468,7 +30455,8 @@ async function cmdModelsScreen(runtime) {
30468
30455
  }
30469
30456
  async function cmdRouteList(runtime) {
30470
30457
  const policy = loadRoutePolicy(runtime.dirs.workdir);
30471
- print(runtime.io, runtime.json, { ok: true, command: "routes list", policy }, [...policy.admitted.map((x2) => `admitted ${x2}`), ...policy.disabled.map((x2) => `disabled ${x2}`)].join("\n") || "no route decisions");
30458
+ const membership = Object.entries(listRoleBindings(runtime.dirs.workdir)).flatMap(([role, ids]) => ids.map((id) => `${role} ${id}`));
30459
+ print(runtime.io, runtime.json, { ok: true, command: "routes list", policy, membership }, [...policy.admitted.map((x2) => `admitted ${x2}`), ...policy.disabled.map((x2) => `disabled ${x2}`), ...membership].join("\n") || "no route decisions");
30472
30460
  return 0;
30473
30461
  }
30474
30462
  async function cmdRouteCount(runtime) {
@@ -37179,10 +37167,10 @@ function suggestWorkflowAndRoles(prompt, options = {}) {
37179
37167
  supported: true,
37180
37168
  prerequisites: [
37181
37169
  "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.",
37182
- ...roles.map((role) => `Set harness: ${role.harness} and model: <your authenticated compatible model selector> in .kxm/agents/${role.agent}.yaml. Admit that exact selector in .kxm/routes.yaml and ensure it is not disabled; this recommendation does not choose a model or change routing.`),
37170
+ ...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.`),
37183
37171
  ...writeStep ? [
37184
37172
  `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.`,
37185
- `If .kxm/roster.yaml exists, it must be readable kxm.developer-roster.v1 with a lineup.writer route matching harness: ${selected.id} and the selected writer model (model or vendor/model), status: admitted, and permissions containing edit.`,
37173
+ `The writer role roster must name a route whose harness is ${selected.id}, whose status is admitted, and whose permissions contain edit.`,
37186
37174
  "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."
37187
37175
  ] : [],
37188
37176
  `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.`
@@ -50874,8 +50862,8 @@ init_telemetry();
50874
50862
  init_routing();
50875
50863
  init_prices();
50876
50864
  import { spawnSync as spawnSync12 } from "node:child_process";
50877
- import { createHash as createHash20 } from "node:crypto";
50878
- import { existsSync as existsSync44, mkdtempSync as mkdtempSync3, readFileSync as readFileSync41, rmSync as rmSync13 } from "node:fs";
50865
+ import { createHash as createHash19 } from "node:crypto";
50866
+ import { existsSync as existsSync44, mkdtempSync as mkdtempSync3, readFileSync as readFileSync40, rmSync as rmSync13 } from "node:fs";
50879
50867
  import { tmpdir as tmpdir2 } from "node:os";
50880
50868
  import { basename as basename11, extname as extname3, join as join54, resolve as resolve32 } from "node:path";
50881
50869
  import { createInterface as createInterface3 } from "node:readline";
@@ -52506,8 +52494,7 @@ ${line}
52506
52494
  // plugins/kxm/src/init-guide-setup.ts
52507
52495
  var import_yaml18 = __toESM(require_dist(), 1);
52508
52496
  init_routes();
52509
- import { createHash as createHash19 } from "node:crypto";
52510
- import { existsSync as existsSync43, mkdirSync as mkdirSync32, readFileSync as readFileSync40, writeFileSync as writeFileSync28 } from "node:fs";
52497
+ import { existsSync as existsSync43, mkdirSync as mkdirSync32, writeFileSync as writeFileSync28 } from "node:fs";
52511
52498
  import { dirname as dirname24, join as join53 } from "node:path";
52512
52499
  var ADMITTED_GUIDE_BINDINGS = Object.freeze({
52513
52500
  "anthropic/claude-fable-5.1": { harness: "claude", provider: "anthropic", model: "fable" },
@@ -52852,13 +52839,12 @@ var WRITER_ROLE_PATTERN = /(writer|implementer|scaffold|backend|frontend|sdk-int
52852
52839
  function isWriterRole(roleSlug) {
52853
52840
  return WRITER_ROLE_PATTERN.test(roleSlug);
52854
52841
  }
52855
- function agentDocument(roleSlug, stage, binding) {
52842
+ function agentDocument(roleSlug, stage) {
52856
52843
  const writer = isWriterRole(roleSlug);
52857
52844
  return {
52858
52845
  schema: "kxm.agent.v1",
52859
52846
  purpose: `${stage.domain} (workflow-guide ${stage.title}; candidate verification is the operator's responsibility)`,
52860
- harness: binding.harness,
52861
- model: { provider: binding.provider, model: binding.model },
52847
+ role: roleSlug,
52862
52848
  tools: { preset: writer ? "workspace-writer" : "read-only" },
52863
52849
  defaultRepositoryAccess: writer ? "none" : "read",
52864
52850
  repositories: { control: writer ? "write" : "read" },
@@ -52889,19 +52875,62 @@ function workflowDocument(workflow) {
52889
52875
  steps
52890
52876
  };
52891
52877
  }
52878
+ var GUIDE_PI_EVIDENCE_SOURCE = "plans/evidence/route-guide-qwen-pi.md";
52879
+ var GUIDE_PI_EVIDENCE_SHA256 = "109f39728b251dd0565e275ae689a6e1d9b889a488d996b157d500c538115759";
52880
+ var GUIDE_PI_NOTE = {
52881
+ harness: "pi",
52882
+ vendor: "openrouter",
52883
+ model: "qwen/qwen3-coder-plus",
52884
+ reason: "Qwen has no supported native harness on this runner, and the operator admitted that exact model for guided setup.",
52885
+ provingCommand: "pi auth check --provider openrouter"
52886
+ };
52887
+ var GUIDE_PI_EVIDENCE_NOTE = `---
52888
+ schema: "kxm.doc.v1"
52889
+ id: "RES-GUIDE-QWEN-PI"
52890
+ type: "research"
52891
+ title: "Research: guided setup Pi Qwen origin"
52892
+ project: "kxm"
52893
+ status: "approved"
52894
+ owner: "@operator"
52895
+ created: "2026-09-26"
52896
+ updated: "2026-09-26"
52897
+ authority: "evidence"
52898
+ confidence: "verified"
52899
+ summary: "Guided setup pins every Pi model origin to this note so a later project.yaml edit cannot change the hash."
52900
+ tags: ["routing", "qwen", "pi", "init-guide"]
52901
+ related: []
52902
+ details:
52903
+ research_status: "complete"
52904
+ target_decision_date: "2026-09-26"
52905
+ ---
52906
+
52907
+ # route-guide-qwen-pi
52908
+
52909
+ 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}\`.
52910
+ `;
52911
+ function guidePiNoteMatches(binding) {
52912
+ return binding.harness === GUIDE_PI_NOTE.harness && binding.provider === GUIDE_PI_NOTE.vendor && binding.model === GUIDE_PI_NOTE.model;
52913
+ }
52892
52914
  function renderGuideSetupFiles(projectRoot, plan) {
52893
- const projectYaml = join53(projectRoot, ".kxm", "project.yaml");
52894
- const projectOrigin = existsSync43(projectYaml) ? { source: ".kxm/project.yaml", sha256: createHash19("sha256").update(readFileSync40(projectYaml)).digest("hex") } : void 0;
52895
52915
  const files = [];
52896
52916
  const roleStages = /* @__PURE__ */ new Map();
52897
52917
  for (const workflow of plan.workflows) {
52898
52918
  for (const stage of workflow.stages) roleStages.set(stage.role, stage);
52899
52919
  }
52920
+ let evidenceQueued = false;
52900
52921
  for (const [role, binding] of [...plan.agents.entries()].sort(([a], [b2]) => a.localeCompare(b2))) {
52901
52922
  const stage = roleStages.get(role);
52902
52923
  if (!stage) continue;
52903
52924
  const routeId2 = role.replaceAll("_", "-");
52904
52925
  const writer = isWriterRole(role);
52926
+ const piMatch = binding.harness === "pi" && guidePiNoteMatches(binding);
52927
+ if (binding.harness === "pi" && !piMatch) {
52928
+ files.push({
52929
+ path: join53(projectRoot, ".kxm", "agents", `${role}.yaml`),
52930
+ content: (0, import_yaml18.stringify)(agentDocument(role, stage))
52931
+ });
52932
+ continue;
52933
+ }
52905
52934
  const modelDocument = {
52906
52935
  schema: "kxm.model.v2",
52907
52936
  id: routeId2,
@@ -52911,10 +52940,19 @@ function renderGuideSetupFiles(projectRoot, plan) {
52911
52940
  status: "admitted",
52912
52941
  permissions: [writer ? "edit" : "read-only"]
52913
52942
  };
52914
- if (binding.harness === "pi" && projectOrigin) modelDocument.origin = projectOrigin;
52943
+ if (piMatch) {
52944
+ modelDocument.origin = { source: GUIDE_PI_EVIDENCE_SOURCE, sha256: GUIDE_PI_EVIDENCE_SHA256 };
52945
+ if (!evidenceQueued) {
52946
+ files.push({
52947
+ path: join53(projectRoot, GUIDE_PI_EVIDENCE_SOURCE),
52948
+ content: GUIDE_PI_EVIDENCE_NOTE
52949
+ });
52950
+ evidenceQueued = true;
52951
+ }
52952
+ }
52915
52953
  files.push({
52916
52954
  path: join53(projectRoot, ".kxm", "agents", `${role}.yaml`),
52917
- content: (0, import_yaml18.stringify)(agentDocument(role, stage, binding))
52955
+ content: (0, import_yaml18.stringify)(agentDocument(role, stage))
52918
52956
  });
52919
52957
  files.push({
52920
52958
  path: join53(projectRoot, ".kxm", "models", `${routeId2}.yaml`),
@@ -53040,7 +53078,7 @@ function applyKxmPackageUpdate(runtime, notice) {
53040
53078
  for (const step of planned) {
53041
53079
  if (step.kind === "verify") {
53042
53080
  if (!verifyReleaseAssetDigest(step.path, step.sha256)) {
53043
- const actual = existsSync44(step.path) ? createHash20("sha256").update(readFileSync41(step.path)).digest("hex") : "missing";
53081
+ const actual = existsSync44(step.path) ? createHash19("sha256").update(readFileSync40(step.path)).digest("hex") : "missing";
53044
53082
  return {
53045
53083
  ok: false,
53046
53084
  error: "release_digest_mismatch",
@@ -53323,7 +53361,7 @@ async function cmdValidate(runtime, fileFlag) {
53323
53361
  return 1;
53324
53362
  }
53325
53363
  try {
53326
- const raw = file ? readFileSync41(file, "utf8") : inline;
53364
+ const raw = file ? readFileSync40(file, "utf8") : inline;
53327
53365
  if (explicitFile && file && !raw.trimStart().startsWith("[")) {
53328
53366
  const value = parseRestrictedYaml2(raw, file);
53329
53367
  const issues = new KxmSchemaRegistry().validate("workflow", value, file);
@@ -53573,7 +53611,7 @@ async function maybeOfferCompletionInstall(runtime) {
53573
53611
  const { rcFile } = completionRcTarget(shell, { env: runtime.env });
53574
53612
  if (rcFile && existsSync44(rcFile)) {
53575
53613
  try {
53576
- if (readFileSync41(rcFile, "utf8").includes(scriptPath)) return;
53614
+ if (readFileSync40(rcFile, "utf8").includes(scriptPath)) return;
53577
53615
  } catch {
53578
53616
  }
53579
53617
  }