UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

1,881 lines • 95.6 kB
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { Ajv2020, type ErrorObject, type ValidateFunction } from "ajv/dist/2020.js";
import {
  RestrictedYamlError,
  KXM_YAML_LIMITS,
  parseRestrictedYaml as parseRestrictedYamlShared,
  type KxmYamlLimits,
} from "./restricted-yaml.mjs";
import { pathToFileURL } from "node:url";
import { validatePolicyDraft } from "./policy-draft.mjs";
import { findYamlBasename, lintWorkforce, lookupById, noteDeprecatedId, STEP_RENAMES } from "./workforce-names.mjs";
import { resolveKxmTemplateBaseline } from "./template.ts";
import { findKxmRepoRoot } from "./repo-root.ts";
import { BUILTIN_HARNESS_IDS, DEFAULT_HARNESS } from "./harness.ts";

export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
export type JsonObject = { [key: string]: JsonValue };
export type { KxmYamlLimits };
export { KXM_YAML_LIMITS };

export type KxmValidationPhase = "discovery" | "parse" | "schema" | "path" | "reference" | "semantic";

export interface KxmConfigIssue {
  phase: KxmValidationPhase;
  code: string;
  file: string;
  message: string;
}

export class KxmConfigError extends Error {
  readonly issues: readonly KxmConfigIssue[];

  constructor(issues: readonly KxmConfigIssue[]) {
    const sorted = sortIssues(issues);
    super(sorted.map((issue) => `${issue.file}: ${issue.code}: ${issue.message}`).join("\n"));
    this.name = "KxmConfigError";
    this.issues = sorted;
  }
}

export type KxmResourceKind = "project" | "repository" | "agent" | "model" | "role" | "environment" | "workflow" | "gate-registry";

export interface KxmResource {
  kind: KxmResourceKind;
  id?: string;
  file: string;
  logicalPath: string;
  value: JsonObject;
}

export interface KxmProjectBundle {
  projectRoot: string;
  project: KxmResource;
  repositories: ReadonlyMap<string, KxmResource>;
  agents: ReadonlyMap<string, KxmResource>;
  models: ReadonlyMap<string, KxmResource>;
  roles: ReadonlyMap<string, KxmResource>;
  workflows: ReadonlyMap<string, KxmResource>;
  environments: readonly KxmResource[];
  gateRegistry?: KxmResource;
  templateProvenance?: JsonObject;
  resources: readonly KxmResource[];
  configRevision: string;
}

export interface KxmConfigOptions {
  schemasDir?: string;
  repositoryBindings?: Readonly<Record<string, string>>;
  registeredExecutors?: Iterable<string>;
  registeredToolPresets?: Iterable<string>;
  registeredHarnesses?: Iterable<string>;
}

export type KxmInitializationMode = "create" | "legacy" | "repair" | "ready";

export interface KxmInitializationPlan {
  mode: KxmInitializationMode;
  inspectedFrom: string;
  projectRoot?: string;
  legacyRoot?: string;
  changesRequired: boolean;
  issues: readonly KxmConfigIssue[];
  legacyInputs: readonly string[];
  configRevision?: string;
}

function defaultKxmSchemaDir(): string {
  return join(findKxmRepoRoot(import.meta.url), "schemas");
}
const DEFAULT_SCHEMA_DIR = defaultKxmSchemaDir();
const RESOURCE_SCHEMA: Readonly<Record<KxmResourceKind, { identity: string; file: string }>> = Object.freeze({
  project: { identity: "kxm.project.v1", file: "project.schema.json" },
  repository: { identity: "kxm.repository.v1", file: "repository.schema.json" },
  agent: { identity: "kxm.agent.v1", file: "agent.schema.json" },
  model: { identity: "kxm.model.v2", file: "model.schema.json" },
  role: { identity: "kxm.role.v2", file: "role.schema.json" },
  environment: { identity: "kxm.environment.v1", file: "environment.schema.json" },
  workflow: { identity: "kxm.workflow.v1", file: "workflow.schema.json" },
  "gate-registry": { identity: "kxm.gate-registry.v1", file: "gate-registry.schema.json" },
});
const IDENTIFIER = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
const WINDOWS_RESERVED = /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³]|conin\$|conout\$|clock\$)$/i;
const BUILTIN_EXECUTORS = ["local", "ssh", "exe-dev"];
/** Builtin preset to the harness one-shot profile. An agent preset may only narrow. */
export const TOOL_PRESET_PROFILES = Object.freeze({
  coordinator: "read-only",
  "read-only": "read-only",
  "workspace-writer": "edit",
  "tests-writer": "edit",
} as const);
const BUILTIN_TOOL_PRESETS = Object.keys(TOOL_PRESET_PROFILES);

export function toolPresetProfile(preset: string): "read-only" | "edit" | undefined {
  if (!Object.hasOwn(TOOL_PRESET_PROFILES, preset)) return undefined;
  return TOOL_PRESET_PROFILES[preset as keyof typeof TOOL_PRESET_PROFILES];
}

/**
 * Set when `preset` is wider than the role permission, or wider than a
 * registered role preset. Same text at load and at dispatch.
 */
export function toolPresetExceedsRoleMessage(
  preset: string,
  roleId: string,
  rolePermission: "edit" | "read-only",
  rolePreset: string | undefined,
): string | undefined {
  const profile = toolPresetProfile(preset);
  if (profile !== "edit") return undefined;
  if (rolePermission === "read-only") {
    return `tools.preset ${preset} exceeds role ${roleId} permission read-only`;
  }
  if (rolePreset !== undefined && toolPresetProfile(rolePreset) === "read-only") {
    return `tools.preset ${preset} exceeds role ${roleId} preset ${rolePreset}`;
  }
  return undefined;
}
const SECRET_VALUE_PATTERNS: readonly RegExp[] = [
  /-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----/,
  /\b(?:ghp|github_pat)_[A-Za-z0-9_]{20,}\b/,
  /\bsk-[A-Za-z0-9_-]{20,}\b/,
  /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/,
  /\bAKIA[A-Z0-9]{16}\b/,
  /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/,
];
function compareCodeUnits(left: string, right: string): number {
  return left < right ? -1 : left > right ? 1 : 0;
}

function sortIssues(issues: readonly KxmConfigIssue[]): KxmConfigIssue[] {
  return [...issues].sort((left, right) => compareCodeUnits(left.file, right.file)
    || compareCodeUnits(left.phase, right.phase)
    || compareCodeUnits(left.code, right.code)
    || compareCodeUnits(left.message, right.message));
}

function issue(phase: KxmValidationPhase, code: string, file: string, message: string): KxmConfigIssue {
  return { phase, code, file, message };
}

function fail(phase: KxmValidationPhase, code: string, file: string, message: string): never {
  throw new KxmConfigError([issue(phase, code, file, message)]);
}

function isJsonObject(value: JsonValue): value is JsonObject {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}

/** Parse the restricted, JSON-compatible YAML profile used by all KXM configuration. */
export function parseRestrictedYaml(
  input: string | Uint8Array,
  label = "<yaml>",
  limits: Readonly<KxmYamlLimits> = KXM_YAML_LIMITS,
): JsonObject {
  try {
    return parseRestrictedYamlShared(input, label, limits);
  } catch (error) {
    if (error instanceof RestrictedYamlError) throw new KxmConfigError(error.issues);
    throw error;
  }
}

function readJsonObject(file: string): Record<string, unknown> {
  const parsed = JSON.parse(readFileSync(file, "utf8")) as unknown;
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${file} is not a JSON object`);
  return parsed as Record<string, unknown>;
}

export class KxmSchemaRegistry {
  readonly schemasDir: string;
  readonly ajv: Ajv2020;
  readonly validators = new Map<KxmResourceKind, ValidateFunction>();
  readonly localBindingsValidator: ValidateFunction;
  readonly templateProvenanceValidator: ValidateFunction;
  readonly initOperationValidator: ValidateFunction;
  readonly permissionDiffValidator: ValidateFunction;
  readonly runEventValidator: ValidateFunction;
  readonly driveReceiptValidator: ValidateFunction;
  readonly coordinatorValidator: ValidateFunction;
  readonly intakeMessageValidator: ValidateFunction;
  readonly syncEventValidator: ValidateFunction;

  constructor(schemasDir = DEFAULT_SCHEMA_DIR) {
    this.schemasDir = resolve(schemasDir);
    this.ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false });
    const common = readJsonObject(join(this.schemasDir, "common.schema.json"));
    this.ajv.addSchema(common);
    for (const definition of Object.values(RESOURCE_SCHEMA)) {
      this.ajv.addSchema(readJsonObject(join(this.schemasDir, definition.file)));
    }
    const localBindingsFile = "local-repository-bindings.schema.json";
    const templateProvenanceFile = "template-provenance.schema.json";
    const initOperationFile = "init-operation.schema.json";
    const permissionDiffFile = "permission-diff.schema.json";
    const runEventFile = "run-event.schema.json";
    const driveReceiptFile = "drive-receipt.schema.json";
    const coordinatorFile = "coordinator.schema.json";
    const intakeMessageFile = "intake-message.schema.json";
    const syncEventFile = "sync-event.schema.json";
    this.ajv.addSchema(readJsonObject(join(this.schemasDir, localBindingsFile)));
    this.ajv.addSchema(readJsonObject(join(this.schemasDir, templateProvenanceFile)));
    this.ajv.addSchema(readJsonObject(join(this.schemasDir, initOperationFile)));
    this.ajv.addSchema(readJsonObject(join(this.schemasDir, permissionDiffFile)));
    this.ajv.addSchema(readJsonObject(join(this.schemasDir, runEventFile)));
    this.ajv.addSchema(readJsonObject(join(this.schemasDir, driveReceiptFile)));
    this.ajv.addSchema(readJsonObject(join(this.schemasDir, coordinatorFile)));
    this.ajv.addSchema(readJsonObject(join(this.schemasDir, intakeMessageFile)));
    this.ajv.addSchema(readJsonObject(join(this.schemasDir, syncEventFile)));
    for (const [kind, definition] of Object.entries(RESOURCE_SCHEMA) as [KxmResourceKind, { identity: string; file: string }][]) {
      const validator = this.ajv.getSchema(`https://schemas.kxm.dev/${definition.file}`);
      if (!validator) throw new Error(`schema did not compile: ${definition.file}`);
      this.validators.set(kind, validator);
    }
    const localBindingsValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${localBindingsFile}`);
    const templateProvenanceValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${templateProvenanceFile}`);
    const initOperationValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${initOperationFile}`);
    const permissionDiffValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${permissionDiffFile}`);
    const runEventValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${runEventFile}`);
    const driveReceiptValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${driveReceiptFile}`);
    const coordinatorValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${coordinatorFile}`);
    const intakeMessageValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${intakeMessageFile}`);
    const syncEventValidator = this.ajv.getSchema(`https://schemas.kxm.dev/${syncEventFile}`);
    if (!localBindingsValidator) throw new Error(`schema did not compile: ${localBindingsFile}`);
    if (!templateProvenanceValidator) throw new Error(`schema did not compile: ${templateProvenanceFile}`);
    if (!initOperationValidator) throw new Error(`schema did not compile: ${initOperationFile}`);
    if (!permissionDiffValidator) throw new Error(`schema did not compile: ${permissionDiffFile}`);
    if (!runEventValidator) throw new Error(`schema did not compile: ${runEventFile}`);
    if (!driveReceiptValidator) throw new Error(`schema did not compile: ${driveReceiptFile}`);
    if (!coordinatorValidator) throw new Error(`schema did not compile: ${coordinatorFile}`);
    if (!intakeMessageValidator) throw new Error(`schema did not compile: ${intakeMessageFile}`);
    if (!syncEventValidator) throw new Error(`schema did not compile: ${syncEventFile}`);
    this.localBindingsValidator = localBindingsValidator;
    this.templateProvenanceValidator = templateProvenanceValidator;
    this.initOperationValidator = initOperationValidator;
    this.permissionDiffValidator = permissionDiffValidator;
    this.runEventValidator = runEventValidator;
    this.driveReceiptValidator = driveReceiptValidator;
    this.coordinatorValidator = coordinatorValidator;
    this.intakeMessageValidator = intakeMessageValidator;
    this.syncEventValidator = syncEventValidator;
  }

  validate(kind: KxmResourceKind, value: JsonObject, file: string): KxmConfigIssue[] {
    const definition = RESOURCE_SCHEMA[kind];
    if (value.schema !== definition.identity) {
      return [issue("schema", "schema_identity_mismatch", file, `expected ${definition.identity}, received ${String(value.schema)}`)];
    }
    const validator = this.validators.get(kind);
    if (!validator) throw new Error(`missing KXM validator for ${kind}`);
    if (validator(value)) return [];
    return (validator.errors ?? []).map((error) => schemaIssue(file, error));
  }

  validateLocalBindings(value: JsonObject, file: string): KxmConfigIssue[] {
    return this.validateAuxiliary(value, file, "kxm.local-repository-bindings.v1", this.localBindingsValidator);
  }

  validateTemplateProvenance(value: JsonObject, file: string): KxmConfigIssue[] {
    return this.validateAuxiliary(value, file, "kxm.template-provenance.v1", this.templateProvenanceValidator);
  }

  validateInitOperation(value: JsonObject, file: string): KxmConfigIssue[] {
    return this.validateAuxiliary(value, file, "kxm.init-operation.v1", this.initOperationValidator);
  }

  validatePermissionDiff(value: JsonObject, file: string): KxmConfigIssue[] {
    return this.validateAuxiliary(value, file, "kxm.permission-diff.v1", this.permissionDiffValidator);
  }

  validateDriveReceipt(value: JsonObject, file: string): KxmConfigIssue[] {
    return this.validateAuxiliary(value, file, "kxm.drive-receipt.v1", this.driveReceiptValidator);
  }

  private validateAuxiliary(value: JsonObject, file: string, identity: string, validator: ValidateFunction): KxmConfigIssue[] {
    if (value.schema !== identity) {
      return [issue("schema", "schema_identity_mismatch", file, `expected ${identity}, received ${String(value.schema)}`)];
    }
    if (validator(value)) return [];
    return (validator.errors ?? []).map((error) => schemaIssue(file, error));
  }
}

