@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.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.130",
3
+ "version": "0.7.131",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -7,7 +7,7 @@ description: Entry point for KXM, the kxm CLI and the Claude Code plugin kxm_* M
7
7
 
8
8
  Pick the skill that owns the request, then follow it. These bundled skills
9
9
  document the current CLI. They do not switch runtime YAML authority, admit
10
- writers, or replace `.kxm/roster.yaml` trusted policy.
10
+ writers, or replace the trusted role and model policy.
11
11
 
12
12
  ## Route by request
13
13
 
@@ -10,6 +10,14 @@ references skills, constrains tools, and declares harness/model choices.
10
10
  A model appearing in a roster is not proof of authentication, capability,
11
11
  or permission to execute an assignment.
12
12
 
13
+ Dispatch reads `.kxm/roles/*.yaml` and `.kxm/models/*.yaml`. An agent binds
14
+ a role with `role:` and does not pin `harness` or `model`. An agent
15
+ `tools.preset` must be a registered preset. Narrowing that preset against
16
+ the role preset is not validated yet (P3). `kxm routes` prints
17
+ admission from `.kxm/routes.yaml` and membership from the role files.
18
+ The developer runner builds its policy object from those same files at
19
+ `refs/remotes/origin/main`.
20
+
13
21
  ## Inspect Before Editing
14
22
 
