@kontextmind/kxm 0.7.128 → 0.7.131

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) 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 +42 -1
  11. package/docs/architecture/inventory.md +1 -1
  12. package/docs/concepts/architecture.md +1 -1
  13. package/docs/concepts/data-and-storage.md +1 -1
  14. package/docs/contracts/README.md +1 -1
  15. package/docs/contracts/migration.md +12 -8
  16. package/docs/contracts/routing.md +3 -3
  17. package/docs/contributing/assignment-runner.md +14 -15
  18. package/docs/contributing/development.md +22 -6
  19. package/docs/contributing/harness-routing-internals.md +8 -8
  20. package/docs/glossary.md +1 -1
  21. package/docs/guides/agent-skills.md +1 -1
  22. package/docs/operations/backup-and-restore.md +7 -7
  23. package/docs/operations/upgrade.md +2 -2
  24. package/docs/reference/cli-reference.md +37 -14
  25. package/docs/reference/config-reference.md +81 -155
  26. package/docs/reference/harness-routing.md +29 -21
  27. package/docs/reference/workflow-catalog.md +4 -4
  28. package/docs/start/first-workflow.md +1 -1
  29. package/examples/project/.kxm/agents/coordinator.yaml +1 -2
  30. package/examples/project/.kxm/agents/critic-1.yaml +1 -3
  31. package/examples/project/.kxm/agents/critic-2.yaml +1 -2
  32. package/examples/project/.kxm/agents/critic-3.yaml +2 -3
  33. package/examples/project/.kxm/agents/implementer.yaml +1 -2
  34. package/examples/project/.kxm/agents/planner.yaml +1 -2
  35. package/examples/project/.kxm/agents/reproducer.yaml +1 -2
  36. package/examples/project/.kxm/agents/reviewer.yaml +1 -2
  37. package/examples/project/.kxm/workflows/fix.yaml +0 -12
  38. package/package.json +8 -8
  39. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  40. package/plugins/kxm/dist/cli.js +2565 -465
  41. package/plugins/kxm/dist/mcp-server.js +1 -1
  42. package/plugins/kxm/dist/runtime-supervisor.js +683 -348
  43. package/plugins/kxm/dist/runtime.js +694 -358
  44. package/plugins/kxm/dist/server.js +17 -14
  45. package/plugins/kxm/package.json +1 -1
  46. package/plugins/kxm/skills/kxm/SKILL.md +1 -1
  47. package/plugins/kxm/skills/kxm-definitions/SKILL.md +8 -0
  48. package/plugins/kxm/src/cli/lanes.ts +109 -4
  49. package/plugins/kxm/src/cli/project.ts +6 -6
  50. package/plugins/kxm/src/database.ts +25 -19
  51. package/plugins/kxm/src/engine.ts +168 -163
  52. package/plugins/kxm/src/init-guide-setup.ts +75 -15
  53. package/plugins/kxm/src/mcp-server.ts +1 -1
  54. package/plugins/kxm/src/oneshot-producer.ts +3 -15
  55. package/plugins/kxm/src/project-config.ts +83 -70
  56. package/plugins/kxm/src/routes.ts +26 -18
  57. package/plugins/kxm/src/runtime-service.ts +1 -0
  58. package/plugins/kxm/src/runtime-store.ts +390 -77
  59. package/plugins/kxm/src/runtime-supervisor.ts +87 -21
  60. package/plugins/kxm/src/suggest.ts +2 -2
  61. package/plugins/kxm/src/template.ts +2 -14
  62. package/schemas/agent.schema.json +8 -0
  63. package/scripts/roster-policy.d.mts +2 -1
  64. package/scripts/roster-policy.mjs +119 -22
  65. package/scripts/run-bounded.mjs +53 -0
@@ -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;
@@ -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`, retired
17
- * `.kxm/roster.json`) or the trusted `.kxm/roster.yaml` policy.
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 { createHash } from "node:crypto";
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, binding: AgentBinding): Record<string, unknown> {
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
- harness: binding.harness,
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`). A Pi model origin hashes `.kxm/project.yaml`
491
- * when that file exists, and is omitted when it does not.
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 (binding.harness === "pi" && projectOrigin) modelDocument.origin = projectOrigin;
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, binding)),
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.128";
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>();