import { readFile, readdir } from "node:fs/promises"; import { join } from "node:path"; import type { ProposedShard, UltraConfig } from "../types.js"; import { effectiveExcludedPaths } from "../security/excluded-defaults.js"; export const ROLES_DIRECTORY = join(".ultra", "roles"); const ALLOWED_KEYS = ["name", "kind", "lens", "model", "evidenceTarget", "narrowScope"] as const; const KINDS = ["scout", "warroom-member"] as const; const CONFIG_ROLES = ["root", "scout", "deep", "arbitration"] as const; export interface RoleDefinition { name: string; kind: (typeof KINDS)[number]; lens: string; model: (typeof CONFIG_ROLES)[number]; evidenceTarget: string; narrowScope: string[]; } function frontMatter(text: string): Record { const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text.trim()); if (!match) throw new Error("UltraPi role definition requires YAML front matter"); const fields: Record = {}; for (const line of match[1]!.split(/\r?\n/)) { if (!line.trim() || line.trimStart().startsWith("#")) continue; const separator = line.indexOf(":"); if (separator < 0) throw new Error(`UltraPi role definition has an unparsable line: ${line.trim()}`); fields[line.slice(0, separator).trim()] = line.slice(separator + 1).trim(); } return fields; } function list(value: string | undefined): string[] { if (!value) return []; const inner = /^\[(.*)\]$/.exec(value.trim()); if (!inner) throw new Error("UltraPi role definition narrowScope must be a bracketed list"); return inner[1]!.split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean); } export function parseRoleDefinition(text: string): RoleDefinition { const fields = frontMatter(text); for (const key of Object.keys(fields)) { if (!(ALLOWED_KEYS as readonly string[]).includes(key)) throw new Error(`UltraPi role definition field ${key} is not supported; a role may only select from what the code already grants`); } const name = fields.name ?? ""; const kind = fields.kind ?? ""; const lens = fields.lens ?? ""; const model = fields.model ?? "scout"; if (!/^[a-z][a-z0-9-]{0,63}$/.test(name)) throw new Error("UltraPi role definition name must be a short slug"); if (!(KINDS as readonly string[]).includes(kind)) throw new Error(`UltraPi role definition kind ${kind} is not supported`); if (!/^[a-z][a-z0-9-]{0,63}$/.test(lens)) throw new Error("UltraPi role definition lens must be a short slug"); if (!(CONFIG_ROLES as readonly string[]).includes(model)) throw new Error(`UltraPi role definition model must reference a configured role, not a provider model: ${model}`); if (!fields.evidenceTarget) throw new Error("UltraPi role definition requires an evidenceTarget"); return { name, kind: kind as RoleDefinition["kind"], lens, model: model as RoleDefinition["model"], evidenceTarget: fields.evidenceTarget, narrowScope: list(fields.narrowScope) }; } export function narrowedScope(granted: readonly string[], narrowTo: readonly string[]): string[] { if (!narrowTo.length) return [...granted]; const normalize = (path: string) => path.replace(/^\.\//, "").replace(/[\\/]+$/, "") || "."; const grantedPaths = granted.map(normalize); const kept = narrowTo.map(normalize).filter((candidate) => grantedPaths.some((root) => root === "." || candidate === root || candidate.startsWith(`${root}/`))); if (!kept.length) throw new Error("UltraPi role definition narrowScope must intersect the granted scope; a role cannot widen it"); return kept; } export function roleShard(role: RoleDefinition, granted: readonly string[]): ProposedShard { return { id: role.name, objective: `Inspect the declared scope through the ${role.lens} lens`, scope: narrowedScope(granted, role.narrowScope), excludedScope: effectiveExcludedPaths(), lens: role.lens, evidenceTarget: role.evidenceTarget, expectedOutput: "facts", canRunIndependently: true, canChangeFinalDecision: true, writeIntent: false, }; } export function roleModel(config: UltraConfig, role: RoleDefinition): string { return config[role.model].model; } export async function loadRoleDefinitions(cwd: string): Promise { let entries: string[]; try { entries = (await readdir(join(cwd, ROLES_DIRECTORY))).filter((entry) => entry.endsWith(".md")).sort(); } catch (error) { if (["ENOENT", "ENOTDIR"].includes((error as NodeJS.ErrnoException).code ?? "")) return []; throw error; } const roles: RoleDefinition[] = []; for (const entry of entries) roles.push(parseRoleDefinition(await readFile(join(cwd, ROLES_DIRECTORY, entry), "utf8"))); const names = new Set(); for (const role of roles) { if (names.has(role.name)) throw new Error(`UltraPi role definition ${role.name} is declared twice`); names.add(role.name); } return roles; }