let cachedRunEventRegistry: KxmSchemaRegistry | undefined;

/** Validate a local run event against `kxm.run-event.v1`. Throws `run_event_invalid`. */
export function validateRunEvent(value: unknown, file: string): void {
  const registry = (cachedRunEventRegistry ??= new KxmSchemaRegistry());
  if (!registry.runEventValidator(value)) {
    throw new KxmConfigError([issue(
      "schema",
      "run_event_invalid",
      file,
      registry.ajv.errorsText(registry.runEventValidator.errors, { separator: "; " }),
    )]);
  }
}

/** Check a derived sync object against `kxm.sync-event.v1`. Returns the
 * schema errors instead of throwing so the transform can fall back to a
 * degraded object and the hub can refuse one event without failing a batch. */
export function syncEventSchemaErrors(value: unknown): string | undefined {
  const registry = (cachedRunEventRegistry ??= new KxmSchemaRegistry());
  if (registry.syncEventValidator(value)) return undefined;
  return registry.ajv.errorsText(registry.syncEventValidator.errors, { separator: "; " });
}

/** Validate a drive receipt against `kxm.drive-receipt.v1`. Throws `drive_receipt_invalid`. */
export function validateDriveReceipt(value: unknown, file: string): void {
  const registry = (cachedRunEventRegistry ??= new KxmSchemaRegistry());
  if (!value || typeof value !== "object" || Array.isArray(value) || (value as JsonObject).schema !== "kxm.drive-receipt.v1") {
    throw new KxmConfigError([issue(
      "schema",
      "drive_receipt_invalid",
      file,
      "expected kxm.drive-receipt.v1",
    )]);
  }
  if (!registry.driveReceiptValidator(value)) {
    throw new KxmConfigError([issue(
      "schema",
      "drive_receipt_invalid",
      file,
      registry.ajv.errorsText(registry.driveReceiptValidator.errors, { separator: "; " }),
    )]);
  }
}

/** Validate a coordinator identity against `kxm.coordinator.v1`. Throws `coordinator_invalid`. */
export function validateCoordinator(value: unknown, file: string): void {
  const registry = (cachedRunEventRegistry ??= new KxmSchemaRegistry());
  if (!value || typeof value !== "object" || Array.isArray(value) || (value as JsonObject).schema !== "kxm.coordinator.v1") {
    throw new KxmConfigError([issue("schema", "coordinator_invalid", file, "expected kxm.coordinator.v1")]);
  }
  if (!registry.coordinatorValidator(value)) {
    throw new KxmConfigError([issue(
      "schema",
      "coordinator_invalid",
      file,
      registry.ajv.errorsText(registry.coordinatorValidator.errors, { separator: "; " }),
    )]);
  }
}

/** Validate an intake message against `kxm.intake-message.v1`. Throws `intake_message_invalid`. */
export function validateIntakeMessage(value: unknown, file: string): void {
  const registry = (cachedRunEventRegistry ??= new KxmSchemaRegistry());
  if (!value || typeof value !== "object" || Array.isArray(value) || (value as JsonObject).schema !== "kxm.intake-message.v1") {
    throw new KxmConfigError([issue("schema", "intake_message_invalid", file, "expected kxm.intake-message.v1")]);
  }
  if (!registry.intakeMessageValidator(value)) {
    throw new KxmConfigError([issue(
      "schema",
      "intake_message_invalid",
      file,
      registry.ajv.errorsText(registry.intakeMessageValidator.errors, { separator: "; " }),
    )]);
  }
}

function schemaIssue(file: string, error: ErrorObject): KxmConfigIssue {
  const location = error.instancePath || "/";
  const suffix = error.params && "additionalProperty" in error.params
    ? ` (${String(error.params.additionalProperty)})`
    : "";
  return issue("schema", `schema_${error.keyword}`, file, `${location} ${error.message ?? "is invalid"}${suffix}`);
}

function canonicalHostPath(path: string): string {
  const resolved = resolve(path);
  try {
    return realpathSync.native(resolved);
  } catch {
    return resolved;
  }
}

function sameHostPath(left: string, right: string): boolean {
  const resolvedLeft = canonicalHostPath(left);
  const resolvedRight = canonicalHostPath(right);
  return process.platform === "win32"
    ? resolvedLeft.toLocaleLowerCase("en-US") === resolvedRight.toLocaleLowerCase("en-US")
    : resolvedLeft === resolvedRight;
}

