[APP-3893] Add support for global rulefiles (e.g. ~/.agents/AGENTS.md)#9325
[APP-3893] Add support for global rulefiles (e.g. ~/.agents/AGENTS.md)#9325vkodithala wants to merge 5 commits intomasterfrom
Conversation
|
I'm starting a first review of this pull request. You can follow along in the session on Warp. I completed the review and posted feedback on this pull request. Comment I completed the review and posted feedback on this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
This PR adds global rule file indexing for ~/.agents/AGENTS.md, wires startup indexing, registers home/global directory watchers, and layers global rules into agent rule context.
Concerns
- Global-only rules now make
find_applicable_rulesreturnSomefor every path, which changes callers that use this as a project-initialization signal. - Read failures for an already indexed global rule can leave stale rule content active.
- The safe logging path can still include path-bearing errors in non-full logs.
Security
- The global watcher registration warning includes
{err}in the safe log branch, butRepoMetadataErrorvariants can contain the user's home path.
Verdict
Found: 0 critical, 3 important, 0 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
| // `safe_warn!` because the path contains the user's home dir, | ||
| // which is PII; we only want the full path on dogfood builds. | ||
| safe_warn!( | ||
| safe: ("Failed to register {} for global rules watching: {err}", source.name()), |
There was a problem hiding this comment.
{err} can contain the canonicalized home subdir path (for example RepoNotFound/watcher errors), so the safe log branch can still leak PII; omit or sanitize the error from the safe message and keep details in full.
| // when a previously-known file disappears, so missing reads | ||
| // here just mean "file never existed," which is the steady | ||
| // state for unconfigured sources. | ||
| if let Some(content) = content_opt { |
There was a problem hiding this comment.
global_rules, so unreadable or non-file replacements can keep stale instructions active; remove the entry and emit a deletion when content_opt is None for an existing path.
| let mut active_rules: Vec<ProjectRule> = self.global_rules.values().cloned().collect(); | ||
| active_rules.extend(project_active_rules); | ||
|
|
||
| if active_rules.is_empty() && available_rule_paths.is_empty() { |
There was a problem hiding this comment.
Some for every path, so callers like /init and the code-review empty state treat every repo as already initialized; keep a project-rule signal separate from global fallback rules.
- Switch the Global/Project zero-state text from `wrappable_text` (always left-aligned within its bounds) to `FormattedTextElement` with `TextAlignment::Center` so the copy renders centered to match the centered Add button below it. - Back-fill PRODUCT.md and TECH.md under specs/APP-3893/ describing the file-based-global-rules feature and its surfacing in Settings. Behavior invariants in PRODUCT.md are referenced by the test plan in TECH.md. Co-Authored-By: Oz <[email protected]>
…roject-only signal
- register_global_source_watcher: drop {err} from the safe: branch of the
two safe_warn! calls. The error and the path can both embed the user's
home directory; only the source name is safe to send to Sentry. The
full: (dogfood) branch keeps the error for diagnostics.
- spawn_global_rule_read: when a read fails for a previously-cached path
(file deleted between FS event and read, perms revoked, replaced with
a non-regular file, ...), remove the entry from global_rules and emit
a GlobalRulesChanged deletion delta. Silently keeping stale rule text
active after the source becomes unreadable would surprise users who
thought they had removed it.
- Split find_applicable_rules into a layered (project + global) entry
point and a project-only find_applicable_project_rules. Two callers
use the project-only signal as 'is this repo initialized?' and would
give wrong answers if globals were layered in:
* /init flow's should_have_available_steps
(terminal/view/init_project/model.rs)
* code-review empty state's 'Repo is initialized with a {file_name}
file.' hint (code_review/code_review_view.rs)
Both callers migrated to find_applicable_project_rules; agent-context
packing in BlocklistAIContextModel still uses find_applicable_rules.
- Added unit tests for the project-only lookup.
- Updated specs/APP-3893/PRODUCT.md (new invariant 13 covering the
is-initialized signal; invariant 4 extended to cover unreadable-file
cases) and specs/APP-3893/TECH.md (project-only accessor, stale-content
drop, safer log shape, two new tests).
Co-Authored-By: Oz <[email protected]>
|
I'm starting a first review of this pull request. You can follow along in the session on Warp. I completed the review and posted feedback on this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
This PR adds file-backed global agent rules from ~/.agents/AGENTS.md, layers them with project rules for agent context, and surfaces detected global rule files in Settings → AI → Rules.
Concerns
- The home-subdir watcher update path can leave a recreated
~/.agentsdirectory unwatched when delete and create events for the same path are coalesced. - File-backed rule path search remains case-sensitive for the query, so the new global file rows do not match the existing cloud-rule search behavior.
- Security pass: no separate security-specific blockers found in the changed lines.
Verdict
Found: 0 critical, 1 important, 1 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
|
|
||
| let subdir_added = | ||
| fs_event.added.contains(&subdir_path) || fs_event.moved.contains_key(&subdir_path); | ||
| if subdir_added { |
There was a problem hiding this comment.
~/.agents unwatched for future edits.
| RuleRow::FileBacked(row) => row | ||
| .file_path | ||
| .to_str() | ||
| .map(|s| s.to_lowercase().contains(search_term)) |
There was a problem hiding this comment.
💡 [SUGGESTION] Normalize the query for file-backed rows too; otherwise searching for AGENTS will not match the displayed ~/.agents/AGENTS.md path even though cloud-rule search is case-insensitive.
| RuleRow::FileBacked(row) => row | |
| .file_path | |
| .to_str() | |
| .map(|s| s.to_lowercase().contains(search_term)) | |
| RuleRow::FileBacked(row) => row | |
| .file_path | |
| .to_str() | |
| .map(|s| s.to_lowercase().contains(&search_term.to_lowercase())) |
There was a problem hiding this comment.
Overview
This PR adds file-based global rules from ~/.agents/AGENTS.md, layers them into agent project-rule context, wires startup/watch updates, and surfaces detected global rule files in Settings alongside cloud rules.
Concerns
- File-backed rule path search is not case-insensitive like cloud-rule search, so uppercase queries such as
AGENTSfail to match the displayed~/.agents/AGENTS.mdpath. - Security pass: no security-specific findings.
Verdict
Found: 0 critical, 1 important, 0 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
| .contains(search_term.to_lowercase().as_str()) | ||
| } | ||
| RuleRow::ProjectScoped(row) => row | ||
| RuleRow::FileBacked(row) => row |
There was a problem hiding this comment.
AGENTS will not match ~/.agents/AGENTS.md; lowercase the query before comparing.
| } | ||
|
|
||
| /// A well-known location under `$HOME` that may contain a global rule file. | ||
| #[cfg_attr(not(feature = "local_fs"), allow(dead_code))] |
There was a problem hiding this comment.
wouldn't it be easier to just make this build only if fs is on instead of allow dead code when it's not?
| Agents, | ||
| } | ||
|
|
||
| #[cfg_attr(not(feature = "local_fs"), allow(dead_code))] |
There was a problem hiding this comment.
ditto here
maybe worth considering the same pattern we use for mcps (having a stub when fs off) to avoid littering these flags
| impl GlobalRuleSource { | ||
| /// Iterates every known global rule source. | ||
| fn iter() -> impl Iterator<Item = Self> { | ||
| [Self::Agents].into_iter() |
There was a problem hiding this comment.
Is there a shorthand for iterating through every enum value in rust instead of hardcoding?
| } | ||
|
|
||
| #[cfg_attr(not(feature = "local_fs"), allow(dead_code))] | ||
| impl GlobalRuleSource { |
There was a problem hiding this comment.
I like this pattern we've started
| /// Active home-subdir directory watchers, keyed by the absolute subdir | ||
| /// path (e.g. `~/.agents`). Keying by path naturally deduplicates if two | ||
| /// [`GlobalRuleSource`] variants ever share the same `home_subdir`. | ||
| #[cfg(feature = "local_fs")] |
There was a problem hiding this comment.
this is odd to me, I would assume that rules are always file based so this entire model should not exist or is stubbed when fs isn't available
peicodes
left a comment
There was a problem hiding this comment.
minor comments, nothing blocking
Closes #9788
Description
Why
Today the agent only sees rule files (
WARP.md/AGENTS.md) discovered under indexed project roots. There's no way to express rules that should apply across all projects — e.g. personal coding-style preferences, "always runcargo fmtbefore returning," etc. — without dropping a copy ofWARP.mdinto every repo.This PR adds global rule files. The first supported location is
~/.agents/AGENTS.md; the design is extensible so we can add more well-known locations (e.g.~/.warp/WARP.md) by appending a single enum variant. File-based globals also surface in Settings → AI → Rules → Global alongside cloud rules, so users can confirm Warp picked their file up and jump to it.Specs:
specs/APP-3893/PRODUCT.mdandspecs/APP-3893/TECH.mddescribe the feature in full.What changes for the agent
When a user sends a query, the agent's
AIAgentContext::ProjectRulesnow contains both layers:Both ride in the same proto field the server already understands, so no proto/server changes are needed. Precedence is
global > project WARP.md > project AGENTS.md; the existing in-directoryWARP.md > AGENTS.mdshadow insideRuleAtPath::respected_ruleis preserved.Mechanics
All new logic lives in
crates/ai/src/project_context/model.rs, gated behindfeature = "local_fs":GlobalRuleSourceenum enumerates known global locations. Each variant exposesname()/home_subdir()/file_pattern()accessors, so adding a new global source is one variant + one match arm per accessor.ProjectContextModelgains:global_rules: BTreeMap<PathBuf, ProjectRule>— discovered rule contents, sorted by path so iteration is deterministic.global_source_watchers: HashMap<PathBuf, ...>— activeDirectoryWatcherregistrations keyed by the watched home subdir (e.g.~/.agents). Path-keying naturally dedups if two sources ever share a subdir.index_global_rules(called once at startup fromapp/src/lib.rs, gated onlocal_fs):ctx.spawnso all disk I/O happens on a background task — startup is not blocked.HomeDirectoryWatcherto handle the home subdir being created/deleted at runtime.DirectoryWatcherper existing subdir for incremental file updates. This mirrors the existing pattern inapp/src/ai/mcp/file_mcp_watcher.rs.find_applicable_rulesnow prepends the global layer onto the project layer when assemblingactive_rules, returningSomewhenever either layer contributes anything.The Settings surface (
app/src/ai/facts/view/rule.rs) renamesProjectScopedRow→FileBackedRow(the row shape is the same: a path + an "Open file" button) and lets the Global tab list cloud rules and file-based globals together. A newProjectContextModel::global_rule_paths()accessor feeds the view; the existingOpenFile(PathBuf)action handles the click.Testing
Automated
Unit tests live in
crates/ai/src/project_context/model_tests.rsand cover the layering logic by inserting synthetic rules directly intoProjectContextModel::global_rulesandpath_to_rules:find_applicable_rulesreturns it.WARP.md→ both appear inactive_rules, ordered global first.WARP.mdandAGENTS.mdin same project dir + a global rule → projectWARP.mdstill shadows projectAGENTS.md; global is appended.None.root_pathfalls back to the parent of the global file.containsassertions sinceBTreeMaporders by path).Run:
cargo nextest run -p ai --features local_fs project_context→ 17/17 passing.cargo fmtandcargo clippy --all-targets --all-features --tests -- -D warningsclean for bothaiandwarp.Manual end-to-end (run these locally to verify) - Demo here!
Each step maps to a numbered behavior invariant in
specs/APP-3893/PRODUCT.md.Setup
cargo run.~/.agents/AGENTS.mdyet).Indexing & agent context (PRODUCT invariants 1–7, 8–12)
~/.agents/AGENTS.mdabsent, fire any agent query. The request payload should not include any global rule.mkdir -p ~/.agents && echo "always run cargo fmt before returning" > ~/.agents/AGENTS.md.active_rule_filesshould contain the file's contents. The agent should also visibly attempt to follow the rule.rm ~/.agents/AGENTS.md. Fire another query. The rule should be gone from the request payload within ~1 second.rm -rf ~/.agentsthen recreate the directory + file. The watcher should re-register and pick the rule up.Layering with project rules (PRODUCT invariants 9, 10)
~/.agents/AGENTS.mdcontaining rule A,cdinto a repo that has both aWARP.md(rule B) and anAGENTS.md(rule C) at its root.active_rule_filesshould contain both A (global) and B (project WARP.md). C should be shadowed by B per the existing in-directory rule.Settings → Rules surface (PRODUCT invariants 13–18)
~/.agents/AGENTS.mdpresent, open Settings → AI → Rules → Global. A row should appear showing~/.agents/AGENTS.mdwith an "Open file" button.rm ~/.agents/AGENTS.md. The row should disappear from the Global tab live, no restart required.Agent Mode
Changelog Entries for Stable
CHANGELOG-IMPROVEMENT: Agents now read rules from
~/.agents/AGENTS.mdso guidelines you want applied across every project no longer need a per-repoWARP.md. The file is also surfaced in Settings → AI → Rules under the Global tab.