Skip to content

[APP-3893] Add support for global rulefiles (e.g. ~/.agents/AGENTS.md)#9325

Open
vkodithala wants to merge 5 commits intomasterfrom
varoon/add-global-rulefiles
Open

[APP-3893] Add support for global rulefiles (e.g. ~/.agents/AGENTS.md)#9325
vkodithala wants to merge 5 commits intomasterfrom
varoon/add-global-rulefiles

Conversation

@vkodithala
Copy link
Copy Markdown
Contributor

@vkodithala vkodithala commented Apr 28, 2026

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 run cargo fmt before returning," etc. — without dropping a copy of WARP.md into 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.md and specs/APP-3893/TECH.md describe the feature in full.

What changes for the agent

When a user sends a query, the agent's AIAgentContext::ProjectRules now contains both layers:

  1. Global rules from each registered home location (new).
  2. Project rules discovered by walking up from the working directory (existing).

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-directory WARP.md > AGENTS.md shadow inside RuleAtPath::respected_rule is preserved.

Mechanics

All new logic lives in crates/ai/src/project_context/model.rs, gated behind feature = "local_fs":

  • A GlobalRuleSource enum enumerates known global locations. Each variant exposes name() / home_subdir() / file_pattern() accessors, so adding a new global source is one variant + one match arm per accessor.
  • ProjectContextModel gains:
    • global_rules: BTreeMap<PathBuf, ProjectRule> — discovered rule contents, sorted by path so iteration is deterministic.
    • global_source_watchers: HashMap<PathBuf, ...> — active DirectoryWatcher registrations 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 from app/src/lib.rs, gated on local_fs):
    1. Reads each registered file via ctx.spawn so all disk I/O happens on a background task — startup is not blocked.
    2. Subscribes to HomeDirectoryWatcher to handle the home subdir being created/deleted at runtime.
    3. Registers a DirectoryWatcher per existing subdir for incremental file updates. This mirrors the existing pattern in app/src/ai/mcp/file_mcp_watcher.rs.
  • find_applicable_rules now prepends the global layer onto the project layer when assembling active_rules, returning Some whenever either layer contributes anything.

The Settings surface (app/src/ai/facts/view/rule.rs) renames ProjectScopedRowFileBackedRow (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 new ProjectContextModel::global_rule_paths() accessor feeds the view; the existing OpenFile(PathBuf) action handles the click.

Testing

Automated

Unit tests live in crates/ai/src/project_context/model_tests.rs and cover the layering logic by inserting synthetic rules directly into ProjectContextModel::global_rules and path_to_rules:

  • Global rule alone, no project rules → find_applicable_rules returns it.
  • Global + project WARP.md → both appear in active_rules, ordered global first.
  • Both WARP.md and AGENTS.md in same project dir + a global rule → project WARP.md still shadows project AGENTS.md; global is appended.
  • No rules anywhere → None.
  • Global-only root_path falls back to the parent of the global file.
  • Multiple global sources both contribute (uses contains assertions since BTreeMap orders by path).

Run: cargo nextest run -p ai --features local_fs project_context → 17/17 passing. cargo fmt and cargo clippy --all-targets --all-features --tests -- -D warnings clean for both ai and warp.

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

  1. Build and run Warp from this branch: cargo run.
  2. Start a fresh shell (no ~/.agents/AGENTS.md yet).

Indexing & agent context (PRODUCT invariants 1–7, 8–12)

  1. With ~/.agents/AGENTS.md absent, fire any agent query. The request payload should not include any global rule.
  2. In a separate terminal: mkdir -p ~/.agents && echo "always run cargo fmt before returning" > ~/.agents/AGENTS.md.
  3. Fire another agent query in Warp. The request's active_rule_files should contain the file's contents. The agent should also visibly attempt to follow the rule.
  4. Edit the file in any external editor, change the sentinel string, save. Fire another query. The new contents should appear.
  5. rm ~/.agents/AGENTS.md. Fire another query. The rule should be gone from the request payload within ~1 second.
  6. Without restarting Warp, recreate the file. The next query should include it again.
  7. (Optional) rm -rf ~/.agents then recreate the directory + file. The watcher should re-register and pick the rule up.

Layering with project rules (PRODUCT invariants 9, 10)

  1. With ~/.agents/AGENTS.md containing rule A, cd into a repo that has both a WARP.md (rule B) and an AGENTS.md (rule C) at its root.
  2. Fire a query. active_rule_files should 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)

  1. With ~/.agents/AGENTS.md present, open Settings → AI → Rules → Global. A row should appear showing ~/.agents/AGENTS.md with an "Open file" button.
  2. Click "Open file". The file should open in the configured editor.
  3. Keep the Settings tab open; in another terminal, rm ~/.agents/AGENTS.md. The row should disappear from the Global tab live, no restart required.
  4. Recreate the file. The row should reappear live.
  5. Click "Add" on the Global tab and create a cloud rule. Both row types should now coexist.
  6. Use the search bar at the top of the Global tab. It should match against both the cloud rule's name/content and the file path.
  7. Delete the cloud rule and the file so neither source is present.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

