diff --git a/.pi/iop-rule-loader.ts b/.pi/iop-rule-loader.ts new file mode 100644 index 0000000..a26b0cb --- /dev/null +++ b/.pi/iop-rule-loader.ts @@ -0,0 +1,98 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +type RuleSpec = { + relativePath: string; +}; + +type LoadedRule = { + relativePath: string; + content: string; +}; + +const entryRules: RuleSpec[] = [ + { relativePath: "agent-ops/rules/common/rules.md" }, +]; + +function findProjectRoot(startDir: string): string { + let current = path.resolve(startDir); + + for (;;) { + const marker = path.join(current, "agent-ops", "rules", "common", "rules.md"); + if (fs.existsSync(marker)) { + return current; + } + + const parent = path.dirname(current); + if (parent === current) { + return path.resolve(startDir); + } + current = parent; + } +} + +function loadEntryRules(projectRoot: string): LoadedRule[] { + return entryRules.flatMap((rule) => { + const fullPath = path.join(projectRoot, rule.relativePath); + if (!fs.existsSync(fullPath)) { + return []; + } + + return [ + { + relativePath: rule.relativePath, + content: fs.readFileSync(fullPath, "utf8").trimEnd(), + }, + ]; + }); +} + +function buildRulesPrompt(projectRoot: string, rules: LoadedRule[]): string { + const blocks = rules + .map( + (rule) => ` +${rule.content} +`, + ) + .join("\n\n"); + + return `## Agent-Ops Common Rules + +The following common rule entry file was loaded automatically from ${projectRoot}. Treat this common rule as already read and follow it together with AGENTS.md. When the common rule names additional session, domain, roadmap, test, or skill rules, load those files on demand when their trigger conditions apply. + +${blocks}`; +} + +export default function iopRuleLoader(pi: ExtensionAPI) { + let projectRoot = ""; + let loadedRules: LoadedRule[] = []; + + pi.on("session_start", async (_event, ctx) => { + projectRoot = findProjectRoot(ctx.cwd); + loadedRules = loadEntryRules(projectRoot); + + if (loadedRules.length > 0) { + const names = loadedRules.map((rule) => rule.relativePath).join(", "); + ctx.ui.notify(`Loaded Agent-Ops common rules: ${names}`, "info"); + } + }); + + pi.on("before_agent_start", async (event, ctx) => { + const root = projectRoot || findProjectRoot(ctx.cwd); + const rules = loadEntryRules(root); + + if (rules.length === 0) { + return; + } + + loadedRules = rules; + projectRoot = root; + + return { + systemPrompt: `${event.systemPrompt} + +${buildRulesPrompt(projectRoot, loadedRules)}`, + }; + }); +} diff --git a/.pi/settings.json b/.pi/settings.json new file mode 100644 index 0000000..e958630 --- /dev/null +++ b/.pi/settings.json @@ -0,0 +1,3 @@ +{ + "extensions": ["iop-rule-loader.ts"] +}