@kontextmind/kxm 0.7.144 → 0.7.146

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 (48) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/README.md +20 -2
  3. package/.kxm/agents/{coordinator.yaml → planner.yaml} +2 -0
  4. package/.kxm/agents/{critic-arch.yaml → reviewer-arch.yaml} +2 -0
  5. package/.kxm/agents/{critic-cli.yaml → reviewer-cli.yaml} +2 -0
  6. package/.kxm/agents/{implementer.yaml → writer.yaml} +2 -0
  7. package/.kxm/roles/planner.yaml +4 -2
  8. package/.kxm/roles/reviewer-arch.yaml +4 -2
  9. package/.kxm/roles/reviewer-cli.yaml +4 -2
  10. package/.kxm/roles/writer.yaml +6 -3
  11. package/.kxm/workflows/default.yaml +20 -12
  12. package/.kxm/workflows/land.yaml +1 -1
  13. package/.kxm/workflows/{review-arch-only.yaml → reviewer-arch-only.yaml} +7 -3
  14. package/.kxm/workflows/{review-cli-only.yaml → reviewer-cli-only.yaml} +7 -3
  15. package/.kxm/workflows/{implement-only.yaml → writer-only.yaml} +8 -4
  16. package/CHANGELOG.md +22 -5
  17. package/docs/contributing/harness-routing-internals.md +17 -11
  18. package/docs/reference/cli-reference.md +23 -24
  19. package/docs/reference/config-reference.md +69 -62
  20. package/docs/reference/harness-routing.md +6 -5
  21. package/docs/reference/workflow-catalog.md +3 -3
  22. package/package.json +3 -3
  23. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  24. package/plugins/kxm/dist/cli.js +1144 -804
  25. package/plugins/kxm/dist/mcp-server.js +1 -1
  26. package/plugins/kxm/dist/runtime-supervisor.js +636 -308
  27. package/plugins/kxm/dist/runtime.js +707 -379
  28. package/plugins/kxm/dist/server.js +49 -5
  29. package/plugins/kxm/package.json +1 -1
  30. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +3 -3
  31. package/plugins/kxm/src/cli/project.ts +2 -1
  32. package/plugins/kxm/src/cli/roles.ts +3 -4
  33. package/plugins/kxm/src/engine.ts +9 -6
  34. package/plugins/kxm/src/mcp-server.ts +1 -1
  35. package/plugins/kxm/src/policy-draft.mjs +11 -5
  36. package/plugins/kxm/src/project-config.ts +49 -11
  37. package/plugins/kxm/src/runtime-service.ts +2 -1
  38. package/plugins/kxm/src/template.ts +39 -10
  39. package/plugins/kxm/src/workflow-manager.ts +34 -28
  40. package/plugins/kxm/src/workforce-names.d.mts +38 -0
  41. package/plugins/kxm/src/workforce-names.mjs +317 -0
  42. package/schemas/agent.schema.json +7 -0
  43. package/schemas/model.schema.json +12 -0
  44. package/schemas/workflow.schema.json +14 -0
  45. package/scripts/harness-run.mjs +1 -1
  46. package/scripts/native-critic.mjs +5 -5
  47. package/scripts/roster-policy.mjs +9 -3
  48. package/scripts/workforce-lint.mjs +18 -0