function containedHostPath(root: string, candidate: string): boolean {
  const path = relative(root, candidate);
  return path === "" || (!isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`));
}

function portableBindingIssue(root: string, pathHint: string, repositoryId: string): KxmConfigIssue | undefined {
  let current = root;
  for (const segment of pathHint === "." ? [] : pathHint.split("/")) {
    current = join(current, segment);
    if (!existsSync(current)) break;
    const stat = lstatSync(current);
    if (stat.isSymbolicLink()) {
      return issue("path", "repository_binding_path_link", ".kxm/project.yaml", `portable pathHint for ${repositoryId} traverses a symbolic link or junction`);
    }
  }
  const binding = resolve(root, ...pathHint.split("/"));
  if (existsSync(binding)) {
    const realRoot = canonicalHostPath(root);
    const realBinding = canonicalHostPath(binding);
    if (!containedHostPath(realRoot, realBinding)) {
      return issue("path", "repository_binding_outside_project", ".kxm/project.yaml", `portable pathHint for ${repositoryId} resolves outside the authoritative project root`);
    }
  }
  return undefined;
}

/** The portable relative-path rule every project-owned pathHint must satisfy. */
export function kxmPortablePath(path: string): boolean {
  return portablePath(path);
}

function portablePath(path: string): boolean {
  if (path === ".") return true;
  if (path.includes("\\") || path.startsWith("/") || /^[A-Za-z]:/.test(path) || /[<>:"|?*\u0000-\u001F]/.test(path)) return false;
  const segments = path.split("/");
  return segments.every((segment) => segment.length > 0
    && segment !== "."
    && segment !== ".."
    && !WINDOWS_RESERVED.test((segment.split(".")[0] ?? segment).replace(/[ .]+$/g, ""))
    && !segment.endsWith(".")
    && !segment.endsWith(" ")
    && !segment.includes(":"));
}

/** The identifier rule every path-derived resource identity must satisfy. */
export function kxmResourceIdentifier(id: string): boolean {
  return id.length <= 64 && IDENTIFIER.test(id) && !WINDOWS_RESERVED.test(id);
}

function resourceIdentifier(id: string): boolean {
  return kxmResourceIdentifier(id);
}

function displayPath(root: string, file: string): string {
  const candidate = relative(root, file).replaceAll("\\", "/");
  return candidate && !candidate.startsWith("../") ? candidate : file.replaceAll("\\", "/");
}

function readResource(
  registry: KxmSchemaRegistry,
  root: string,
  file: string,
  logicalPath: string,
  kind: KxmResourceKind,
  id?: string,
  containmentRoot = root,
): KxmResource {
  const label = displayPath(root, file);
  let stat;
  try {
    stat = lstatSync(file);
  } catch {
    fail("discovery", "resource_missing", label, "resource does not exist");
  }
  if (stat.isSymbolicLink()) fail("path", "resource_symlink", label, "configuration resources must not be symbolic links");
  const boundary = resolve(containmentRoot);
  let parent = dirname(file);
  while (parent !== boundary) {
    if (parent === dirname(parent)) fail("path", "resource_outside_project", label, "resource escapes the project root");
    let parentStat;
    try { parentStat = lstatSync(parent); }
    catch { fail("discovery", "resource_parent_missing", label, "resource parent does not exist"); }
    if (parentStat.isSymbolicLink()) fail("path", "resource_parent_symlink", label, "configuration resource parents must not be symbolic links");
    parent = dirname(parent);
  }
  if (!stat.isFile()) fail("path", "resource_not_file", label, "configuration resource must be a regular file");
  return { kind, ...(id === undefined ? {} : { id }), file, logicalPath, value: resourceValue(registry, readFileSync(file), label, kind) };
}

function resourceValue(registry: KxmSchemaRegistry, input: string | Uint8Array, label: string, kind: KxmResourceKind): JsonObject {
  const value = parseRestrictedYaml(input, label);
  const issues = registry.validate(kind, value, label);
  if (issues.length > 0) throw new KxmConfigError(issues);
  return value;
}

function readTemplateProvenance(registry: KxmSchemaRegistry, root: string): JsonObject | undefined {
  const file = join(root, ".kxm", "template-provenance.yaml");
  if (!existsSync(file)) return undefined;
  const label = ".kxm/template-provenance.yaml";
  const stat = lstatSync(file);
  if (stat.isSymbolicLink() || !stat.isFile()) {
    fail("path", "resource_not_file", label, "template provenance must be a regular file, not a link or directory");
  }
  const configRoot = join(root, ".kxm");
  let parent = dirname(file);
  while (parent !== root) {
    const parentStat = lstatSync(parent);
    if (parentStat.isSymbolicLink() || !parentStat.isDirectory()) {
      fail("path", "resource_parent_symlink", label, "template provenance parents must be regular directories");
    }
    if (parent === configRoot) break;
    parent = dirname(parent);
  }
  const value = parseRestrictedYaml(readFileSync(file), label);
  const issues = registry.validateTemplateProvenance(value, label);
  if (issues.length > 0) throw new KxmConfigError(issues);
  if (!resolveKxmTemplateBaseline(value)) {
    fail("semantic", "template_provenance_revision_invalid", label, "template provenance does not exactly match a supported built-in baseline");
  }
  const files = Array.isArray(value.files) ? value.files : [];
  const seen = new Set<string>();
  let prior = "";
  let managedBytes = 0;
  for (const candidate of files) {
    const record = objectValue(candidate);
    const path = record && stringValue(record.path);
    if (!path) continue;
    managedBytes += typeof record.bytes === "number" ? record.bytes : 0;
    const folded = path.toLocaleLowerCase("en-US");
    if (!path.startsWith(".kxm/") || path === label || !portablePath(path)) {
      fail("path", "template_provenance_path_invalid", label, `managed path ${path} is not a portable project-configuration path`);
    }
    if (seen.has(folded)) fail("path", "template_provenance_path_collision", label, `managed path ${path} collides after case folding`);
    if (prior && compareCodeUnits(prior, path) >= 0) fail("semantic", "template_provenance_order_invalid", label, "managed file entries must use strict code-unit order");
    seen.add(folded);
    prior = path;
  }
  if (managedBytes > 8 * 1024 * 1024) {
    fail("parse", "template_provenance_bounds_exceeded", label, "managed template manifest exceeds 8388608 bytes");
  }
  return value;
}

function listNamedResources(
  registry: KxmSchemaRegistry,
  root: string,
  directory: string,
  logicalDirectory: string,
  kind: "agent" | "model" | "role" | "workflow",
  replacedId?: string,
  collected?: KxmConfigIssue[],
): Map<string, KxmResource> {
  const resources = new Map<string, KxmResource>();
  if (!existsSync(directory)) return resources;
  const stat = lstatSync(directory);
  if (stat.isSymbolicLink() || !stat.isDirectory()) {
    fail("path", "resource_directory_invalid", displayPath(root, directory), "resource directory must be a regular directory");
  }
  const issues: KxmConfigIssue[] = [];
  const entries = readdirSync(directory, { withFileTypes: true }).sort((left, right) => compareCodeUnits(left.name, right.name));
  for (const entry of entries) {
    if (entry.isDirectory()) {
      issues.push(issue("path", "nested_resource_directory", displayPath(root, join(directory, entry.name)), "nested resource directories are not allowed"));
      continue;
    }
    if (!entry.name.toLowerCase().endsWith(".yaml") && !entry.name.toLowerCase().endsWith(".yml")) continue;
    if (!entry.isFile() || extname(entry.name) !== ".yaml") {
      issues.push(issue("path", "resource_filename_invalid", displayPath(root, join(directory, entry.name)), "resource must be a regular file with the exact .yaml extension"));
      continue;
    }
    const id = basename(entry.name, ".yaml");
    if (kind === "model" && id === "inventory") continue;
    if (id === replacedId) continue;
    if (!resourceIdentifier(id)) {
      issues.push(issue("path", "resource_id_invalid", displayPath(root, join(directory, entry.name)), `filename-derived identity ${id} is invalid or platform-reserved`));
      continue;
    }
    const collision = [...resources.keys()].find((candidate) => candidate.toLocaleLowerCase("en-US") === id.toLocaleLowerCase("en-US"));
    if (collision) {
      issues.push(issue("path", "resource_id_collision", displayPath(root, join(directory, entry.name)), `${id} case-folds to existing ${collision}`));
      continue;
    }
    try {
      const resource = readResource(registry, root, join(directory, entry.name), `${logicalDirectory}/${id}.yaml`, kind, id);
      resources.set(id, resource);
    } catch (error) {
      if (error instanceof KxmConfigError) issues.push(...error.issues);
      else throw error;
    }
  }
  if (issues.length > 0) {
    if (collected) collected.push(...issues);
    else throw new KxmConfigError(issues);
  }
  return resources;
}

function valuesOf(object: JsonObject, field: string): JsonValue[] {
  const value = object[field];
  return Array.isArray(value) ? value : [];
}

function objectValue(value: JsonValue | undefined): JsonObject | undefined {
  return value !== undefined && isJsonObject(value) ? value : undefined;
}

function stringValue(value: JsonValue | undefined): string | undefined {
  return typeof value === "string" ? value : undefined;
}

function roleById(roles: ReadonlyMap<string, KxmResource>, roleId: string): KxmResource | undefined {
  const direct = roles.get(roleId);
  if (direct) return direct;
  const folded = roleId.toLocaleLowerCase("en-US");
  for (const role of roles.values()) {
    if ((role.id ?? "").toLocaleLowerCase("en-US") === folded) return role;
  }
  return undefined;
}

function numberValue(value: JsonValue | undefined, fallback: number): number {
  return typeof value === "number" ? value : fallback;
}

function names(value: JsonValue | undefined): string[] {
  return Array.isArray(value) ? value.filter((candidate): candidate is string => typeof candidate === "string") : [];
}

/** Resolve an agent, workflow, or route, including a deprecated alias. */
export function lookupKxmResource<T extends { id?: string; value: JsonObject }>(
  resources: Iterable<T>,
  requested: string,
  kind: "agent" | "workflow" | "route",
): T | undefined {
  const records = [...resources].map((resource) => ({
    id: resource.id,
    aliases: names(resource.value.aliases),
    resource,
  }));
  return lookupById(records, requested, kind)?.record.resource;
}

function mapResource(resource: KxmResource, map: Map<string, KxmResource>, issues: KxmConfigIssue[]): void {
  const id = resource.id;
  if (!id) return;
  const collision = [...map.keys()].find((candidate) => candidate.toLocaleLowerCase("en-US") === id.toLocaleLowerCase("en-US"));
  if (collision) {
    issues.push(issue("path", "resource_id_collision", resource.logicalPath, `${id} case-folds to existing ${collision}`));
    return;
  }
  map.set(id, resource);
}

function validatePortablePaths(resource: KxmResource, issues: KxmConfigIssue[]): void {
  const candidates: { path: string; label: string }[] = [];
  if (resource.kind === "project") {
    for (const candidate of valuesOf(resource.value, "repositories")) {
      const entry = objectValue(candidate);
      const pathHint = entry && stringValue(entry.pathHint);
      if (pathHint) candidates.push({ path: pathHint, label: "repositories[].pathHint" });
    }
  }
  if (resource.kind === "environment") {
    const path = objectValue(resource.value.path);
    if (path) {
      for (const field of ["prepend", "append"] as const) {
        for (const candidate of names(path[field])) candidates.push({ path: candidate, label: `path.${field}[]` });
      }
    }
  }
  for (const candidate of candidates) {
    if (!portablePath(candidate.path)) {
      issues.push(issue("path", "portable_path_invalid", resource.logicalPath, `${candidate.label} is not a normalized forward-slash relative path: ${candidate.path}`));
    }
  }
}

function selectorCandidates(
  selectorValue: JsonValue | undefined,
  models: ReadonlyMap<string, KxmResource>,
  file: string,
  label: string,
  issues: KxmConfigIssue[],
): KxmResource[] {
  const selector = objectValue(selectorValue);
  if (!selector) return [];
  const profile = stringValue(selector.profile);
  if (profile) {
    const model = models.get(profile);
    if (!model) issues.push(issue("reference", "model_profile_unknown", file, `${label} references unknown model profile ${profile}`));
    return model ? [model] : [];
  }
  const tag = stringValue(selector.tag);
  if (!tag) return [];
  const capabilities = new Set(names(selector.capabilities));
  const candidates = [...models.values()].filter((model) => names(model.value.tags).includes(tag)
    && [...capabilities].every((capability) => names(model.value.capabilities).includes(capability)));
  if (candidates.length === 0) issues.push(issue("reference", "model_tag_unresolved", file, `${label} selector tag ${tag} has no matching profile`));
  return candidates;
}

interface ModelCandidate {
  key: string;
  profile?: string;
  provider?: string;
  model?: string;
}

function modelCandidates(selectorValue: JsonValue | undefined, models: ReadonlyMap<string, KxmResource>): ModelCandidate[] {
  const selector = objectValue(selectorValue);
  if (!selector) return [];
  const profile = stringValue(selector.profile);
  if (profile) {
    const resource = models.get(profile);
    if (!resource) return [];
    return [{
      key: `profile:${profile}`,
      profile,
      ...(stringValue(resource.value.provider) || stringValue(resource.value.vendor) ? { provider: (stringValue(resource.value.provider) ?? stringValue(resource.value.vendor))! } : {}),
      ...(stringValue(resource.value.model) ? { model: stringValue(resource.value.model)! } : {}),
    }];
  }
  const tag = stringValue(selector.tag);
  if (tag) {
    const capabilities = new Set(names(selector.capabilities));
    return [...models.values()]
      .filter((resource) => names(resource.value.tags).includes(tag)
        && [...capabilities].every((capability) => names(resource.value.capabilities).includes(capability)))
      .map((resource) => ({
        key: `profile:${resource.id ?? "unknown"}`,
        ...(resource.id ? { profile: resource.id } : {}),
        ...(stringValue(resource.value.provider) || stringValue(resource.value.vendor) ? { provider: (stringValue(resource.value.provider) ?? stringValue(resource.value.vendor))! } : {}),
        ...(stringValue(resource.value.model) ? { model: stringValue(resource.value.model)! } : {}),
      }));
  }
  const provider = stringValue(selector.provider);
  const model = stringValue(selector.model);
  return provider && model ? [{ key: `direct:${provider}/${model}`, provider, model }] : [];
}

function compatibleModel(left: ModelCandidate, right: ModelCandidate): boolean {
  if (left.profile && right.profile) return left.profile === right.profile;
  return left.provider === right.provider && left.model === right.model;
}

function diversityCandidates(
  stepSelector: JsonValue | undefined,
  allowedAgents: readonly string[],
  agents: ReadonlyMap<string, KxmResource>,
  models: ReadonlyMap<string, KxmResource>,
): ModelCandidate[] {
  const stepCandidates = modelCandidates(stepSelector, models);
  const resolved: ModelCandidate[] = [];
  for (const agentId of allowedAgents) {
    const agentSelector = agents.get(agentId)?.value.model;
    const agentCandidates = modelCandidates(agentSelector, models);
    if (stepSelector === undefined) resolved.push(...agentCandidates);
    else if (agentSelector === undefined) resolved.push(...stepCandidates);
    else resolved.push(...stepCandidates.filter((candidate) => agentCandidates.some((allowed) => compatibleModel(candidate, allowed))));
  }
  if (allowedAgents.length === 0) resolved.push(...stepCandidates);
  return [...new Map(resolved.map((candidate) => [candidate.key, candidate])).values()];
}

function validateEnvironment(resource: KxmResource, issues: KxmConfigIssue[]): void {
  const values = objectValue(resource.value.values) ?? {};
  const secretNames = new Set<string>();
  for (const candidate of valuesOf(resource.value, "secrets")) {
    const secret = objectValue(candidate);
    const name = secret && stringValue(secret.name);
    if (!name) continue;
    if (secretNames.has(name)) issues.push(issue("semantic", "secret_name_duplicate", resource.logicalPath, `secret environment name ${name} is duplicated`));
    secretNames.add(name);
    if (Object.hasOwn(values, name)) issues.push(issue("semantic", "environment_name_conflict", resource.logicalPath, `${name} appears in both values and secrets`));
  }
  for (const [name, value] of Object.entries(values)) {
    if (typeof value === "string" && SECRET_VALUE_PATTERNS.some((pattern) => pattern.test(value))) {
      issues.push(issue("semantic", "probable_secret_value", resource.logicalPath, `${name} resembles a credential and must be represented by secrets[].ref`));
    }
  }
}

function accessRank(value: JsonValue | undefined): number {
  return value === "write" ? 2 : value === "read" ? 1 : 0;
}

function validateToolScope(agent: KxmResource, step: JsonObject, file: string, stepId: string, issues: KxmConfigIssue[]): void {
  const requested = objectValue(step.tools);
  if (!requested) return;
  const ceiling = objectValue(agent.value.tools);
  if (!ceiling) {
    issues.push(issue("semantic", "tool_scope_expansion", file, `${stepId} requests tools but agent ${agent.id ?? "unknown"} declares no tool ceiling`));
    return;
  }
  const requestedPreset = stringValue(requested.preset);
  const ceilingPreset = stringValue(ceiling.preset);
  if (requestedPreset !== ceilingPreset) {
    issues.push(issue("semantic", "tool_scope_expansion", file, `${stepId} tool preset ${String(requestedPreset)} does not preserve agent ${agent.id ?? "unknown"} preset ${String(ceilingPreset)}`));
  }
  const allowed = new Set(names(ceiling.allow));
  const denied = new Set(names(ceiling.deny));
  for (const tool of names(requested.allow)) {
    if (!allowed.has(tool) || denied.has(tool)) issues.push(issue("semantic", "tool_scope_expansion", file, `${stepId} requests tool ${tool} beyond agent ${agent.id ?? "unknown"} explicit allowlist`));
  }
  const requestedDenied = new Set(names(requested.deny));
  for (const tool of denied) {
    if (!requestedDenied.has(tool)) issues.push(issue("semantic", "tool_scope_expansion", file, `${stepId} drops agent ${agent.id ?? "unknown"} denial for ${tool}`));
  }
}

function validateToolPolicy(resource: KxmResource, policy: JsonObject | undefined, label: string, issues: KxmConfigIssue[]): void {
  if (!policy) return;
  const allowed = new Set(names(policy.allow));
  for (const tool of names(policy.deny)) {
    if (allowed.has(tool)) issues.push(issue("semantic", "tool_policy_contradiction", resource.logicalPath, `${label} both allows and denies ${tool}`));
  }
}

/** Validate an in-memory resource set with the same semantic rules as a loaded project. */
export function validateKxmResources(
  resources: ReadonlyMap<string, { kind: KxmResourceKind; id?: string; value: JsonObject }>,
  options: KxmConfigOptions = {},
): KxmConfigIssue[] {
  const make = (logicalPath: string, kind: KxmResourceKind, id: string | undefined, value: JsonObject): KxmResource => ({
    kind,
    ...(id === undefined ? {} : { id }),
    file: logicalPath,
    logicalPath,
    value,
  });
  assertNoRegisteredGates(options);
  let gateRegistry: KxmResource | undefined;
  let project: KxmResource | undefined;
  const repositories = new Map<string, KxmResource>();
  const agents = new Map<string, KxmResource>();
  const models = new Map<string, KxmResource>();
  const workflows = new Map<string, KxmResource>();
  const environments: KxmResource[] = [];
  for (const [logicalPath, resource] of resources) {
    const made = make(logicalPath, resource.kind, resource.id, resource.value);
    if (resource.kind === "project") project = made;
    else if (resource.kind === "repository" && resource.id) repositories.set(resource.id, made);
    else if (resource.kind === "agent" && resource.id) agents.set(resource.id, made);
    else if (resource.kind === "model" && resource.id) models.set(resource.id, made);
    else if (resource.kind === "workflow" && resource.id) workflows.set(resource.id, made);
    else if (resource.kind === "environment") environments.push(made);
    else if (resource.kind === "gate-registry") gateRegistry = made;
  }
  if (!project) return [issue("discovery", "project_definition_missing", ".kxm/project.yaml", "resource set has no project definition")];
  return validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry);
}

function validateAgentScope(
  agent: KxmResource,
  step: JsonObject,
  repositories: ReadonlySet<string>,
  file: string,
  stepId: string,
  issues: KxmConfigIssue[],
): void {
  validateToolScope(agent, step, file, stepId, issues);
  const agentRepositories = objectValue(agent.value.repositories) ?? {};
  const defaultAccess = agent.value.defaultRepositoryAccess;
  for (const [repositoryId, requested] of Object.entries(objectValue(step.repositories) ?? {})) {
    if (!repositories.has(repositoryId)) continue;
    const ceiling = agentRepositories[repositoryId] ?? defaultAccess ?? "none";
    if (accessRank(requested) > accessRank(ceiling)) {
      issues.push(issue("semantic", "repository_scope_expansion", file, `${stepId} requests ${String(requested)} access to ${repositoryId} beyond agent ${agent.id ?? "unknown"} ceiling ${String(ceiling)}`));
    }
  }
  const grants = new Set(valuesOf(agent.value, "secrets").map((candidate) => objectValue(candidate)).filter((candidate): candidate is JsonObject => Boolean(candidate)).map((candidate) => stringValue(candidate.ref)).filter((candidate): candidate is string => Boolean(candidate)));
  for (const candidate of valuesOf(step, "secrets")) {
    const reference = stringValue(objectValue(candidate)?.ref);
    if (reference && !grants.has(reference)) {
      issues.push(issue("semantic", "secret_scope_expansion", file, `${stepId} requests secret ${reference} beyond agent ${agent.id ?? "unknown"} ceiling`));
    }
  }
}

/** The outcomes a gate step settles on, by `expect` (computeGateEvidenceOutcome
 * in runtime-store.ts): nothing else ever reaches a gate step's `on` map. */
const GATE_STEP_OUTCOMES = {
  pass: ["passed", "implementation-failure"],
  fail: ["passed", "repro-missing"],
} as const satisfies Record<"pass" | "fail", readonly string[]>;

/** A gate step that routes on an outcome it never produces (such as `failed`)
 * instead of one it does can never settle: the Runtime records the produced
 * outcome, finds no transition for it, and leaves the attempt unsettled. An
 * extra dead key next to every produced outcome still settles, and the built-in
 * template has always declared `failed` beside `implementation-failure`, so only
 * the combination is refused. Misspellings are gate_outcome_renamed's. */
function gateOutcomeImpossible(step: JsonObject, stepId: string, file: string): KxmConfigIssue | undefined {
  const expect = step.expect === "fail" ? "fail" : "pass";
  const produced: readonly string[] = GATE_STEP_OUTCOMES[expect];
  const declared = Object.keys(objectValue(step.on) ?? {});
  const impossible = declared.filter((outcome) => !produced.includes(outcome) && outcome !== "implementation_failure" && outcome !== "repro_missing");
  const missing = produced.filter((outcome) => !declared.includes(outcome));
  if (impossible.length === 0 || missing.length === 0) return undefined;
  return issue("semantic", "gate_outcome_impossible", file, `${stepId} declares ${impossible.join(", ")}, which a gate step with expect ${expect} never produces; it settles on ${produced.join(" or ")}, so declare ${missing.join(" and ")}`);
}

function transition(value: JsonValue): { target?: string; maxTransitions?: number; terminalStatus?: string } {
  if (typeof value === "string") return { target: value };
  const object = objectValue(value);
  const target = stringValue(object?.target);
  const terminalStatus = stringValue(object?.terminalStatus);
  return {
    ...(target === undefined ? {} : { target }),
    ...(typeof object?.maxTransitions === "number" ? { maxTransitions: object.maxTransitions } : {}),
    ...(terminalStatus === undefined ? {} : { terminalStatus }),
  };
}

function validateWorkflow(
  workflow: KxmResource,
  agents: ReadonlyMap<string, KxmResource>,
  models: ReadonlyMap<string, KxmResource>,
  repositories: ReadonlySet<string>,
  gates: ReadonlySet<string> | undefined,
  issues: KxmConfigIssue[],
): void {
  const file = workflow.logicalPath;
  const coordinator = stringValue(workflow.value.coordinator) ?? "coordinator";
  if (!lookupKxmResource(agents.values(), coordinator, "agent")) issues.push(issue("reference", "coordinator_unknown", file, `coordinator ${coordinator} does not exist`));
  const steps = valuesOf(workflow.value, "steps").map((candidate) => objectValue(candidate)).filter((candidate): candidate is JsonObject => Boolean(candidate));
  const stepIndex = new Map<string, number>();
  const primarySteps = new Set<string>();
  for (const [index, step] of steps.entries()) {
    const id = stringValue(step.id);
    if (!id) continue;
    if (stepIndex.has(id)) issues.push(issue("semantic", "step_id_duplicate", file, `step ${id} is duplicated`));
    else {
      stepIndex.set(id, index);
      primarySteps.add(id);
    }
  }
  for (const [index, step] of steps.entries()) {
    const id = stringValue(step.id);
    if (!id) continue;
    const aliases = new Set(names(step.aliases));
    for (const [oldId, newId] of STEP_RENAMES) {
      if (id === newId) aliases.add(oldId);
      if (id === oldId) aliases.add(newId);
    }
    for (const alias of aliases) {
      if (alias === id || primarySteps.has(alias) || stepIndex.has(alias)) continue;
      stepIndex.set(alias, index);
    }
  }

  for (const field of ["reproOracle", "planHash"] as const) {
    const oracle = objectValue(workflow.value[field]);
    if (!oracle) continue;
    const stageId = stringValue(oracle.stageId);
    const evidenceKey = stringValue(oracle.evidenceKey);
    const index = stageId === undefined ? undefined : stepIndex.get(stageId);
    if (index === undefined) {
      issues.push(issue("reference", "oracle_stage_unknown", file, `${field} references unknown stage ${String(stageId)}`));
      continue;
    }
    const evidence = new Set(valuesOf(steps[index] ?? {}, "requiredEvidence").map((candidate) => stringValue(objectValue(candidate)?.key)).filter((candidate): candidate is string => Boolean(candidate)));
    if (!evidenceKey || !evidence.has(evidenceKey)) issues.push(issue("reference", "oracle_evidence_unknown", file, `${field} references undeclared evidence ${String(evidenceKey)} in ${stageId}`));
  }
  const planStages = new Set(names(workflow.value.requirePlanHash));
  if (planStages.size > 0 && !objectValue(workflow.value.planHash)) issues.push(issue("semantic", "plan_hash_missing", file, "requirePlanHash requires planHash"));
  for (const stageId of planStages) {
    if (!stepIndex.has(stageId)) issues.push(issue("reference", "plan_hash_stage_unknown", file, `requirePlanHash references unknown stage ${stageId}`));
  }

  const adjacency = steps.map(() => new Set<number>());
  const terminalReachable = new Set<number>();
  const completedTerminalSources = new Set<number>();
  let hasBackEdge = false;
  for (const [index, step] of steps.entries()) {
    const stepId = stringValue(step.id) ?? `step-${index}`;
    const kind = stringValue(step.kind);
    validateToolPolicy(workflow, objectValue(step.tools), `${stepId}.tools`, issues);
    const primaryAgentId = stringValue(step.agent);
    if ((kind === "agent" || kind === "moa") && primaryAgentId && !lookupKxmResource(agents.values(), primaryAgentId, "agent")) {
      issues.push(issue("reference", "agent_unknown", file, `${stepId} references unknown agent ${primaryAgentId}`));
    }
    const gate = stringValue(step.gate);
    if (kind === "gate" && gate) {
      if (!gates) issues.push(issue("reference", "gate_registry_missing", file, `${stepId} requires .kxm/gates.yaml`));
      else if (!gates.has(gate)) issues.push(issue("reference", "gate_unknown", file, `${stepId} references unregistered gate ${gate}`));
    }
    if (step.expect !== undefined && (kind !== "gate" || !["pass", "fail"].includes(String(step.expect)))) {
      issues.push(issue("semantic", "gate_expect_invalid", file, `${stepId} expect is gate-only pass or fail`));
    }
    if (kind === "gate" && Object.keys(objectValue(step.on) ?? {}).some((outcome) => outcome === "implementation_failure" || outcome === "repro_missing")) {
      issues.push(issue("semantic", "gate_outcome_renamed", file, `${stepId} must use implementation-failure and repro-missing`));
    }
    const impossibleOutcome = kind === "gate" ? gateOutcomeImpossible(step, stepId, file) : undefined;
    if (impossibleOutcome) issues.push(impossibleOutcome);
    for (const repositoryId of Object.keys(objectValue(step.repositories) ?? {})) {
      if (!repositories.has(repositoryId)) issues.push(issue("reference", "repository_unknown", file, `${stepId} references unknown repository ${repositoryId}`));
    }
    if ((kind === "agent" || kind === "moa") && step.model !== undefined) {
      issues.push(issue("semantic", "producer_route_unsupported", file, `${stepId} model is not honored; remove model from the step`));
    }
    selectorCandidates(step.model, models, file, `${stepId}.model`, issues);

    const assignment = objectValue(step.assignments);
    const declaredAgents = assignment ? names(assignment.allowedAgents) : [];
    const allowedAgents = declaredAgents.length > 0 ? declaredAgents : primaryAgentId ? [primaryAgentId] : [];
    for (const agentId of allowedAgents) {
      const agent = lookupKxmResource(agents.values(), agentId, "agent");
      if (!agent) issues.push(issue("reference", "assignment_agent_unknown", file, `${stepId} allows unknown agent ${agentId}`));
      else {
        validateAgentScope(agent, step, repositories, file, stepId, issues);
        if (step.model !== undefined && agent.value.model !== undefined) {
          const requestedModels = modelCandidates(step.model, models);
          const ceilingModels = modelCandidates(agent.value.model, models);
          const effectiveModels = requestedModels.filter((requested) => ceilingModels.some((ceiling) => compatibleModel(requested, ceiling)));
          // A workflow selector is always intersected with the selected
          // agent's model ceiling; it never replaces that ceiling. Reject an
          // empty intersection now so dispatch cannot fall back to the raw,
          // broader selector later.
          if (requestedModels.length > 0 && effectiveModels.length === 0) {
            issues.push(issue("semantic", "model_selector_incompatible", file, `${stepId} model selector has no candidate within agent ${agentId} ceiling`));
          }
        }
      }
    }
    if (primaryAgentId && allowedAgents.length > 0 && !allowedAgents.includes(primaryAgentId)) {
      issues.push(issue("semantic", "primary_agent_ineligible", file, `${stepId} primary agent ${primaryAgentId} is not in allowedAgents`));
    }

    const minimum = numberValue(assignment?.minimum, 1);
    const target = numberValue(assignment?.target, minimum);
    const maximum = numberValue(assignment?.maximum, target);
    const maxParallel = numberValue(assignment?.maxParallel, maximum);
    if (!(minimum <= target && target <= maximum)) issues.push(issue("semantic", "assignment_bounds_invalid", file, `${stepId} must satisfy minimum <= target <= maximum`));
    if (maxParallel > maximum) issues.push(issue("semantic", "assignment_parallelism_invalid", file, `${stepId} maxParallel exceeds maximum`));
    if (assignment && allowedAgents.length > 0 && maximum > allowedAgents.length && kind === "moa") {
      issues.push(issue("semantic", "assignment_pool_too_small", file, `${stepId} maximum ${maximum} exceeds ${allowedAgents.length} eligible agents`));
    }
    const maxWriteRepositories = assignment && typeof assignment.maxWriteRepositories === "number" ? assignment.maxWriteRepositories : undefined;
    if (maxWriteRepositories !== undefined) {
      const writable = Object.values(objectValue(step.repositories) ?? {}).filter((access) => access === "write").length;
      if (maxWriteRepositories > writable) issues.push(issue("semantic", "write_repository_bound_invalid", file, `${stepId} maxWriteRepositories exceeds writable repository scope`));
    }

    const join = objectValue(step.join);
    const minimumPassed = join && typeof join.minimumPassed === "number" ? join.minimumPassed : undefined;
    if (minimumPassed !== undefined && minimumPassed > maximum) issues.push(issue("semantic", "join_impossible", file, `${stepId} minimumPassed exceeds assignment maximum`));

    const distinctBy = names(assignment?.distinctBy);
    if (distinctBy.length > 0) {
      const candidates = diversityCandidates(step.model, allowedAgents, agents, models);
      for (const dimension of distinctBy) {
        const distinct = new Set(candidates.map((candidate) => candidate[dimension as "profile" | "provider" | "model"]).filter((value): value is string => Boolean(value)));
        if (distinct.size < target) issues.push(issue("semantic", "model_diversity_impossible", file, `${stepId} requires ${target} distinct ${dimension} values but resolves ${distinct.size}`));
      }
    }

    const evidenceKeys = new Set<string>();
    for (const candidate of valuesOf(step, "requiredEvidence")) {
      const requirement = objectValue(candidate);
      if (!requirement) continue;
      const key = stringValue(requirement.key);
      if (key) {
        if (evidenceKeys.has(key)) issues.push(issue("semantic", "evidence_key_duplicate", file, `${stepId} evidence key ${key} is duplicated`));
        evidenceKeys.add(key);
      }
      const policy = objectValue(requirement.producerPolicy);
      if (!policy) continue;
      const eligible = names(policy.eligibleAgents);
      const producerMinimum = numberValue(policy.minimumProducers, 1);
      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`));
      for (const agentId of eligible) {
        if (!lookupKxmResource(agents.values(), agentId, "agent")) issues.push(issue("reference", "producer_agent_unknown", file, `${stepId}.${String(key)} references unknown producer ${agentId}`));
        if (allowedAgents.length > 0 && !allowedAgents.includes(agentId)) issues.push(issue("semantic", "producer_agent_ineligible", file, `${stepId}.${String(key)} producer ${agentId} is outside allowedAgents`));
      }
      const degradation = objectValue(policy.degradation);
      if (degradation && numberValue(degradation.minimumProducers, 1) >= producerMinimum) issues.push(issue("semantic", "producer_degradation_invalid", file, `${stepId}.${String(key)} degradation must lower minimumProducers`));
    }

    const writes = Object.values(objectValue(step.repositories) ?? {}).some((access) => access === "write");
    const planHashStage = stringValue(objectValue(workflow.value.planHash)?.stageId);
    const planHashIndex = planHashStage === undefined ? undefined : stepIndex.get(planHashStage);
    if (writes && planHashIndex !== undefined && index > planHashIndex && !planStages.has(stepId)) {
      issues.push(issue("semantic", "mutation_missing_plan_hash", file, `${stepId} mutates after the approved-plan stage but is absent from requirePlanHash`));
    }

    const outcomes = objectValue(step.on) ?? {};
    if (Object.keys(outcomes).length === 0) issues.push(issue("semantic", "step_transitions_missing", file, `${stepId} has no declared transitions`));
    for (const [outcome, rawTransition] of Object.entries(outcomes)) {
      const parsed = transition(rawTransition);
      if (parsed.target === "$terminal") {
        terminalReachable.add(index);
        if (parsed.terminalStatus === "completed") completedTerminalSources.add(index);
        continue;
      }
      if (parsed.target && parsed.target !== "$terminal" && !primarySteps.has(parsed.target) && stepIndex.has(parsed.target)) {
        const canonical = stringValue(steps[stepIndex.get(parsed.target) ?? -1]?.id);
        if (canonical) noteDeprecatedId("step", parsed.target, canonical);
      }
      const targetIndex = parsed.target === undefined ? undefined : stepIndex.get(parsed.target);
      if (targetIndex === undefined) {
        issues.push(issue("reference", "transition_target_unknown", file, `${stepId}.${outcome} references unknown target ${String(parsed.target)}`));
        continue;
      }
      adjacency[index]?.add(targetIndex);
      if (targetIndex <= index) {
        hasBackEdge = true;
        if (parsed.maxTransitions === undefined) issues.push(issue("semantic", "back_edge_unbounded", file, `${stepId}.${outcome} back-edge requires maxTransitions`));
      }
    }
  }

  if (hasBackEdge && typeof objectValue(workflow.value.limits)?.maxTransitions !== "number") {
    issues.push(issue("semantic", "workflow_cycle_unbounded", file, "workflow with back-edges requires limits.maxTransitions"));
  }

  const readyIndex = stepIndex.get("ready");
  const verifyIndex = stepIndex.get("verify");
  if (readyIndex !== undefined && verifyIndex !== undefined) {
    const verifyTargets = adjacency[verifyIndex] ?? new Set();
    if (!verifyTargets.has(readyIndex)) {
      issues.push(issue("semantic", "verify_must_precede_ready", file, "verify must transition to ready"));
    }
    for (const [srcIndex, targets] of adjacency.entries()) {
      if (targets.has(readyIndex) && srcIndex !== verifyIndex) {
        issues.push(issue("semantic", "verify_must_precede_ready", file, `step ${String(steps[srcIndex]?.id)} transitions to ready bypassing verify`));
      }
    }
  }
  if (steps.length > 0) {
    const reachable = new Set<number>([0]);
    const queue = [0];
    while (queue.length > 0) {
      const current = queue.shift();
      if (current === undefined) break;
      for (const target of adjacency[current] ?? []) if (!reachable.has(target)) { reachable.add(target); queue.push(target); }
    }
    for (const [index, step] of steps.entries()) if (!reachable.has(index)) issues.push(issue("semantic", "step_unreachable", file, `step ${String(step.id)} is unreachable from the first step`));

    for (const [requiredIndex, requiredStep] of steps.entries()) {
      if (requiredStep.kind !== "gate" && requiredStep.kind !== "approval") continue;
      const reachableWithoutRequired = new Set<number>();
      const queueWithoutRequired = requiredIndex === 0 ? [] : [0];
      if (requiredIndex !== 0) reachableWithoutRequired.add(0);
      while (queueWithoutRequired.length > 0) {
        const current = queueWithoutRequired.shift();
        if (current === undefined) break;
        for (const target of adjacency[current] ?? []) {
          if (target === requiredIndex || reachableWithoutRequired.has(target)) continue;
          reachableWithoutRequired.add(target);
          queueWithoutRequired.push(target);
        }
      }
      if ([...completedTerminalSources].some((source) => source !== requiredIndex && reachableWithoutRequired.has(source))) {
        issues.push(issue("semantic", "required_step_bypass", file, `a completed path can bypass required ${String(requiredStep.id)}`));
      }
    }

    const canTerminate = new Set(terminalReachable);
    let changed = true;
    while (changed) {
      changed = false;
      for (const [index, targets] of adjacency.entries()) {
        if (!canTerminate.has(index) && [...targets].some((target) => canTerminate.has(target))) { canTerminate.add(index); changed = true; }
      }
    }
    for (const index of reachable) if (!canTerminate.has(index)) issues.push(issue("semantic", "step_cannot_terminate", file, `step ${String(steps[index]?.id)} has no path to a terminal transition`));
  }
}