15
23
  ```bash
@@ -6,7 +6,7 @@ import { createInterface } from "node:readline";
6
6
  import { applyRestorePlan, createBackup, databaseError, planBackup, planRestore, type RestorePlan } from "../database.ts";
7
7
  import { readLiveHubClaim } from "../hub-autostart.ts";
8
8
  import { refreshModelInventory } from "../model-inventory.ts";
9
- import { listInventoryModels, listRoleBindings, loadRoutePolicy, setRouteState, updateRouteState } from "../routes.ts";
9
+ import { listInventoryModels, listRoleBindings, loadRoutePolicy, rolesForInventoryModel, setRouteState, updateRouteState } from "../routes.ts";
10
10
  import {
11
11
  KxmConfigError,
12
12
  discoverKxmProjectRoot,
@@ -889,8 +889,7 @@ export async function cmdModelsScreen(runtime: Runtime): Promise<number> {
889
889
  try {
890
890
  while (true) {
891
891
  const policy = loadRoutePolicy(runtime.dirs.workdir);
892
- const roleBindings = listRoleBindings(runtime.dirs.workdir);
893
- runtime.io.stdout(models.map((m, i) => `${i + 1}. ${m} [${policy.admitted.includes(m) ? "admitted" : policy.disabled.includes(m) ? "disabled" : "unset"}] roles:${Object.entries(roleBindings).filter(([, xs]) => xs.includes(m)).map(([r]) => r).join(",") || "-"}`).join("\n") + "\n");
892
+ runtime.io.stdout(models.map((m, i) => `${i + 1}. ${m} [${policy.admitted.includes(m) ? "admitted" : policy.disabled.includes(m) ? "disabled" : "unset"}] roles:${rolesForInventoryModel(runtime.dirs.workdir, m).join(",") || "-"}`).join("\n") + "\n");
894
893
  const command = (await ask("[a]dmit [d]isable [r]ole-add [x]ole-remove [q]uit: ")).trim().toLowerCase();
895
894
  if (command === "q" || command === "quit") return 0;
896
895
  const index = Number.parseInt((await ask("model number: ")).trim(), 10) - 1;
@@ -910,7 +909,8 @@ export async function cmdModelsScreen(runtime: Runtime): Promise<number> {
910
909
 
911
910
  export async function cmdRouteList(runtime: Runtime): Promise<number> {
912
911
  const policy = loadRoutePolicy(runtime.dirs.workdir);
913
- print(runtime.io, runtime.json, { ok: true, command: "routes list", policy }, [...policy.admitted.map((x) => `admitted ${x}`), ...policy.disabled.map((x) => `disabled ${x}`)].join("\n") || "no route decisions");
912
+ const membership = Object.entries(listRoleBindings(runtime.dirs.workdir)).flatMap(([role, ids]) => ids.map((id) => `${role} ${id}`));
913
+ print(runtime.io, runtime.json, { ok: true, command: "routes list", policy, membership }, [...policy.admitted.map((x) => `admitted ${x}`), ...policy.disabled.map((x) => `disabled ${x}`), ...membership].join("\n") || "no route decisions");
914
914
  return 0;
915
915
  }
916
916
 
@@ -2,7 +2,7 @@ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { parse } from "yaml";
5
- import { isRouteAdmitted, listRoleBindings } from "./routes.ts";
5
+ import { listRoleBindings, loadRoutePolicy } from "./routes.ts";
6
6
  import { applyAuthoringWitness, captureWorktreeWitness } from "./worktree-witness.ts";
7
7
  import {
8
8
  buildFormalContextPacket,
@@ -1342,26 +1342,30 @@ export interface KxmPanelDispatchSeams {
1342
1342
 
1343
1343
  export const kxmPanelDispatchSeams: KxmPanelDispatchSeams = {};
1344
1344
 
1345
- /** Selectors named by v2 route files: the model id, `vendor/model`, and `harness/model`. */
1346
- function selectorsNamedByRoutes(projectRoot: string, routeIds: readonly string[]): string[] {
1347
- const selectors = new Set<string>();
1348
- for (const routeId of routeIds) {
1349
- const file = join(projectRoot, ".kxm", "models", `${routeId}.yaml`);
1350
- if (!existsSync(file)) continue;
1351
- let parsed: { harness?: unknown; model?: unknown; vendor?: unknown };
1352
- try {
1353
- parsed = parse(readFileSync(file, "utf8")) as { harness?: unknown; model?: unknown; vendor?: unknown };
1354
- } catch {
1355
- continue;
1356
- }
1357
- const model = typeof parsed.model === "string" ? parsed.model : "";
1358
- const vendor = typeof parsed.vendor === "string" ? parsed.vendor : "";
1359
- const harness = typeof parsed.harness === "string" ? parsed.harness : "";
1360
- if (model) selectors.add(model);
1361
- if (vendor && model) selectors.add(`${vendor}/${model}`);
1362
- if (harness && model) selectors.add(`${harness}/${model}`);
1345
+ interface ResolvedProducerRoute {
1346
+ provider: string;
1347
+ model: string;
1348
+ selector: string;
1349
+ harness: string;
1350
+ routeId: string;
1351
+ role: string;
1352
+ permissions: string[];
1353
+ effort?: string | undefined;
1354
+ }
1355
+
1356
+ function readYamlFile(file: string): Record<string, unknown> | undefined {
1357
+ return readModelDocument(file);
1358
+ }
1359
+
1360
+ function readModelDocument(file: string): Record<string, unknown> | undefined {
1361
+ if (!existsSync(file)) return undefined;
1362
+ try {
1363
+ const parsed = parse(readFileSync(file, "utf8")) as unknown;
1364
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
1365
+ return parsed as Record<string, unknown>;
1366
+ } catch {
1367
+ return undefined;
1363
1368
  }
1364
- return [...selectors];
1365
1369
  }
1366
1370
 
1367
1371
  function unreconciledPanelAttemptId(state: KxmRunState): string | undefined {
@@ -1383,97 +1387,119 @@ function resolveProducerRoute(
1383
1387
  projectRoot: string,
1384
1388
  step: KxmCompiledStep,
1385
1389
  agentId: string,
1386
- ): { provider: string; model: string; selector: string } | { error: Omit<KxmRunHandoff, "stepId"> } {
1387
- let agentModel: string | undefined;
1388
- const agentFile = join(projectRoot, ".kxm", "agents", `${agentId}.yaml`);
1389
- if (existsSync(agentFile)) {
1390
- try {
1391
- const parsed = parse(readFileSync(agentFile, "utf8")) as Record<string, unknown>;
1392
- if (typeof parsed?.model === "string") {
1393
- agentModel = parsed.model;
1394
- } else if (parsed?.model && typeof parsed.model === "object" && !Array.isArray(parsed.model)) {
1395
- // The agent schema requires the object form (`{provider, model}`); reading only the
1396
- // string form left every agent without a route unless it was literally named
1397
- // `implementer`, which alone hit the hard-coded fallback below. A declared model
1398
- // must actually drive the producer route. Model ids may themselves be namespaced
1399
- // (`qwen/qwen3-coder-plus` under `openrouter`) — the provider is the only field
1400
- // that may not contain the separator, because it is what the selector splits on.
1401
- // A malformed declaration is an error, not an absence: falling through to the
1402
- // fallback would silently route an agent that declared `openrouter` to Grok.
1403
- const declared = parsed.model as { provider?: unknown; model?: unknown };
1404
- const providerOk = typeof declared.provider === "string" && declared.provider.length > 0 && !declared.provider.includes("/");
1405
- const modelOk = typeof declared.model === "string" && declared.model.length > 0 && !declared.model.startsWith("/") && !declared.model.endsWith("/");
1406
- if (providerOk && modelOk) {
1407
- agentModel = `${declared.provider}/${declared.model}`;
1408
- } else {
1409
- return {
1410
- error: {
1411
- reason: "step_unsupported",
1412
- field: "model",
1413
- detail: "producer_route_unsupported: invalid model declaration",
1414
- },
1415
- };
1416
- }
1417
- }
1418
- } catch {
1419
- // ignore
1420
- }
1421
- }
1422
-
1423
- let selector = typeof step.model === "string" ? step.model : agentModel;
1424
- if (!selector) {
1425
- if (agentId === "implementer") {
1426
- selector = "xai/grok-4.6";
1427
- }
1428
- }
1429
- if (!selector) {
1390
+ ): ResolvedProducerRoute | { error: Omit<KxmRunHandoff, "stepId"> } {
1391
+ if ((step.kind === "agent" || step.kind === "moa") && step.model !== undefined) {
1430
1392
  return {
1431
1393
  error: {
1432
1394
  reason: "step_unsupported",
1433
1395
  field: "model",
1434
- detail: "producer_route_unsupported: agent or step has no model declared",
1396
+ detail: "producer_route_unsupported: agent step model is not honored",
1435
1397
  },
1436
1398
  };
1437
1399
  }
1438
-
1439
- const slash = selector.indexOf("/");
1440
- if (slash <= 0 || slash === selector.length - 1) {
1400
+ const agent = readYamlFile(join(projectRoot, ".kxm", "agents", `${agentId}.yaml`));
1401
+ const role = typeof agent?.role === "string" ? agent.role : "";
1402
+ if (!role) {
1441
1403
  return {
1442
1404
  error: {
1443
1405
  reason: "step_unsupported",
1444
1406
  field: "model",
1445
- detail: `producer_route_unsupported: invalid selector '${selector}'`,
1407
+ detail: "producer_route_unsupported: agent has no role",
1446
1408
  },
1447
1409
  };
1448
1410
  }
1449
-
1450
- if (!isRouteAdmitted(projectRoot, selector)) {
1411
+ const roleDoc = readYamlFile(join(projectRoot, ".kxm", "roles", `${role}.yaml`));
1412
+ const roster = Array.isArray(roleDoc?.roster) ? roleDoc.roster : undefined;
1413
+ if (!roster) {
1451
1414
  return {
1452
1415
  error: {
1453
1416
  reason: "step_unsupported",
1454
1417
  field: "model",
1455
- detail: `producer_route_unsupported: model '${selector}' is not admitted`,
1418
+ detail: `producer_route_unsupported: role '${role}' has no roster`,
1456
1419
  },
1457
1420
  };
1458
1421
  }
1459
-
1460
- const role = agentId === "implementer" ? "writer" : agentId;
1461
- const roleBindings = listRoleBindings(projectRoot);
1462
- const roster = roleBindings[role];
1463
- const rosterSelectors = roster ? selectorsNamedByRoutes(projectRoot, roster) : undefined;
1464
- if (rosterSelectors && !rosterSelectors.includes(selector)) {
1422
+ const policy = loadRoutePolicy(projectRoot);
1423
+ for (const entry of roster) {
1424
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
1425
+ const record = entry as { route?: unknown; effort?: unknown };
1426
+ if (typeof record.route !== "string" || record.route.length === 0) continue;
1427
+ const modelPath = join(projectRoot, ".kxm", "models", `${record.route}.yaml`);
1428
+ const doc = readModelDocument(modelPath);
1429
+ if (!doc) {
1430
+ return {
1431
+ error: {
1432
+ reason: "step_unsupported",
1433
+ field: "model",
1434
+ detail: `producer_route_unsupported: model file '.kxm/models/${record.route}.yaml' is missing or unreadable`,
1435
+ },
1436
+ };
1437
+ }
1438
+ if (doc.status !== "admitted") continue;
1439
+ const modelName = typeof doc.model === "string" ? doc.model : "";
1440
+ const vendor = typeof doc.vendor === "string" ? doc.vendor : "";
1441
+ const harness = typeof doc.harness === "string" ? doc.harness : "";
1442
+ if (!modelName || !vendor || !harness) continue;
1443
+ const candidates = [modelName, `${vendor}/${modelName}`, `${harness}/${modelName}`];
1444
+ const admittedSelector = candidates.find((candidate) => policy.admitted.includes(candidate));
1445
+ const disabledSelector = candidates.find((candidate) => policy.disabled.includes(candidate));
1446
+ if (!admittedSelector && disabledSelector) {
1447
+ return {
1448
+ error: {
1449
+ reason: "step_unsupported",
1450
+ field: "model",
1451
+ detail: `producer_route_unsupported: first admitted route '${record.route}' for role '${role}' is disabled`,
1452
+ },
1453
+ };
1454
+ }
1455
+ const selector = admittedSelector ?? (modelName.includes("/") ? modelName : `${vendor}/${modelName}`);
1456
+ if (policy.disabled.includes(selector)) {
1457
+ return {
1458
+ error: {
1459
+ reason: "step_unsupported",
1460
+ field: "model",
1461
+ detail: `producer_route_unsupported: first admitted route '${record.route}' for role '${role}' is disabled`,
1462
+ },
1463
+ };
1464
+ }
1465
+ if (!policy.admitted.includes(selector)) {
1466
+ return {
1467
+ error: {
1468
+ reason: "step_unsupported",
1469
+ field: "model",
1470
+ detail: `producer_route_unsupported: model '${selector}' is not admitted`,
1471
+ },
1472
+ };
1473
+ }
1474
+ const slash = selector.indexOf("/");
1475
+ if (slash <= 0 || slash === selector.length - 1) {
1476
+ return {
1477
+ error: {
1478
+ reason: "step_unsupported",
1479
+ field: "model",
1480
+ detail: `producer_route_unsupported: invalid selector '${selector}'`,
1481
+ },
1482
+ };
1483
+ }
1484
+ const permissions = Array.isArray(doc.permissions) ? doc.permissions.filter((item): item is string => typeof item === "string") : [];
1465
1485
  return {
1466
- error: {
1467
- reason: "step_unsupported",
1468
- field: "model",
1469
- detail: `producer_route_unsupported: model '${selector}' not in role '${role}' roster`,
1470
- },
1486
+ provider: selector.slice(0, slash),
1487
+ model: selector.slice(slash + 1),
1488
+ selector,
1489
+ harness,
1490
+ routeId: record.route,
1491
+ role,
1492
+ permissions,
1493
+ ...(typeof record.effort === "string" ? { effort: record.effort } : {}),
1471
1494
  };
1472
1495
  }
1473
-
1474
- const provider = selector.slice(0, slash);
1475
- const model = selector.slice(slash + 1);
1476
- return { provider, model, selector };
1496
+ return {
1497
+ error: {
1498
+ reason: "step_unsupported",
1499
+ field: "model",
1500
+ detail: `producer_route_unsupported: role '${role}' has no admitted route`,
1501
+ },
1502
+ };
1477
1503
  }
1478
1504
 
1479
1505
  function projectAgentStepTimeoutMs(context: KxmRuntimeContext, run: KxmRunRecord): number {
@@ -1708,7 +1734,7 @@ function prepareDispatch(
1708
1734
  const wideTimeout = unsupportedAgentStepTimeout(step, projectAgentStepTimeoutMs(context, run));
1709
1735
  if (wideTimeout) return { kind: "return", state, handoff: { ...wideTimeout, stepId } };
1710
1736
 
1711
- let resolvedRoute: { provider: string; model: string; selector: string } | undefined;
1737
+ let resolvedRoute: ResolvedProducerRoute | undefined;
1712
1738
  if (producerId !== "driver-simulated") {
1713
1739
  const allowed = step.assignments.allowedAgents;
1714
1740
  const agentId = (allowed && allowed.length > 0 && allowed[0])
@@ -1722,8 +1748,7 @@ function prepareDispatch(
1722
1748
  const writeRefusal = unsupportedLiveWrite(
1723
1749
  context.projectRoot,
1724
1750
  step,
1725
- agentId,
1726
- resolvedRoute.selector,
1751
+ resolvedRoute,
1727
1752
  loadKxmRunPlanEnvelope(context.eventStore, run).projectLimits.maxConcurrentRuns,
1728
1753
  );
1729
1754
  if (writeRefusal) return { kind: "return", state, handoff: { ...writeRefusal, stepId } };
@@ -1753,7 +1778,7 @@ function prepareDispatch(
1753
1778
  for (const event of events) context.eventStore.appendEvent(event);
1754
1779
  const entered = foldStoredKxmRun(context, run);
1755
1780
  persistKxmRunState(context, runId, entered, events[events.length - 1]!.sequence);
1756
- const first = birthMember(context, {
1781
+ const firstBirth = birthMember(context, {
1757
1782
  run,
1758
1783
  plan,
1759
1784
  step,
@@ -1764,6 +1789,10 @@ function prepareDispatch(
1764
1789
  resolvedRoute,
1765
1790
  dispatchSources,
1766
1791
  });
1792
+ if ("handoff" in firstBirth) {
1793
+ return { kind: "return", state: entered, handoff: firstBirth.handoff };
1794
+ }
1795
+ const first = firstBirth;
1767
1796
  return {
1768
1797
  kind: "panel",
1769
1798
  panel: {
@@ -1822,10 +1851,10 @@ function birthMember(
1822
1851
  stepAttempt: number;
1823
1852
  enterRunning?: boolean | undefined;
1824
1853
  producerId?: ("driver-simulated" | "pi" | string) | undefined;
1825
- resolvedRoute?: { provider: string; model: string; selector: string } | undefined;
1854
+ resolvedRoute?: ResolvedProducerRoute | undefined;
1826
1855
  dispatchSources: DispatchContextSources;
1827
1856
  },
1828
- ): PreparedDispatch {
1857
+ ): PreparedDispatch | { handoff: KxmRunHandoff } {
1829
1858
  const run = requireRun(context, input.run.runId);
1830
1859
  const folded = foldStoredKxmRun(context, run);
1831
1860
  if (!birthAllowed(folded, input.step)) {
@@ -1848,9 +1877,10 @@ function birthMember(
1848
1877
  let resolvedRoute = input.resolvedRoute;
1849
1878
  if (!resolvedRoute && input.producerId && input.producerId !== "driver-simulated") {
1850
1879
  const routeResult = resolveProducerRoute(context.projectRoot, input.step, agentId);
1851
- if (!("error" in routeResult)) {
1852
- resolvedRoute = routeResult;
1880
+ if ("error" in routeResult) {
1881
+ return { handoff: { ...routeResult.error, stepId: input.stepId } };
1853
1882
  }
1883
+ resolvedRoute = routeResult;
1854
1884
  }
1855
1885
  const dispatchContext = assembleDispatchContext(input.dispatchSources, {
1856
1886
  projectId: run.projectId,
@@ -1958,13 +1988,13 @@ function birthMember(
1958
1988
  allowedOutcomes: input.step.outcomes,
1959
1989
  signal: controller.signal,
1960
1990
  prompt: input.step.instructions ? `${input.step.instructions}\n\n${generatedPrompt}` : generatedPrompt,
1961
- thinking: input.stepAttempt <= 1 ? "low" : "medium",
1962
1991
  permission: Object.values(input.step.repositories).some((access) => access === "write") ? "edit" : "read-only",
1963
1992
  ...((input.step.kind === "agent" || input.step.kind === "moa") && input.step.timeoutMs !== undefined
1964
1993
  ? { timeoutMs: input.step.timeoutMs }
1965
1994
  : {}),
1966
1995
  contextPacket,
1967
- ...(resolvedRoute ? { provider: resolvedRoute.provider, model: resolvedRoute.model } : {}),
1996
+ ...(resolvedRoute?.effort !== undefined ? { thinking: resolvedRoute.effort } : {}),
1997
+ ...(resolvedRoute ? { harness: resolvedRoute.harness, provider: resolvedRoute.provider, model: resolvedRoute.model } : {}),
1968
1998
  },
1969
1999
  controller,
1970
2000
  state: next,
@@ -2157,13 +2187,14 @@ async function drivePanel(
2157
2187
  pending.set(member.attemptId, work);
2158
2188
  };
2159
2189
 
2190
+ let routeHandoff: KxmRunHandoff | undefined;
2160
2191
  const tryBirth = (): PreparedDispatch | undefined => {
2161
- if (stopBirths) return undefined;
2192
+ if (stopBirths || routeHandoff) return undefined;
2162
2193
  return context.eventStore.transaction(() => {
2163
2194
  const run = requireRun(context, runId);
2164
2195
  const state = foldStoredKxmRun(context, run);
2165
2196
  if (!birthAllowed(state, panel.step)) return undefined;
2166
- return birthMember(context, {
2197
+ const born = birthMember(context, {
2167
2198
  run,
2168
2199
  plan: panel.plan,
2169
2200
  step: panel.step,
@@ -2172,6 +2203,11 @@ async function drivePanel(
2172
2203
  producerId: producer.id,
2173
2204
  dispatchSources: panel.dispatchSources,
2174
2205
  });
2206
+ if ("handoff" in born) {
2207
+ routeHandoff = born.handoff;
2208
+ return undefined;
2209
+ }
2210
+ return born;
2175
2211
  });
2176
2212
  };
2177
2213
 
@@ -2218,6 +2254,10 @@ async function drivePanel(
2218
2254
  while (!stopBirths && !settlementFailed) {
2219
2255
  try {
2220
2256
  const next = tryBirth();
2257
+ if (routeHandoff) {
2258
+ stopBirths = true;
2259
+ break;
2260
+ }
2221
2261
  if (!next) break;
2222
2262
  launch(next);
2223
2263
  } catch {
@@ -2228,6 +2268,13 @@ async function drivePanel(
2228
2268
  return unreconciledHandoff(state, panel.stepId);
2229
2269
  }
2230
2270
  }
2271
+ if (routeHandoff) {
2272
+ const handoff = routeHandoff;
2273
+ abortOwned();
2274
+ await drainPendingInvoked();
2275
+ const state = foldStoredKxmRun(context, requireRun(context, runId));
2276
+ return { state, handoff };
2277
+ }
2231
2278
  if (pending.size === 0) break;
2232
2279
  const finished = await Promise.race(pending.values());
2233
2280
  pending.delete(finished.attemptId);
@@ -2852,26 +2899,28 @@ export function kxmLiveRunPrerequisites(
2852
2899
  if (step.kind !== "agent" && step.kind !== "moa") continue;
2853
2900
  const agentIds = step.assignments.allowedAgents.length > 0 ? step.assignments.allowedAgents : [step.agent];
2854
2901
  for (const agentId of agentIds) {
2855
- const agent = bundle.agents.get(agentId);
2856
- const harness = String(agent?.value.harness ?? bundle.project.value.defaultHarness ?? "pi");
2902
+ const route = resolveProducerRoute(projectRoot, step, agentId);
2903
+ if ("error" in route) {
2904
+ const detail = route.error.detail === "producer_route_unsupported: agent step model is not honored"
2905
+ ? `${agentId}: agent step model is not honored; remove model from the step`
2906
+ : `${agentId}: ${route.error.detail}; set role in .kxm/agents/${agentId}.yaml and admit the route model with kxm routes admit --model <provider/model>`;
2907
+ prerequisites.push({ ...route.error, stepId, detail });
2908
+ continue;
2909
+ }
2910
+ const harness = route.harness;
2857
2911
  const permission = Object.values(step.repositories).includes("write") ? "edit" : "read-only";
2858
2912
  if (!BUILTIN_HARNESSES.some((entry) => entry.id === harness && entry.oneShot) || !oneShotPermissionArgs(harness, permission)) {
2859
2913
  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` });
