From 4cd214f53c5c85fd4c39c2172f3642d394a3da9b Mon Sep 17 00:00:00 2001 From: toki Date: Sat, 4 Jul 2026 10:17:40 +0900 Subject: [PATCH] feat: add .pi/ config for iop-rule-loader --- .pi/iop-rule-loader.ts | 98 ++++++++++++++++++++++++++++++++++++++++++ .pi/settings.json | 3 ++ 2 files changed, 101 insertions(+) create mode 100644 .pi/iop-rule-loader.ts create mode 100644 .pi/settings.json 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"] +}