UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

435 lines (411 loc) • 17.8 kB
import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; import { basename, join } from "node:path"; import { KxmConfigError, discoverGitRoot, loadKxmProject, assertNoRegisteredGates, planKxmInitialization, type JsonObject, type KxmConfigIssue, type KxmConfigOptions, type KxmInitializationPlan, } from "./project-config.ts"; import { planKxmLocalBindings, readKxmLocalBindings, withKxmLocalBindingLock, writeKxmLocalBindings, type KxmLocalBindingLock, type KxmLocalBindingStoreOptions, } from "./bindings.ts"; import { commitKxmInitTransaction, hasKxmInitTransaction, inspectKxmInitTransaction, planKxmTemplateRepair, prepareAndApplyKxmCreate, prepareAndApplyKxmRepair, resumeKxmInitTransaction, type KxmRepairRuntimeOptions, type KxmTemplateRepairPlan, } from "./repair.ts"; import { CURRENT_KXM_TEMPLATE_VARIANT, renderKxmTemplate, type KxmTemplateVariant, } from "./template.ts"; export interface KxmInitOptions extends KxmConfigOptions { projectId?: string; projectName?: string; dryRun?: boolean; localStateRoot?: string; /** Test/future-template injection; the CLI always uses the current built-in variant. */ templateVariant?: KxmTemplateVariant; /** Fault injection used by crash-recovery tests. */ testFaultAt?: "prepared" | "first-resource" | "provenance" | "verified"; } export interface KxmInitResult { action: "planned" | "created" | "joined" | "repaired" | "resumed" | "validated"; plan: KxmInitializationPlan; projectRoot?: string; configRevision?: string; localBindingFile?: string; bindingsChanged?: boolean; repairPlan?: KxmTemplateRepairPlan; resumePending?: boolean; transactionKind?: "create" | "repair"; files: readonly string[]; } const PROJECT_ID = /^prj_[A-Za-z0-9][A-Za-z0-9_-]{5,127}$/; function initIssue(code: string, file: string, message: string): KxmConfigIssue { return { phase: "discovery", code, file, message }; } function normalizedProjectName(root: string, requested?: string): string { const name = (requested ?? basename(root) ?? "KXM Project").trim(); if (name.length < 1 || name.length > 120 || /[\u0000\r\n]/.test(name)) { throw new KxmConfigError([initIssue("project_name_invalid", ".kxm/project.yaml", "project name must contain 1-120 characters on one line")]); } return name; } function generatedProjectId(requested?: string): string { const id = requested?.trim() || `prj_${randomUUID().replaceAll("-", "")}`; if (!PROJECT_ID.test(id) || id.length > 144) { throw new KxmConfigError([initIssue("project_id_invalid", ".kxm/project.yaml", "project ID must satisfy the kxm.project.v1 opaque ID grammar and use the prj_ prefix")]); } return id; } function configOptions(options: KxmInitOptions, repositoryBindings: Readonly<Record<string, string>>): KxmConfigOptions { assertNoRegisteredGates(options); return { ...(options.schemasDir === undefined ? {} : { schemasDir: options.schemasDir }), repositoryBindings, ...(options.registeredExecutors === undefined ? {} : { registeredExecutors: options.registeredExecutors }), ...(options.registeredToolPresets === undefined ? {} : { registeredToolPresets: options.registeredToolPresets }), }; } function repairOptions( options: KxmInitOptions, repositoryBindings: Readonly<Record<string, string>>, ): KxmRepairRuntimeOptions { return { ...configOptions(options, repositoryBindings), templateVariant: options.templateVariant ?? CURRENT_KXM_TEMPLATE_VARIANT, ...(options.testFaultAt === undefined ? {} : { testFaultAt: options.testFaultAt }), }; } function bindingStoreOptions(options: KxmInitOptions): KxmLocalBindingStoreOptions { return { ...(options.localStateRoot === undefined ? {} : { stateRoot: options.localStateRoot }), ...(options.schemasDir === undefined ? {} : { schemasDir: options.schemasDir }), }; } function memberRepositoryIds(project: JsonObject): Set<string> { const repositories = Array.isArray(project.repositories) ? project.repositories : []; return new Set(repositories.flatMap((candidate) => { if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return []; const record = candidate as JsonObject; return record.role === "member" && typeof record.id === "string" ? [record.id] : []; })); } function completedPlan(projectRoot: string, options: KxmConfigOptions): KxmInitializationPlan { return planKxmInitialization(projectRoot, options); } function finalizeBindingUpdate( bundle: ReturnType<typeof loadKxmProject>, persisted: ReturnType<typeof readKxmLocalBindings>, allBindings: Readonly<Record<string, string>>, explicitBindings: Readonly<Record<string, string>>, storeOptions: KxmLocalBindingStoreOptions, mutationLock: KxmLocalBindingLock | undefined, dryRun: boolean, ): { localBindingFile?: string; bindingsChanged?: boolean } { const memberIds = validateBindingIdentities(bundle, persisted, explicitBindings); if (Object.keys(explicitBindings).length === 0) { return persisted ? { localBindingFile: persisted.file } : {}; } const memberBindings = Object.fromEntries(Object.entries(allBindings) .filter(([repositoryId]) => memberIds.has(repositoryId))); const projectId = String(bundle.project.value.id); const bindingPlan = planKxmLocalBindings(bundle.projectRoot, projectId, memberBindings, storeOptions); if (dryRun) return { localBindingFile: bindingPlan.file, bindingsChanged: bindingPlan.written }; if (bindingPlan.written && !mutationLock) throw new Error("project mutation lock is required to persist repository bindings"); const result = bindingPlan.written ? writeKxmLocalBindings(bundle.projectRoot, projectId, memberBindings, storeOptions, mutationLock) : bindingPlan; return { localBindingFile: result.file, bindingsChanged: result.written }; } function plannedRepairResult( plan: KxmInitializationPlan, projectRoot: string, repairPlan?: KxmTemplateRepairPlan, ): KxmInitResult { return { action: "planned", plan, projectRoot, ...(repairPlan === undefined ? {} : { repairPlan }), files: [], }; } function validateBindingIdentities( bundle: ReturnType<typeof loadKxmProject>, persisted: ReturnType<typeof readKxmLocalBindings>, explicit: Readonly<Record<string, string>>, ): Set<string> { const projectId = String(bundle.project.value.id); if (persisted && persisted.projectId !== projectId) { throw new KxmConfigError([initIssue("local_binding_project_id_mismatch", "Runtime-local repository bindings", "binding record belongs to a different project identity")]); } const memberIds = memberRepositoryIds(bundle.project.value); for (const repositoryId of Object.keys(persisted?.repositories ?? {})) { if (!memberIds.has(repositoryId)) { throw new KxmConfigError([initIssue("local_binding_repository_invalid", "Runtime-local repository bindings", `persisted binding ${repositoryId} is not a member repository`)]); } } for (const repositoryId of Object.keys(explicit)) { if (!memberIds.has(repositoryId)) { throw new KxmConfigError([initIssue("local_binding_repository_invalid", "Runtime-local repository bindings", `explicit binding ${repositoryId} is not a member repository`)]); } } return memberIds; } /** * Unified initialization: create, resume a pinned operation, apply a * provenance-backed conflict-free template repair, join with local member * bindings, or return a non-mutating migration/conflict plan. */ function initializeKxmProjectAtGitRoot( start: string, gitRoot: string, options: KxmInitOptions, mutationLock?: KxmLocalBindingLock, ): KxmInitResult { const storeOptions = bindingStoreOptions(options); const persisted = existsSync(join(gitRoot, ".kxm", "project.yaml")) ? readKxmLocalBindings(gitRoot, storeOptions) : undefined; const repositoryBindings = Object.fromEntries([ ...Object.entries(persisted?.repositories ?? {}), ...Object.entries(options.repositoryBindings ?? {}), ]); const loaderOptions = configOptions(options, repositoryBindings); const transactionOptions = repairOptions(options, repositoryBindings); const plan = planKxmInitialization(start, loaderOptions); if (plan.mode === "legacy") { // Classified **before** any transaction or lock work. A legacy tree used to reach the // recovery branch first, so an interrupted create/repair journal made ordinary init // acquire the project mutation lock, clean or resume the transaction, and write files // while reporting `mode: "legacy"`. Nothing is resumed, cleaned, or written here: the // pending journal stays exactly as it is until the legacy inputs are dealt with. return { action: "planned", plan, ...(plan.projectRoot ? { projectRoot: plan.projectRoot } : {}), files: [] }; } if (hasKxmInitTransaction(gitRoot)) { const operation = inspectKxmInitTransaction(gitRoot, options.schemasDir); if (!operation) { if (options.dryRun) return { action: "planned", plan, projectRoot: gitRoot, resumePending: true, files: [] }; if (!mutationLock) throw new Error("project mutation lock is required to clean an empty transaction"); resumeKxmInitTransaction(gitRoot, transactionOptions); } else if (options.dryRun) { return { action: "planned", plan, projectRoot: gitRoot, resumePending: true, transactionKind: operation.kind, files: operation.files.map((file) => file.path), }; } else { if (!mutationLock) throw new Error("project mutation lock is required to resume initialization"); if (options.projectId !== undefined && options.projectId !== operation.projectId) { throw new KxmConfigError([initIssue("resume_project_id_mismatch", "--project-id", "requested project ID differs from the pinned initialization transaction")]); } if (options.projectName !== undefined && options.projectName !== operation.projectName) { throw new KxmConfigError([initIssue("resume_project_name_mismatch", "--name", "requested project name differs from the pinned initialization transaction")]); } if (operation.kind === "repair" && Object.keys(options.repositoryBindings ?? {}).length > 0) { const currentBundle = loadKxmProject(gitRoot, loaderOptions); finalizeBindingUpdate( currentBundle, persisted, repositoryBindings, options.repositoryBindings ?? {}, storeOptions, mutationLock, false, ); } const result = resumeKxmInitTransaction(gitRoot, transactionOptions); if (!result) throw new Error("initialization transaction disappeared while holding the project lock"); const bindingResult = finalizeBindingUpdate( result.bundle, persisted, repositoryBindings, options.repositoryBindings ?? {}, storeOptions, mutationLock, false, ); commitKxmInitTransaction(gitRoot, options.schemasDir); return { action: "resumed", plan: completedPlan(gitRoot, loaderOptions), projectRoot: gitRoot, configRevision: result.bundle.configRevision, transactionKind: result.kind, ...bindingResult, files: result.files, }; } } if (plan.mode === "repair") { const projectRoot = plan.projectRoot ?? gitRoot; let templateRepair: KxmTemplateRepairPlan | undefined; try { templateRepair = planKxmTemplateRepair(projectRoot, transactionOptions); } catch (error) { if (!(error instanceof KxmConfigError)) throw error; return plannedRepairResult({ ...plan, issues: [...plan.issues, ...error.issues] }, projectRoot); } if (!templateRepair?.canApply) return plannedRepairResult(plan, projectRoot, templateRepair); if (options.dryRun) return plannedRepairResult(plan, projectRoot, templateRepair); if (Object.keys(options.repositoryBindings ?? {}).length > 0) { const blockedRepair: KxmTemplateRepairPlan = { ...templateRepair, canApply: false, issues: [...templateRepair.issues, initIssue( "repair_binding_requires_ready_project", "--repository", "a first-time binding update cannot be combined with repair of an invalid project; establish a valid project or existing binding first", )], }; return plannedRepairResult(plan, projectRoot, blockedRepair); } if (!mutationLock) throw new Error("project mutation lock is required to apply template repair"); const repaired = prepareAndApplyKxmRepair(templateRepair, transactionOptions); const bindingResult = finalizeBindingUpdate( repaired.bundle, persisted, repositoryBindings, options.repositoryBindings ?? {}, storeOptions, mutationLock, false, ); commitKxmInitTransaction(projectRoot, options.schemasDir); return { action: "repaired", plan: completedPlan(projectRoot, loaderOptions), projectRoot, configRevision: repaired.bundle.configRevision, repairPlan: templateRepair, ...bindingResult, files: repaired.files, }; } if (plan.mode === "ready") { const projectRoot = plan.projectRoot ?? gitRoot; const bundle = loadKxmProject(projectRoot, loaderOptions); const templateRepair = planKxmTemplateRepair(projectRoot, transactionOptions); if (templateRepair?.changesRequired) { if (!templateRepair.canApply || options.dryRun) return plannedRepairResult(plan, projectRoot, templateRepair); if (!mutationLock) throw new Error("project mutation lock is required to apply template repair"); const bindingResult = finalizeBindingUpdate( bundle, persisted, repositoryBindings, options.repositoryBindings ?? {}, storeOptions, mutationLock, false, ); const repaired = prepareAndApplyKxmRepair(templateRepair, transactionOptions); finalizeBindingUpdate( repaired.bundle, persisted, repositoryBindings, options.repositoryBindings ?? {}, storeOptions, mutationLock, false, ); commitKxmInitTransaction(projectRoot, options.schemasDir); return { action: "repaired", plan: completedPlan(projectRoot, loaderOptions), projectRoot, configRevision: repaired.bundle.configRevision, repairPlan: templateRepair, ...bindingResult, files: repaired.files, }; } const bindingResult = finalizeBindingUpdate( bundle, persisted, repositoryBindings, options.repositoryBindings ?? {}, storeOptions, mutationLock, options.dryRun === true, ); if (options.dryRun) { return { action: "planned", plan, projectRoot, configRevision: bundle.configRevision, ...bindingResult, files: [], }; } return { action: bindingResult.bindingsChanged ? "joined" : "validated", plan, projectRoot, configRevision: bundle.configRevision, ...bindingResult, files: [], }; } if (plan.projectRoot !== gitRoot) { throw new KxmConfigError([initIssue("git_root_required", ".", "kxm init must run inside the authoritative Git worktree")]); } const projectId = generatedProjectId(options.projectId); const projectName = normalizedProjectName(gitRoot, options.projectName); const rendered = renderKxmTemplate(projectId, projectName, options.templateVariant ?? CURRENT_KXM_TEMPLATE_VARIANT); const files = [...rendered.files.keys()]; if (options.dryRun) return { action: "planned", plan, projectRoot: gitRoot, files }; if (!mutationLock) throw new Error("project mutation lock is required to create configuration"); if (existsSync(join(gitRoot, ".kxm"))) { throw new KxmConfigError([initIssue("workspace_changed", ".kxm", "workspace changed after planning; existing .kxm state was not overwritten")]); } const created = prepareAndApplyKxmCreate(gitRoot, rendered, transactionOptions); commitKxmInitTransaction(gitRoot, options.schemasDir); return { action: "created", plan: completedPlan(gitRoot, loaderOptions), projectRoot: gitRoot, configRevision: created.bundle.configRevision, files: created.files, }; } export function initializeKxmProject(start = process.cwd(), options: KxmInitOptions = {}): KxmInitResult { assertNoRegisteredGates(options); const gitRoot = discoverGitRoot(start); if (!gitRoot) { throw new KxmConfigError([initIssue("git_root_required", ".", "kxm init must run inside the authoritative Git worktree")]); } if (options.projectId !== undefined) generatedProjectId(options.projectId); if (options.projectName !== undefined) normalizedProjectName(gitRoot, options.projectName); if (options.dryRun) return initializeKxmProjectAtGitRoot(start, gitRoot, options); const preflight = initializeKxmProjectAtGitRoot(start, gitRoot, { ...options, dryRun: true }); const mutationRequired = preflight.resumePending === true || preflight.plan.mode === "create" || (preflight.repairPlan?.canApply === true && !(preflight.plan.mode === "repair" && Object.keys(options.repositoryBindings ?? {}).length > 0)) || (Object.keys(options.repositoryBindings ?? {}).length > 0 && preflight.plan.mode === "ready"); if (!mutationRequired) return initializeKxmProjectAtGitRoot(start, gitRoot, options); const storeOptions = bindingStoreOptions(options); return withKxmLocalBindingLock(gitRoot, storeOptions, (lock) => initializeKxmProjectAtGitRoot(start, gitRoot, options, lock)); }