function validateModelReferences(models: ReadonlyMap<string, KxmResource>, issues: KxmConfigIssue[]): void {
  for (const model of models.values()) {
    for (const [index, fallback] of valuesOf(model.value, "fallbacks").entries()) {
      selectorCandidates(fallback, models, model.logicalPath, `fallbacks[${index}]`, issues);
    }
  }
  const visiting = new Set<string>();
  const visited = new Set<string>();
  const walk = (id: string, path: string[]): void => {
    if (visiting.has(id)) {
      issues.push(issue("semantic", "model_fallback_cycle", models.get(id)?.logicalPath ?? ".kxm/models", `fallback profile cycle: ${[...path, id].join(" -> ")}`));
      return;
    }
    if (visited.has(id)) return;
    visiting.add(id);
    const model = models.get(id);
    if (model) {
      for (const fallback of valuesOf(model.value, "fallbacks")) {
        const profile = stringValue(objectValue(fallback)?.profile);
        if (profile && models.has(profile)) walk(profile, [...path, id]);
      }
    }
    visiting.delete(id);
    visited.add(id);
  };
  for (const id of models.keys()) walk(id, []);
}

function validateBundle(
  project: KxmResource,
  repositories: ReadonlyMap<string, KxmResource>,
  agents: ReadonlyMap<string, KxmResource>,
  models: ReadonlyMap<string, KxmResource>,
  workflows: ReadonlyMap<string, KxmResource>,
  environments: readonly KxmResource[],
  options: KxmConfigOptions,
  gateRegistry?: KxmResource,
  projectRoot?: string,
  roles: ReadonlyMap<string, KxmResource> = new Map(),
): KxmConfigIssue[] {
  const issues: KxmConfigIssue[] = [];
  const entries = valuesOf(project.value, "repositories").map((candidate) => objectValue(candidate)).filter((candidate): candidate is JsonObject => Boolean(candidate));
  const repositoryIds = new Set<string>();
  const foldedRepositories = new Map<string, string>();
  let controls = 0;
  for (const entry of entries) {
    const id = stringValue(entry.id);
    if (!id) continue;
    const folded = id.toLocaleLowerCase("en-US");
    const previous = foldedRepositories.get(folded);
    if (previous) issues.push(issue("path", "repository_id_collision", project.logicalPath, `${id} case-folds to existing ${previous}`));
    else foldedRepositories.set(folded, id);
    repositoryIds.add(id);
    if (entry.role === "control") controls += 1;
  }
  if (controls !== 1) issues.push(issue("semantic", "control_repository_count", project.logicalPath, `expected exactly one control repository, received ${controls}`));
  for (const repository of repositories.values()) {
    if (repository.value.projectId !== project.value.id) issues.push(issue("reference", "repository_project_mismatch", repository.logicalPath, "projectId does not match the authoritative project"));
    const id = stringValue(repository.value.repositoryId);
    if (!id || !repositoryIds.has(id)) issues.push(issue("reference", "repository_definition_unknown", repository.logicalPath, `repositoryId ${String(id)} is not bound by project.yaml`));
    if (repository.id !== id) issues.push(issue("reference", "repository_binding_identity_mismatch", repository.logicalPath, `binding ${String(repository.id)} declares repositoryId ${String(id)}`));
  }
  for (const resource of [project, ...environments]) validatePortablePaths(resource, issues);
  for (const environment of environments) validateEnvironment(environment, issues);

  const executors = new Set(options.registeredExecutors ?? BUILTIN_EXECUTORS);
  const gates = gateRegistry ? new Set(Object.keys(objectValue(gateRegistry.value.gates) ?? {})) : undefined;
  for (const [id, value] of Object.entries(objectValue(gateRegistry?.value.gates) ?? {})) {
    const definition = objectValue(value);
    const executable = Array.isArray(definition?.argv) ? definition.argv[0] : undefined;
    if (definition?.kind === "command" && typeof executable === "string" &&
        (executable === "." || executable === ".." || executable.includes("\\") || (!executable.startsWith("/") && executable.includes("/")))) {
      issues.push(issue("semantic", "gate_executable_invalid", ".kxm/gates.yaml", `${id} argv[0] must be a bare executable or absolute POSIX path`));
    }
  }
  const presets = new Set(options.registeredToolPresets ?? BUILTIN_TOOL_PRESETS);
  const harnesses = new Set(options.registeredHarnesses ?? BUILTIN_HARNESS_IDS);
  const defaultExecutor = stringValue(project.value.defaultExecutor);
  if (defaultExecutor && !executors.has(defaultExecutor)) issues.push(issue("reference", "executor_unknown", project.logicalPath, `defaultExecutor ${defaultExecutor} is not registered`));
  const defaultHarness = stringValue(project.value.defaultHarness) ?? DEFAULT_HARNESS;
  if (!harnesses.has(defaultHarness)) issues.push(issue("reference", "harness_unknown", project.logicalPath, `defaultHarness ${defaultHarness} is not registered`));

  for (const agent of agents.values()) {
    if (agent.value.model !== undefined || agent.value.harness !== undefined) {
      issues.push(issue("semantic", "retired_agent_routing_fields", agent.logicalPath, "routing resolves from role; remove model and harness"));
    }
    validateToolPolicy(agent, objectValue(agent.value.tools), "tools", issues);
    const executor = stringValue(agent.value.executor);
    if (executor && !executors.has(executor)) issues.push(issue("reference", "executor_unknown", agent.logicalPath, `executor ${executor} is not registered`));
    const preset = stringValue(objectValue(agent.value.tools)?.preset);
    if (preset && !presets.has(preset)) issues.push(issue("reference", "tool_preset_unknown", agent.logicalPath, `tool preset ${preset} is not registered`));
    const roleId = stringValue(agent.value.role);
    const role = roleId ? roleById(roles, roleId) : undefined;
    const rolePermission = stringValue(role?.value.permission);
    if (preset && role && (rolePermission === "edit" || rolePermission === "read-only")) {
      const message = toolPresetExceedsRoleMessage(
        preset,
        role.id ?? roleId ?? "role",
        rolePermission,
        stringValue(objectValue(role.value.tools)?.preset),
      );
      if (message) issues.push(issue("semantic", "tool_preset_exceeds_role", agent.logicalPath, message));
    }
    for (const repositoryId of Object.keys(objectValue(agent.value.repositories) ?? {})) {
      if (!repositoryIds.has(repositoryId)) issues.push(issue("reference", "repository_unknown", agent.logicalPath, `references unknown repository ${repositoryId}`));
    }
  }
  for (const role of roles.values()) {
    const permission = stringValue(role.value.permission);
    const rolePreset = stringValue(objectValue(role.value.tools)?.preset);
    if (permission === "read-only" && rolePreset && toolPresetProfile(rolePreset) === "edit") {
      issues.push(issue("semantic", "tool_preset_exceeds_role", role.logicalPath, `tools.preset ${rolePreset} exceeds permission read-only`));
    }
  }
  validateModelReferences(models, issues);

  const defaultWorkflow = stringValue(project.value.defaultWorkflow) ?? "default";
  if (!lookupKxmResource(workflows.values(), defaultWorkflow, "workflow")) issues.push(issue("reference", "default_workflow_unknown", project.logicalPath, `default workflow ${defaultWorkflow} does not exist`));
  for (const workflow of workflows.values()) validateWorkflow(workflow, agents, models, repositoryIds, gates, issues);

  // Developer policy is the role files under .kxm/roles/ (with the model
  // files they name). scripts/harness-run.mjs only supplies ceilings.
  if (projectRoot && existsSync(join(projectRoot, ".kxm", "roster.yaml"))) {
    issues.push(issue("semantic", "retired_roster_file", ".kxm/roster.yaml", "create the role and model files and delete roster.yaml"));
  }
  if (projectRoot && existsSync(join(projectRoot, ".kxm", "roles"))) {
    issues.push(...developerRolePolicyIssues(projectRoot));
  }
  if (projectRoot) {
    issues.push(...rosterModelIssues(projectRoot, roles));
    issues.push(...originHashIssues(projectRoot, models));
  }

  return sortIssues(issues);
}