2860
2914
  continue;
2861
2915
  }
2862
- const route = resolveProducerRoute(projectRoot, step, agentId);
2863
- if ("error" in route) {
2864
- 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>` });
2865
- continue;
2866
- }
2867
- const writeRefusal = unsupportedLiveWrite(projectRoot, step, agentId, route.selector, envelope.projectLimits.maxConcurrentRuns);
2916
+ const writeRefusal = unsupportedLiveWrite(projectRoot, step, route, envelope.projectLimits.maxConcurrentRuns);
2868
2917
  if (writeRefusal) {
2869
2918
  prerequisites.push({ ...writeRefusal, stepId });
2870
2919
  continue;
2871
2920
  }
2872
2921
  const compatible = validateHarnessModelPair(harness, route);
2873
2922
  if (!compatible.valid) {
2874
- 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` });
2923
+ 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` });
2875
2924
  }
2876
2925
  }
2877
2926
  }
@@ -2951,31 +3000,9 @@ function unsupportedStep(
2951
3000
  return undefined;
2952
3001
  }
2953
3002
 
2954
- function readYamlRecord(path: string): Record<string, unknown> | undefined {
2955
- if (!existsSync(path)) return undefined;
2956
- try {
2957
- const parsed = parse(readFileSync(path, "utf8")) as unknown;
2958
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as Record<string, unknown>;
2959
- } catch {
2960
- return undefined;
2961
- }
2962
- return undefined;
2963
- }
2964
-
2965
- function agentHarness(projectRoot: string, agentId: string): string | undefined {
2966
- const harness = readYamlRecord(join(projectRoot, ".kxm", "agents", `${agentId}.yaml`))?.harness;
2967
- return typeof harness === "string" && harness.length > 0 ? harness : undefined;
2968
- }
2969
-
2970
- function projectDefaultHarness(projectRoot: string): string {
2971
- const harness = readYamlRecord(join(projectRoot, ".kxm", "project.yaml"))?.defaultHarness;
2972
- return typeof harness === "string" && harness.length > 0 ? harness : "pi";
2973
- }
2974
-
2975
3003
  /**
2976
3004
  * Live write steps run only on an audited writer profile, and only when the
2977
- * developer roster (when present) lists that harness and model as an edit writer.
2978
- * A missing roster is a fresh project: route admission is the other gate.
3005
+ * writer roster names that route as admitted with edit.
2979
3006
  *
2980
3007
  * The authoring witness fingerprints the one project checkout around each
2981
3008
  * spawn, so it can only attribute a change to a lone writer. A write step with
@@ -2987,8 +3014,7 @@ function projectDefaultHarness(projectRoot: string): string {
2987
3014
  function unsupportedLiveWrite(
2988
3015
  projectRoot: string,
2989
3016
  step: KxmCompiledStep,
2990
- agentId: string,
2991
- selector: string,
3017
+ route: ResolvedProducerRoute,
2992
3018
  maxConcurrentRuns: number,
2993
3019
  ): Omit<KxmRunHandoff, "stepId"> | undefined {
2994
3020
  if (!Object.values(step.repositories).some((access) => access === "write")) return undefined;
@@ -3006,44 +3032,23 @@ function unsupportedLiveWrite(
3006
3032
  detail: "live write steps require limits.maxConcurrentRuns of 1; concurrent runs share one checkout",
3007
3033
  };
3008
3034
  }
3009
- const harness = agentHarness(projectRoot, agentId) ?? projectDefaultHarness(projectRoot);
3010
- if (!oneShotWriterArgs(harness)) {
3035
+ if (!oneShotWriterArgs(route.harness)) {
3011
3036
  return {
3012
3037
  reason: "step_unsupported",
3013
3038
  field: "repositories",
3014
- detail: `live write steps require an audited writer profile; ${harness} has none`,
3039
+ detail: `live write steps require an audited writer profile; ${route.harness} has none`,
3015
3040
  };
3016
3041
  }
3017
- const rosterPath = join(projectRoot, ".kxm", "roster.yaml");
3018
- if (!existsSync(rosterPath)) return undefined;
3019
- const roster = readYamlRecord(rosterPath);
3020
- if (!roster || roster.schema !== "kxm.developer-roster.v1") {
3021
- return { reason: "step_unsupported", field: "model", detail: "live write steps require a readable kxm.developer-roster.v1" };
3022
- }
3023
- const routes = roster.routes;
3024
- const lineup = roster.lineup;
3025
- const writerIds = lineup && typeof lineup === "object" && !Array.isArray(lineup)
3026
- ? (lineup as Record<string, unknown>).writer
3027
- : undefined;
3028
- if (!routes || typeof routes !== "object" || Array.isArray(routes) || !Array.isArray(writerIds)) {
3029
- return { reason: "step_unsupported", field: "model", detail: "developer roster has no writer lineup" };
3030
- }
3031
- const allowed = writerIds.some((id) => {
3032
- if (typeof id !== "string") return false;
3033
- const route = (routes as Record<string, unknown>)[id];
3034
- if (!route || typeof route !== "object" || Array.isArray(route)) return false;
3035
- const record = route as Record<string, unknown>;
3036
- if (record.harness !== harness || record.status !== "admitted") return false;
3037
- if (!Array.isArray(record.permissions) || !record.permissions.includes("edit")) return false;
3038
- const model = typeof record.model === "string" ? record.model : "";
3039
- const vendor = typeof record.vendor === "string" ? record.vendor : "";
3040
- return model === selector || (vendor.length > 0 && `${vendor}/${model}` === selector);
3041
- });
3042
- if (!allowed) {
3042
+ const writerIds = listRoleBindings(projectRoot).writer;
3043
+ if (!Array.isArray(writerIds) || writerIds.length === 0) {
3044
+ return { reason: "step_unsupported", field: "model", detail: "writer roster has no admitted edit route" };
3045
+ }
3046
+ const named = writerIds.includes(route.routeId);
3047
+ if (!named || !route.permissions.includes("edit")) {
3043
3048
  return {
3044
3049
  reason: "step_unsupported",
3045
3050
  field: "model",
3046
- detail: `live write route ${selector} on ${harness} is not on the developer roster writer lineup`,
3051
+ detail: `live write route ${route.selector} on ${route.harness} is not an admitted edit route on the writer roster`,
3047
3052
  };
3048
3053
  }
3049
3054
  return undefined;