import crypto from "node:crypto"; import type { AppConfig, RoleConfig } from "../config.js"; import { SkillLoader, type LoadedSkill } from "./skill-loader.js"; export interface ResolvedRole extends RoleConfig { loadedSkills: LoadedSkill[]; fingerprint: string; bootstrap: string; } export class RoleRegistry { private readonly roles = new Map(); constructor(private readonly config: AppConfig) { const loader = new SkillLoader(config.skills); for (const role of config.roles) { const loadedSkills = role.skills.map((id) => loader.load(id)); const fingerprint = crypto.createHash("sha256").update(JSON.stringify({ id: role.id, backend: role.backend, workspace: role.workspace, persona: role.persona, policy: role.policy, skills: loadedSkills.map(({ id, file, hash }) => ({ id, file, hash })) })).digest("hex"); this.roles.set(role.id, { ...role, loadedSkills, fingerprint, bootstrap: buildBootstrap(role, loadedSkills) }); } } get(id?: string): ResolvedRole { const roleId = id || this.config.defaultRole; const role = this.roles.get(roleId); if (!role) throw new Error(`Unknown role: ${roleId}`); return role; } has(id: string): boolean { return this.roles.has(id); } list(): string[] { return [...this.roles.keys()].sort(); } defaultId(): string { return this.config.defaultRole; } } function buildBootstrap(role: RoleConfig, skills: LoadedSkill[]): string { return [ "Initialize this ACP session with the following role. Treat these instructions as persistent context. Reply only with READY.", `Role: ${role.id}`, `Workspace: ${role.workspace}`, role.persona ? `Persona:\n${role.persona}` : "Persona: general coding assistant", `Permission policy enforced by the ACP client: ${JSON.stringify(role.policy)}`, ...skills.map((skill) => `Skill ${skill.id} (${skill.file}):\n${skill.content}`) ].join("\n\n"); }