function rosterModelIssues(projectRoot: string, roles: ReadonlyMap<string, KxmResource>): KxmConfigIssue[] {
  const issues: KxmConfigIssue[] = [];
  for (const role of roles.values()) {
    for (const entry of valuesOf(role.value, "roster")) {
      const route = stringValue(objectValue(entry)?.route);
      if (!route) continue;
      const resolved = findYamlBasename(join(projectRoot, ".kxm", "models"), route, "route") ?? route;
      const logical = `.kxm/models/${resolved}.yaml`;
      const file = join(projectRoot, ".kxm", "models", `${resolved}.yaml`);
      let harness: string | undefined;
      try {
        if (existsSync(file)) harness = stringValue(parseRestrictedYaml(readFileSync(file, "utf8"), logical).harness);
      } catch {
        harness = undefined;
      }
      if (!harness) {
        issues.push(issue("semantic", "roster_model_unreadable", role.logicalPath, `roster route ${route} does not resolve to a readable ${logical} carrying harness`));
      }
    }
  }
  return issues;
}

function originHashIssues(projectRoot: string, models: ReadonlyMap<string, KxmResource>): KxmConfigIssue[] {
  const issues: KxmConfigIssue[] = [];
  for (const model of models.values()) {
    const origin = objectValue(model.value.origin);
    const source = origin ? stringValue(origin.source) : undefined;
    const digest = origin ? stringValue(origin.sha256) : undefined;
    if (!source || !digest) continue;
    const file = resolve(projectRoot, source);
    const escaped = relative(projectRoot, file);
    if (isAbsolute(source) || escaped.startsWith("..") || isAbsolute(escaped)) {
      issues.push(issue("semantic", "origin_evidence_missing", model.logicalPath, `missing evidence bytes for ${source}`));
      continue;
    }
    let actual: string;
    try {
      actual = createHash("sha256").update(readFileSync(file, "utf8"), "utf8").digest("hex");
    } catch (error) {
      const code = (error as NodeJS.ErrnoException).code;
      if (code === "ENOENT" || code === "EACCES" || code === "EPERM" || code === "EISDIR") {
        issues.push(issue("semantic", "origin_evidence_missing", model.logicalPath, `missing evidence bytes for ${source}`));
        continue;
      }
      throw error;
    }
    if (actual !== digest) {
      issues.push(issue("semantic", "origin_hash_mismatch", model.logicalPath, "origin evidence hash does not match supplied bytes"));
    }
  }
  return issues;
}

function developerCeilings(): {
  ok: true;
  ROUTES: Record<string, { provider: string; permissions: readonly string[]; models?: readonly string[]; roles?: readonly string[]; efforts?: readonly string[] }>;
  NATIVE_PI_BRAKE_PROVIDERS: string[];
  PI_ALLOWED_PROVIDERS: string[];
  PI_NATIVE_VENDOR_PROVIDERS: Record<string, string>;
} | { ok: false; detail: string } {
  const script = join(findKxmRepoRoot(import.meta.url), "scripts", "harness-run.mjs");
  let loaded;
  try {
    loaded = spawnSync(process.execPath, ["--input-type=module", "-e", `const m = await import(${JSON.stringify(pathToFileURL(script).href)}); process.stdout.write(JSON.stringify({ROUTES:m.ROUTES,NATIVE_PI_BRAKE_PROVIDERS:m.NATIVE_PI_BRAKE_PROVIDERS,PI_ALLOWED_PROVIDERS:m.PI_ALLOWED_PROVIDERS,PI_NATIVE_VENDOR_PROVIDERS:m.PI_NATIVE_VENDOR_PROVIDERS}))`], {
      encoding: "utf8",
      timeout: 15000,
    });
  } catch (error) {
    return { ok: false, detail: error instanceof Error ? error.message : String(error) };
  }
  if (loaded.error) {
    const code = (loaded.error as NodeJS.ErrnoException).code;
    const detail = code === "ETIMEDOUT" || loaded.signal ? `timed out after 15s (${loaded.error.message})` : loaded.error.message;
    return { ok: false, detail };
  }
  if (loaded.status !== 0 || !loaded.stdout) {
    const stderr = (loaded.stderr ?? "").trim();
    return { ok: false, detail: stderr || `scripts/harness-run.mjs exited ${loaded.status}` };
  }
  try {
    return { ok: true, ...JSON.parse(loaded.stdout) as { ROUTES: Record<string, { provider: string; permissions: readonly string[]; models?: readonly string[]; roles?: readonly string[]; efforts?: readonly string[] }>; NATIVE_PI_BRAKE_PROVIDERS: string[]; PI_ALLOWED_PROVIDERS: string[]; PI_NATIVE_VENDOR_PROVIDERS: Record<string, string> } };
  } catch (error) {
    return { ok: false, detail: `ceilings JSON parse failed: ${error instanceof Error ? error.message : String(error)}` };
  }
}

