You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A user who works across editors keeps one shared rules directory (for example .agents/rules, where Antigravity IDE injects its rules) and wants opencode-rules to load the same files. Today discovery is hard-coded to the global rules directory and the project's .opencode/rules, so the only workaround is duplicating or symlinking rule files into a location the plugin already scans. Issue #79 asks for a setting that adds custom paths to rule discovery; the owner deferred it to v2, where plugin options can live inline in opencode.jsonc without a standalone config file.
Solution
Support an additionalRuleDirectories plugin option. Users list extra directories (absolute, ~/-relative, or relative to the project root) in the options object of the plugin entry, and every .md/.mdc file found recursively in those directories is discovered, matched, and delivered exactly like a built-in rule. The TUI sidebar lists rules from extra directories too: rules under the session's project root appear under Project, everything else under Global.
The same option belongs on the TUI plugin entry in cli.json so the sidebar sees the same rule set.
User Stories
As a user of Antigravity IDE and opencode, I want to point opencode-rules at .agents/rules, so that the same rule files drive both tools without duplication.
As a user with shared rules across repositories, I want to add an absolute directory path, so that one global location feeds every project.
As a user with a home-directory rules folder, I want to write ~/my-rules, so that I do not have to hard-code my home path.
As a user with a project-local extra directory, I want to write a relative path, so that the setting works in every checkout without editing machine-specific paths.
As a user with several extra rule sources, I want to list multiple directories, so that all of them load in one session.
As a user who lists a directory that does not exist yet, I want discovery to ignore it quietly, so that work-in-progress configuration does not break startup.
As a user with subfolders under an extra directory, I want recursive discovery, so that I can organize rules the same way as built-in directories.
As a user who accidentally repeats a directory or points at an already-scanned one, I want rules deduplicated, so that no rule is injected twice.
As a user who mistypes the option value (wrong type, empty string), I want the plugin to fall back to built-in discovery and log the problem in debug mode, so that a bad option never breaks my session.
As a user who sets no option at all, I want discovery behavior unchanged, so that upgrading changes nothing about my current setup.
As a TUI user, I want rules from extra directories shown in the sidebar, so that I can see what the agent receives.
As a TUI user, I want extra-directory rules under my project shown in the Project section and shared ones in the Global section, so that the grouping reflects where the rule lives.
As a TUI user, I want the same option available in cli.json, so that the sidebar and the server agree on the rule set.
As a rule author, I want rules from extra directories to match on the same metadata conditions (globs, keywords, fileContains, model, agent, branch, os, ci, and the rest), so that I can scope them like any other rule.
As a rule author, I want hook rules from extra directories to fire like built-in ones, so that hook-based guidance is not limited by location.
As a user resuming or compacting sessions, I want delivery deduplication to keep working for extra-directory rules, so that no rule text is repeated after compaction.
As a user moving a rule between .opencode/rules and an extra directory, I want the rule's delivered identity to stay stable (derived from its absolute path), so that history-deduplication behavior does not change unexpectedly.
As a debugging user, I want extra-directory discovery and skipped invalid option entries logged when OPENCODE_RULES_DEBUG is set, so that I can tell why a rule did not load.
As a maintainer, I want option parsing and path resolution in one config module shared by the server and TUI entries, so that the two surfaces cannot drift.
As a reader of the README, I want a documented example using .agents/rules and a note that the TUI needs the same option in cli.json, so that setup is copy-pasteable.
Implementation Decisions
New config seam. A new module in a new config domain (/plugin-options) owns plugin-option handling for both plugin entries. It is a type-level and behavior-level contract:
parsePluginOptions(raw: Readonly<Record<string, unknown>> | undefined): PluginOptions — pure validation/normalization. Result: { additionalRuleDirectories: string[] }. Accepts a single string or an array; trims entries; drops empty/non-string values with a debug log; never throws.
resolveAdditionalRuleDirectories(entries: readonly string[], projectDir: string | null): string[] — expands a leading ~ to the home directory (respecting HOME, falling back to os.homedir() as getGlobalRulesDir already does), resolves relative entries against projectDir via path.resolve, skips relative entries when projectDir is null (debug log), and deduplicates resolved paths.
Option shape.additionalRuleDirectories accepts a string or an array of strings. It is read from the options object of the plugin entry: context.options on the server, ctx.options in the TUI. A bare-string plugin entry (no options) means no extra directories. The option never replaces built-in discovery.
Discovery signature.discoverRuleFiles(projectDir?: string, options?: RuleDiscoveryOptions) where RuleDiscoveryOptions carries additionalDirectories?: readonly string[] (already resolved, absolute). Scan order: global directory, project .opencode/rules, then each additional directory in user order. Additional directories use the existing recursive scanner: .md/.mdc only, dot-prefixed entries skipped, missing directories ignored (ENOENT swallowed), other read errors logged.
Relative paths. A discovered rule's relativePath is relative to the root of the directory it came from, so nested organization is preserved in delivery and the sidebar. Delivery identity remains the absolute filePath.
Dedupe. Directory resolution dedupes resolved directory strings; discovery additionally dedupes discovered files by absolute filePath across all sources, first occurrence winning (a user pointing at .opencode/rules must not double-inject).
Runtime threading. The runtime factory gains an optional list of resolved additional directories; when it performs default discovery it forwards them. An explicit ruleFiles list (test seam) bypasses discovery as today. The orchestrator and delivery engine are untouched.
Server entry. In setup, parse context.options, resolve against context.location.directory, and pass the resolved list into runtime creation. Malformed options degrade to built-in discovery only.
TUI entry and sidebar. The TUI entry parses ctx.options once at setup and passes the parsed entries to the sidebar. Because the relevant project directory is per session, the sidebar resolves entries against the session project root when loading rules; with no project root, relative entries are skipped while absolute and ~/ entries still load. The sidebar load helper accepts resolved additional directories and forwards them to discovery.
Sidebar classification.classifyRuleScope becomes project-root containment: a discovered rule whose path lies under the session project root is Project, otherwise Global (using path.join(projectDir, '')/separator-boundary comparison, not a raw startsWith). This replaces the current .opencode/rules-prefix check and automatically classifies .agents/rules under the project as Project.
Docs. README Configuration gains the option with an .agents/rules example for opencode.jsonc plus a cli.json parity note; the Project Structure section lists the new module; AGENTS.md's domain list includes src/config/.
Rule semantics unchanged. Extra-directory rules use the same metadata parsing, matching, hook, delivery, persistence, and dedupe paths as built-in rules. Discovery stays initialization-time/snapshot-time (no watching); the sidebar continues to re-run discovery on refresh.
Testing Decisions
Good tests assert external behavior: given a config/options input and a directory tree, which rules are discovered, how they are classified, and what text reaches the model. No assertions on internal call ordering or private helpers.
Config module tests (new, colocated). Parsing: single string, array, trimming, empty array, invalid types/entries dropped, undefined/empty object produce no directories. Resolution: ~/ expansion against a fixture home, relative entries against a project dir, absolute passthrough, null project dir skips only relative entries, dedupe of repeated/overlapping entries. Prior art: saveEnv/restoreEnv fixtures with HOME/XDG_CONFIG_HOME used by the discovery suites.
Discovery tests (extend existing suites). Additional directories scanned after the project directory; recursive subdirectories; missing directory ignored; dot-entries skipped; relativePath relative to the additional root; duplicate file across sources discovered once. Prior art: the discoverRuleFiles suites using setupTestDirs/getTestDirs and clearRuleCache.
Runtime/delivery integration. With the runtime constructed for a project whose options include .agents/rules, a conditional rule from that directory matches its condition and its content is delivered through the existing synthetic admission channel; an unconditional rule is delivered durably. Prior art: src/index.integration.test.ts mock-context flow and wireRuntime.
TUI data tests.classifyRuleScope containment cases (project root, subdirectory, separator boundary, outside, null project dir) replace/extend the existing .opencode/rules prefix tests; loadSidebarRules discovers rules from resolved additional directories. Prior art: tui/data/rules.test.ts.
Build gates.aube run lint, aubx tsc --noEmit, aube run test:run all green. The rule-discovery runtime export contract test must stay green without new runtime exports (the discovery options payload can be a type-only export).
Out of Scope
Automatic discovery of .agents/rules (or any IDE directory) by default; users opt in.
Environment-variable expansion ($HOME, ${VAR}) in configured paths; only ~/ is supported.
Symlink-aware deduplication (realpath); dedupe is path-string based.
File watching or hot reload of additional directories.
Per-directory match overrides, priority/weighting, or a new sidebar section for custom directories.
A standalone plugin config file; options live inline in the existing plugin entries.
Any v1 (beta) channel support for the option.
Changes to built-in directory precedence or to rule matching/filter semantics.
Config shape references: the v2 config docs show per-plugin options in opencode.jsonc via the { "package", "options" } entry form, and the TUI cli.json plugin entries use the same object shape. The server and TUI plugins are loaded and configured independently, so both entries need the option for full parity.
The Antigravity scenario from the issue maps to "additionalRuleDirectories": [".agents/rules"], but no .agents-specific behavior is introduced.
Problem Statement
A user who works across editors keeps one shared rules directory (for example
.agents/rules, where Antigravity IDE injects its rules) and wants opencode-rules to load the same files. Today discovery is hard-coded to the global rules directory and the project's.opencode/rules, so the only workaround is duplicating or symlinking rule files into a location the plugin already scans. Issue #79 asks for a setting that adds custom paths to rule discovery; the owner deferred it to v2, where plugin options can live inline inopencode.jsoncwithout a standalone config file.Solution
Support an
additionalRuleDirectoriesplugin option. Users list extra directories (absolute,~/-relative, or relative to the project root) in theoptionsobject of the plugin entry, and every.md/.mdcfile found recursively in those directories is discovered, matched, and delivered exactly like a built-in rule. The TUI sidebar lists rules from extra directories too: rules under the session's project root appear under Project, everything else under Global.Example (
opencode.jsonc):{ "plugins": [ { "package": "opencode-rules@next", "options": { "additionalRuleDirectories": [".agents/rules"] } } ] }The same option belongs on the TUI plugin entry in
cli.jsonso the sidebar sees the same rule set.User Stories
.agents/rules, so that the same rule files drive both tools without duplication.~/my-rules, so that I do not have to hard-code my home path.cli.json, so that the sidebar and the server agree on the rule set.globs,keywords,fileContains,model,agent,branch,os,ci, and the rest), so that I can scope them like any other rule..opencode/rulesand an extra directory, I want the rule's delivered identity to stay stable (derived from its absolute path), so that history-deduplication behavior does not change unexpectedly.OPENCODE_RULES_DEBUGis set, so that I can tell why a rule did not load..agents/rulesand a note that the TUI needs the same option incli.json, so that setup is copy-pasteable.Implementation Decisions
configdomain (/plugin-options) owns plugin-option handling for both plugin entries. It is a type-level and behavior-level contract:parsePluginOptions(raw: Readonly<Record<string, unknown>> | undefined): PluginOptions— pure validation/normalization. Result:{ additionalRuleDirectories: string[] }. Accepts a single string or an array; trims entries; drops empty/non-string values with a debug log; never throws.resolveAdditionalRuleDirectories(entries: readonly string[], projectDir: string | null): string[]— expands a leading~to the home directory (respectingHOME, falling back toos.homedir()asgetGlobalRulesDiralready does), resolves relative entries againstprojectDirviapath.resolve, skips relative entries whenprojectDiris null (debug log), and deduplicates resolved paths.additionalRuleDirectoriesaccepts a string or an array of strings. It is read from theoptionsobject of the plugin entry:context.optionson the server,ctx.optionsin the TUI. A bare-string plugin entry (no options) means no extra directories. The option never replaces built-in discovery.discoverRuleFiles(projectDir?: string, options?: RuleDiscoveryOptions)whereRuleDiscoveryOptionscarriesadditionalDirectories?: readonly string[](already resolved, absolute). Scan order: global directory, project.opencode/rules, then each additional directory in user order. Additional directories use the existing recursive scanner:.md/.mdconly, dot-prefixed entries skipped, missing directories ignored (ENOENT swallowed), other read errors logged.relativePathis relative to the root of the directory it came from, so nested organization is preserved in delivery and the sidebar. Delivery identity remains the absolutefilePath.filePathacross all sources, first occurrence winning (a user pointing at.opencode/rulesmust not double-inject).ruleFileslist (test seam) bypasses discovery as today. The orchestrator and delivery engine are untouched.setup, parsecontext.options, resolve againstcontext.location.directory, and pass the resolved list into runtime creation. Malformed options degrade to built-in discovery only.ctx.optionsonce at setup and passes the parsed entries to the sidebar. Because the relevant project directory is per session, the sidebar resolves entries against the session project root when loading rules; with no project root, relative entries are skipped while absolute and~/entries still load. The sidebar load helper accepts resolved additional directories and forwards them to discovery.classifyRuleScopebecomes project-root containment: a discovered rule whose path lies under the session project root is Project, otherwise Global (usingpath.join(projectDir, '')/separator-boundary comparison, not a rawstartsWith). This replaces the current.opencode/rules-prefix check and automatically classifies.agents/rulesunder the project as Project..agents/rulesexample foropencode.jsoncplus acli.jsonparity note; the Project Structure section lists the new module;AGENTS.md's domain list includessrc/config/.Testing Decisions
undefined/empty object produce no directories. Resolution:~/expansion against a fixture home, relative entries against a project dir, absolute passthrough, null project dir skips only relative entries, dedupe of repeated/overlapping entries. Prior art:saveEnv/restoreEnvfixtures withHOME/XDG_CONFIG_HOMEused by the discovery suites.relativePathrelative to the additional root; duplicate file across sources discovered once. Prior art: thediscoverRuleFilessuites usingsetupTestDirs/getTestDirsandclearRuleCache..agents/rules, a conditional rule from that directory matches its condition and its content is delivered through the existing synthetic admission channel; an unconditional rule is delivered durably. Prior art:src/index.integration.test.tsmock-context flow andwireRuntime.classifyRuleScopecontainment cases (project root, subdirectory, separator boundary, outside, null project dir) replace/extend the existing.opencode/rulesprefix tests;loadSidebarRulesdiscovers rules from resolved additional directories. Prior art:tui/data/rules.test.ts.aube run lint,aubx tsc --noEmit,aube run test:runall green. The rule-discovery runtime export contract test must stay green without new runtime exports (the discovery options payload can be a type-only export).Out of Scope
.agents/rules(or any IDE directory) by default; users opt in.$HOME,${VAR}) in configured paths; only~/is supported.realpath); dedupe is path-string based.beta) channel support for the option.Further Notes
optionsinopencode.jsoncvia the{ "package", "options" }entry form, and the TUIcli.jsonplugin entries use the same object shape. The server and TUI plugins are loaded and configured independently, so both entries need the option for full parity."additionalRuleDirectories": [".agents/rules"], but no.agents-specific behavior is introduced.Blocked by: None.