Skip to content

feat: グローバル + プロジェクト階層ルールファイル読み込み#72

Merged
terisuke merged 1 commit intodevfrom
feat/rules-loading
Apr 5, 2026
Merged

feat: グローバル + プロジェクト階層ルールファイル読み込み#72
terisuke merged 1 commit intodevfrom
feat/rules-loading

Conversation

@terisuke
Copy link
Copy Markdown

@terisuke terisuke commented Apr 5, 2026

What

~/.opencode/rules/.md と {project}/.opencode/rules/.md からルールファイルを自動読み込み。

Why

Claude Code は ~/.claude/rules/ からルールを読み込むが、OpenCode には対応機能がなかった。

Closes #48

Changes

  • session/instruction.ts: systemPaths() に rules ディレクトリスキャン追加
  • 同名ファイルはプロジェクト側が優先
  • OPENCODE_DISABLE_PROJECT_CONFIG フラグ対応(trust boundary 保護)
  • test/session/instruction-rules.test.ts: 6テスト追加

Review

  • code-reviewer Agent: completed
  • Codex CLI: HIGH fixed — OPENCODE_DISABLE_PROJECT_CONFIG チェック追加済み

Test plan

  • bun test test/session/instruction-rules.test.ts — 6 pass
  • bun test test/session/instruction.test.ts — 9 pass
  • typecheck 13/13 pass

Adds support for loading *.md rule files from:
- ~/.opencode/rules/ (global, all projects)
- {project}/.opencode/rules/ (project-specific, overrides global by filename)

Rules are injected into the system prompt alongside AGENTS.md content.

Closes #48

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 5, 2026 10:47
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 5, 2026

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@terisuke terisuke merged commit 085967b into dev Apr 5, 2026
4 of 8 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds automatic loading of Markdown “rules” files from both the user’s global rules directory and the project’s rules directory, so they can be incorporated into the session system instructions alongside existing instruction sources.

Changes:

  • Extend Instruction.systemPaths() to include ~/.opencode/rules/*.md and {project}/.opencode/rules/*.md, with project files overriding global files by filename.
  • Respect OPENCODE_DISABLE_PROJECT_CONFIG so project rules don’t cross the trust boundary when project config is disabled.
  • Add a dedicated test suite validating rules discovery, overriding behavior, and file filtering.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
packages/opencode/src/session/instruction.ts Adds global + project rules directory scanning and override logic to Instruction.systemPaths().
packages/opencode/test/session/instruction-rules.test.ts Adds tests for rules discovery, override precedence, trust-boundary behavior, and .md-only filtering.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +152 to +161
const projectRuleFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG
? []
: yield* fs
.glob("*.md", {
cwd: path.join(Instance.directory, ".opencode", "rules"),
absolute: true,
include: "file",
})
.pipe(Effect.catch(() => Effect.succeed([] as string[])))

Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Project rules are globbed from path.join(Instance.directory, ".opencode", "rules"), which only works when the instance directory is the project root. If the user runs OpenCode from a subdirectory, rules placed at {worktree}/.opencode/rules/*.md won’t be discovered. Consider using Instance.worktree (git root) for the project rules directory, or fs.findUp(".opencode/rules", Instance.directory, Instance.worktree) to locate the rules dir within the project boundary.

Suggested change
const projectRuleFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG
? []
: yield* fs
.glob("*.md", {
cwd: path.join(Instance.directory, ".opencode", "rules"),
absolute: true,
include: "file",
})
.pipe(Effect.catch(() => Effect.succeed([] as string[])))
const projectRulesDir = Flag.OPENCODE_DISABLE_PROJECT_CONFIG
? []
: yield* fs.findUp(".opencode/rules", Instance.directory, Instance.worktree)
const projectRuleFiles =
Flag.OPENCODE_DISABLE_PROJECT_CONFIG || projectRulesDir.length === 0
? []
: yield* fs
.glob("*.md", {
cwd: path.resolve(projectRulesDir[0]),
absolute: true,
include: "file",
})
.pipe(Effect.catch(() => Effect.succeed([] as string[])))

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +57
test("loads *.md files from project rules directory", async () => {
await using homeTmp = await tmpdir()
await using projectTmp = await tmpdir({
init: async (dir) => {
await Bun.write(path.join(dir, ".opencode", "rules", "local.md"), "# Local Rules")
},
})

const originalHome = process.env.OPENCODE_TEST_HOME
process.env.OPENCODE_TEST_HOME = homeTmp.path

try {
await Instance.provide({
directory: projectTmp.path,
fn: async () => {
const paths = await Instruction.systemPaths()
expect(paths.has(path.join(projectTmp.path, ".opencode", "rules", "local.md"))).toBe(true)
},
})
} finally {
process.env.OPENCODE_TEST_HOME = originalHome
}
})
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite doesn’t currently cover the common case where OpenCode is invoked from a subdirectory: Instance.directory is nested but rules live in {worktree}/.opencode/rules/*.md. Add a test that provides an instance rooted in a nested directory and verifies project rules in the worktree root are still loaded (and still override globals by filename).

Copilot uses AI. Check for mistakes.
import path from "path"
import { Instruction } from "../../src/session/instruction"
import { Instance } from "../../src/project/instance"
import { Global } from "../../src/global"
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global is imported but not used in this test file. Removing the unused import will keep the test code tidy and avoid unused-import lint failures if/when stricter TS options or a linter are enabled.

Suggested change
import { Global } from "../../src/global"

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rules: グローバル + プロジェクト階層ルールファイル読み込み

2 participants