function developerRolePolicyIssues(projectRoot: string): KxmConfigIssue[] {
  // Product checkouts also have .kxm/roles/ for dispatch. The ceiling check
  // is this package's developer policy, so it runs when the loaded project
  // is that package.
  if (resolve(projectRoot) !== findKxmRepoRoot(import.meta.url)) return [];
  const lintIssues = lintWorkforce(projectRoot).errors.map((entry) => issue("semantic", entry.code, entry.file, entry.message));
  const rolesDir = join(projectRoot, ".kxm", "roles");
  const modelsDir = join(projectRoot, ".kxm", "models");
  const roles: Record<string, JsonObject> = {};
  const models: Record<string, JsonObject> = {};
  const evidence: Record<string, string> = {};
  const parseIssues: KxmConfigIssue[] = [];
  const readMap = (dir: string, into: Record<string, JsonObject>, skipInventory: boolean) => {
    if (!existsSync(dir)) return;
    for (const name of readdirSync(dir)) {
      if (!name.endsWith(".yaml")) continue;
      const id = basename(name, ".yaml");
      if (skipInventory && id === "inventory") continue;
      try {
        const value = parseRestrictedYaml(readFileSync(join(dir, name), "utf8"), `${dir}/${name}`);
        into[id] = value;
      } catch (error) {
        if (error instanceof KxmConfigError) {
          parseIssues.push(...error.issues);
          continue;
        }
        throw error;
      }
    }
  };
  readMap(rolesDir, roles, false);
  readMap(modelsDir, models, true);
  for (const model of Object.values(models)) {
    const origin = objectValue(model.origin);
    const source = origin ? stringValue(origin.source) : undefined;
    if (!source || evidence[source] !== undefined) continue;
    const evidenceFile = isAbsolute(source) ? source : join(projectRoot, source);
    if (existsSync(evidenceFile)) evidence[source] = readFileSync(evidenceFile, "utf8");
  }
  const ceilings = developerCeilings();
  if (!ceilings.ok) {
    return [...parseIssues, ...lintIssues, issue("semantic", "developer_ceilings_unavailable", ".kxm/models", `developer ceilings could not be loaded: ${ceilings.detail}`)];
  }
  const result = validatePolicyDraft({ models, roles, evidence }, {
    ceilings: ceilings.ROUTES,
    nativePiBrakeProviders: ceilings.NATIVE_PI_BRAKE_PROVIDERS,
    piAllowedProviders: ceilings.PI_ALLOWED_PROVIDERS,
    piNativeVendorProviders: ceilings.PI_NATIVE_VENDOR_PROVIDERS,
    vendorAliases: { "x-ai": "xai", moonshotai: "moonshot", "google-ai": "google", qwen: "alibaba" },
  });
  if (result.ok) return [...parseIssues, ...lintIssues];
  return [...parseIssues, ...lintIssues, ...result.issues.map((entry) => issue(entry.phase, entry.code, entry.file, entry.message))];
}

export function kxmCanonicalJson(value: JsonValue): string {
  if (value === null || typeof value !== "object") return JSON.stringify(value);
  if (Array.isArray(value)) return `[${value.map((candidate) => kxmCanonicalJson(candidate)).join(",")}]`;
  return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${kxmCanonicalJson(value[key] as JsonValue)}`).join(",")}}`;
}

function canonicalize(value: JsonValue): string {
  return kxmCanonicalJson(value);
}

function bundleRevision(resources: readonly KxmResource[]): string {
  const hash = createHash("sha256");
  for (const resource of [...resources].sort((left, right) => compareCodeUnits(left.logicalPath, right.logicalPath))) {
    hash.update(resource.logicalPath, "utf8");
    hash.update("\0", "utf8");
    hash.update(canonicalize(resource.value), "utf8");
    hash.update("\0", "utf8");
  }
  return `sha256:${hash.digest("hex")}`;
}

function gitEnvironment(): NodeJS.ProcessEnv {
  return Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.toUpperCase().startsWith("GIT_")));
}

/** Ask Git for the authoritative nearest worktree root with Git overrides removed. */
export function discoverGitRoot(start = process.cwd()): string | undefined {
  let current = resolve(start);
  if (existsSync(current) && !lstatSync(current).isDirectory()) current = dirname(current);
  const result = spawnSync("git", ["-C", current, "rev-parse", "--show-toplevel"], {
    encoding: "utf8",
    env: gitEnvironment(),
    timeout: 5000,
    windowsHide: true,
  });
  if (result.status !== 0 || result.error || !result.stdout.trim()) return undefined;
  const root = resolve(result.stdout.trim());
  return existsSync(root) && lstatSync(root).isDirectory() ? root : undefined;
}

/** Find the authoritative KXM project only at the nearest Git worktree root. */
export function discoverKxmProjectRoot(start = process.cwd()): string | undefined {
  const gitRoot = discoverGitRoot(start);
  return gitRoot && existsSync(join(gitRoot, ".kxm", "project.yaml")) ? gitRoot : undefined;
}

/** The former caller-supplied gate allowlist cannot substitute for reviewed YAML. */
export function assertNoRegisteredGates(options: object): void {
  if ("registeredGates" in options) {
    throw new KxmConfigError([issue("semantic", "registered_gates_removed", ".kxm/gates.yaml", "registeredGates was removed; declare gates in .kxm/gates.yaml")]);
  }
}

/** Load and semantically validate one complete, path-derived KXM configuration bundle. */
export function loadKxmProject(projectRoot: string, options: KxmConfigOptions = {}): KxmProjectBundle {
  return loadProjectBundle(projectRoot, options);
}

/**
 * What `loadKxmProject` would refuse once `.kxm/workflows/<workflowId>.yaml` held
 * `document`, found without writing it: the same parser, schema and bundle rules,
 * with `document` standing in for any file of that name.
 */
export function kxmWorkflowWriteIssues(
  projectRoot: string,
  workflowId: string,
  document: string,
  options: KxmConfigOptions = {},
): readonly KxmConfigIssue[] {
  return candidateIssues(projectRoot, options, { kind: "workflow", id: workflowId, document });
}

/**
 * What `loadKxmProject` would refuse once `.kxm/roles/<roleId>.yaml` held `document`,
 * found without writing it. The loader reads only the writer role, so for any other
 * id this is the project's load as it stands.
 */
export function kxmRoleWriteIssues(
  projectRoot: string,
  roleId: string,
  document: string,
  options: KxmConfigOptions = {},
): readonly KxmConfigIssue[] {
  return candidateIssues(projectRoot, options, { kind: "role", id: roleId, document });
}

interface WriteCandidate {
  kind: "workflow" | "role";
  id: string;
  document: string;
}

function candidateIssues(projectRoot: string, options: KxmConfigOptions, candidate: WriteCandidate): readonly KxmConfigIssue[] {
  try {
    loadProjectBundle(projectRoot, options, candidate);
    return [];
  } catch (error) {
    if (error instanceof KxmConfigError) return error.issues;
    throw error;
  }
}

