From 7c66aae0b8dda143b25b4c58a72b36caed1212c2 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 21 May 2026 16:03:12 +0900 Subject: [PATCH] refactor(rules-engine): centralize rule constants and AGENTS.md walk-up Promote the project-rule constants (PROJECT_MARKERS, PROJECT_RULE_SUBDIRS, PROJECT_RULE_FILES, OPENCODE_USER_RULE_DIRS, USER_RULE_DIR, GITHUB_INSTRUCTIONS_PATTERN, RULE_EXTENSIONS, AGENTS_FILENAME, etc.) and the findAgentsMdUp walk-up helper out of the agents-md-core and rules-injector adapters and into @oh-my-opencode/rules-engine, the single owner of rule discovery. - packages/agents-md-core/ drops the findAgentsMdUp/AgentsMdDiscoveryInput wrappers (now sourced directly from rules-engine) and its constants module re-exports AGENTS_FILENAME from rules-engine instead of duplicating it. - src/hooks/directory-agents-injector/finder.ts pulls findAgentsMdUp from rules-engine directly while still re-exporting resolveFilePath from agents-md-core. - src/hooks/rules-injector/constants.ts becomes a pure re-export shim over the rules-engine constants. Add packages/agents-md-core/src/injector.test.ts to lock the root-skipping AGENTS.md injection order so future changes to findAgentsMdUp cannot silently regress the [Directory Context: ...] block format the injector emits. Tests: bun test packages/agents-md-core packages/rules-engine src/hooks/directory-agents-injector src/hooks/rules-injector --- packages/agents-md-core/src/constants.ts | 2 +- packages/agents-md-core/src/finder.ts | 13 --- packages/agents-md-core/src/index.ts | 3 +- packages/agents-md-core/src/injector.test.ts | 82 +++++++++++++++++++ packages/agents-md-core/src/injector.ts | 13 ++- packages/agents-md-core/src/types.ts | 8 -- packages/rules-engine/src/index.ts | 13 +++ src/hooks/directory-agents-injector/finder.ts | 6 +- src/hooks/rules-injector/constants.ts | 36 ++------ 9 files changed, 117 insertions(+), 59 deletions(-) create mode 100644 packages/agents-md-core/src/injector.test.ts diff --git a/packages/agents-md-core/src/constants.ts b/packages/agents-md-core/src/constants.ts index 5744adc59..8e5a77346 100644 --- a/packages/agents-md-core/src/constants.ts +++ b/packages/agents-md-core/src/constants.ts @@ -1,4 +1,4 @@ -export const AGENTS_FILENAME = "AGENTS.md"; +export { AGENTS_FILENAME } from "../../rules-engine/src/constants"; export const TRUNCATION_NOTICE_PREFIX = "\n\n[Note: Content was truncated to save context window space. For full context, please read the file directly: "; diff --git a/packages/agents-md-core/src/finder.ts b/packages/agents-md-core/src/finder.ts index b7cde4795..894e7b2bc 100644 --- a/packages/agents-md-core/src/finder.ts +++ b/packages/agents-md-core/src/finder.ts @@ -1,20 +1,7 @@ -import { - findAgentsMdUp as findAgentsMdUpCore, -} from "@oh-my-opencode/rules-engine"; import { isAbsolute, resolve } from "node:path"; -import type { AgentsMdDiscoveryInput } from "./types"; - export function resolveFilePath(rootDirectory: string, path: string): string | null { if (!path) return null; if (isAbsolute(path)) return path; return resolve(rootDirectory, path); } - -export async function findAgentsMdUp(input: AgentsMdDiscoveryInput): Promise { - return findAgentsMdUpCore({ - startDir: input.startDir, - rootDir: input.rootDir, - cache: input.cache, - }); -} diff --git a/packages/agents-md-core/src/index.ts b/packages/agents-md-core/src/index.ts index 7be9f1ab3..a4e0ce46c 100644 --- a/packages/agents-md-core/src/index.ts +++ b/packages/agents-md-core/src/index.ts @@ -1,11 +1,10 @@ export { AGENTS_FILENAME } from "./constants"; -export { findAgentsMdUp, resolveFilePath } from "./finder"; +export { resolveFilePath } from "./finder"; export { formatAgentsMdContextBlock } from "./formatter"; export { getSessionCache } from "./injection-cache"; export { processFilePathForAgentsInjection } from "./injector"; export type { AgentsMdContextOutput, - AgentsMdDiscoveryInput, AgentsMdInjectedPathsStorage, AgentsMdTruncator, TruncationResult, diff --git a/packages/agents-md-core/src/injector.test.ts b/packages/agents-md-core/src/injector.test.ts new file mode 100644 index 000000000..561301b66 --- /dev/null +++ b/packages/agents-md-core/src/injector.test.ts @@ -0,0 +1,82 @@ +import { randomUUID } from "node:crypto"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "bun:test"; + +import { processFilePathForAgentsInjection } from "./injector"; + +describe("processFilePathForAgentsInjection", () => { + const sessionCaches = new Map>(); + const storageBackfill = new Map>(); + const storage = { + loadInjectedPaths: (sessionID: string): Set => + storageBackfill.get(sessionID) ?? new Set(), + saveInjectedPaths: (sessionID: string, paths: Set): void => { + storageBackfill.set(sessionID, paths); + }, + }; + + const truncator = { + truncate: async (_sessionID: string, content: string) => ({ + result: content, + truncated: false, + }), + }; + + let rootDirectory = ""; + + afterEach(() => { + if (rootDirectory) { + rmSync(rootDirectory, { recursive: true, force: true }); + } + rootDirectory = ""; + sessionCaches.clear(); + storageBackfill.clear(); + }); + + it("injects AGENTS.md chain in root-skipping order with unchanged context format", async () => { + // given + rootDirectory = join(tmpdir(), `agents-md-core-injector-${randomUUID()}`); + const srcDirectory = join(rootDirectory, "src"); + const nestedDirectory = join(srcDirectory, "components"); + mkdirSync(nestedDirectory, { recursive: true }); + + const rootAgents = "# root"; + const srcAgents = "# src"; + const nestedAgents = "# nested"; + + writeFileSync(join(rootDirectory, "AGENTS.md"), rootAgents); + writeFileSync(join(srcDirectory, "AGENTS.md"), srcAgents); + writeFileSync(join(nestedDirectory, "AGENTS.md"), nestedAgents); + writeFileSync(join(nestedDirectory, "button.ts"), "export const button = true;\n"); + + const output = { + title: "read result", + output: "base output", + metadata: {}, + }; + + const srcAgentsPath = join(srcDirectory, "AGENTS.md"); + const nestedAgentsPath = join(nestedDirectory, "AGENTS.md"); + const expectedOutput = + "base output" + + `\n\n[Directory Context: ${srcAgentsPath}]\n${srcAgents}` + + `\n\n[Directory Context: ${nestedAgentsPath}]\n${nestedAgents}`; + + // when + await processFilePathForAgentsInjection({ + rootDirectory, + truncator, + sessionCaches, + storage, + filePath: join(nestedDirectory, "button.ts"), + sessionID: "session-regression", + output, + }); + + // then + expect(output.output).toBe(expectedOutput); + expect(output.output).not.toContain(rootAgents); + }); +}); diff --git a/packages/agents-md-core/src/injector.ts b/packages/agents-md-core/src/injector.ts index c42e52a23..161b84ccb 100644 --- a/packages/agents-md-core/src/injector.ts +++ b/packages/agents-md-core/src/injector.ts @@ -1,8 +1,12 @@ -import type { AgentsMdCache } from "@oh-my-opencode/rules-engine"; +import { + findAgentsMdUp, + type AgentsMdCache, + type FindAgentsMdUpInput, +} from "@oh-my-opencode/rules-engine"; import { promises as fsPromises } from "node:fs"; import { dirname } from "node:path"; -import { findAgentsMdUp, resolveFilePath } from "./finder"; +import { resolveFilePath } from "./finder"; import { formatAgentsMdContextBlock } from "./formatter"; import { getSessionCache } from "./injection-cache"; import type { @@ -33,11 +37,12 @@ export async function processFilePathForAgentsInjection(input: { storage: input.storage, }); - const agentsPaths = await findAgentsMdUp({ + const agentsMdDiscoveryInput: FindAgentsMdUpInput = { startDir: dir, rootDir: input.rootDirectory, cache: input.agentsMdCache, - }); + }; + const agentsPaths = await findAgentsMdUp(agentsMdDiscoveryInput); let dirty = false; for (const agentsPath of agentsPaths) { diff --git a/packages/agents-md-core/src/types.ts b/packages/agents-md-core/src/types.ts index 13aeac427..2dfb38def 100644 --- a/packages/agents-md-core/src/types.ts +++ b/packages/agents-md-core/src/types.ts @@ -1,5 +1,3 @@ -import type { AgentsMdCache } from "@oh-my-opencode/rules-engine"; - export interface TruncationResult { readonly result: string; readonly truncated: boolean; @@ -19,9 +17,3 @@ export interface AgentsMdInjectedPathsStorage { loadInjectedPaths(sessionID: string): Set; saveInjectedPaths(sessionID: string, paths: Set): void; } - -export interface AgentsMdDiscoveryInput { - readonly startDir: string; - readonly rootDir: string; - readonly cache?: AgentsMdCache; -} diff --git a/packages/rules-engine/src/index.ts b/packages/rules-engine/src/index.ts index b9f11f9f9..5512a833c 100644 --- a/packages/rules-engine/src/index.ts +++ b/packages/rules-engine/src/index.ts @@ -6,6 +6,19 @@ export { shouldApplyRule, createContentHash, isDuplicateByContentHash, isDuplica export { findProjectRoot, clearProjectRootCache } from "./project-root"; export { calculateDistance } from "./distance"; export { findRuleFilesRecursive, safeRealpathSync } from "./scanner"; +export { + AGENTS_FILENAME, + EXCLUDED_DIRS, + GITHUB_INSTRUCTIONS_PATTERN, + GLOBAL_DISTANCE, + OPENCODE_USER_RULE_DIRS, + PROJECT_MARKERS, + PROJECT_RULE_FILES, + PROJECT_RULE_SUBDIRS, + RULE_EXTENSIONS, + SOURCE_PRIORITY, + USER_RULE_DIR, +} from "./constants"; export type { AgentsMdCache, DirectoryScanEntry, diff --git a/src/hooks/directory-agents-injector/finder.ts b/src/hooks/directory-agents-injector/finder.ts index d75cbdd8d..d94ab3e76 100644 --- a/src/hooks/directory-agents-injector/finder.ts +++ b/src/hooks/directory-agents-injector/finder.ts @@ -1,4 +1,2 @@ -export { - findAgentsMdUp, - resolveFilePath, -} from "@oh-my-opencode/agents-md-core"; +export { resolveFilePath } from "@oh-my-opencode/agents-md-core"; +export { findAgentsMdUp } from "@oh-my-opencode/rules-engine"; diff --git a/src/hooks/rules-injector/constants.ts b/src/hooks/rules-injector/constants.ts index 5d62f49e7..6e52e6caa 100644 --- a/src/hooks/rules-injector/constants.ts +++ b/src/hooks/rules-injector/constants.ts @@ -2,30 +2,12 @@ import { join } from "node:path"; import { OPENCODE_STORAGE } from "../../shared"; export const RULES_INJECTOR_STORAGE = join(OPENCODE_STORAGE, "rules-injector"); -export const PROJECT_MARKERS = [ - ".git", - "pyproject.toml", - "package.json", - "Cargo.toml", - "go.mod", - ".venv", -]; - -export const PROJECT_RULE_SUBDIRS: [string, string][] = [ - [".github", "instructions"], - [".cursor", "rules"], - [".claude", "rules"], - [".omo", "rules"], -]; - -export const PROJECT_RULE_FILES: string[] = [ - ".github/copilot-instructions.md", -]; - -export const GITHUB_INSTRUCTIONS_PATTERN = /\.instructions\.md$/; - -export const USER_RULE_DIR = ".claude/rules"; - -export const OPENCODE_USER_RULE_DIRS = [".omo/rules", ".opencode/rules"]; - -export const RULE_EXTENSIONS = [".md", ".mdc"]; +export { + GITHUB_INSTRUCTIONS_PATTERN, + OPENCODE_USER_RULE_DIRS, + PROJECT_MARKERS, + PROJECT_RULE_FILES, + PROJECT_RULE_SUBDIRS, + RULE_EXTENSIONS, + USER_RULE_DIR, +} from "@oh-my-opencode/rules-engine";