@@ -14958,6 +14958,7 @@ var MODES = Object.freeze(["headless", "interactive", "either"]);
14958
14958
  var MODEL_KEYS = Object.freeze([
14959
14959
  "schema",
14960
14960
  "id",
14961
+ "aliases",
14961
14962
  "harness",
14962
14963
  "model",
14963
14964
  "vendor",
@@ -14996,8 +14997,51 @@ var FALLBACK_REVERT = Object.freeze(["next_run", "never"]);
14996
14997
  var ROSTER_ENTRY_KEYS = Object.freeze(["route", "effort", "mode"]);
14997
14998
  var CRITIC_PURPOSES = Object.freeze(["reviewer-arch", "reviewer-cli"]);
14998
14999
 
14999
- // plugins/kxm/src/template.ts
15000
+ // plugins/kxm/src/workforce-names.mjs
15000
15001
  var import_yaml2 = __toESM(require_dist(), 1);
15002
+ var ROLE_IDS = Object.freeze([
15003
+ "writer",
15004
+ "planner",
15005
+ "reviewer-arch",
15006
+ "reviewer-cli",
15007
+ "experiment"
15008
+ ]);
15009
+ var ROUTE_RENAMES = Object.freeze([
15010
+ ["grok-native", "grok-grok-4-7"],
15011
+ ["qwen-openrouter-pi", "pi-qwen3-coder-plus-openrouter"],
15012
+ ["gemini-agy", "agy-gemini-3-8-flash-high"],
15013
+ ["fable-claude", "claude-fable"],
15014
+ ["sol-codex", "codex-gpt-5-6-sol"],
15015
+ ["grok-default", "grok-grok-4-6"],
15016
+ ["fable-default", "claude-fable"]
15017
+ ]);
15018
+ var AGENT_RENAMES = Object.freeze([
15019
+ ["coordinator", "planner"],
15020
+ ["implementer", "writer"],
15021
+ ["critic-arch", "reviewer-arch"],
15022
+ ["critic-cli", "reviewer-cli"]
15023
+ ]);
15024
+ var WORKFLOW_RENAMES = Object.freeze([
15025
+ ["implement-only", "writer-only"],
15026
+ ["review-arch-only", "reviewer-arch-only"],
15027
+ ["review-cli-only", "reviewer-cli-only"]
15028
+ ]);
15029
+ var STEP_RENAMES = Object.freeze([
15030
+ ["implement", "writer"],
15031
+ ["critic-arch", "reviewer-arch"],
15032
+ ["review-arch", "reviewer-arch"],
15033
+ ["critic-cli", "reviewer-cli"],
15034
+ ["review-cli", "reviewer-cli"]
15035
+ ]);
15036
+ var RENAMES = Object.freeze({
15037
+ route: ROUTE_RENAMES,
15038
+ agent: AGENT_RENAMES,
15039
+ workflow: WORKFLOW_RENAMES,
15040
+ step: STEP_RENAMES
15041
+ });
15042
+
15043
+ // plugins/kxm/src/template.ts
15044
+ var import_yaml3 = __toESM(require_dist(), 1);
15001
15045
 
15002
15046
  // plugins/kxm/src/repo-root.ts
15003
15047
  import { existsSync } from "node:fs";
@@ -18253,7 +18297,7 @@ function explainContextItem(id, pool) {
18253
18297
  }
18254
18298
 
18255
18299
  // plugins/kxm/src/memory.ts
18256
- var import_yaml3 = __toESM(require_dist(), 1);
18300
+ var import_yaml4 = __toESM(require_dist(), 1);
18257
18301
  import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
18258
18302
  import { extname as extname2, join as join3, resolve as resolve2 } from "node:path";
18259
18303
  var MEMORY_SCHEMA = "kxm.memory.v1";
@@ -18285,7 +18329,7 @@ function parseFrontmatter(content) {
18285
18329
  }
18286
18330
  function parseMemoryRecord(raw, filename = "memory.md") {
18287
18331
  const { frontmatter, body } = parseFrontmatter(raw);
18288
- const data = (0, import_yaml3.parse)(frontmatter);
18332
+ const data = (0, import_yaml4.parse)(frontmatter);
18289
18333
  if (!data || typeof data !== "object" || Array.isArray(data)) {
18290
18334
  throw new Error(`invalid YAML frontmatter in ${filename}`);
18291
18335
  }
@@ -18622,7 +18666,7 @@ function boundedRefs(value, field) {
18622
18666
  }
18623
18667
 
18624
18668
  // plugins/kxm/src/skills.ts
18625
- var import_yaml4 = __toESM(require_dist(), 1);
18669
+ var import_yaml5 = __toESM(require_dist(), 1);
18626
18670
  import { createHash as createHash4 } from "node:crypto";
18627
18671
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, renameSync, rmSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
18628
18672
  import { dirname as dirname4, join as join4 } from "node:path";
@@ -18666,7 +18710,7 @@ function parseSkillFrontmatter(content) {
18666
18710
  return { frontmatter: null, body: content };
18667
18711
  }
18668
18712
  try {
18669
- const parsed = (0, import_yaml4.parse)(rawFm);
18713
+ const parsed = (0, import_yaml5.parse)(rawFm);
18670
18714
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
18671
18715
  return { frontmatter: parsed, body: rawBody };
18672
18716
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.144",
3
+ "version": "0.7.146",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -22,7 +22,7 @@ to run in their own terminal or in Claude Code.
22
22
  `ready`, `repair`, or `legacy`, and `issues` lists anything to fix first.
23
23
  2. `kxm init --name "<display name>"` prints
24
24
  `initialized KXM project at <root>`. It writes `.kxm/project.yaml`,
25
- `.kxm/agents/coordinator.yaml`, `.kxm/agents/implementer.yaml`,
25
+ `.kxm/agents/planner.yaml`, `.kxm/agents/writer.yaml`,
26
26
  `.kxm/gates.yaml`, `.kxm/repo/repo.yaml`, `.kxm/template-provenance.yaml`,
27
27
  and `.kxm/workflows/default.yaml`. Outside Git it fails with
28
28
  `git_root_required`. In an existing project, plain `kxm init` validates,
@@ -44,7 +44,7 @@ After the user's commit:
44
44
  1. `kxm workflow add first --template spec-and-plan` prints
45
45
  `Added workflow 'first' to local (<root>/.kxm/workflows/first.yaml)`. Add
46
46
  `--dry-run` first to see the path without writing. The template has two
47
- steps, `plan` then `review-arch`, both run by the `coordinator` agent with
47
+ steps, `plan` then `reviewer-arch`, both run by the `planner` agent with
48
48
  `repositories: control: read`. It writes nothing and runs no test command,
49
49
  and a failed review goes back to `plan` at most twice.
50
50
  2. `kxm init` validates it and prints `validated KXM project at <root>`.
@@ -71,7 +71,7 @@ After the user's commit:
71
71
  4. `kxm runs status <runId>` prints `created`.
72
72
  5. `kxm runs drive <runId> --simulated --wait --timeout-ms 60000` prints a
73
73
  `kxm.drive-receipt.v1` whose settlement is terminal `completed`, and exits 0.
74
- The run moves from `plan` to `review-arch` to `completed`. Always pass
74
+ The run moves from `plan` to `reviewer-arch` to `completed`. Always pass
75
75
  `--simulated`; without it, drive calls live harnesses.
76
76
  6. `kxm runs status <runId>` prints `completed … (receipt verified)`.
77
77
  7. `kxm runs receipt <runId>` and `kxm runs list`. Cancel a stuck run with
@@ -11,6 +11,7 @@ import { runVisionGate } from "../vision-gate.ts";
11
11
  import {
12
12
  KxmConfigError,
13
13
  discoverKxmProjectRoot,
14
+ lookupKxmResource,
14
15
  type KxmInitializationPlan,
15
16
  } from "../project-config.ts";
16
17
  import { initializeKxmProject } from "../init.ts";
@@ -439,7 +440,7 @@ export function resolveKxmRunTarget(
439
440
  }
440
441
  const bundle = loadKxmProject(projectRoot, {});
441
442
  const workflowId = workflow ?? String(bundle.project.value.defaultWorkflow ?? "default");
442
- if (!bundle.workflows.has(workflowId)) {
443
+ if (!lookupKxmResource(bundle.workflows.values(), workflowId, "workflow")) {
443
444
  print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_workflow_unknown", workflow: workflowId, ...envelope }, `workflow ${workflowId} does not exist in this project`);
444
445
  return 1;
445
446
  }
@@ -15,6 +15,7 @@ import {
15
15
  type KxmRoleDefinition,
16
16
  } from "../role.ts";
17
17
  import { discoverKxmProjectRoot, kxmRoleWriteIssues } from "../project-config.ts";
18
+ import { findYamlBasename } from "../workforce-names.mjs";
18
19
  import { ensureKxmSupervisor, kxmRuntimeRequest } from "../runtime-supervisor.ts";
19
20
  import { projectRuntimeOwnsRun } from "../runtime-store.ts";
20
21
  import { resumeWorkflowFromRuling, type WorkflowRun } from "../workflow.ts";
@@ -185,8 +186,7 @@ export async function cmdRoleAdd(
185
186
  // after its roster is parsed, for both local and global scope.
186
187
  const projectRoot = discoverKxmProjectRoot(runtime.cwd) ?? runtime.cwd;
187
188
  const refuseMissingRoute = (routeId: string): boolean => {
188
- const modelFile = join(projectRoot, ".kxm", "models", `${routeId}.yaml`);
189
- if (existsSync(modelFile)) return false;
189
+ if (findYamlBasename(join(projectRoot, ".kxm", "models"), routeId, "route")) return false;
190
190
  runtime.io.stderr(`kxm: route '${routeId}' is not a file under .kxm/models/\n`);
191
191
  return true;
192
192
  };
@@ -398,8 +398,7 @@ export async function cmdRoleModify(
398
398
  if (options.addRoute) {
399
399
  const routeId = options.addRoute;
400
400
  const projectRoot = discoverKxmProjectRoot(runtime.cwd) ?? runtime.cwd;
401
- const modelFile = join(projectRoot, ".kxm", "models", `${routeId}.yaml`);
402
- if (!existsSync(modelFile)) {
401
+ if (!findYamlBasename(join(projectRoot, ".kxm", "models"), routeId, "route")) {
403
402
  runtime.io.stderr(`kxm: route '${routeId}' is not a file under .kxm/models/\n`);
404
403
  return 1;
405
404
  }
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { parse } from "yaml";
5
5
  import { listRoleBindings, loadRoutePolicy } from "./routes.ts";
6
+ import { findYamlBasename } from "./workforce-names.mjs";
6
7
  import { applyAuthoringWitness, captureWorktreeWitness } from "./worktree-witness.ts";
7
8
  import {
8
9
  buildFormalContextPacket,
@@ -101,7 +102,7 @@ import {
101
102
  type KxmRunRecord,
102
103
  type KxmRunStatus,
103
104
  } from "./runtime-store.ts";
104
- import { KxmConfigError, kxmCanonicalJson, type JsonValue, type KxmProjectBundle } from "./project-config.ts";
105
+ import { KxmConfigError, kxmCanonicalJson, lookupKxmResource, type JsonValue, type KxmProjectBundle } from "./project-config.ts";
105
106
  import { evaluateArtifactsGate } from "./engine-artifacts.ts";
106
107
  import { createCommandObserver } from "./engine-command.ts";
107
108
  import {
@@ -277,7 +278,7 @@ export function pinKxmCompiledPlan(
277
278
  if (String(bundle.project.value.id) !== run.projectId) {
278
279
  throw runtimeError("run_owner_mismatch", runId, "bundle project does not match the run");
279
280
  }
280
- const workflow = bundle.workflows.get(run.workflowId);
281
+ const workflow = lookupKxmResource(bundle.workflows.values(), run.workflowId, "workflow");
281
282
  if (!workflow) throw runtimeError("run_workflow_unknown", run.workflowId, `workflow ${run.workflowId} does not exist in this project`);
282
283
  const revisions = kxmPolicyRevisions(bundle, options);
283
284
  if (
@@ -1397,7 +1398,8 @@ function resolveProducerRoute(
1397
1398
  },
1398
1399
  };
1399
1400
  }
1400
- const agent = readYamlFile(join(projectRoot, ".kxm", "agents", `${agentId}.yaml`));
1401
+ const agentFile = findYamlBasename(join(projectRoot, ".kxm", "agents"), agentId, "agent") ?? agentId;
1402
+ const agent = readYamlFile(join(projectRoot, ".kxm", "agents", `${agentFile}.yaml`));
1401
1403
  const role = typeof agent?.role === "string" ? agent.role : "";
1402
1404
  if (!role) {
1403
1405
  return {
@@ -1424,7 +1426,8 @@ function resolveProducerRoute(
1424
1426
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
1425
1427
  const record = entry as { route?: unknown; effort?: unknown };
1426
1428
  if (typeof record.route !== "string" || record.route.length === 0) continue;
1427
- const modelPath = join(projectRoot, ".kxm", "models", `${record.route}.yaml`);
1429
+ const routeFile = findYamlBasename(join(projectRoot, ".kxm", "models"), record.route, "route") ?? record.route;
1430
+ const modelPath = join(projectRoot, ".kxm", "models", `${routeFile}.yaml`);
1428
1431
  const doc = readModelDocument(modelPath);
1429
1432
  if (!doc) {
1430
1433
  return {
@@ -1487,7 +1490,7 @@ function resolveProducerRoute(
1487
1490
  model: selector.slice(slash + 1),
1488
1491
  selector,
1489
1492
  harness,
1490
- routeId: record.route,
1493
+ routeId: routeFile,
1491
1494
  role,
1492
1495
  permissions,
1493
1496
  ...(typeof record.effort === "string" ? { effort: record.effort } : {}),
@@ -2871,7 +2874,7 @@ export function kxmLiveRunPrerequisites(
2871
2874
  workflowId: string,
2872
2875
  projectRoot: string,
2873
2876
  ): KxmRunHandoff[] {
2874
- const workflow = bundle.workflows.get(workflowId);
2877
+ const workflow = lookupKxmResource(bundle.workflows.values(), workflowId, "workflow");
2875
2878
  if (!workflow) throw runtimeError("run_workflow_unknown", workflowId, `workflow ${workflowId} does not exist in this project`);
2876
2879
  const plan = compileKxmWorkflow({ id: workflowId, value: workflow.value, logicalPath: workflow.logicalPath });
2877
2880
  const envelope = { plan, projectLimits: kxmProjectAdmissionLimits(bundle), gates: pinnedGatesForCompiledPlan(plan, bundle, projectRoot) };
@@ -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.144";
14
+ const VERSION = "0.7.146";
15
15
  const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
16
16
  const inbox = new Map<string, MessageRecord>();
17
17
  const notifiedInbox = new Set<string>();
@@ -20,7 +20,7 @@ const TOKEN = /[\s\x00-\x1f]/u;
20
20
  const EFFORTS = Object.freeze(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
21
21
  const MODES = Object.freeze(["headless", "interactive", "either"]);
22
22
  const MODEL_KEYS = Object.freeze([
23
- "schema", "id", "harness", "model", "vendor", "status", "permissions", "origin",
23
+ "schema", "id", "aliases", "harness", "model", "vendor", "status", "permissions", "origin",
24
24
  "thinking", "tags", "capabilities", "priority", "fallbacks", "limits",
25
25
  ]);
26
26
  const ROLE_KEYS = Object.freeze([
@@ -226,6 +226,9 @@ function validateModelShape(document, id, label, issues) {
226
226
  if (document.id !== undefined && document.id !== id) {
227
227
  issues.push(issue("schema", "identity_mismatch", label, `declared id ${String(document.id)} does not match ${id}`));
228
228
  }
229
+ if (document.aliases !== undefined && !identifierList(document.aliases)) {
230
+ issues.push(issue("schema", "schema_pattern", label, "aliases must be unique identifiers"));
231
+ }
229
232
  if (document.harness !== undefined && !identifier(document.harness)) {
230
233
  issues.push(issue("schema", "schema_pattern", label, "harness must be an identifier"));
231
234
  }
@@ -511,9 +514,12 @@ function validateRoleSemantics(role, id, label, models, options, issues) {
511
514
  const admitted = role.roster
512
515
  .map((entry) => models.get(entry.route))
513
516
  .filter((model) => model && admittedRoute(model));
514
- if (admitted.length !== 1) {
515
- issues.push(issue("semantic", "critic_route_count", label, "required critic purpose files must list exactly one admitted route"));
516
- } else if (admitted[0].permissions.length !== 1 || admitted[0].permissions[0] !== "read-only" || role.permission !== "read-only") {
517
+ // The first roster entry is the required critic. Later entries are
518
+ // fallbacks so one provider outage does not stop the role. Every entry
519
+ // stays read-only; vendor independence uses the first entry only.
520
+ if (admitted.length < 1) {
521
+ issues.push(issue("semantic", "critic_route_count", label, "required critic purpose files must list at least one admitted route"));
522
+ } else if (role.permission !== "read-only" || admitted.some((model) => model.permissions.length !== 1 || model.permissions[0] !== "read-only")) {
517
523
  issues.push(issue("semantic", "permission_escalation", label, "critic purpose files must be read-only"));
518
524
  }
519
525
  }
@@ -549,7 +555,7 @@ function validateCriticVendors(roles, models, options, issues) {
549
555
  const admitted = (role.roster ?? [])
550
556
  .map((entry) => models.get(entry.route))
551
557
  .filter((model) => model && admittedRoute(model));
552
- if (admitted.length === 1) critics.push({ purpose, vendor: canonicalVendor(admitted[0].vendor, aliases) });
558
+ if (admitted.length >= 1) critics.push({ purpose, vendor: canonicalVendor(admitted[0].vendor, aliases) });
553
559
  }
554
560
  if (critics.length === 2 && critics[0].vendor === critics[1].vendor) {
555
561
  issues.push(issue("semantic", "critic_vendor_collision", "roles/reviewer-arch", "required critics must have independent vendors"));
@@ -11,6 +11,7 @@ import {
11
11
  } from "./restricted-yaml.mjs";
12
12
  import { pathToFileURL } from "node:url";
13
13
  import { validatePolicyDraft } from "./policy-draft.mjs";
14
+ import { findYamlBasename, lintWorkforce, lookupById, noteDeprecatedId, STEP_RENAMES } from "./workforce-names.mjs";
14
15
  import { resolveKxmTemplateBaseline } from "./template.ts";
15
16
  import { findKxmRepoRoot } from "./repo-root.ts";
16
17
  import { BUILTIN_HARNESS_IDS, DEFAULT_HARNESS } from "./harness.ts";
@@ -590,6 +591,20 @@ function names(value: JsonValue | undefined): string[] {
590
591
  return Array.isArray(value) ? value.filter((candidate): candidate is string => typeof candidate === "string") : [];
591
592
  }
592
593
 
594
+ /** Resolve an agent, workflow, or route, including a deprecated alias. */
595
+ export function lookupKxmResource<T extends { id?: string; value: JsonObject }>(
596
+ resources: Iterable<T>,
597
+ requested: string,
598
+ kind: "agent" | "workflow" | "route",
599
+ ): T | undefined {
600
+ const records = [...resources].map((resource) => ({
601
+ id: resource.id,
602
+ aliases: names(resource.value.aliases),
603
+ resource,
604
+ }));
605
+ return lookupById(records, requested, kind)?.record.resource;
606
+ }
607
+
593
608
  function mapResource(resource: KxmResource, map: Map<string, KxmResource>, issues: KxmConfigIssue[]): void {
594
609
  const id = resource.id;
595
610
  if (!id) return;
@@ -872,14 +887,31 @@ function validateWorkflow(
872
887
  ): void {
873
888
  const file = workflow.logicalPath;
874
889
  const coordinator = stringValue(workflow.value.coordinator) ?? "coordinator";
875
- if (!agents.has(coordinator)) issues.push(issue("reference", "coordinator_unknown", file, `coordinator ${coordinator} does not exist`));
890
+ if (!lookupKxmResource(agents.values(), coordinator, "agent")) issues.push(issue("reference", "coordinator_unknown", file, `coordinator ${coordinator} does not exist`));
876
891
  const steps = valuesOf(workflow.value, "steps").map((candidate) => objectValue(candidate)).filter((candidate): candidate is JsonObject => Boolean(candidate));
877
892
  const stepIndex = new Map<string, number>();
893
+ const primarySteps = new Set<string>();
878
894
  for (const [index, step] of steps.entries()) {
879
895
  const id = stringValue(step.id);
880
896
  if (!id) continue;
881
897
  if (stepIndex.has(id)) issues.push(issue("semantic", "step_id_duplicate", file, `step ${id} is duplicated`));
882
- else stepIndex.set(id, index);
898
+ else {
899
+ stepIndex.set(id, index);
900
+ primarySteps.add(id);
901
+ }
902
+ }
903
+ for (const [index, step] of steps.entries()) {
904
+ const id = stringValue(step.id);
905
+ if (!id) continue;
906
+ const aliases = new Set(names(step.aliases));
907
+ for (const [oldId, newId] of STEP_RENAMES) {
908
+ if (id === newId) aliases.add(oldId);
909
+ if (id === oldId) aliases.add(newId);
910
+ }
911
+ for (const alias of aliases) {
912
+ if (alias === id || primarySteps.has(alias) || stepIndex.has(alias)) continue;
913
+ stepIndex.set(alias, index);
914
+ }
883
915
  }
884
916
 
885
917
  for (const field of ["reproOracle", "planHash"] as const) {
@@ -910,7 +942,7 @@ function validateWorkflow(
910
942
  const kind = stringValue(step.kind);
911
943
  validateToolPolicy(workflow, objectValue(step.tools), `${stepId}.tools`, issues);
912
944
  const primaryAgentId = stringValue(step.agent);
913
- if ((kind === "agent" || kind === "moa") && primaryAgentId && !agents.has(primaryAgentId)) {
945
+ if ((kind === "agent" || kind === "moa") && primaryAgentId && !lookupKxmResource(agents.values(), primaryAgentId, "agent")) {
914
946
  issues.push(issue("reference", "agent_unknown", file, `${stepId} references unknown agent ${primaryAgentId}`));
915
947
  }
916
948
  const gate = stringValue(step.gate);
@@ -938,7 +970,7 @@ function validateWorkflow(
938
970
  const declaredAgents = assignment ? names(assignment.allowedAgents) : [];
939
971
  const allowedAgents = declaredAgents.length > 0 ? declaredAgents : primaryAgentId ? [primaryAgentId] : [];
940
972
  for (const agentId of allowedAgents) {
941
- const agent = agents.get(agentId);
973
+ const agent = lookupKxmResource(agents.values(), agentId, "agent");
942
974
  if (!agent) issues.push(issue("reference", "assignment_agent_unknown", file, `${stepId} allows unknown agent ${agentId}`));
943
975
  else {
944
976
  validateAgentScope(agent, step, repositories, file, stepId, issues);
@@ -1003,7 +1035,7 @@ function validateWorkflow(
1003
1035
  const producerMinimum = numberValue(policy.minimumProducers, 1);
1004
1036
  if (producerMinimum > target || producerMinimum > eligible.length) issues.push(issue("semantic", "producer_minimum_impossible", file, `${stepId}.${String(key)} producer minimum exceeds its eligible or target pool`));
1005
1037
  for (const agentId of eligible) {
1006
- if (!agents.has(agentId)) issues.push(issue("reference", "producer_agent_unknown", file, `${stepId}.${String(key)} references unknown producer ${agentId}`));
1038
+ if (!lookupKxmResource(agents.values(), agentId, "agent")) issues.push(issue("reference", "producer_agent_unknown", file, `${stepId}.${String(key)} references unknown producer ${agentId}`));
1007
1039
  if (allowedAgents.length > 0 && !allowedAgents.includes(agentId)) issues.push(issue("semantic", "producer_agent_ineligible", file, `${stepId}.${String(key)} producer ${agentId} is outside allowedAgents`));
1008
1040
  }
1009
1041
  const degradation = objectValue(policy.degradation);
@@ -1026,6 +1058,10 @@ function validateWorkflow(
1026
1058
  if (parsed.terminalStatus === "completed") completedTerminalSources.add(index);
1027
1059
  continue;
1028
1060
  }
1061
+ if (parsed.target && parsed.target !== "$terminal" && !primarySteps.has(parsed.target) && stepIndex.has(parsed.target)) {
1062
+ const canonical = stringValue(steps[stepIndex.get(parsed.target) ?? -1]?.id);
1063
+ if (canonical) noteDeprecatedId("step", parsed.target, canonical);
1064
+ }
1029
1065
  const targetIndex = parsed.target === undefined ? undefined : stepIndex.get(parsed.target);
1030
1066
  if (targetIndex === undefined) {
1031
1067
  issues.push(issue("reference", "transition_target_unknown", file, `${stepId}.${outcome} references unknown target ${String(parsed.target)}`));
@@ -1195,7 +1231,7 @@ function validateBundle(
1195
1231
  validateModelReferences(models, issues);
1196
1232
 
1197
1233
  const defaultWorkflow = stringValue(project.value.defaultWorkflow) ?? "default";
1198
- if (!workflows.has(defaultWorkflow)) issues.push(issue("reference", "default_workflow_unknown", project.logicalPath, `default workflow ${defaultWorkflow} does not exist`));
1234
+ if (!lookupKxmResource(workflows.values(), defaultWorkflow, "workflow")) issues.push(issue("reference", "default_workflow_unknown", project.logicalPath, `default workflow ${defaultWorkflow} does not exist`));
1199
1235
  for (const workflow of workflows.values()) validateWorkflow(workflow, agents, models, repositoryIds, gates, issues);
1200
1236
 
1201
1237
  // Developer policy is the role files under .kxm/roles/ (with the model
@@ -1220,8 +1256,9 @@ function rosterModelIssues(projectRoot: string, roles: ReadonlyMap<string, KxmRe
1220
1256
  for (const entry of valuesOf(role.value, "roster")) {
1221
1257
  const route = stringValue(objectValue(entry)?.route);
1222
1258
  if (!route) continue;
1223
- const logical = `.kxm/models/${route}.yaml`;
1224
- const file = join(projectRoot, ".kxm", "models", `${route}.yaml`);
1259
+ const resolved = findYamlBasename(join(projectRoot, ".kxm", "models"), route, "route") ?? route;
1260
+ const logical = `.kxm/models/${resolved}.yaml`;
1261
+ const file = join(projectRoot, ".kxm", "models", `${resolved}.yaml`);
1225
1262
  let harness: string | undefined;
1226
1263
  try {
1227
1264
  if (existsSync(file)) harness = stringValue(parseRestrictedYaml(readFileSync(file, "utf8"), logical).harness);
@@ -1305,6 +1342,7 @@ function developerRolePolicyIssues(projectRoot: string): KxmConfigIssue[] {
1305
1342
  // is this package's developer policy, so it runs when the loaded project
1306
1343
  // is that package.
1307
1344
  if (resolve(projectRoot) !== findKxmRepoRoot(import.meta.url)) return [];
1345
+ const lintIssues = lintWorkforce(projectRoot).errors.map((entry) => issue("semantic", entry.code, entry.file, entry.message));
1308
1346
  const rolesDir = join(projectRoot, ".kxm", "roles");
1309
1347
  const modelsDir = join(projectRoot, ".kxm", "models");
1310
1348
  const roles: Record<string, JsonObject> = {};
@@ -1340,7 +1378,7 @@ function developerRolePolicyIssues(projectRoot: string): KxmConfigIssue[] {
1340
1378
  }
1341
1379
  const ceilings = developerCeilings();
1342
1380
  if (!ceilings.ok) {
1343
- return [...parseIssues, issue("semantic", "developer_ceilings_unavailable", ".kxm/models", `developer ceilings could not be loaded: ${ceilings.detail}`)];
1381
+ return [...parseIssues, ...lintIssues, issue("semantic", "developer_ceilings_unavailable", ".kxm/models", `developer ceilings could not be loaded: ${ceilings.detail}`)];
1344
1382
  }
1345
1383
  const result = validatePolicyDraft({ models, roles, evidence }, {
1346
1384
  ceilings: ceilings.ROUTES,
@@ -1349,8 +1387,8 @@ function developerRolePolicyIssues(projectRoot: string): KxmConfigIssue[] {
1349
1387
  piNativeVendorProviders: ceilings.PI_NATIVE_VENDOR_PROVIDERS,
1350
1388
  vendorAliases: { "x-ai": "xai", moonshotai: "moonshot", "google-ai": "google", qwen: "alibaba" },
1351
1389
  });
1352
- if (result.ok) return parseIssues;
1353
- return [...parseIssues, ...result.issues.map((entry) => issue(entry.phase, entry.code, entry.file, entry.message))];
1390
+ if (result.ok) return [...parseIssues, ...lintIssues];
1391
+ return [...parseIssues, ...lintIssues, ...result.issues.map((entry) => issue(entry.phase, entry.code, entry.file, entry.message))];
1354
1392
  }
1355
1393
 
1356
1394
  export function kxmCanonicalJson(value: JsonValue): string {
@@ -5,6 +5,7 @@ import type { ContextItem } from "./context.ts";
5
5
  import {
6
6
  KxmConfigError,
7
7
  loadKxmProject,
8
+ lookupKxmResource,
8
9
  kxmCanonicalJson,
9
10
  type JsonObject,
10
11
  type JsonValue,
@@ -402,7 +403,7 @@ export function acceptKxmRun(
402
403
  const now = options.now ?? new Date().toISOString();
403
404
  const monotonicNs = options.monotonicNs ?? kxmMonotonicNs();
404
405
 
405
- const workflow = bundle.workflows.get(request.workflowId);
406
+ const workflow = lookupKxmResource(bundle.workflows.values(), request.workflowId, "workflow");
406
407
  if (!workflow) {
407
408
  throw runtimeError("run_workflow_unknown", ".kxm/workflows", `workflow ${request.workflowId} does not exist in this project`);
408
409
  }
@@ -175,35 +175,64 @@ function coreTemplate(projectId: string, projectName: string, variant: KxmTempla
175
175
  ]);
176
176
  if (variant === "v4-registry") {
177
177
  const coordinator = files.get(".kxm/agents/coordinator.yaml");
178
- if (coordinator) files.set(".kxm/agents/coordinator.yaml", { ...coordinator, role: "planner" });
178
+ if (coordinator) {
179
+ files.delete(".kxm/agents/coordinator.yaml");
180
+ files.set(".kxm/agents/planner.yaml", { ...coordinator, role: "planner", aliases: ["coordinator"] });
181
+ }
179
182
  const implementer = files.get(".kxm/agents/implementer.yaml");
180
- if (implementer) files.set(".kxm/agents/implementer.yaml", { ...implementer, role: "writer" });
183
+ if (implementer) {
184
+ files.delete(".kxm/agents/implementer.yaml");
185
+ files.set(".kxm/agents/writer.yaml", { ...implementer, role: "writer", aliases: ["implementer"] });
186
+ }
181
187
  const workflow = files.get(".kxm/workflows/default.yaml");
182
188
  if (workflow) {
183
189
  const limits = { ...(workflow.limits as JsonObject) };
184
190
  delete limits.maxAgentTimeMs;
185
- files.set(".kxm/workflows/default.yaml", { ...workflow, limits });
191
+ const steps = (workflow.steps as JsonObject[]).map((step) => {
192
+ if (step.id === "plan") {
193
+ const on = { ...(step.on as JsonObject), passed: "writer" };
194
+ return { ...step, agent: "planner", on };
195
+ }
196
+ if (step.id === "implement") {
197
+ const assignments = { ...(step.assignments as JsonObject), allowedAgents: ["writer"] };
198
+ return { ...step, id: "writer", aliases: ["implement"], agent: "writer", assignments };
199
+ }
200
+ if (step.id === "verify") {
201
+ const on = { ...(step.on as JsonObject) };
202
+ const failure = on["implementation-failure"];
203
+ if (failure && typeof failure === "object" && !Array.isArray(failure)) {
204
+ on["implementation-failure"] = { ...(failure as JsonObject), target: "writer" };
205
+ }
206
+ return { ...step, on };
207
+ }
208
+ return step;
209
+ });
210
+ files.set(".kxm/workflows/default.yaml", { ...workflow, coordinator: "planner", limits, steps });
186
211
  }
187
212
  // The two models the template names, and nothing else. A fresh project can
188
213
  // be driven without falling through to an unadmitted default model.
214
+ // Route ids follow <harness>-<model-slug>. grok-default and fable-default
215
+ // remain aliases so an existing --route flag still resolves.
189
216
  files.set(".kxm/routes.yaml", {
190
217
  schema: "kxm.routes.v2",
191
- updatedAt: "2026-09-23T00:00:00.000Z",
218
+ updatedAt: "2026-09-27T00:00:00.000Z",
192
219
  admitted: ["anthropic/fable", "xai/grok-4.6"],
193
220
  disabled: [],
194
221
  });
195
- files.set(".kxm/models/grok-default.yaml", {
222
+ files.set(".kxm/models/grok-grok-4-6.yaml", {
196
223
  schema: "kxm.model.v2",
197
- id: "grok-default",
224
+ id: "grok-grok-4-6",
225
+ aliases: ["grok-default"],
198
226
  harness: "grok",
199
227
  model: "grok-4.6",
200
228
  vendor: "xai",
201
229
  status: "admitted",
202
230
  permissions: ["edit"],
203
231
  });
204
- files.set(".kxm/models/fable-default.yaml", {
232
+ files.set(".kxm/models/claude-fable.yaml", {
205
233
  schema: "kxm.model.v2",
206
- id: "fable-default",
234
+ id: "claude-fable",
235
+ aliases: ["fable-default"],
207
236
  harness: "claude",
208
237
  model: "fable",
209
238
  vendor: "anthropic",
@@ -216,7 +245,7 @@ function coreTemplate(projectId: string, projectName: string, variant: KxmTempla
216
245
  purpose: "writer",
217
246
  permission: "edit",
218
247
  description: "Primary implementation agent.",
219
- roster: [{ route: "grok-default" }],
248
+ roster: [{ route: "grok-grok-4-6", effort: "medium" }],
220
249
  });
221
250
  files.set(".kxm/roles/planner.yaml", {
222
251
  schema: "kxm.role.v2",
@@ -224,7 +253,7 @@ function coreTemplate(projectId: string, projectName: string, variant: KxmTempla
224
253
  purpose: "planner",
225
254
  permission: "read-only",
226
255
  description: "Plans the change before implementation.",
227
- roster: [{ route: "fable-default" }],
256
+ roster: [{ route: "claude-fable", effort: "medium" }],
228
257
  });
229
258
  files.set(".kxm/gates.yaml", {
230
259
  schema: "kxm.gate-registry.v1",