@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
@@ -14783,7 +14783,7 @@ import { spawn as spawn3 } from "node:child_process";
14783
14783
  import { createHash as createHash15, createHmac, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
14784
14784
  import { chmodSync as chmodSync2, existsSync as existsSync16, lstatSync as lstatSync6, mkdirSync as mkdirSync9, readFileSync as readFileSync13, renameSync as renameSync5, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "node:fs";
14785
14785
  import { createServer } from "node:http";
14786
- import { dirname as dirname11, isAbsolute as isAbsolute7, join as join18 } from "node:path";
14786
+ import { dirname as dirname11, isAbsolute as isAbsolute8, join as join18 } from "node:path";
14787
14787
 
14788
14788
  // plugins/kxm/src/repo-root.ts
14789
14789
  import { existsSync } from "node:fs";
@@ -15663,21 +15663,9 @@ function coreTemplate(projectId, projectName, variant) {
15663
15663
  ]);
15664
15664
  if (variant === "v4-registry") {
15665
15665
  const coordinator = files.get(".kxm/agents/coordinator.yaml");
15666
- if (coordinator) {
15667
- files.set(".kxm/agents/coordinator.yaml", {
15668
- ...coordinator,
15669
- harness: "claude",
15670
- model: { provider: "anthropic", model: "fable" }
15671
- });
15672
- }
15666
+ if (coordinator) files.set(".kxm/agents/coordinator.yaml", { ...coordinator, role: "planner" });
15673
15667
  const implementer = files.get(".kxm/agents/implementer.yaml");
15674
- if (implementer) {
15675
- files.set(".kxm/agents/implementer.yaml", {
15676
- ...implementer,
15677
- harness: "grok",
15678
- model: { provider: "xai", model: "grok-4.6" }
15679
- });
15680
- }
15668
+ if (implementer) files.set(".kxm/agents/implementer.yaml", { ...implementer, role: "writer" });
15681
15669
  const workflow = files.get(".kxm/workflows/default.yaml");
15682
15670
  if (workflow) {
15683
15671
  const limits = { ...workflow.limits };
@@ -17316,7 +17304,7 @@ function readTemplateProvenance(registry, root) {
17316
17304
  }
17317
17305
  return value;
17318
17306
  }
17319
- function listNamedResources(registry, root, directory, logicalDirectory, kind, replacedId) {
17307
+ function listNamedResources(registry, root, directory, logicalDirectory, kind, replacedId, collected) {
17320
17308
  const resources = /* @__PURE__ */ new Map();
17321
17309
  if (!existsSync3(directory)) return resources;
17322
17310
  const stat = lstatSync(directory);
@@ -17355,7 +17343,10 @@ function listNamedResources(registry, root, directory, logicalDirectory, kind, r
17355
17343
  else throw error;
17356
17344
  }
17357
17345
  }
17358
- if (issues.length > 0) throw new KxmConfigError(issues);
17346
+ if (issues.length > 0) {
17347
+ if (collected) collected.push(...issues);
17348
+ else throw new KxmConfigError(issues);
17349
+ }
17359
17350
  return resources;
17360
17351
  }
17361
17352
  function valuesOf(object, field) {
@@ -17619,6 +17610,9 @@ function validateWorkflow(workflow, agents, models, repositories, gates, issues)
17619
17610
  for (const repositoryId of Object.keys(objectValue(step.repositories) ?? {})) {
17620
17611
  if (!repositories.has(repositoryId)) issues.push(issue3("reference", "repository_unknown", file, `${stepId} references unknown repository ${repositoryId}`));
17621
17612
  }
17613
+ if ((kind === "agent" || kind === "moa") && step.model !== void 0) {
17614
+ issues.push(issue3("semantic", "producer_route_unsupported", file, `${stepId} model is not honored; remove model from the step`));
17615
+ }
17622
17616
  selectorCandidates(step.model, models, file, `${stepId}.model`, issues);
17623
17617
  const assignment = objectValue(step.assignments);
17624
17618
  const declaredAgents = assignment ? names(assignment.allowedAgents) : [];
@@ -17801,7 +17795,7 @@ function validateModelReferences(models, issues) {
17801
17795
  };
17802
17796
  for (const id of models.keys()) walk(id, []);
17803
17797
  }
17804
- function validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, projectRoot, writerRole) {
17798
+ function validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, projectRoot, roles = /* @__PURE__ */ new Map()) {
17805
17799
  const issues = [];
17806
17800
  const entries = valuesOf(project.value, "repositories").map((candidate) => objectValue(candidate)).filter((candidate) => Boolean(candidate));
17807
17801
  const repositoryIds = /* @__PURE__ */ new Set();
@@ -17842,84 +17836,84 @@ function validateBundle(project, repositories, agents, models, workflows, enviro
17842
17836
  const defaultHarness = stringValue(project.value.defaultHarness) ?? DEFAULT_HARNESS;
17843
17837
  if (!harnesses.has(defaultHarness)) issues.push(issue3("reference", "harness_unknown", project.logicalPath, `defaultHarness ${defaultHarness} is not registered`));
17844
17838
  for (const agent of agents.values()) {
17839
+ if (agent.value.model !== void 0 || agent.value.harness !== void 0) {
17840
+ issues.push(issue3("semantic", "retired_agent_routing_fields", agent.logicalPath, "routing resolves from role; remove model and harness"));
17841
+ }
17845
17842
  validateToolPolicy(agent, objectValue(agent.value.tools), "tools", issues);
17846
17843
  const executor = stringValue(agent.value.executor);
17847
17844
  if (executor && !executors.has(executor)) issues.push(issue3("reference", "executor_unknown", agent.logicalPath, `executor ${executor} is not registered`));
17848
- const harness = stringValue(agent.value.harness) ?? defaultHarness;
17849
- if (!harnesses.has(harness)) issues.push(issue3("reference", "harness_unknown", agent.logicalPath, `harness ${harness} is not registered`));
17850
17845
  const preset = stringValue(objectValue(agent.value.tools)?.preset);
17851
17846
  if (preset && !presets.has(preset)) issues.push(issue3("reference", "tool_preset_unknown", agent.logicalPath, `tool preset ${preset} is not registered`));
17852
17847
  for (const repositoryId of Object.keys(objectValue(agent.value.repositories) ?? {})) {
17853
17848
  if (!repositoryIds.has(repositoryId)) issues.push(issue3("reference", "repository_unknown", agent.logicalPath, `references unknown repository ${repositoryId}`));
17854
17849
  }
17855
- const candidates = selectorCandidates(agent.value.model, models, agent.logicalPath, "model", issues);
17856
- const declaredHarness = stringValue(agent.value.harness);
17857
- if (declaredHarness && harnesses.has(declaredHarness)) {
17858
- for (const candidate of candidates) {
17859
- const candidateProvider = stringValue(candidate.value.provider);
17860
- const candidateModel = stringValue(candidate.value.model);
17861
- const validation = validateHarnessModelPair(declaredHarness, { provider: candidateProvider, model: candidateModel });
17862
- if (!validation.valid) {
17863
- issues.push(issue3("semantic", validation.issue ?? "harness_unhosted_model", agent.logicalPath, validation.message ?? `harness ${declaredHarness} cannot host model ${candidateModel ?? candidate.logicalPath}`));
17864
- }
17865
- }
17866
- }
17867
17850
  }
17868
17851
  validateModelReferences(models, issues);
17869
17852
  const defaultWorkflow = stringValue(project.value.defaultWorkflow) ?? "default";
17870
17853
  if (!workflows.has(defaultWorkflow)) issues.push(issue3("reference", "default_workflow_unknown", project.logicalPath, `default workflow ${defaultWorkflow} does not exist`));
17871
17854
  for (const workflow of workflows.values()) validateWorkflow(workflow, agents, models, repositoryIds, gates, issues);
17855
+ if (projectRoot && existsSync3(join2(projectRoot, ".kxm", "roster.yaml"))) {
17856
+ issues.push(issue3("semantic", "retired_roster_file", ".kxm/roster.yaml", "create the role and model files and delete roster.yaml"));
17857
+ }
17858
+ if (projectRoot && existsSync3(join2(projectRoot, ".kxm", "roles"))) {
17859
+ issues.push(...developerRolePolicyIssues(projectRoot));
17860
+ }
17872
17861
  if (projectRoot) {
17873
- const writerRolePath = join2(projectRoot, ".kxm", "roles", "writer.yaml");
17874
- const implementerAgent = agents.get("implementer") ?? agents.get("writer");
17875
- if ((writerRole !== void 0 || existsSync3(writerRolePath)) && implementerAgent) {
17862
+ issues.push(...rosterModelIssues(projectRoot, roles));
17863
+ issues.push(...originHashIssues(projectRoot, models));
17864
+ }
17865
+ return sortIssues3(issues);
17866
+ }
17867
+ function rosterModelIssues(projectRoot, roles) {
17868
+ const issues = [];
17869
+ for (const role of roles.values()) {
17870
+ for (const entry of valuesOf(role.value, "roster")) {
17871
+ const route = stringValue(objectValue(entry)?.route);
17872
+ if (!route) continue;
17873
+ const logical = `.kxm/models/${route}.yaml`;
17874
+ const file = join2(projectRoot, ".kxm", "models", `${route}.yaml`);
17875
+ let harness;
17876
17876
  try {
17877
- const rawRole = parseRestrictedYaml2(writerRole ?? readFileSync(writerRolePath, "utf8"));
17878
- const roleObj = objectValue(rawRole);
17879
- const rosterEntries = valuesOf(roleObj ?? {}, "roster").map((candidate) => objectValue(candidate)).filter((entry) => Boolean(entry));
17880
- const enabledRosterModels = rosterEntries.flatMap((entry) => {
17881
- const direct = stringValue(entry.model);
17882
- const route = stringValue(entry.route);
17883
- const named = direct ? [direct] : [];
17884
- if (route) {
17885
- const modelFile = join2(projectRoot, ".kxm", "models", `${route}.yaml`);
17886
- if (existsSync3(modelFile)) {
17887
- try {
17888
- const modelDoc = objectValue(parseRestrictedYaml2(readFileSync(modelFile, "utf8"), modelFile));
17889
- const model = modelDoc ? stringValue(modelDoc.model) : void 0;
17890
- const vendor = modelDoc ? stringValue(modelDoc.vendor) : void 0;
17891
- const harness = modelDoc ? stringValue(modelDoc.harness) : void 0;
17892
- if (model) named.push(model);
17893
- if (vendor && model) named.push(`${vendor}/${model}`);
17894
- if (harness && model) named.push(`${harness}/${model}`);
17895
- } catch {
17896
- }
17897
- }
17898
- }
17899
- return named;
17900
- });
17901
- const agentModelObj = objectValue(implementerAgent.value.model);
17902
- const agentModelStr = stringValue(implementerAgent.value.model);
17903
- const agentProvider = agentModelObj ? stringValue(agentModelObj.provider) : void 0;
17904
- const agentModel = agentModelObj ? stringValue(agentModelObj.model) : agentModelStr;
17905
- const canonicalAgentModel = agentProvider && agentModel ? `${agentProvider}/${agentModel}` : agentModel;
17906
- if (enabledRosterModels.length > 0 && canonicalAgentModel) {
17907
- const matches = enabledRosterModels.some((rm) => rm === canonicalAgentModel || rm === agentModel || rm.endsWith(`/${agentModel}`));
17908
- if (!matches) {
17909
- 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}`));
17910
- }
17911
- }
17912
- } catch (error) {
17913
- if (error instanceof KxmConfigError) {
17914
- issues.push(...error.issues);
17915
- }
17877
+ if (existsSync3(file)) harness = stringValue(parseRestrictedYaml2(readFileSync(file, "utf8"), logical).harness);
17878
+ } catch {
17879
+ harness = void 0;
17880
+ }
17881
+ if (!harness) {
17882
+ issues.push(issue3("semantic", "roster_model_unreadable", role.logicalPath, `roster route ${route} does not resolve to a readable ${logical} carrying harness`));
17916
17883
  }
17917
17884
  }
17918
17885
  }
17919
- if (projectRoot && existsSync3(join2(projectRoot, ".kxm", "roster.yaml"))) {
17920
- issues.push(...developerRolePolicyIssues(projectRoot));
17886
+ return issues;
17887
+ }
17888
+ function originHashIssues(projectRoot, models) {
17889
+ const issues = [];
17890
+ for (const model of models.values()) {
17891
+ const origin = objectValue(model.value.origin);
17892
+ const source = origin ? stringValue(origin.source) : void 0;
17893
+ const digest2 = origin ? stringValue(origin.sha256) : void 0;
17894
+ if (!source || !digest2) continue;
17895
+ const file = resolve(projectRoot, source);
17896
+ const escaped = relative(projectRoot, file);
17897
+ if (isAbsolute(source) || escaped.startsWith("..") || isAbsolute(escaped)) {
17898
+ issues.push(issue3("semantic", "origin_evidence_missing", model.logicalPath, `missing evidence bytes for ${source}`));
17899
+ continue;
17900
+ }
17901
+ let actual;
17902
+ try {
17903
+ actual = createHash3("sha256").update(readFileSync(file, "utf8"), "utf8").digest("hex");
17904
+ } catch (error) {
17905
+ const code = error.code;
17906
+ if (code === "ENOENT" || code === "EACCES" || code === "EPERM" || code === "EISDIR") {
17907
+ issues.push(issue3("semantic", "origin_evidence_missing", model.logicalPath, `missing evidence bytes for ${source}`));
17908
+ continue;
17909
+ }
17910
+ throw error;
17911
+ }
17912
+ if (actual !== digest2) {
17913
+ issues.push(issue3("semantic", "origin_hash_mismatch", model.logicalPath, "origin evidence hash does not match supplied bytes"));
17914
+ }
17921
17915
  }
17922
- return sortIssues3(issues);
17916
+ return issues;
17923
17917
  }
17924
17918
  function developerCeilings() {
17925
17919
  const script = join2(findKxmRepoRoot(import.meta.url), "scripts", "harness-run.mjs");
@@ -17948,6 +17942,7 @@ function developerCeilings() {
17948
17942
  }
17949
17943
  }
17950
17944
  function developerRolePolicyIssues(projectRoot) {
17945
+ if (resolve(projectRoot) !== findKxmRepoRoot(import.meta.url)) return [];
17951
17946
  const rolesDir = join2(projectRoot, ".kxm", "roles");
17952
17947
  const modelsDir = join2(projectRoot, ".kxm", "models");
17953
17948
  const roles = {};
@@ -17968,6 +17963,7 @@ function developerRolePolicyIssues(projectRoot) {
17968
17963
  parseIssues.push(...error.issues);
17969
17964
  continue;
17970
17965
  }
17966
+ throw error;
17971
17967
  }
17972
17968
  }
17973
17969
  };
@@ -17982,7 +17978,7 @@ function developerRolePolicyIssues(projectRoot) {
17982
17978
  }
17983
17979
  const ceilings = developerCeilings();
17984
17980
  if (!ceilings.ok) {
17985
- return [...parseIssues, issue3("semantic", "developer_ceilings_unavailable", ".kxm/roster.yaml", `developer ceilings could not be loaded: ${ceilings.detail}`)];
17981
+ return [...parseIssues, issue3("semantic", "developer_ceilings_unavailable", ".kxm/models", `developer ceilings could not be loaded: ${ceilings.detail}`)];
17986
17982
  }
17987
17983
  const result = validatePolicyDraft({ models, roles, evidence }, {
17988
17984
  ceilings: ceilings.ROUTES,
@@ -18071,7 +18067,8 @@ function loadProjectBundle(projectRoot, options, candidate) {
18071
18067
  }
18072
18068
  if (earlyIssues.length > 0) throw new KxmConfigError(earlyIssues);
18073
18069
  const agents = listNamedResources(registry, root, join2(root, ".kxm", "agents"), ".kxm/agents", "agent");
18074
- const models = listNamedResources(registry, root, join2(root, ".kxm", "models"), ".kxm/models", "model");
18070
+ const modelIssues = [];
18071
+ const models = listNamedResources(registry, root, join2(root, ".kxm", "models"), ".kxm/models", "model", void 0, modelIssues);
18075
18072
  const roles = listNamedResources(registry, root, join2(root, ".kxm", "roles"), ".kxm/roles", "role", writerRole === void 0 ? void 0 : "writer");
18076
18073
  if (writerRole !== void 0) {
18077
18074
  const logicalPath = ".kxm/roles/writer.yaml";
@@ -18198,8 +18195,8 @@ function loadProjectBundle(projectRoot, options, candidate) {
18198
18195
  }
18199
18196
  }
18200
18197
  }
18201
- if (loadIssues.length > 0) throw new KxmConfigError(loadIssues);
18202
- const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root, writerRole);
18198
+ if (loadIssues.length > 0) throw new KxmConfigError([...loadIssues, ...modelIssues]);
18199
+ const issues = [...modelIssues, ...validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root, roles)];
18203
18200
  if (issues.length > 0) throw new KxmConfigError(issues);
18204
18201
  const resources = [project, ...repositories.values(), ...agents.values(), ...models.values(), ...roles.values(), ...workflows.values(), ...environments, ...gateRegistry ? [gateRegistry] : []].sort((left, right) => compareCodeUnits4(left.logicalPath, right.logicalPath));
18205
18202
  return {
@@ -18244,9 +18241,10 @@ function legacyConfigFilesAt(root) {
18244
18241
  }
18245
18242
 
18246
18243
  // plugins/kxm/src/runtime-store.ts
18244
+ import { spawnSync as spawnSync3 } from "node:child_process";
18247
18245
  import { createHash as createHash6, randomUUID } from "node:crypto";
18248
- import { existsSync as existsSync6, lstatSync as lstatSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
18249
- import { dirname as dirname5, resolve as resolve6 } from "node:path";
18246
+ import { existsSync as existsSync6, lstatSync as lstatSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, realpathSync as realpathSync3, statSync, writeFileSync as writeFileSync2 } from "node:fs";
18247
+ import { dirname as dirname5, isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
18250
18248
 
18251
18249
  // plugins/kxm/src/sqlite.ts
18252
18250
  import { existsSync as existsSync4 } from "node:fs";
@@ -18760,22 +18758,25 @@ function ensureWalJournalMode(database, file, description, timeoutMs = 5e3) {
18760
18758
  Atomics.wait(sleeper, 0, 0, 10);
18761
18759
  }
18762
18760
  }
18761
+ function assertDatabaseFile(file, description) {
18762
+ checkedParent(file, description);
18763
+ const stat = lstatSync2(file, { throwIfNoEntry: false });
18764
+ if (stat) {
18765
+ if (stat.isSymbolicLink() || !stat.isFile()) {
18766
+ throw databaseError("runtime_path_invalid", description, `${description} must be a regular file, not a link or directory`);
18767
+ }
18768
+ }
18769
+ for (const sidecar of [`${file}-wal`, `${file}-shm`]) {
18770
+ const info = lstatSync2(sidecar, { throwIfNoEntry: false });
18771
+ if (info?.isSymbolicLink()) {
18772
+ throw databaseError("runtime_path_invalid", description, `${description} sidecar must not be a link`);
18773
+ }
18774
+ }
18775
+ }
18763
18776
  function openDatabase(file, description, spec) {
18764
18777
  const isMemory = file === ":memory:";
18765
18778
  if (!isMemory) {
18766
- checkedParent(file, description);
18767
- const stat = lstatSync2(file, { throwIfNoEntry: false });
18768
- if (stat) {
18769
- if (stat.isSymbolicLink() || !stat.isFile()) {
18770
- throw databaseError("runtime_path_invalid", description, `${description} must be a regular file, not a link or directory`);
18771
- }
18772
- }
18773
- for (const sidecar of [`${file}-wal`, `${file}-shm`]) {
18774
- const info = lstatSync2(sidecar, { throwIfNoEntry: false });
18775
- if (info?.isSymbolicLink()) {
18776
- throw databaseError("runtime_path_invalid", description, `${description} sidecar must not be a link`);
18777
- }
18778
- }
18779
+ assertDatabaseFile(file, description);
18779
18780
  }
18780
18781
  const database = new DatabaseSync(file);
18781
18782
  let transaction = false;
@@ -18804,7 +18805,7 @@ function openDatabase(file, description, spec) {
18804
18805
  throw databaseError(
18805
18806
  "runtime_schema_outdated",
18806
18807
  file,
18807
- `${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file to start fresh and let its owning process recreate it (\`kxm hub start\` for hub state, the Runtime for registry/event stores); \`kxm init\` is project-only and rebuilds no database \u2014 upgrading old state in place is deliberately unsupported`
18808
+ `${description} is schema version ${version}; this build requires ${spec.version}. Delete the state file to start fresh and let its owning process recreate it (\`kxm hub start\` for hub state, the Runtime for event stores). The Runtime registry copy from schema 1 to 2 is the one exception. \`kxm init\` is project-only and rebuilds no database. Upgrading any other old state in place is deliberately unsupported`
18808
18809
  );
18809
18810
  }
18810
18811
  if (spec.tables) {
@@ -18917,10 +18918,10 @@ function runtimeIssue(phase, code, file, message) {
18917
18918
  function runtimeError(code, file, message) {
18918
18919
  return new KxmConfigError([runtimeIssue("semantic", code, file, message)]);
18919
18920
  }
18920
- var KXM_REGISTRY_SCHEMA_VERSION = 1;
18921
+ var KXM_REGISTRY_SCHEMA_VERSION = 2;
18921
18922
  var REGISTRY_TABLES = {
18922
18923
  supervisor: ["singleton_id", "runtime_id", "pid", "port", "token_hash", "started_at", "heartbeat_at", "state"],
18923
- projects: ["project_id", "project_root", "project_key", "home_runtime_id", "config_revision", "registered_at"]
18924
+ projects: ["project_id", "project_root", "project_key", "home_runtime_id", "config_revision", "registered_at", "lane_of"]
18924
18925
  };
18925
18926
  var REGISTRY_SCHEMA = `
18926
18927
  CREATE TABLE supervisor (
@@ -18934,14 +18935,117 @@ CREATE TABLE supervisor (
18934
18935
  state TEXT NOT NULL
18935
18936
  ) STRICT;
18936
18937
  CREATE TABLE projects (
18937
- project_id TEXT PRIMARY KEY,
18938
+ project_key TEXT PRIMARY KEY,
18939
+ project_id TEXT NOT NULL,
18938
18940
  project_root TEXT NOT NULL,
18939
- project_key TEXT NOT NULL UNIQUE,
18940
18941
  home_runtime_id TEXT NOT NULL,
18941
18942
  config_revision TEXT,
18942
- registered_at TEXT NOT NULL
18943
+ registered_at TEXT NOT NULL,
18944
+ lane_of TEXT
18943
18945
  ) STRICT;
18946
+ CREATE INDEX projects_by_project_id ON projects (project_id);
18944
18947
  `;
18948
+ var PROJECT_COLUMNS = "project_id, project_root, project_key, home_runtime_id, config_revision, registered_at, lane_of";
18949
+ function registrationFromRow(row) {
18950
+ return {
18951
+ projectId: row.project_id,
18952
+ projectRoot: row.project_root,
18953
+ projectKey: row.project_key,
18954
+ homeRuntimeId: row.home_runtime_id,
18955
+ ...row.config_revision !== null ? { configRevision: row.config_revision } : {},
18956
+ ...row.lane_of !== null ? { laneOf: row.lane_of } : {},
18957
+ registeredAt: row.registered_at
18958
+ };
18959
+ }
18960
+ function controlRootDirectoryExists(projectRoot) {
18961
+ const stat = statSync(projectRoot, { throwIfNoEntry: false });
18962
+ return Boolean(stat?.isDirectory());
18963
+ }
18964
+ function gitEnv() {
18965
+ return Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.toUpperCase().startsWith("GIT_")));
18966
+ }
18967
+ function gitCommonDirectory(projectRoot) {
18968
+ const result = spawnSync3("git", ["-C", projectRoot, "rev-parse", "--git-common-dir"], {
18969
+ encoding: "utf8",
18970
+ env: gitEnv(),
18971
+ timeout: 3e4,
18972
+ windowsHide: true
18973
+ });
18974
+ if (result.status !== 0) return void 0;
18975
+ const text = (result.stdout ?? "").trim();
18976
+ if (!text) return void 0;
18977
+ const absolute = isAbsolute4(text) ? text : resolve6(projectRoot, text);
18978
+ let canonical;
18979
+ try {
18980
+ canonical = realpathSync3.native(absolute);
18981
+ } catch {
18982
+ canonical = resolve6(absolute);
18983
+ }
18984
+ return process.platform === "win32" ? canonical.toLocaleLowerCase("en-US") : canonical;
18985
+ }
18986
+ function isPrimaryWorktree(projectRoot, commonDir) {
18987
+ if (commonDir === void 0) return false;
18988
+ const dotGit = resolve6(projectRoot, ".git");
18989
+ let canonical;
18990
+ try {
18991
+ canonical = realpathSync3.native(dotGit);
18992
+ } catch {
18993
+ canonical = resolve6(dotGit);
18994
+ }
18995
+ if (process.platform === "win32") canonical = canonical.toLocaleLowerCase("en-US");
18996
+ return canonical === commonDir;
18997
+ }
18998
+ function migrateRegistryLanes(file) {
18999
+ const stat = lstatSync3(file, { throwIfNoEntry: false });
19000
+ if (!stat) return;
19001
+ assertDatabaseFile(file, "runtime registry");
19002
+ const database = new DatabaseSync(file);
19003
+ let transaction = false;
19004
+ try {
19005
+ database.exec("PRAGMA busy_timeout = 5000");
19006
+ const current = database.prepare("PRAGMA user_version").get();
19007
+ if ((current?.user_version ?? 0) !== 1) return;
19008
+ database.exec("BEGIN IMMEDIATE");
19009
+ transaction = true;
19010
+ const locked = database.prepare("PRAGMA user_version").get();
19011
+ if ((locked?.user_version ?? 0) !== 1) {
19012
+ database.exec("ROLLBACK");
19013
+ transaction = false;
19014
+ return;
19015
+ }
19016
+ if (!tableColumns(database, "projects").includes("lane_of")) {
19017
+ database.exec(`
19018
+ CREATE TABLE projects_v2 (
19019
+ project_key TEXT PRIMARY KEY,
19020
+ project_id TEXT NOT NULL,
19021
+ project_root TEXT NOT NULL,
19022
+ home_runtime_id TEXT NOT NULL,
19023
+ config_revision TEXT,
19024
+ registered_at TEXT NOT NULL,
19025
+ lane_of TEXT
19026
+ ) STRICT;
19027
+ INSERT INTO projects_v2 (project_key, project_id, project_root, home_runtime_id, config_revision, registered_at, lane_of)
19028
+ SELECT project_key, project_id, project_root, home_runtime_id, config_revision, registered_at, NULL FROM projects;
19029
+ DROP TABLE projects;
19030
+ ALTER TABLE projects_v2 RENAME TO projects;
19031
+ CREATE INDEX projects_by_project_id ON projects (project_id);
19032
+ `);
19033
+ }
19034
+ database.exec("PRAGMA user_version = 2");
19035
+ database.exec("COMMIT");
19036
+ transaction = false;
19037
+ } catch (error) {
19038
+ if (transaction) {
19039
+ try {
19040
+ database.exec("ROLLBACK");
19041
+ } catch {
19042
+ }
19043
+ }
19044
+ throw error;
19045
+ } finally {
19046
+ database.close();
19047
+ }
19048
+ }
18945
19049
  function readKxmSupervisorRecord(database) {
18946
19050
  const row = database.prepare("SELECT runtime_id, pid, port, token_hash, started_at, heartbeat_at, state FROM supervisor WHERE singleton_id = 1").get();
18947
19051
  return row ? {
@@ -18959,6 +19063,7 @@ var KxmRuntimeRegistry = class {
18959
19063
  database;
18960
19064
  constructor(path) {
18961
19065
  this.path = resolve6(path);
19066
+ migrateRegistryLanes(this.path);
18962
19067
  this.database = openDatabase(this.path, "runtime registry", {
18963
19068
  schema: REGISTRY_SCHEMA,
18964
19069
  version: KXM_REGISTRY_SCHEMA_VERSION,
@@ -19037,57 +19142,188 @@ var KxmRuntimeRegistry = class {
19037
19142
  supervisor() {
19038
19143
  return this.readSupervisorRow();
19039
19144
  }
19040
- /** Register or revalidate a project's home binding. Home Runtime is immutable. */
19041
- /** All projects registered to this Runtime, for restart recovery: the
19042
- * supervisor needs to reopen their contexts so pending outbox rows resume
19043
- * syncing and presence keeps beating. */
19145
+ /** All projects registered to this Runtime, including lane roots, for restart
19146
+ * recovery: the supervisor reopens their contexts so pending outbox rows
19147
+ * resume syncing and presence keeps beating. */
19044
19148
  projectsForRuntime(homeRuntimeId) {
19045
19149
  const rows = this.database.prepare(
19046
19150
  "SELECT project_root, project_id FROM projects WHERE home_runtime_id = ? ORDER BY registered_at"
19047
19151
  ).all(homeRuntimeId);
19048
19152
  return rows.map((row) => ({ projectRoot: row.project_root, projectId: row.project_id }));
19049
19153
  }
19154
+ /**
19155
+ * Register or revalidate a control root. The home runtime of a live row is
19156
+ * immutable. A second root whose git common directory matches a live row
19157
+ * for the same project is a lane, unless the registering root is the primary
19158
+ * worktree and the live home row is not: then the primary becomes the home
19159
+ * row (same home runtime id) and every other live row of that repository
19160
+ * becomes its lane. A primary never becomes a lane. A root whose directory
19161
+ * is gone is replaced. A live root that is a different repository is
19162
+ * `project_home_conflict`.
19163
+ */
19050
19164
  registerProject(registration) {
19051
19165
  const projectRoot = resolve6(registration.projectRoot);
19052
19166
  const projectKey = projectRuntimeKey(projectRoot);
19167
+ const candidateCommon = gitCommonDirectory(projectRoot);
19168
+ const commonByRoot = /* @__PURE__ */ new Map();
19169
+ for (let attempt = 0; attempt < 8; attempt += 1) {
19170
+ this.database.exec("BEGIN IMMEDIATE");
19171
+ let rootsToMeasure;
19172
+ try {
19173
+ const byKey = this.projectRow("SELECT " + PROJECT_COLUMNS + " FROM projects WHERE project_key = ?", projectKey);
19174
+ if (byKey) {
19175
+ const problems = [];
19176
+ if (byKey.project_id !== registration.projectId) problems.push(`control root is already bound to a different project id ${byKey.project_id}`);
19177
+ if (byKey.home_runtime_id !== registration.homeRuntimeId) problems.push(`project ${registration.projectId} home runtime is immutable and cannot be rebound`);
19178
+ if (problems.length > 0) {
19179
+ this.database.exec("ROLLBACK");
19180
+ throw runtimeError("project_home_conflict", ".kxm/project.yaml", problems.join("; "));
19181
+ }
19182
+ const result = registrationFromRow(byKey);
19183
+ this.database.exec("COMMIT");
19184
+ return result;
19185
+ }
19186
+ const rows = this.database.prepare(
19187
+ `SELECT ${PROJECT_COLUMNS} FROM projects WHERE project_id = ? ORDER BY registered_at`
19188
+ ).all(registration.projectId);
19189
+ const live = rows.filter((row) => controlRootDirectoryExists(row.project_root));
19190
+ const dead = rows.filter((row) => !controlRootDirectoryExists(row.project_root));
19191
+ const unknown = live.filter((row) => !commonByRoot.has(row.project_root));
19192
+ if (unknown.length > 0) {
19193
+ rootsToMeasure = unknown.map((row) => row.project_root);
19194
+ this.database.exec("ROLLBACK");
19195
+ } else if (live.length > 0) {
19196
+ const anchor = live.find((row) => {
19197
+ const dir = commonByRoot.get(row.project_root);
19198
+ return candidateCommon !== void 0 && dir === candidateCommon;
19199
+ });
19200
+ if (!anchor) {
19201
+ this.database.exec("ROLLBACK");
19202
+ throw runtimeError(
19203
+ "project_home_conflict",
19204
+ ".kxm/project.yaml",
19205
+ `project ${registration.projectId} is already bound to a different control root`
19206
+ );
19207
+ }
19208
+ if (live.some((row) => row.home_runtime_id !== registration.homeRuntimeId)) {
19209
+ this.database.exec("ROLLBACK");
19210
+ throw runtimeError(
19211
+ "project_home_conflict",
19212
+ ".kxm/project.yaml",
19213
+ `project ${registration.projectId} home runtime is immutable and cannot be rebound`
19214
+ );
19215
+ }
19216
+ const sameRepo = live.filter((row) => commonByRoot.get(row.project_root) === candidateCommon);
19217
+ const home = sameRepo.find((row) => row.lane_of === null);
19218
+ const registeringIsPrimary = isPrimaryWorktree(projectRoot, candidateCommon);
19219
+ const homeIsPrimary = home !== void 0 && isPrimaryWorktree(home.project_root, commonByRoot.get(home.project_root));
19220
+ if (registeringIsPrimary && !homeIsPrimary) {
19221
+ this.insertHome(registration, projectRoot, projectKey);
19222
+ for (const row of sameRepo) {
19223
+ this.database.prepare("UPDATE projects SET lane_of = ? WHERE project_key = ?").run(projectKey, row.project_key);
19224
+ }
19225
+ this.database.exec("COMMIT");
19226
+ registration.logger?.({
19227
+ event: "project_home_promoted",
19228
+ projectId: registration.projectId,
19229
+ projectRoot,
19230
+ homeRuntimeId: registration.homeRuntimeId,
19231
+ previousHomeRoot: home?.project_root ?? null,
19232
+ previousHomeKey: home?.project_key ?? null
19233
+ });
19234
+ return this.insertedRegistration(registration, projectRoot, projectKey);
19235
+ }
19236
+ if (registeringIsPrimary) {
19237
+ this.database.exec("ROLLBACK");
19238
+ throw runtimeError(
19239
+ "project_home_conflict",
19240
+ ".kxm/project.yaml",
19241
+ `project ${registration.projectId} primary checkout is already the home row`
19242
+ );
19243
+ }
19244
+ const laneOf = anchor.lane_of ?? anchor.project_key;
19245
+ this.database.prepare(`
19246
+ INSERT INTO projects (project_key, project_id, project_root, home_runtime_id, config_revision, registered_at, lane_of)
19247
+ VALUES (?, ?, ?, ?, ?, ?, ?)
19248
+ `).run(projectKey, registration.projectId, projectRoot, registration.homeRuntimeId, registration.configRevision ?? null, registration.now, laneOf);
19249
+ const result = this.insertedRegistration(registration, projectRoot, projectKey, laneOf);
19250
+ this.database.exec("COMMIT");
19251
+ return result;
19252
+ } else if (dead.length > 0) {
19253
+ const replaced = dead[0];
19254
+ if (dead.some((row) => row.home_runtime_id !== registration.homeRuntimeId)) {
19255
+ this.database.exec("ROLLBACK");
19256
+ throw runtimeError(
19257
+ "project_home_conflict",
19258
+ ".kxm/project.yaml",
19259
+ `project ${registration.projectId} home runtime is immutable and cannot be rebound`
19260
+ );
19261
+ }
19262
+ this.database.prepare("DELETE FROM projects WHERE project_id = ?").run(registration.projectId);
19263
+ this.insertHome(registration, projectRoot, projectKey);
19264
+ this.database.exec("COMMIT");
19265
+ registration.logger?.({
19266
+ event: "project_registration_replaced",
19267
+ projectId: registration.projectId,
19268
+ projectRoot,
19269
+ replacedRoot: replaced.project_root,
19270
+ replacedCount: dead.length,
19271
+ replacedHomeRuntimeId: replaced.home_runtime_id,
19272
+ replacedProjectKey: replaced.project_key
19273
+ });
19274
+ return this.insertedRegistration(registration, projectRoot, projectKey);
19275
+ } else {
19276
+ this.insertHome(registration, projectRoot, projectKey);
19277
+ this.database.exec("COMMIT");
19278
+ return this.insertedRegistration(registration, projectRoot, projectKey);
19279
+ }
19280
+ } catch (error) {
19281
+ try {
19282
+ this.database.exec("ROLLBACK");
19283
+ } catch {
19284
+ }
19285
+ throw error;
19286
+ }
19287
+ for (const root of rootsToMeasure ?? []) {
19288
+ commonByRoot.set(root, gitCommonDirectory(root));
19289
+ }
19290
+ }
19291
+ throw runtimeError(
19292
+ "runtime_registry_busy",
19293
+ projectRoot,
19294
+ `project ${registration.projectId} registration could not be rechecked`
19295
+ );
19296
+ }
19297
+ /** Remove one control root. Returns false when that root was not registered. */
19298
+ unregisterProject(projectRoot) {
19053
19299
  this.database.exec("BEGIN IMMEDIATE");
19054
19300
  try {
19055
- const byId = this.database.prepare("SELECT project_id, project_root, project_key, home_runtime_id, config_revision, registered_at FROM projects WHERE project_id = ?").get(registration.projectId);
19056
- const byKey = this.database.prepare("SELECT project_id, project_root, project_key, home_runtime_id, config_revision, registered_at FROM projects WHERE project_key = ?").get(projectKey);
19057
- const existing = byId ?? byKey;
19058
- if (existing) {
19059
- const problems = [];
19060
- if (byId && byId.project_key !== projectKey) problems.push(`project ${registration.projectId} is already bound to a different control root`);
19061
- if (byKey && byKey.project_id !== registration.projectId) problems.push(`control root is already bound to a different project id ${byKey.project_id}`);
19062
- if (existing.home_runtime_id !== registration.homeRuntimeId) problems.push(`project ${registration.projectId} home runtime is immutable and cannot be rebound`);
19063
- if (problems.length > 0) {
19064
- this.database.exec("ROLLBACK");
19065
- throw runtimeError("project_home_conflict", ".kxm/project.yaml", problems.join("; "));
19066
- }
19067
- const result = {
19068
- projectId: existing.project_id,
19069
- projectRoot: existing.project_root,
19070
- projectKey: existing.project_key,
19071
- homeRuntimeId: existing.home_runtime_id,
19072
- ...existing.config_revision !== null ? { configRevision: existing.config_revision } : {},
19073
- registeredAt: existing.registered_at
19074
- };
19075
- this.database.exec("COMMIT");
19076
- return result;
19301
+ const removed = this.deleteRegisteredRoot(projectRoot);
19302
+ this.database.exec("COMMIT");
19303
+ return removed;
19304
+ } catch (error) {
19305
+ try {
19306
+ this.database.exec("ROLLBACK");
19307
+ } catch {
19077
19308
  }
19078
- this.database.prepare(`
19079
- INSERT INTO projects (project_id, project_root, project_key, home_runtime_id, config_revision, registered_at)
19080
- VALUES (?, ?, ?, ?, ?, ?)
19081
- `).run(registration.projectId, projectRoot, projectKey, registration.homeRuntimeId, registration.configRevision ?? null, registration.now);
19309
+ throw error;
19310
+ }
19311
+ }
19312
+ /**
19313
+ * Delete the row only when `supervisorIsLive` is false for the supervisor
19314
+ * row read inside this write transaction. A live supervisor keeps the row;
19315
+ * the caller uses the supervisor route instead of editing the file.
19316
+ */
19317
+ unregisterProjectIfIdle(projectRoot, supervisorIsLive) {
19318
+ this.database.exec("BEGIN IMMEDIATE");
19319
+ try {
19320
+ if (supervisorIsLive(this.readSupervisorRow())) {
19321
+ this.database.exec("ROLLBACK");
19322
+ return "supervisor_live";
19323
+ }
19324
+ const removed = this.deleteRegisteredRoot(projectRoot);
19082
19325
  this.database.exec("COMMIT");
19083
- return {
19084
- projectId: registration.projectId,
19085
- projectRoot,
19086
- projectKey,
19087
- homeRuntimeId: registration.homeRuntimeId,
19088
- ...registration.configRevision !== void 0 ? { configRevision: registration.configRevision } : {},
19089
- registeredAt: registration.now
19090
- };
19326
+ return removed ? "removed" : "absent";
19091
19327
  } catch (error) {
19092
19328
  try {
19093
19329
  this.database.exec("ROLLBACK");
@@ -19096,28 +19332,65 @@ var KxmRuntimeRegistry = class {
19096
19332
  throw error;
19097
19333
  }
19098
19334
  }
19335
+ /** How many other rows share this home root's project. Zero when this root is a lane or missing. */
19336
+ homeLaneCount(projectRoot) {
19337
+ const row = this.projectRow(`SELECT ${PROJECT_COLUMNS} FROM projects WHERE project_key = ?`, projectRuntimeKey(resolve6(projectRoot)));
19338
+ if (!row || row.lane_of !== null) return 0;
19339
+ const count = this.database.prepare(
19340
+ "SELECT COUNT(*) AS total FROM projects WHERE project_id = ? AND project_key != ?"
19341
+ ).get(row.project_id, row.project_key);
19342
+ return Number(count?.total ?? 0);
19343
+ }
19344
+ /** Caller holds the write transaction. Does not commit. */
19345
+ deleteRegisteredRoot(projectRoot) {
19346
+ const projectKey = projectRuntimeKey(resolve6(projectRoot));
19347
+ const row = this.projectRow(`SELECT ${PROJECT_COLUMNS} FROM projects WHERE project_key = ?`, projectKey);
19348
+ if (!row) return false;
19349
+ this.database.prepare("DELETE FROM projects WHERE project_key = ?").run(projectKey);
19350
+ if (row.lane_of === null) {
19351
+ const successor = this.database.prepare(
19352
+ "SELECT project_key FROM projects WHERE project_id = ? ORDER BY registered_at LIMIT 1"
19353
+ ).get(row.project_id);
19354
+ if (successor) {
19355
+ this.database.prepare("UPDATE projects SET lane_of = NULL WHERE project_key = ?").run(successor.project_key);
19356
+ this.database.prepare(
19357
+ "UPDATE projects SET lane_of = ? WHERE project_id = ? AND project_key != ? AND (lane_of IS NULL OR lane_of = ?)"
19358
+ ).run(successor.project_key, row.project_id, successor.project_key, projectKey);
19359
+ }
19360
+ }
19361
+ return true;
19362
+ }
19363
+ /** The home row for a project id, or the earliest row when every row is a lane. */
19099
19364
  project(projectId) {
19100
- const row = this.database.prepare("SELECT project_id, project_root, project_key, home_runtime_id, config_revision, registered_at FROM projects WHERE project_id = ?").get(projectId);
19101
- return row ? {
19102
- projectId: row.project_id,
19103
- projectRoot: row.project_root,
19104
- projectKey: row.project_key,
19105
- homeRuntimeId: row.home_runtime_id,
19106
- ...row.config_revision !== null ? { configRevision: row.config_revision } : {},
19107
- registeredAt: row.registered_at
19108
- } : void 0;
19365
+ const row = this.projectRow(
19366
+ `SELECT ${PROJECT_COLUMNS} FROM projects WHERE project_id = ? ORDER BY CASE WHEN lane_of IS NULL THEN 0 ELSE 1 END, registered_at LIMIT 1`,
19367
+ projectId
19368
+ );
19369
+ return row ? registrationFromRow(row) : void 0;
19109
19370
  }
19110
19371
  projectByRoot(projectRoot) {
19111
- const key = projectRuntimeKey(projectRoot);
19112
- const row = this.database.prepare("SELECT project_id, project_root, project_key, home_runtime_id, config_revision, registered_at FROM projects WHERE project_key = ?").get(key);
19113
- return row ? {
19114
- projectId: row.project_id,
19115
- projectRoot: row.project_root,
19116
- projectKey: row.project_key,
19117
- homeRuntimeId: row.home_runtime_id,
19118
- ...row.config_revision !== null ? { configRevision: row.config_revision } : {},
19119
- registeredAt: row.registered_at
19120
- } : void 0;
19372
+ const row = this.projectRow(`SELECT ${PROJECT_COLUMNS} FROM projects WHERE project_key = ?`, projectRuntimeKey(projectRoot));
19373
+ return row ? registrationFromRow(row) : void 0;
19374
+ }
19375
+ projectRow(sql, parameter) {
19376
+ return this.database.prepare(sql).get(parameter);
19377
+ }
19378
+ insertHome(registration, projectRoot, projectKey) {
19379
+ this.database.prepare(`
19380
+ INSERT INTO projects (project_key, project_id, project_root, home_runtime_id, config_revision, registered_at, lane_of)
19381
+ VALUES (?, ?, ?, ?, ?, ?, NULL)
19382
+ `).run(projectKey, registration.projectId, projectRoot, registration.homeRuntimeId, registration.configRevision ?? null, registration.now);
19383
+ }
19384
+ insertedRegistration(registration, projectRoot, projectKey, laneOf) {
19385
+ return {
19386
+ projectId: registration.projectId,
19387
+ projectRoot,
19388
+ projectKey,
19389
+ homeRuntimeId: registration.homeRuntimeId,
19390
+ ...registration.configRevision !== void 0 ? { configRevision: registration.configRevision } : {},
19391
+ ...laneOf !== void 0 ? { laneOf } : {},
19392
+ registeredAt: registration.now
19393
+ };
19121
19394
  }
19122
19395
  };
19123
19396
  var KXM_RUN_EVENT_SCHEMA = "kxm.run-event.v1";
@@ -23559,7 +23832,8 @@ function openKxmRuntimeContext(projectRoot, options) {
23559
23832
  projectRoot: bundle.projectRoot,
23560
23833
  homeRuntimeId: options.homeRuntimeId,
23561
23834
  configRevision: bundle.configRevision,
23562
- now: options.now ?? (/* @__PURE__ */ new Date()).toISOString()
23835
+ now: options.now ?? (/* @__PURE__ */ new Date()).toISOString(),
23836
+ ...options.logger ? { logger: options.logger } : {}
23563
23837
  });
23564
23838
  const eventStore = new KxmRunEventStore(join7(paths.projectsDir, registration.projectKey, "run-events.db"));
23565
23839
  registerKxmRuntimeHandle(eventStore.path);
@@ -24022,7 +24296,7 @@ async function beginOneShotEvidence(root, intent, sensitive) {
24022
24296
  // plugins/kxm/src/prices.ts
24023
24297
  var import_yaml3 = __toESM(require_dist(), 1);
24024
24298
  import { createHash as createHash10 } from "node:crypto";
24025
- import { existsSync as existsSync8, readFileSync as readFileSync6, statSync, writeFileSync as writeFileSync3 } from "node:fs";
24299
+ import { existsSync as existsSync8, readFileSync as readFileSync6, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
24026
24300
  import { join as join9 } from "node:path";
24027
24301
 
24028
24302
  // plugins/kxm/src/price-calc.ts
@@ -24175,7 +24449,7 @@ function parsePriceCatalog(text) {
24175
24449
  return catalog;
24176
24450
  }
24177
24451
  function loadPriceCatalog(rootOrPath) {
24178
- const candidatePath = existsSync8(join9(rootOrPath, ".kxm", "prices.yaml")) ? join9(rootOrPath, ".kxm", "prices.yaml") : existsSync8(join9(rootOrPath, "prices.yaml")) ? join9(rootOrPath, "prices.yaml") : existsSync8(rootOrPath) && statSync(rootOrPath).isFile() ? rootOrPath : void 0;
24452
+ const candidatePath = existsSync8(join9(rootOrPath, ".kxm", "prices.yaml")) ? join9(rootOrPath, ".kxm", "prices.yaml") : existsSync8(join9(rootOrPath, "prices.yaml")) ? join9(rootOrPath, "prices.yaml") : existsSync8(rootOrPath) && statSync2(rootOrPath).isFile() ? rootOrPath : void 0;
24179
24453
  if (!candidatePath || !existsSync8(candidatePath)) {
24180
24454
  return void 0;
24181
24455
  }
@@ -24202,14 +24476,14 @@ var import_yaml4 = __toESM(require_dist(), 1);
24202
24476
 
24203
24477
  // plugins/kxm/src/routes.ts
24204
24478
  var RETIRED_POLICY = ".kxm/producers.yaml";
24205
- var empty = () => ({ schema: "kxm.routes.v2", updatedAt: (/* @__PURE__ */ new Date()).toISOString(), admitted: [], disabled: [], roles: {} });
24479
+ var empty = () => ({ schema: "kxm.routes.v2", updatedAt: (/* @__PURE__ */ new Date()).toISOString(), admitted: [], disabled: [] });
24206
24480
  function loadRoutePolicy(root) {
24207
24481
  if (existsSync9(join10(root, RETIRED_POLICY))) throw new Error(`retired ${RETIRED_POLICY} present; use .kxm/routes.yaml (kxm.routes.v2)`);
24208
24482
  const path = join10(root, ".kxm", "routes.yaml");
24209
24483
  if (!existsSync9(path)) return empty();
24210
24484
  const value = (0, import_yaml6.parse)(readFileSync7(path, "utf8"));
24211
24485
  if (value?.schema !== "kxm.routes.v2" || !Array.isArray(value.admitted)) throw new Error("invalid .kxm/routes.yaml");
24212
- return { schema: "kxm.routes.v2", updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : (/* @__PURE__ */ new Date()).toISOString(), admitted: value.admitted.filter((x) => typeof x === "string"), disabled: Array.isArray(value.disabled) ? value.disabled.filter((x) => typeof x === "string") : [], roles: value.roles && typeof value.roles === "object" ? Object.fromEntries(Object.entries(value.roles).filter(([, v]) => Array.isArray(v)).map(([k, v]) => [k, v.filter((x) => typeof x === "string")])) : {} };
24486
+ return { schema: "kxm.routes.v2", updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : (/* @__PURE__ */ new Date()).toISOString(), admitted: value.admitted.filter((x) => typeof x === "string"), disabled: Array.isArray(value.disabled) ? value.disabled.filter((x) => typeof x === "string") : [] };
24213
24487
  }
24214
24488
  function listRoleBindings(root) {
24215
24489
  const dir = join10(root, ".kxm", "roles");
@@ -24222,15 +24496,11 @@ function listRoleBindings(root) {
24222
24496
  }
24223
24497
  return result;
24224
24498
  }
24225
- function isRouteAdmitted(root, modelId) {
24226
- const policy = loadRoutePolicy(root);
24227
- return policy.admitted.includes(modelId) && !policy.disabled.includes(modelId);
24228
- }
24229
24499
 
24230
24500
  // plugins/kxm/src/worktree-witness.ts
24231
- import { spawnSync as spawnSync3 } from "node:child_process";
24501
+ import { spawnSync as spawnSync4 } from "node:child_process";
24232
24502
  function gitText(cwd, args) {
24233
- const result = spawnSync3("git", ["-C", cwd, ...args], {
24503
+ const result = spawnSync4("git", ["-C", cwd, ...args], {
24234
24504
  encoding: "utf8",
24235
24505
  windowsHide: true,
24236
24506
  timeout: 15e3
@@ -24543,7 +24813,7 @@ function pruneContextPacket(packet, tokenBudget) {
24543
24813
  }
24544
24814
 
24545
24815
  // plugins/kxm/src/dispatch-context.ts
24546
- import { spawnSync as spawnSync4 } from "node:child_process";
24816
+ import { spawnSync as spawnSync5 } from "node:child_process";
24547
24817
  import { readdirSync as readdirSync8 } from "node:fs";
24548
24818
  import { extname as extname3, join as join13 } from "node:path";
24549
24819
 
@@ -25303,7 +25573,7 @@ function loadAuthoredMemory(repoRoot) {
25303
25573
  // plugins/kxm/src/skills.ts
25304
25574
  var import_yaml8 = __toESM(require_dist(), 1);
25305
25575
  import { createHash as createHash11 } from "node:crypto";
25306
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync7, readFileSync as readFileSync9, renameSync, rmSync, statSync as statSync2, writeFileSync as writeFileSync6 } from "node:fs";
25576
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync7, readFileSync as readFileSync9, renameSync, rmSync, statSync as statSync3, writeFileSync as writeFileSync6 } from "node:fs";
25307
25577
  import { dirname as dirname7, join as join12 } from "node:path";
25308
25578
  var SKILL_CANDIDATE_SCHEMA = "kxm.skill-candidate.v1";
25309
25579
  var SKILL_EVALUATION_SCHEMA = "kxm.skill-evaluation.v1";
@@ -25683,7 +25953,7 @@ function boundedList(value, field) {
25683
25953
  return [...new Set(refs)];
25684
25954
  }
25685
25955
  function readdirSorted(dir) {
25686
- return readdirSync7(dir).filter((entry) => statSync2(join12(dir, entry)).isDirectory()).sort();
25956
+ return readdirSync7(dir).filter((entry) => statSync3(join12(dir, entry)).isDirectory()).sort();
25687
25957
  }
25688
25958
 
25689
25959
  // plugins/kxm/src/dispatch-context.ts
@@ -25744,7 +26014,7 @@ function gitEnvironment2() {
25744
26014
  return Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.toUpperCase().startsWith("GIT_")));
25745
26015
  }
25746
26016
  function committedGateGap(projectRoot) {
25747
- const result = spawnSync4("git", [
26017
+ const result = spawnSync5("git", [
25748
26018
  "--no-optional-locks",
25749
26019
  "-C",
25750
26020
  projectRoot,
@@ -25886,11 +26156,11 @@ function assembleDispatchContext(sources, input) {
25886
26156
  import { join as join14 } from "node:path";
25887
26157
 
25888
26158
  // plugins/kxm/src/artifacts-exist.ts
25889
- import { lstatSync as lstatSync5, realpathSync as realpathSync3, statSync as statSync3 } from "node:fs";
25890
- import { isAbsolute as isAbsolute4, relative as relative4, resolve as resolve8 } from "node:path";
26159
+ import { lstatSync as lstatSync5, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
26160
+ import { isAbsolute as isAbsolute5, relative as relative4, resolve as resolve8 } from "node:path";
25891
26161
  function staysUnder(root, candidate) {
25892
26162
  const child = relative4(root, candidate);
25893
- return child === "" || !isAbsolute4(child) && child !== ".." && !child.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`);
26163
+ return child === "" || !isAbsolute5(child) && child !== ".." && !child.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`);
25894
26164
  }
25895
26165
  function verifyArtifactExists(rootInput, pathInput) {
25896
26166
  const lexicalRoot = resolve8(rootInput);
@@ -25901,12 +26171,12 @@ function verifyArtifactExists(rootInput, pathInput) {
25901
26171
  let realRoot;
25902
26172
  let realPath;
25903
26173
  try {
25904
- realRoot = realpathSync3(lexicalRoot);
26174
+ realRoot = realpathSync4(lexicalRoot);
25905
26175
  } catch {
25906
26176
  return { ok: false, error: "artifact_unreadable", path: lexicalPath };
25907
26177
  }
25908
26178
  try {
25909
- realPath = realpathSync3(lexicalPath);
26179
+ realPath = realpathSync4(lexicalPath);
25910
26180
  } catch {
25911
26181
  return { ok: false, error: "artifact_missing", path: lexicalPath };
25912
26182
  }
@@ -25918,7 +26188,7 @@ function verifyArtifactExists(rootInput, pathInput) {
25918
26188
  if (link.isSymbolicLink()) {
25919
26189
  return { ok: false, error: "artifact_outside_workspace_assets", path: lexicalPath };
25920
26190
  }
25921
- const artifact = statSync3(realPath);
26191
+ const artifact = statSync4(realPath);
25922
26192
  if (!artifact.isFile()) return { ok: false, error: "artifact_not_file", path: lexicalPath };
25923
26193
  if (artifact.size < 1) return { ok: false, error: "artifact_empty", path: lexicalPath };
25924
26194
  return { ok: true, path: realPath, bytes: artifact.size };
@@ -27793,25 +28063,18 @@ function commitCommandUncertainty(context, prepared, token, reason, observation,
27793
28063
  }
27794
28064
  }
27795
28065
  var kxmPanelDispatchSeams = {};
27796
- function selectorsNamedByRoutes(projectRoot, routeIds) {
27797
- const selectors = /* @__PURE__ */ new Set();
27798
- for (const routeId2 of routeIds) {
27799
- const file = join15(projectRoot, ".kxm", "models", `${routeId2}.yaml`);
27800
- if (!existsSync12(file)) continue;
27801
- let parsed;
27802
- try {
27803
- parsed = (0, import_yaml9.parse)(readFileSync10(file, "utf8"));
27804
- } catch {
27805
- continue;
27806
- }
27807
- const model = typeof parsed.model === "string" ? parsed.model : "";
27808
- const vendor = typeof parsed.vendor === "string" ? parsed.vendor : "";
27809
- const harness = typeof parsed.harness === "string" ? parsed.harness : "";
27810
- if (model) selectors.add(model);
27811
- if (vendor && model) selectors.add(`${vendor}/${model}`);
27812
- if (harness && model) selectors.add(`${harness}/${model}`);
28066
+ function readYamlFile(file) {
28067
+ return readModelDocument(file);
28068
+ }
28069
+ function readModelDocument(file) {
28070
+ if (!existsSync12(file)) return void 0;
28071
+ try {
28072
+ const parsed = (0, import_yaml9.parse)(readFileSync10(file, "utf8"));
28073
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
28074
+ return parsed;
28075
+ } catch {
28076
+ return void 0;
27813
28077
  }
27814
- return [...selectors];
27815
28078
  }
27816
28079
  function unreconciledPanelAttemptId(state) {
27817
28080
  const current = state.currentStep;
@@ -27828,82 +28091,118 @@ function unreconciledPanelAttemptId(state) {
27828
28091
  return void 0;
27829
28092
  }
27830
28093
  function resolveProducerRoute(projectRoot, step, agentId) {
27831
- let agentModel;
27832
- const agentFile = join15(projectRoot, ".kxm", "agents", `${agentId}.yaml`);
27833
- if (existsSync12(agentFile)) {
27834
- try {
27835
- const parsed = (0, import_yaml9.parse)(readFileSync10(agentFile, "utf8"));
27836
- if (typeof parsed?.model === "string") {
27837
- agentModel = parsed.model;
27838
- } else if (parsed?.model && typeof parsed.model === "object" && !Array.isArray(parsed.model)) {
27839
- const declared = parsed.model;
27840
- const providerOk = typeof declared.provider === "string" && declared.provider.length > 0 && !declared.provider.includes("/");
27841
- const modelOk = typeof declared.model === "string" && declared.model.length > 0 && !declared.model.startsWith("/") && !declared.model.endsWith("/");
27842
- if (providerOk && modelOk) {
27843
- agentModel = `${declared.provider}/${declared.model}`;
27844
- } else {
27845
- return {
27846
- error: {
27847
- reason: "step_unsupported",
27848
- field: "model",
27849
- detail: "producer_route_unsupported: invalid model declaration"
27850
- }
27851
- };
27852
- }
27853
- }
27854
- } catch {
27855
- }
27856
- }
27857
- let selector = typeof step.model === "string" ? step.model : agentModel;
27858
- if (!selector) {
27859
- if (agentId === "implementer") {
27860
- selector = "xai/grok-4.6";
27861
- }
27862
- }
27863
- if (!selector) {
28094
+ if ((step.kind === "agent" || step.kind === "moa") && step.model !== void 0) {
27864
28095
  return {
27865
28096
  error: {
27866
28097
  reason: "step_unsupported",
27867
28098
  field: "model",
27868
- detail: "producer_route_unsupported: agent or step has no model declared"
28099
+ detail: "producer_route_unsupported: agent step model is not honored"
27869
28100
  }
27870
28101
  };
27871
28102
  }
27872
- const slash = selector.indexOf("/");
27873
- if (slash <= 0 || slash === selector.length - 1) {
28103
+ const agent = readYamlFile(join15(projectRoot, ".kxm", "agents", `${agentId}.yaml`));
28104
+ const role = typeof agent?.role === "string" ? agent.role : "";
28105
+ if (!role) {
27874
28106
  return {
27875
28107
  error: {
27876
28108
  reason: "step_unsupported",
27877
28109
  field: "model",
27878
- detail: `producer_route_unsupported: invalid selector '${selector}'`
28110
+ detail: "producer_route_unsupported: agent has no role"
27879
28111
  }
27880
28112
  };
27881
28113
  }
27882
- if (!isRouteAdmitted(projectRoot, selector)) {
28114
+ const roleDoc = readYamlFile(join15(projectRoot, ".kxm", "roles", `${role}.yaml`));
28115
+ const roster = Array.isArray(roleDoc?.roster) ? roleDoc.roster : void 0;
28116
+ if (!roster) {
27883
28117
  return {
27884
28118
  error: {
27885
28119
  reason: "step_unsupported",
27886
28120
  field: "model",
27887
- detail: `producer_route_unsupported: model '${selector}' is not admitted`
28121
+ detail: `producer_route_unsupported: role '${role}' has no roster`
27888
28122
  }
27889
28123
  };
27890
28124
  }
27891
- const role = agentId === "implementer" ? "writer" : agentId;
27892
- const roleBindings = listRoleBindings(projectRoot);
27893
- const roster = roleBindings[role];
27894
- const rosterSelectors = roster ? selectorsNamedByRoutes(projectRoot, roster) : void 0;
27895
- if (rosterSelectors && !rosterSelectors.includes(selector)) {
28125
+ const policy = loadRoutePolicy(projectRoot);
28126
+ for (const entry of roster) {
28127
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
28128
+ const record2 = entry;
28129
+ if (typeof record2.route !== "string" || record2.route.length === 0) continue;
28130
+ const modelPath = join15(projectRoot, ".kxm", "models", `${record2.route}.yaml`);
28131
+ const doc = readModelDocument(modelPath);
28132
+ if (!doc) {
28133
+ return {
28134
+ error: {
28135
+ reason: "step_unsupported",
28136
+ field: "model",
28137
+ detail: `producer_route_unsupported: model file '.kxm/models/${record2.route}.yaml' is missing or unreadable`
28138
+ }
28139
+ };
28140
+ }
28141
+ if (doc.status !== "admitted") continue;
28142
+ const modelName = typeof doc.model === "string" ? doc.model : "";
28143
+ const vendor = typeof doc.vendor === "string" ? doc.vendor : "";
28144
+ const harness = typeof doc.harness === "string" ? doc.harness : "";
28145
+ if (!modelName || !vendor || !harness) continue;
28146
+ const candidates = [modelName, `${vendor}/${modelName}`, `${harness}/${modelName}`];
28147
+ const admittedSelector = candidates.find((candidate) => policy.admitted.includes(candidate));
28148
+ const disabledSelector = candidates.find((candidate) => policy.disabled.includes(candidate));
28149
+ if (!admittedSelector && disabledSelector) {
28150
+ return {
28151
+ error: {
28152
+ reason: "step_unsupported",
28153
+ field: "model",
28154
+ detail: `producer_route_unsupported: first admitted route '${record2.route}' for role '${role}' is disabled`
28155
+ }
28156
+ };
28157
+ }
28158
+ const selector = admittedSelector ?? (modelName.includes("/") ? modelName : `${vendor}/${modelName}`);
28159
+ if (policy.disabled.includes(selector)) {
28160
+ return {
28161
+ error: {
28162
+ reason: "step_unsupported",
28163
+ field: "model",
28164
+ detail: `producer_route_unsupported: first admitted route '${record2.route}' for role '${role}' is disabled`
28165
+ }
28166
+ };
28167
+ }
28168
+ if (!policy.admitted.includes(selector)) {
28169
+ return {
28170
+ error: {
28171
+ reason: "step_unsupported",
28172
+ field: "model",
28173
+ detail: `producer_route_unsupported: model '${selector}' is not admitted`
28174
+ }
28175
+ };
28176
+ }
28177
+ const slash = selector.indexOf("/");
28178
+ if (slash <= 0 || slash === selector.length - 1) {
28179
+ return {
28180
+ error: {
28181
+ reason: "step_unsupported",
28182
+ field: "model",
28183
+ detail: `producer_route_unsupported: invalid selector '${selector}'`
28184
+ }
28185
+ };
28186
+ }
28187
+ const permissions = Array.isArray(doc.permissions) ? doc.permissions.filter((item) => typeof item === "string") : [];
27896
28188
  return {
27897
- error: {
27898
- reason: "step_unsupported",
27899
- field: "model",
27900
- detail: `producer_route_unsupported: model '${selector}' not in role '${role}' roster`
27901
- }
28189
+ provider: selector.slice(0, slash),
28190
+ model: selector.slice(slash + 1),
28191
+ selector,
28192
+ harness,
28193
+ routeId: record2.route,
28194
+ role,
28195
+ permissions,
28196
+ ...typeof record2.effort === "string" ? { effort: record2.effort } : {}
27902
28197
  };
27903
28198
  }
27904
- const provider = selector.slice(0, slash);
27905
- const model = selector.slice(slash + 1);
27906
- return { provider, model, selector };
28199
+ return {
28200
+ error: {
28201
+ reason: "step_unsupported",
28202
+ field: "model",
28203
+ detail: `producer_route_unsupported: role '${role}' has no admitted route`
28204
+ }
28205
+ };
27907
28206
  }
27908
28207
  function projectAgentStepTimeoutMs(context, run) {
27909
28208
  return loadKxmRunPlanEnvelope(context.eventStore, run).projectLimits.agentStepTimeoutMs ?? KXM_DEFAULT_AGENT_STEP_TIMEOUT_MS;
@@ -28121,8 +28420,7 @@ function prepareDispatch(context, runId, producerId, dispatchSources) {
28121
28420
  const writeRefusal = unsupportedLiveWrite(
28122
28421
  context.projectRoot,
28123
28422
  step,
28124
- agentId,
28125
- resolvedRoute.selector,
28423
+ resolvedRoute,
28126
28424
  loadKxmRunPlanEnvelope(context.eventStore, run).projectLimits.maxConcurrentRuns
28127
28425
  );
28128
28426
  if (writeRefusal) return { kind: "return", state, handoff: { ...writeRefusal, stepId } };
@@ -28151,7 +28449,7 @@ function prepareDispatch(context, runId, producerId, dispatchSources) {
28151
28449
  for (const event of events) context.eventStore.appendEvent(event);
28152
28450
  const entered = foldStoredKxmRun(context, run);
28153
28451
  persistKxmRunState(context, runId, entered, events[events.length - 1].sequence);
28154
- const first = birthMember(context, {
28452
+ const firstBirth = birthMember(context, {
28155
28453
  run,
28156
28454
  plan,
28157
28455
  step,
@@ -28162,6 +28460,10 @@ function prepareDispatch(context, runId, producerId, dispatchSources) {
28162
28460
  resolvedRoute,
28163
28461
  dispatchSources
28164
28462
  });
28463
+ if ("handoff" in firstBirth) {
28464
+ return { kind: "return", state: entered, handoff: firstBirth.handoff };
28465
+ }
28466
+ const first = firstBirth;
28165
28467
  return {
28166
28468
  kind: "panel",
28167
28469
  panel: {
@@ -28224,9 +28526,10 @@ function birthMember(context, input) {
28224
28526
  let resolvedRoute = input.resolvedRoute;
28225
28527
  if (!resolvedRoute && input.producerId && input.producerId !== "driver-simulated") {
28226
28528
  const routeResult = resolveProducerRoute(context.projectRoot, input.step, agentId);
28227
- if (!("error" in routeResult)) {
28228
- resolvedRoute = routeResult;
28529
+ if ("error" in routeResult) {
28530
+ return { handoff: { ...routeResult.error, stepId: input.stepId } };
28229
28531
  }
28532
+ resolvedRoute = routeResult;
28230
28533
  }
28231
28534
  const dispatchContext = assembleDispatchContext(input.dispatchSources, {
28232
28535
  projectId: run.projectId,
@@ -28333,11 +28636,11 @@ function birthMember(context, input) {
28333
28636
  prompt: input.step.instructions ? `${input.step.instructions}
28334
28637
 
28335
28638
  ${generatedPrompt}` : generatedPrompt,
28336
- thinking: input.stepAttempt <= 1 ? "low" : "medium",
28337
28639
  permission: Object.values(input.step.repositories).some((access) => access === "write") ? "edit" : "read-only",
28338
28640
  ...(input.step.kind === "agent" || input.step.kind === "moa") && input.step.timeoutMs !== void 0 ? { timeoutMs: input.step.timeoutMs } : {},
28339
28641
  contextPacket,
28340
- ...resolvedRoute ? { provider: resolvedRoute.provider, model: resolvedRoute.model } : {}
28642
+ ...resolvedRoute?.effort !== void 0 ? { thinking: resolvedRoute.effort } : {},
28643
+ ...resolvedRoute ? { harness: resolvedRoute.harness, provider: resolvedRoute.provider, model: resolvedRoute.model } : {}
28341
28644
  },
28342
28645
  controller,
28343
28646
  state: next
@@ -28482,13 +28785,14 @@ async function drivePanel(context, panel, producer) {
28482
28785
  })();
28483
28786
  pending.set(member.attemptId, work);
28484
28787
  };
28788
+ let routeHandoff;
28485
28789
  const tryBirth = () => {
28486
- if (stopBirths) return void 0;
28790
+ if (stopBirths || routeHandoff) return void 0;
28487
28791
  return context.eventStore.transaction(() => {
28488
28792
  const run = requireRun(context, runId);
28489
28793
  const state = foldStoredKxmRun(context, run);
28490
28794
  if (!birthAllowed(state, panel.step)) return void 0;
28491
- return birthMember(context, {
28795
+ const born = birthMember(context, {
28492
28796
  run,
28493
28797
  plan: panel.plan,
28494
28798
  step: panel.step,
@@ -28497,6 +28801,11 @@ async function drivePanel(context, panel, producer) {
28497
28801
  producerId: producer.id,
28498
28802
  dispatchSources: panel.dispatchSources
28499
28803
  });
28804
+ if ("handoff" in born) {
28805
+ routeHandoff = born.handoff;
28806
+ return void 0;
28807
+ }
28808
+ return born;
28500
28809
  });
28501
28810
  };
28502
28811
  const settleInvoked = (member, produced) => {
@@ -28540,6 +28849,10 @@ async function drivePanel(context, panel, producer) {
28540
28849
  while (!stopBirths && !settlementFailed) {
28541
28850
  try {
28542
28851
  const next = tryBirth();
28852
+ if (routeHandoff) {
28853
+ stopBirths = true;
28854
+ break;
28855
+ }
28543
28856
  if (!next) break;
28544
28857
  launch(next);
28545
28858
  } catch {
@@ -28550,6 +28863,13 @@ async function drivePanel(context, panel, producer) {
28550
28863
  return unreconciledHandoff(state, panel.stepId);
28551
28864
  }
28552
28865
  }
28866
+ if (routeHandoff) {
28867
+ const handoff = routeHandoff;
28868
+ abortOwned();
28869
+ await drainPendingInvoked();
28870
+ const state = foldStoredKxmRun(context, requireRun(context, runId));
28871
+ return { state, handoff };
28872
+ }
28553
28873
  if (pending.size === 0) break;
28554
28874
  const finished = await Promise.race(pending.values());
28555
28875
  pending.delete(finished.attemptId);
@@ -29113,25 +29433,7 @@ function unsupportedStep(plan, step, producerId) {
29113
29433
  }
29114
29434
  return void 0;
29115
29435
  }
29116
- function readYamlRecord(path) {
29117
- if (!existsSync12(path)) return void 0;
29118
- try {
29119
- const parsed = (0, import_yaml9.parse)(readFileSync10(path, "utf8"));
29120
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
29121
- } catch {
29122
- return void 0;
29123
- }
29124
- return void 0;
29125
- }
29126
- function agentHarness(projectRoot, agentId) {
29127
- const harness = readYamlRecord(join15(projectRoot, ".kxm", "agents", `${agentId}.yaml`))?.harness;
29128
- return typeof harness === "string" && harness.length > 0 ? harness : void 0;
29129
- }
29130
- function projectDefaultHarness(projectRoot) {
29131
- const harness = readYamlRecord(join15(projectRoot, ".kxm", "project.yaml"))?.defaultHarness;
29132
- return typeof harness === "string" && harness.length > 0 ? harness : "pi";
29133
- }
29134
- function unsupportedLiveWrite(projectRoot, step, agentId, selector, maxConcurrentRuns) {
29436
+ function unsupportedLiveWrite(projectRoot, step, route, maxConcurrentRuns) {
29135
29437
  if (!Object.values(step.repositories).some((access) => access === "write")) return void 0;
29136
29438
  if (step.assignments.maximum !== 1) {
29137
29439
  return {
@@ -29147,42 +29449,23 @@ function unsupportedLiveWrite(projectRoot, step, agentId, selector, maxConcurren
29147
29449
  detail: "live write steps require limits.maxConcurrentRuns of 1; concurrent runs share one checkout"
29148
29450
  };
29149
29451
  }
29150
- const harness = agentHarness(projectRoot, agentId) ?? projectDefaultHarness(projectRoot);
29151
- if (!oneShotWriterArgs(harness)) {
29452
+ if (!oneShotWriterArgs(route.harness)) {
29152
29453
  return {
29153
29454
  reason: "step_unsupported",
29154
29455
  field: "repositories",
29155
- detail: `live write steps require an audited writer profile; ${harness} has none`
29456
+ detail: `live write steps require an audited writer profile; ${route.harness} has none`
29156
29457
  };
29157
29458
  }
29158
- const rosterPath = join15(projectRoot, ".kxm", "roster.yaml");
29159
- if (!existsSync12(rosterPath)) return void 0;
29160
- const roster = readYamlRecord(rosterPath);
29161
- if (!roster || roster.schema !== "kxm.developer-roster.v1") {
29162
- return { reason: "step_unsupported", field: "model", detail: "live write steps require a readable kxm.developer-roster.v1" };
29163
- }
29164
- const routes = roster.routes;
29165
- const lineup = roster.lineup;
29166
- const writerIds = lineup && typeof lineup === "object" && !Array.isArray(lineup) ? lineup.writer : void 0;
29167
- if (!routes || typeof routes !== "object" || Array.isArray(routes) || !Array.isArray(writerIds)) {
29168
- return { reason: "step_unsupported", field: "model", detail: "developer roster has no writer lineup" };
29169
- }
29170
- const allowed = writerIds.some((id) => {
29171
- if (typeof id !== "string") return false;
29172
- const route = routes[id];
29173
- if (!route || typeof route !== "object" || Array.isArray(route)) return false;
29174
- const record2 = route;
29175
- if (record2.harness !== harness || record2.status !== "admitted") return false;
29176
- if (!Array.isArray(record2.permissions) || !record2.permissions.includes("edit")) return false;
29177
- const model = typeof record2.model === "string" ? record2.model : "";
29178
- const vendor = typeof record2.vendor === "string" ? record2.vendor : "";
29179
- return model === selector || vendor.length > 0 && `${vendor}/${model}` === selector;
29180
- });
29181
- if (!allowed) {
29459
+ const writerIds = listRoleBindings(projectRoot).writer;
29460
+ if (!Array.isArray(writerIds) || writerIds.length === 0) {
29461
+ return { reason: "step_unsupported", field: "model", detail: "writer roster has no admitted edit route" };
29462
+ }
29463
+ const named = writerIds.includes(route.routeId);
29464
+ if (!named || !route.permissions.includes("edit")) {
29182
29465
  return {
29183
29466
  reason: "step_unsupported",
29184
29467
  field: "model",
29185
- detail: `live write route ${selector} on ${harness} is not on the developer roster writer lineup`
29468
+ detail: `live write route ${route.selector} on ${route.harness} is not an admitted edit route on the writer roster`
29186
29469
  };
29187
29470
  }
29188
29471
  return void 0;
@@ -29619,20 +29902,11 @@ function determineOutcome(text, allowedOutcomes) {
29619
29902
  return "failed";
29620
29903
  }
29621
29904
  function createKxmOneShotProducer(options = {}) {
29622
- const defaultHarness = options.defaultHarness ?? "claude";
29623
29905
  const running = /* @__PURE__ */ new Map();
29624
29906
  let closed = false;
29625
- function resolveHarnessForRequest(request) {
29907
+ function requireHarness(request) {
29626
29908
  if (request.harness) return request.harness;
29627
- if (options.resolveHarness) {
29628
- const resolved = options.resolveHarness(request.agentId, request.runId);
29629
- if (resolved) return resolved;
29630
- }
29631
- if (options.resolveModel) {
29632
- const resolved = options.resolveModel(request.agentId, request.runId);
29633
- if (resolved?.harness) return resolved.harness;
29634
- }
29635
- return defaultHarness;
29909
+ throw new Error("producer_harness_required");
29636
29910
  }
29637
29911
  function parseModelString(spec, harness) {
29638
29912
  const trimmed = spec.trim();
@@ -29694,7 +29968,7 @@ function createKxmOneShotProducer(options = {}) {
29694
29968
  }
29695
29969
  async function executeRequest(request) {
29696
29970
  const startTime = Date.now();
29697
- const harness = resolveHarnessForRequest(request);
29971
+ const harness = requireHarness(request);
29698
29972
  const resolved = resolveModelForRequest(request, harness);
29699
29973
  if (closed) throw new Error("oneshot_producer_closed");
29700
29974
  const cancelled = () => ({
@@ -30041,7 +30315,7 @@ var RuntimeHubClient = class {
30041
30315
  // plugins/kxm/src/hub-binding.ts
30042
30316
  import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync11, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
30043
30317
  import { homedir as homedir2 } from "node:os";
30044
- import { dirname as dirname8, isAbsolute as isAbsolute5, join as join16, resolve as resolve9 } from "node:path";
30318
+ import { dirname as dirname8, isAbsolute as isAbsolute6, join as join16, resolve as resolve9 } from "node:path";
30045
30319
  var HUB_BINDING_SCHEMA = "kxm.hub-binding.v1";
30046
30320
  var HubBindingError = class extends Error {
30047
30321
  constructor(message) {
@@ -30052,17 +30326,17 @@ var HubBindingError = class extends Error {
30052
30326
  function resolveUserStateRoot(env) {
30053
30327
  const explicit = env.KXM_STATE_HOME?.trim();
30054
30328
  if (explicit) {
30055
- if (!isAbsolute5(explicit)) throw new HubBindingError("local_state_root_not_absolute");
30329
+ if (!isAbsolute6(explicit)) throw new HubBindingError("local_state_root_not_absolute");
30056
30330
  return resolve9(explicit);
30057
30331
  }
30058
30332
  if (process.platform === "win32") {
30059
30333
  const localAppData = env.LOCALAPPDATA?.trim();
30060
- const base2 = localAppData && isAbsolute5(localAppData) ? localAppData : join16(homedir2(), "AppData", "Local");
30334
+ const base2 = localAppData && isAbsolute6(localAppData) ? localAppData : join16(homedir2(), "AppData", "Local");
30061
30335
  return resolve9(base2, "KXM");
30062
30336
  }
30063
30337
  if (process.platform === "darwin") return resolve9(homedir2(), "Library", "Application Support", "KXM");
30064
30338
  const xdgState = env.XDG_STATE_HOME?.trim();
30065
- const base = xdgState && isAbsolute5(xdgState) ? xdgState : join16(homedir2(), ".local", "state");
30339
+ const base = xdgState && isAbsolute6(xdgState) ? xdgState : join16(homedir2(), ".local", "state");
30066
30340
  return resolve9(base, "kxm");
30067
30341
  }
30068
30342
  function hubBindingFile(env = process.env) {
@@ -30113,7 +30387,7 @@ function readHubBinding(env = process.env) {
30113
30387
  // plugins/kxm/src/hub-env.ts
30114
30388
  import { existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync12, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "node:fs";
30115
30389
  import { homedir as homedir3 } from "node:os";
30116
- import { dirname as dirname9, isAbsolute as isAbsolute6, join as join17, resolve as resolve10 } from "node:path";
30390
+ import { dirname as dirname9, isAbsolute as isAbsolute7, join as join17, resolve as resolve10 } from "node:path";
30117
30391
  var HUB_ENV_SCHEMA = "kxm.hub-env.v1";
30118
30392
  var HubEnvError = class extends Error {
30119
30393
  constructor(message) {
@@ -30124,17 +30398,17 @@ var HubEnvError = class extends Error {
30124
30398
  function resolveUserStateRoot2(env) {
30125
30399
  const explicit = env.KXM_STATE_HOME?.trim();
30126
30400
  if (explicit) {
30127
- if (!isAbsolute6(explicit)) throw new HubEnvError("local_state_root_not_absolute");
30401
+ if (!isAbsolute7(explicit)) throw new HubEnvError("local_state_root_not_absolute");
30128
30402
  return resolve10(explicit);
30129
30403
  }
30130
30404
  if (process.platform === "win32") {
30131
30405
  const localAppData = env.LOCALAPPDATA?.trim();
30132
- const base2 = localAppData && isAbsolute6(localAppData) ? localAppData : join17(homedir3(), "AppData", "Local");
30406
+ const base2 = localAppData && isAbsolute7(localAppData) ? localAppData : join17(homedir3(), "AppData", "Local");
30133
30407
  return resolve10(base2, "KXM");
30134
30408
  }
30135
30409
  if (process.platform === "darwin") return resolve10(homedir3(), "Library", "Application Support", "KXM");
30136
30410
  const xdgState = env.XDG_STATE_HOME?.trim();
30137
- const base = xdgState && isAbsolute6(xdgState) ? xdgState : join17(homedir3(), ".local", "state");
30411
+ const base = xdgState && isAbsolute7(xdgState) ? xdgState : join17(homedir3(), ".local", "state");
30138
30412
  return resolve10(base, "kxm");
30139
30413
  }
30140
30414
  function hubEnvFile(env = process.env) {
@@ -30185,7 +30459,7 @@ function resolveClientHubAuthToken(env, project) {
30185
30459
  }
30186
30460
 
30187
30461
  // plugins/kxm/src/logger.ts
30188
- import { appendFileSync, existsSync as existsSync15, mkdirSync as mkdirSync8, renameSync as renameSync4, statSync as statSync4, unlinkSync as unlinkSync2 } from "node:fs";
30462
+ import { appendFileSync, existsSync as existsSync15, mkdirSync as mkdirSync8, renameSync as renameSync4, statSync as statSync5, unlinkSync as unlinkSync2 } from "node:fs";
30189
30463
  import { dirname as dirname10 } from "node:path";
30190
30464
  var LOG_LEVEL_PRIORITY = {
30191
30465
  debug: 10,
@@ -30256,7 +30530,7 @@ function createLogger(options) {
30256
30530
  let currentSize = 0;
30257
30531
  if (filePath && existsSync15(filePath)) {
30258
30532
  try {
30259
- currentSize = statSync4(filePath).size;
30533
+ currentSize = statSync5(filePath).size;
30260
30534
  } catch {
30261
30535
  currentSize = 0;
30262
30536
  }
@@ -30403,6 +30677,24 @@ function readRecentSupervisorError(paths) {
30403
30677
  return void 0;
30404
30678
  }
30405
30679
  }
30680
+ var SETTLED_RUN_STATUS = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
30681
+ function kxmSupervisorRecordIsLive(record2) {
30682
+ return supervisorStatusOf(record2).running;
30683
+ }
30684
+ function unsettledRunIds(context) {
30685
+ const ids = [];
30686
+ for (const stored of context.eventStore.runsForProject(context.projectId, 1e4)) {
30687
+ let status = stored.status;
30688
+ try {
30689
+ status = projectKxmRunReadOnly(context, stored.runId).status;
30690
+ } catch {
30691
+ ids.push(stored.runId);
30692
+ continue;
30693
+ }
30694
+ if (!SETTLED_RUN_STATUS.has(status)) ids.push(stored.runId);
30695
+ }
30696
+ return ids;
30697
+ }
30406
30698
  function supervisorStatusOf(record2) {
30407
30699
  if (!record2) return { running: false };
30408
30700
  const heartbeatAgeMs = Date.now() - Date.parse(record2.heartbeatAt);
@@ -30751,7 +31043,7 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, e
30751
31043
  }
30752
31044
  };
30753
31045
  const contextFor = (projectRoot) => {
30754
- if (!isAbsolute7(projectRoot)) {
31046
+ if (!isAbsolute8(projectRoot)) {
30755
31047
  throw runtimeError("runtime_request_invalid", "projectRoot", "projectRoot must be an absolute path");
30756
31048
  }
30757
31049
  const key = projectRuntimeKey(projectRoot);
@@ -30817,6 +31109,62 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, e
30817
31109
  sendJson(response, 200, { ok: true, projectId: context.projectId, retried, outbox: context.eventStore.outboxStatus() });
30818
31110
  return;
30819
31111
  }
31112
+ if (request.method === "POST" && url.pathname === "/v1/projects/unregister") {
31113
+ const body = await readJsonBody(request);
31114
+ const projectRoot = typeof body.projectRoot === "string" ? body.projectRoot : "";
31115
+ const force = body.force === true;
31116
+ if (!projectRoot || !isAbsolute8(projectRoot)) {
31117
+ sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: "projectRoot must be an absolute path" });
31118
+ return;
31119
+ }
31120
+ if (!force) {
31121
+ const key2 = projectRuntimeKey(projectRoot);
31122
+ const existing2 = contexts.get(key2);
31123
+ let openRunIds = [];
31124
+ if (existing2) {
31125
+ openRunIds = unsettledRunIds(existing2);
31126
+ } else {
31127
+ const registration = registry.projectByRoot(projectRoot);
31128
+ const eventsPath = join18(paths.projectsDir, registration?.projectKey ?? projectRuntimeKey(projectRoot), "run-events.db");
31129
+ if (registration && existsSync16(eventsPath)) {
31130
+ const store = new KxmRunEventStore(eventsPath);
31131
+ try {
31132
+ openRunIds = store.runsForProject(registration.projectId, 1e4).filter((run) => !SETTLED_RUN_STATUS.has(run.status)).map((run) => run.runId);
31133
+ } finally {
31134
+ store.close();
31135
+ }
31136
+ }
31137
+ }
31138
+ if (openRunIds.length > 0) {
31139
+ sendJson(response, 409, {
31140
+ ok: false,
31141
+ error: "runtime_project_busy",
31142
+ message: `unsettled run ${openRunIds.join(", ")}`,
31143
+ runIds: openRunIds
31144
+ });
31145
+ return;
31146
+ }
31147
+ const laneCount = registry.homeLaneCount(projectRoot);
31148
+ if (laneCount > 0) {
31149
+ sendJson(response, 409, {
31150
+ ok: false,
31151
+ error: "runtime_project_has_lanes",
31152
+ message: `home root still has ${laneCount} lane(s)`
31153
+ });
31154
+ return;
31155
+ }
31156
+ }
31157
+ const key = projectRuntimeKey(projectRoot);
31158
+ const existing = contexts.get(key);
31159
+ const unregistered = registry.unregisterProject(projectRoot);
31160
+ if (existing) {
31161
+ contexts.delete(key);
31162
+ syncStatuses.delete(key);
31163
+ closeKxmRuntimeContext(existing);
31164
+ }
31165
+ sendJson(response, 200, { ok: true, unregistered });
31166
+ return;
31167
+ }
30820
31168
  if (request.method === "POST" && url.pathname === "/v1/runs") {
30821
31169
  const body = await readJsonBody(request);
30822
31170
  const projectRoot = typeof body.projectRoot === "string" ? body.projectRoot : "";
@@ -30870,24 +31218,7 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, e
30870
31218
  const delayMs = typeof body.delayMs === "number" && body.delayMs > 0 ? body.delayMs : 0;
30871
31219
  const createProducer = () => body.mode === "live" ? createKxmOneShotProducer({
30872
31220
  projectRoot,
30873
- timeoutMs: kxmProjectAdmissionLimits(bundle).agentStepTimeoutMs,
30874
- defaultHarness: String(bundle.project.value.defaultHarness ?? "pi"),
30875
- resolveHarness: (agentId) => {
30876
- const agent = bundle.agents.get(agentId);
30877
- return typeof agent?.value.harness === "string" ? agent.value.harness : void 0;
30878
- },
30879
- resolveModel: (agentId) => {
30880
- const agent = bundle.agents.get(agentId);
30881
- const model = agent?.value.model;
30882
- if (!model || typeof model !== "object" || Array.isArray(model)) return void 0;
30883
- const value = model;
30884
- const provider = typeof value.provider === "string" ? value.provider : void 0;
30885
- const modelName = typeof value.model === "string" ? value.model : void 0;
30886
- if (!provider || !modelName || !isRouteAdmitted(projectRoot, `${provider}/${modelName}`)) {
30887
- throw new Error("producer_route_not_admitted");
30888
- }
30889
- return { provider, model: modelName };
30890
- }
31221
+ timeoutMs: kxmProjectAdmissionLimits(bundle).agentStepTimeoutMs
30891
31222
  }) : createKxmSimulatedProducer(async () => {
30892
31223
  if (delayMs > 0) {
30893
31224
  await new Promise((r) => setTimeout(r, delayMs));
@@ -31040,7 +31371,10 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, e
31040
31371
  sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: `project ${requestedProjectId} is not the bound project ${context.projectId}` });
31041
31372
  return;
31042
31373
  }
31043
- const runs = context.eventStore.runsForProject(requestedProjectId, 50).map((stored) => {
31374
+ const limitText = url.searchParams.get("limit");
31375
+ const parsedLimit = limitText === null ? 50 : Number(limitText);
31376
+ const limit = Number.isInteger(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 1e4) : 50;
31377
+ const runs = context.eventStore.runsForProject(requestedProjectId, limit).map((stored) => {
31044
31378
  try {
31045
31379
  return projectKxmRunReadOnly(context, stored.runId);
31046
31380
  } catch (error) {
@@ -31314,6 +31648,7 @@ export {
31314
31648
  hashKxmSupervisorToken,
31315
31649
  hashKxmTokenProof,
31316
31650
  kxmRuntimeRequest,
31651
+ kxmSupervisorRecordIsLive,
31317
31652
  kxmSupervisorStatus,
31318
31653
  kxmSupervisorTokenFile,
31319
31654
  readKxmSupervisorToken,