Changelog Entries for Stable

CHANGELOG-IMPROVEMENT: Agents now read rules from ~/.agents/AGENTS.md so guidelines you want applied across every project no longer need a per-repo WARP.md. The file is also surfaced in Settings → AI → Rules under the Global tab.

@cla-bot cla-bot Bot added the cla-signed label Apr 28, 2026
@vkodithala vkodithala marked this pull request as draft April 28, 2026 22:26
@vkodithala vkodithala changed the title Add indexing and watching of global rule files [ Apr 28, 2026
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented Apr 28, 2026

@vkodithala

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 /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

I completed the review and posted feedback on this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@vkodithala vkodithala changed the title [ [APP= Apr 28, 2026
@vkodithala vkodithala changed the title [APP= [APP-3893] Add Apr 28, 2026
@vkodithala vkodithala changed the title [APP-3893] Add [APP-3893] Add support for global rulefiles Apr 28, 2026
@vkodithala vkodithala changed the title [APP-3893] Add support for global rulefiles [APP-3893] Add support for global rulefiles (~/.agents/AGENTS.md) Apr 28, 2026
Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

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

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_rules return Some for 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, but RepoMetadataError variants 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

Comment thread crates/ai/src/project_context/model.rs Outdated
// `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()),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] [SECURITY] {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.

Comment thread crates/ai/src/project_context/model.rs Outdated
// 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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] A read error on a previously indexed global rule leaves the old content in 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.

Comment thread crates/ai/src/project_context/model.rs Outdated
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() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] Once a global rule exists this returns 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.

@vkodithala vkodithala changed the title [APP-3893] Add support for global rulefiles (~/.agents/AGENTS.md) [APP-3893] Add support for global rulefiles (e.g. ~/.agents/AGENTS.md) Apr 28, 2026
vkodithala and others added 3 commits April 28, 2026 19:15
- 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]>
@vkodithala vkodithala marked this pull request as ready for review April 29, 2026 01:19
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented Apr 29, 2026

@vkodithala

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 /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

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

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 ~/.agents directory 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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] Handle subdir deletion before addition here; a delete+recreate within the debounce window can make both flags true, so the add path no-ops against the old watcher and the delete path then removes it, leaving the recreated ~/.agents unwatched for future edits.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this is probably valid

Comment on lines +134 to 137
RuleRow::FileBacked(row) => row
.file_path
.to_str()
.map(|s| s.to_lowercase().contains(search_term))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Suggested change
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()))

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

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

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 AGENTS fail to match the displayed ~/.agents/AGENTS.md path.
  • 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] File-backed rule search lowercases the path but compares it to the raw query, so searching 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))]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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))]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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")]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

@peicodes peicodes left a comment

Choose a reason for hiding this comment

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

minor comments, nothing blocking

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.

Add support for ~/.agents/AGENTS.md

2 participants