function loadProjectBundle(
  projectRoot: string,
  options: KxmConfigOptions,
  candidate?: WriteCandidate,
): KxmProjectBundle {
  const workflowCandidate = candidate?.kind === "workflow" ? candidate : undefined;
  // A case-insensitive filesystem serves `Writer.yaml` to the loader's `writer.yaml` lookup.
  const writerRole = candidate?.kind === "role" && candidate.id.toLocaleLowerCase("en-US") === "writer" ? candidate.document : undefined;
  assertNoRegisteredGates(options);
  const root = resolve(projectRoot);
  const legacyPresent = legacyConfigFilesAt(root);
  if (legacyPresent.length > 0) {
    // Nothing can unlock this any more. The migration path was deleted under the
    // single-operator decision, so a tree still holding legacy JSON fails closed here
    // rather than being loadable on proof of a past conversion.
    throw new KxmConfigError(legacyPresent.map((file) => issue(
      "semantic",
      "legacy_state_unsupported",
      file,
      "legacy JSON configuration is present and this build does not migrate it: delete these files once their YAML replacements exist in .kxm/, or initialise a fresh project",
    )));
  }
  const registry = new KxmSchemaRegistry(options.schemasDir);
  const project = readResource(registry, root, join(root, ".kxm", "project.yaml"), ".kxm/project.yaml", "project");
  const earlyIssues: KxmConfigIssue[] = [];
  validatePortablePaths(project, earlyIssues);
  const templateProvenance = readTemplateProvenance(registry, root);
  if (templateProvenance && templateProvenance.inputs && typeof templateProvenance.inputs === "object" && !Array.isArray(templateProvenance.inputs)) {
    const provenanceProjectId = stringValue((templateProvenance.inputs as JsonObject).projectId);
    if (provenanceProjectId !== stringValue(project.value.id)) {
      earlyIssues.push(issue("semantic", "template_provenance_project_mismatch", ".kxm/template-provenance.yaml", "template provenance belongs to a different project identity"));
    }
  }
  const declaredRepositoryIds = new Set(valuesOf(project.value, "repositories")
    .map((candidate) => stringValue(objectValue(candidate)?.id))
    .filter((candidate): candidate is string => candidate !== undefined));
  for (const repositoryId of Object.keys(options.repositoryBindings ?? {}).sort(compareCodeUnits)) {
    if (!resourceIdentifier(repositoryId)) {
      earlyIssues.push(issue("path", "repository_binding_id_invalid", ".kxm/project.yaml", `host-local binding identity ${repositoryId} is invalid`));
    } else if (!declaredRepositoryIds.has(repositoryId)) {
      earlyIssues.push(issue("reference", "repository_binding_unknown", ".kxm/project.yaml", `host-local binding references unknown repository ${repositoryId}`));
    }
  }
  if (earlyIssues.length > 0) throw new KxmConfigError(earlyIssues);

  const agents = listNamedResources(registry, root, join(root, ".kxm", "agents"), ".kxm/agents", "agent");
  const modelIssues: KxmConfigIssue[] = [];
  const models = listNamedResources(registry, root, join(root, ".kxm", "models"), ".kxm/models", "model", undefined, modelIssues);
  const roles = listNamedResources(registry, root, join(root, ".kxm", "roles"), ".kxm/roles", "role", writerRole === undefined ? undefined : "writer");
  if (writerRole !== undefined) {
    const logicalPath = ".kxm/roles/writer.yaml";
    roles.set("writer", { kind: "role", id: "writer", file: join(root, logicalPath), logicalPath, value: resourceValue(registry, writerRole, logicalPath, "role") });
  }
  const workflows = listNamedResources(registry, root, join(root, ".kxm", "workflows"), ".kxm/workflows", "workflow", workflowCandidate?.id);
  if (workflowCandidate) {
    const { id, document } = workflowCandidate;
    const logicalPath = `.kxm/workflows/${id}.yaml`;
    if (!resourceIdentifier(id)) fail("path", "resource_id_invalid", logicalPath, `filename-derived identity ${id} is invalid or platform-reserved`);
    const collision = [...workflows.keys()].find((candidate) => candidate.toLocaleLowerCase("en-US") === id.toLocaleLowerCase("en-US"));
    if (collision) fail("path", "resource_id_collision", logicalPath, `${id} case-folds to existing ${collision}`);
    workflows.set(id, { kind: "workflow", id, file: join(root, logicalPath), logicalPath, value: resourceValue(registry, document, logicalPath, "workflow") });
  }
  const gatePath = join(root, ".kxm", "gates.yaml");
  const gateRegistry = existsSync(gatePath) ? readResource(registry, root, gatePath, ".kxm/gates.yaml", "gate-registry") : undefined;
  const environments: KxmResource[] = [];
  const repositories = new Map<string, KxmResource>();
  const loadIssues: KxmConfigIssue[] = [];

  const projectEnvironment = join(root, ".kxm", "project", "env.yaml");
  if (existsSync(projectEnvironment)) {
    try { environments.push(readResource(registry, root, projectEnvironment, ".kxm/project/env.yaml", "environment")); }
    catch (error) { if (error instanceof KxmConfigError) loadIssues.push(...error.issues); else throw error; }
  }

  const seenBindings = new Map<string, string>();
  for (const candidate of valuesOf(project.value, "repositories")) {
    const entry = objectValue(candidate);
    const repositoryId = entry && stringValue(entry.id);
    const pathHint = entry && stringValue(entry.pathHint);
    const role = entry && stringValue(entry.role);
    const required = entry?.required !== false;
    if (!repositoryId) continue;

    const hasLocalBinding = Object.hasOwn(options.repositoryBindings ?? {}, repositoryId);
    const localBinding = options.repositoryBindings?.[repositoryId];
    if (hasLocalBinding && (!localBinding || !isAbsolute(localBinding))) {
      loadIssues.push(issue("path", "repository_binding_not_absolute", ".kxm/project.yaml", `host-local binding for ${repositoryId} must be absolute`));
      continue;
    }
    if (role === "control" && localBinding && !sameHostPath(localBinding, root)) {
      loadIssues.push(issue("path", "control_repository_binding_invalid", ".kxm/project.yaml", `control repository ${repositoryId} must bind to the authoritative project root`));
      continue;
    }
    if (role === "control" && pathHint !== undefined && pathHint !== ".") {
      loadIssues.push(issue("path", "control_repository_path_invalid", ".kxm/project.yaml", `control repository ${repositoryId} must use pathHint .`));
      continue;
    }
    if (!hasLocalBinding && role !== "control" && pathHint && portablePath(pathHint)) {
      const pathIssue = portableBindingIssue(root, pathHint, repositoryId);
      if (pathIssue) {
        loadIssues.push(pathIssue);
        continue;
      }
    }
    const binding = hasLocalBinding && localBinding
      ? resolve(localBinding)
      : role === "control"
        ? root
        : pathHint && portablePath(pathHint)
          ? resolve(root, ...pathHint.split("/"))
          : undefined;
    if (!binding) {
      if (required) loadIssues.push(issue("discovery", "repository_binding_missing", ".kxm/project.yaml", `required repository ${repositoryId} has no portable pathHint or host-local binding`));
      continue;
    }
    if (!existsSync(binding)) {
      if (required || hasLocalBinding) loadIssues.push(issue("discovery", "repository_binding_unavailable", ".kxm/project.yaml", `${hasLocalBinding ? "explicit" : "required"} repository binding ${repositoryId} is unavailable`));
      continue;
    }
    const bindingStat = lstatSync(binding);
    if (bindingStat.isSymbolicLink() || !bindingStat.isDirectory()) {
      loadIssues.push(issue("path", "repository_binding_invalid", ".kxm/project.yaml", `repository binding ${repositoryId} must be a regular directory`));
      continue;
    }
    if (role === "member") {
      const repositoryGitRoot = discoverGitRoot(binding);
      if (!repositoryGitRoot || !sameHostPath(repositoryGitRoot, binding)) {
        loadIssues.push(issue("discovery", "repository_git_root_invalid", ".kxm/project.yaml", `member repository ${repositoryId} must bind to its authoritative Git worktree root`));
        continue;
      }
    }
    // A member worktree is authoritative for its own repo.yaml/env.yaml, so the legacy
    // refusal has to cover it too — checking only the control root let a member full of
    // legacy JSON load normally. Throws before a single byte of that member is read.
    const memberLegacy = binding === root ? [] : legacyConfigFilesAt(binding);
    if (memberLegacy.length > 0) {
      throw new KxmConfigError(memberLegacy.map((file) => issue(
        "semantic",
        "legacy_state_unsupported",
        `${repositoryId}/${file}`,
        `repository ${repositoryId} holds legacy JSON configuration and this build does not migrate it: delete those files once their YAML replacements exist, or bind a clean worktree`,
      )));
    }
    const foldedBinding = canonicalHostPath(binding).toLocaleLowerCase("en-US");
    const priorBinding = seenBindings.get(foldedBinding);
    if (priorBinding && priorBinding !== repositoryId) {
      loadIssues.push(issue("path", "repository_binding_collision", ".kxm/project.yaml", `${repositoryId} and ${priorBinding} use the same repository binding`));
      continue;
    }
    seenBindings.set(foldedBinding, repositoryId);
    const repositoryFile = join(binding, ".kxm", "repo", "repo.yaml");
    let definitionValidated = false;
    if (existsSync(repositoryFile)) {
      try {
        const resource = readResource(registry, root, repositoryFile, `.kxm/repositories/${repositoryId}/repo.yaml`, "repository", repositoryId, binding);
        if (resource.value.repositoryId !== repositoryId) {
          loadIssues.push(issue("reference", "repository_binding_identity_mismatch", resource.logicalPath, `binding ${repositoryId} declares repositoryId ${String(resource.value.repositoryId)}`));
        } else {
          mapResource(resource, repositories, loadIssues);
          definitionValidated = repositories.get(repositoryId) === resource;
        }
      } catch (error) {
        if (error instanceof KxmConfigError) loadIssues.push(...error.issues);
        else throw error;
      }
    } else if (required || hasLocalBinding) {
      loadIssues.push(issue("discovery", "repository_definition_missing", `.kxm/repositories/${repositoryId}/repo.yaml`, `${hasLocalBinding ? "explicit" : "required"} repository ${repositoryId} has no repo.yaml`));
    }
    const environmentFile = join(binding, ".kxm", "repo", "env.yaml");
    if (existsSync(environmentFile)) {
      if (!definitionValidated) {
        loadIssues.push(issue("reference", "repository_environment_without_definition", `.kxm/repositories/${repositoryId}/env.yaml`, `repository ${repositoryId} environment requires a validated matching repo.yaml`));
      } else {
        try { environments.push(readResource(registry, root, environmentFile, `.kxm/repositories/${repositoryId}/env.yaml`, "environment", undefined, binding)); }
        catch (error) { if (error instanceof KxmConfigError) loadIssues.push(...error.issues); else throw error; }
      }
    }
  }
  if (loadIssues.length > 0) throw new KxmConfigError([...loadIssues, ...modelIssues]);

  const issues = [...modelIssues, ...validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root, roles)];
  if (issues.length > 0) throw new KxmConfigError(issues);
  const resources = [project, ...repositories.values(), ...agents.values(), ...models.values(), ...roles.values(), ...workflows.values(), ...environments, ...(gateRegistry ? [gateRegistry] : [])]
    .sort((left, right) => compareCodeUnits(left.logicalPath, right.logicalPath));
  return {
    projectRoot: root,
    project,
    repositories,
    agents,
    models,
    roles,
    workflows,
    environments,
    ...(gateRegistry ? { gateRegistry } : {}),
    ...(templateProvenance === undefined ? {} : { templateProvenance }),
    resources,
    configRevision: bundleRevision(resources),
  };
}



function legacyInputsAt(root: string): string[] {
  const candidates = [
    ".kxm/config/agents.json",
    ".kxm/config/gates.json",
    ".kxm/config/workflows",
    ".kxm/state/kxm.db",
  ];
  return candidates.filter((candidate) => existsSync(join(root, ...candidate.split("/"))));
}

const LEGACY_CONFIG_FILES = [".kxm/config/agents.json", ".kxm/config/gates.json"] as const;

/** Individual legacy configuration files (not directories, not runtime state) that a migration receipt must cover. */
export function legacyConfigFilesAt(root: string): string[] {
  const files: string[] = [];
  // Never traverse linked parent components for authoritative legacy bytes;
  // surface the linked path itself so readers reject it fail-closed.
  for (const component of [".kxm", ".kxm/config"] as const) {
    const absolute = join(root, ...component.split("/"));
    if (existsSync(absolute) && lstatSync(absolute).isSymbolicLink()) return [component];
  }
  for (const candidate of LEGACY_CONFIG_FILES) {
    if (existsSync(join(root, ...candidate.split("/")))) files.push(candidate);
  }
  const workflowsDir = join(root, ".kxm", "config", "workflows");
  if (existsSync(workflowsDir)) {
    const stat = lstatSync(workflowsDir);
    if (stat.isSymbolicLink()) {
      // Never traverse a link for authoritative legacy bytes; surface the
      // linked directory itself so readers reject it fail-closed.
      files.push(".kxm/config/workflows");
      return files;
    }
    if (stat.isDirectory()) {
      for (const entry of readdirSync(workflowsDir, { withFileTypes: true })
        .filter((candidate) => (candidate.isFile() || candidate.isSymbolicLink()) && candidate.name.endsWith(".json"))
        .sort((left, right) => compareCodeUnits(left.name, right.name))) {
        // Linked workflow files are listed so readers reject them fail-closed
        // (legacy_source_invalid) instead of silently omitting them.
        files.push(`.kxm/config/workflows/${entry.name}`);
      }
    }
  }
  return files;
}





function discoverLegacyRoot(start: string): { root: string; inputs: string[] } | undefined {
  let initial = resolve(start);
  if (existsSync(initial) && !lstatSync(initial).isDirectory()) initial = dirname(initial);
  // Never cross the nearest Git worktree boundary and accidentally select a
  // parent project or host-level ~/.kxm state.
  const candidate = discoverGitRoot(initial) ?? initial;
  const inputs = legacyInputsAt(candidate);
  return inputs.length > 0 ? { root: candidate, inputs } : undefined;
}

/**
 * Classify init without changing files. Applying create/repair is a separate, explicit
 * operation; a legacy-only tree is **reported and never written** — this build carries no
 * migration path, so init says what to do instead of doing it.
 */
export function planKxmInitialization(start = process.cwd(), options: KxmConfigOptions = {}): KxmInitializationPlan {
  const inspectedFrom = resolve(start);
  const projectRoot = discoverKxmProjectRoot(inspectedFrom);
  if (projectRoot) {
    try {
      const bundle = loadKxmProject(projectRoot, options);
      return { mode: "ready", inspectedFrom, projectRoot, changesRequired: false, issues: [], legacyInputs: [], configRevision: bundle.configRevision };
    } catch (error) {
      if (!(error instanceof KxmConfigError)) throw error;
      const legacyIssues = error.issues.filter((candidate) => candidate.code === "legacy_state_unsupported");
      const legacyInputs = legacyInputsAt(projectRoot);
      if (legacyInputs.length > 0 || legacyIssues.length > 0) {
        // Terminal for **any** legacy input, including one that lives in a bound member
        // worktree. Classifying a member's legacy JSON as `repair` let init take the
        // mutation lock and resume an interrupted create/repair journal, installing
        // resources into a tree the loader had just refused.
        return {
          mode: "legacy",
          inspectedFrom,
          projectRoot,
          legacyRoot: projectRoot,
          changesRequired: true,
          issues: [...error.issues, ...(legacyInputs.length > 0
            ? [issue("discovery", "legacy_state_unsupported", ".kxm", "legacy configuration is present and this build does not migrate it: initialise a fresh project directory and carry over the YAML definitions you want to keep")]
            : [])],
          legacyInputs: [...new Set([...legacyInputs, ...legacyIssues.map((candidate) => candidate.file)])],
        };
      }
      return { mode: "repair", inspectedFrom, projectRoot, changesRequired: true, issues: error.issues, legacyInputs: [] };
    }
  }
  const legacy = discoverLegacyRoot(inspectedFrom);
  if (legacy) {
    return {
      mode: "legacy",
      inspectedFrom,
      legacyRoot: legacy.root,
      changesRequired: true,
      issues: [issue("discovery", "legacy_state_unsupported", legacy.root, "legacy KXM/Mesh configuration discovered; this build does not migrate it — initialise a fresh project directory and copy the YAML definitions you want to keep")],
      legacyInputs: legacy.inputs,
    };
  }
  const gitRoot = discoverGitRoot(inspectedFrom);
  const candidateRoot = gitRoot ?? inspectedFrom;
  if (existsSync(join(candidateRoot, ".kxm"))) {
    return {
      mode: "repair",
      inspectedFrom,
      projectRoot: candidateRoot,
      changesRequired: true,
      issues: [issue("discovery", "project_definition_missing", ".kxm/project.yaml", "partial .kxm state has no authoritative project.yaml")],
      legacyInputs: [],
    };
  }
  return { mode: "create", inspectedFrom, projectRoot: candidateRoot, changesRequired: true, issues: [], legacyInputs: [] };
}