Skip to content

feat(v2): configurable additional rule directories via plugin options #80

Description

@frap129

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 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.

Example (opencode.jsonc):

{
  "plugins": [
    {
      "package": "opencode-rules@next",
      "options": {
        "additionalRuleDirectories": [".agents/rules"]
      }
    }
  ]
}

The same option belongs on the TUI plugin entry in cli.json so the sidebar sees the same rule set.

User Stories

  1. 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.
  2. As a user with shared rules across repositories, I want to add an absolute directory path, so that one global location feeds every project.
  3. 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.
  4. 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.
  5. As a user with several extra rule sources, I want to list multiple directories, so that all of them load in one session.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. As a user who sets no option at all, I want discovery behavior unchanged, so that upgrading changes nothing about my current setup.
  11. As a TUI user, I want rules from extra directories shown in the sidebar, so that I can see what the agent receives.
  12. 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.
  13. 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.
  14. 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.
  15. 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.
  16. 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.
  17. 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.
  18. 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.
  19. 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.
  20. 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.

Further Notes

  • Implements Feature Request: Allow adding custom path to rule-discovery #79. The owner's guidance there (defer to v2, inline configuration, no standalone config file) is the constraint this spec follows.
  • 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.
  • Blocked by: None.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentFully specified, ready for an AFK agentv2Part of the opencode v2 migration effort

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions