@kontextmind/kxm
Version:
KXM local-first multi-agent orchestration and operator dashboard
252 lines (222 loc) • 7.34 kB
text/typescript
import { randomUUID } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { parse, stringify } from "yaml";
export const GOAL_SCHEMA = "kxm.goal.v1" as const;
export const TASK_SCHEMA = "kxm.task.v1" as const;
export type GoalStatus = "active" | "achieved" | "abandoned";
export type TaskStatus = "todo" | "in_progress" | "blocked" | "in_review" | "done";
export type TrackerType = "github" | "jira" | "gitlab" | "none";
export interface GoalRecord {
schema: typeof GOAL_SCHEMA;
id: string;
title: string;
area: string;
status: GoalStatus;
successMetrics: string[];
targetDate?: string;
createdAt: string;
updatedAt: string;
}
export interface TaskAcceptanceCriterion {
id?: string;
description: string;
required: boolean;
}
export interface TaskTrackerSync {
tracker: TrackerType;
issueKey: string;
syncStatus: "synced" | "pending" | "failed";
lastSyncedAt?: string;
}
export interface TaskRecord {
schema: typeof TASK_SCHEMA;
id: string;
goalId?: string;
title: string;
objective: string;
acceptanceCriteria: TaskAcceptanceCriterion[];
status: TaskStatus;
assignedWorkflow?: string;
workflowRunId?: string;
trackerSync?: TaskTrackerSync;
createdAt: string;
updatedAt: string;
}
function goalsDirectory(repoRoot: string): string {
return resolve(repoRoot, ".kxm", "goals");
}
function tasksDirectory(repoRoot: string): string {
return resolve(repoRoot, ".kxm", "tasks");
}
export function goalFilePath(repoRoot: string, goalId: string): string {
return join(goalsDirectory(repoRoot), `${goalId}.yaml`);
}
export function taskFilePath(repoRoot: string, taskId: string): string {
return join(tasksDirectory(repoRoot), `${taskId}.yaml`);
}
export function createGoal(
repoRoot: string,
input: {
title: string;
area?: string | undefined;
successMetrics?: string[] | undefined;
targetDate?: string | undefined;
},
options: { dryRun?: boolean | undefined } = {},
): GoalRecord {
const id = `goal_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
const now = new Date().toISOString();
const record: GoalRecord = {
schema: GOAL_SCHEMA,
id,
title: input.title,
area: input.area ?? "software-engineering",
status: "active",
successMetrics: input.successMetrics ?? [],
...(input.targetDate ? { targetDate: input.targetDate } : {}),
createdAt: now,
updatedAt: now,
};
if (options.dryRun) return record;
mkdirSync(goalsDirectory(repoRoot), { recursive: true });
writeFileSync(goalFilePath(repoRoot, id), stringify(record).trim() + "\n", "utf8");
return record;
}
export function listGoals(repoRoot: string): GoalRecord[] {
const dir = goalsDirectory(repoRoot);
if (!existsSync(dir)) return [];
const goals: GoalRecord[] = [];
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile() && (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml"))) {
try {
const text = readFileSync(join(dir, entry.name), "utf8");
const doc = parse(text) as GoalRecord;
if (doc && doc.schema === GOAL_SCHEMA) goals.push(doc);
} catch {
// skip invalid
}
}
}
return goals.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
}
export function createTask(
repoRoot: string,
input: {
goalId?: string | undefined;
title: string;
objective: string;
acceptanceCriteria?: TaskAcceptanceCriterion[] | undefined;
assignedWorkflow?: string | undefined;
trackerSync?: {
tracker: TrackerType;
issueKey: string;
} | undefined;
},
options: { dryRun?: boolean | undefined } = {},
): TaskRecord {
const id = `task_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
const now = new Date().toISOString();
const record: TaskRecord = {
schema: TASK_SCHEMA,
id,
...(input.goalId ? { goalId: input.goalId } : {}),
title: input.title,
objective: input.objective,
acceptanceCriteria: input.acceptanceCriteria ?? [],
status: "todo",
...(input.assignedWorkflow ? { assignedWorkflow: input.assignedWorkflow } : {}),
...(input.trackerSync
? {
trackerSync: {
tracker: input.trackerSync.tracker,
issueKey: input.trackerSync.issueKey,
syncStatus: "pending",
},
}
: {}),
createdAt: now,
updatedAt: now,
};
if (options.dryRun) return record;
mkdirSync(tasksDirectory(repoRoot), { recursive: true });
writeFileSync(taskFilePath(repoRoot, id), stringify(record).trim() + "\n", "utf8");
return record;
}
export function listTasks(
repoRoot: string,
options: { goalId?: string | undefined; status?: TaskStatus | undefined } = {},
): TaskRecord[] {
const dir = tasksDirectory(repoRoot);
if (!existsSync(dir)) return [];
const tasks: TaskRecord[] = [];
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile() && (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml"))) {
try {
const text = readFileSync(join(dir, entry.name), "utf8");
const doc = parse(text) as TaskRecord;
if (doc && doc.schema === TASK_SCHEMA) {
if (options.goalId && doc.goalId !== options.goalId) continue;
if (options.status && doc.status !== options.status) continue;
tasks.push(doc);
}
} catch {
// skip invalid
}
}
}
return tasks.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
}
export function getTask(repoRoot: string, taskId: string): TaskRecord | undefined {
const dir = tasksDirectory(repoRoot);
const filePath = join(dir, `${taskId}.yaml`);
if (!existsSync(filePath)) return undefined;
try {
const text = readFileSync(filePath, "utf8");
const doc = parse(text) as TaskRecord;
return doc && doc.schema === TASK_SCHEMA ? doc : undefined;
} catch {
return undefined;
}
}
export function updateTaskStatus(
repoRoot: string,
taskId: string,
status: TaskStatus,
options: { workflowRunId?: string; dryRun?: boolean | undefined } = {},
): TaskRecord {
const task = getTask(repoRoot, taskId);
if (!task) {
throw new Error(`Task ${taskId} not found`);
}
task.status = status;
if (options.workflowRunId) task.workflowRunId = options.workflowRunId;
task.updatedAt = new Date().toISOString();
if (!options.dryRun) writeFileSync(taskFilePath(repoRoot, taskId), stringify(task).trim() + "\n", "utf8");
return task;
}
export function syncTaskWithTracker(
repoRoot: string,
taskId: string,
options: {
mockRemoteState?: string;
commentReceipt?: string;
dryRun?: boolean | undefined;
} = {},
): TaskRecord {
const task = getTask(repoRoot, taskId);
if (!task) {
throw new Error(`Task ${taskId} not found`);
}
if (!task.trackerSync) {
throw new Error(`Task ${taskId} does not have an associated issue board tracker`);
}
const now = new Date().toISOString();
task.trackerSync.syncStatus = "synced";
task.trackerSync.lastSyncedAt = now;
task.updatedAt = now;
if (!options.dryRun) writeFileSync(taskFilePath(repoRoot, taskId), stringify(task).trim() + "\n", "utf8");
return task;
}