import { readFile, realpath } from "node:fs/promises"; import { homedir } from "node:os"; import { dirname, resolve } from "node:path"; import { stripFrontmatter } from "@earendil-works/pi-coding-agent"; import type { HostSkillExpansionEvidence } from "./host-contracts.ts"; export type CanonicalSkillName = "tdd" | "ak-cross-m-review"; export type CanonicalSkillSnapshot = Readonly<{ raw: string; path: string; baseDir: string; body: string; /** Plain skill text — no length/digest identity shell (ADR 0031). */ snapshotIdentity: Readonly<{ text: string }>; }>; export type CanonicalSkillEvidence = Readonly<{ name: Name; location: string; content: string; userMessage: string; }>; export type CanonicalSkillBinding = Readonly<{ name: Name; snapshot: CanonicalSkillSnapshot; /** * Accept host expansion evidence when name/location/content match the binding * and userMessage equals the captured original request (ADR 0032). */ captureExpansion( evidence: HostSkillExpansionEvidence | undefined, originalRequest: string, ): CanonicalSkillEvidence | undefined; }>; export type AnyCanonicalSkillBinding = | CanonicalSkillBinding<"tdd"> | CanonicalSkillBinding<"ak-cross-m-review">; export function captureCanonicalSkillExpansion( name: Name, snapshot: CanonicalSkillSnapshot, configuredPath: string, evidence: HostSkillExpansionEvidence | undefined, originalRequest: string, ): CanonicalSkillEvidence | undefined { const matchedPath = evidence?.location === configuredPath ? configuredPath : evidence?.location === snapshot.path ? snapshot.path : undefined; const expectedContent = matchedPath === undefined ? undefined : snapshot.body; const prefixedContent = matchedPath === undefined ? undefined : `References are relative to ${dirname(matchedPath)}.\n\n${snapshot.body}`; if ( evidence?.name !== name || matchedPath === undefined || (evidence.content !== expectedContent && evidence.content !== prefixedContent) || evidence.userMessage !== originalRequest ) { return undefined; } return Object.freeze({ ...evidence, name }); } export class CanonicalSkillUnavailableError extends Error { readonly code = "canonical-skill-unavailable" as const; constructor(readonly skillName: CanonicalSkillName, path: string, cause: unknown) { super(`Canonical ${skillName} Skill is unavailable at ${path}`, { cause }); this.name = "CanonicalSkillUnavailableError"; } } export async function loadCanonicalSkillBinding( name: CanonicalSkillName, ): Promise { const configuredPath = resolve( homedir(), `.agents/skills/${name}/SKILL.md`, ); let path: string; let raw: string; try { path = await realpath(configuredPath); raw = await readFile(path, "utf8"); } catch (error) { throw new CanonicalSkillUnavailableError(name, configuredPath, error); } const body = stripFrontmatter(raw).trim(); if (body.length === 0) { throw new Error(`Canonical ${name} Skill is empty at ${path}`); } const snapshot: CanonicalSkillSnapshot = Object.freeze({ raw, path, baseDir: dirname(path), body, snapshotIdentity: Object.freeze({ text: raw }), }); const binding: CanonicalSkillBinding = { name, snapshot, captureExpansion(evidence, originalRequest) { return captureCanonicalSkillExpansion( name, snapshot, configuredPath, evidence, originalRequest, ); }, }; return Object.freeze(binding) as AnyCanonicalSkillBinding; }