import { promises as fs } from "node:fs"; import * as path from "node:path"; import type { ChangedFile } from "./types.ts"; export interface ScopedInstruction { path: string; content: string; appliesTo: string[]; } function isInside(root: string, candidate: string): boolean { const relative = path.relative(root, candidate); return ( relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) ); } export async function resolveInstructions( repositoryRoot: string, files: ChangedFile[], ): Promise { const root = await fs.realpath(repositoryRoot); const matches = new Map>(); for (const file of files) { const absoluteFile = path.resolve(root, file.path); if (!isInside(root, absoluteFile)) throw new Error(`Changed path escapes repository root: ${file.path}`); let directory = path.dirname(absoluteFile); while (isInside(root, directory)) { const instructionPath = path.join(directory, "AGENTS.md"); try { const stat = await fs.stat(instructionPath); if (stat.isFile()) { const applicable = matches.get(instructionPath) ?? new Set(); applicable.add(file.path); matches.set(instructionPath, applicable); } } catch (error) { if ( !( error instanceof Error && "code" in error && error.code === "ENOENT" ) ) throw error; } if (directory === root) break; directory = path.dirname(directory); } } const paths = [...matches.keys()].sort((left, right) => { const leftDepth = path.relative(root, left).split(path.sep).length; const rightDepth = path.relative(root, right).split(path.sep).length; return leftDepth - rightDepth || left.localeCompare(right); }); return Promise.all( paths.map(async (instructionPath) => ({ path: path.relative(root, instructionPath), content: await fs.readFile(instructionPath, "utf8"), appliesTo: [...(matches.get(instructionPath) ?? [])].sort(), })), ); }