Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/tool-release-baselines.json
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@
],
"github_repo": "cline/cline",
"release_tag_regex": "^v[0-9]+\\.[0-9]+\\.[0-9]+$",
"last_known_version": "v4.0.12",
"last_known_version": "v4.1.2",
"tracked": true,
"notes": "Tracks only the core Cline vX.Y.Z release train. The repository also publishes desktop-v*, cli-v*, sdk/*, and other interleaved releases that do not correspond to the validator surfaces.",
"changes_of_interest": {
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ editors/
├── vscode/ # VS Code extension
├── jetbrains/ # JetBrains IDE plugin
└── zed/ # Zed extension
knowledge-base/ # 442 rules, 75+ sources, rules.json
knowledge-base/ # 443 rules, 75+ sources, rules.json

tests/fixtures/ # Test cases by category
```
Expand Down Expand Up @@ -178,7 +178,7 @@ cargo run --bin agnix-mcp # Run MCP server

## Rules Reference

442 rules defined in `knowledge-base/rules.json` (source of truth)
443 rules defined in `knowledge-base/rules.json` (source of truth)


Human-readable docs: `knowledge-base/VALIDATION-RULES.md`
Expand All @@ -190,7 +190,7 @@ Format: `[CATEGORY]-[NUMBER]` (AS-004, CC-HK-001, etc.)
## Current State

- v0.37.3 - Production-ready with full validation pipeline
- 442 validation rules across 40 validators
- 443 validation rules across 40 validators

- 4200+ passing tests
- LSP + MCP servers with VS Code extension
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **CC-MEM-014 fired on Cursor `.mdc` files**. `ClaudeMdValidator` is registered for `FileType::CursorRule` for its generic prose checks, so a `.cursor/rules/*.mdc` file was reported as `CLAUDE.md has N non-empty lines, exceeding the recommended 200 line limit` - wrong filename, wrong threshold (Cursor documents 500), wrong tool attribution. Now scoped to Claude memory files.
- **Stale evidence URLs for 18 rules**. `docs.github.com/en/copilot/customizing-copilot` 301-redirects and `docs.cline.bot/features/cline-rules/overview` 308-redirects; both are updated in `rules.json` and `.github/spec-baselines.json`, so those two sentinel sources can match again.

### Added
- **XP-009: Codex Instruction Chain Exceeds project_doc_max_bytes**. Codex's 32 KiB `project_doc_max_bytes` cap is cumulative across the instruction chain, not per-file - it "stops adding files once the combined size reaches the limit". XP-007 checks each `AGENTS.md` in isolation, so a project split across several mid-size files was truncated with no diagnostic at all. The new project-level rule builds the chain the way Codex discovers it (root down, at most one file per directory, `AGENTS.override.md` before `AGENTS.md`), sums root-first, and reports on the file where the running total crosses the limit - that file and everything deeper is what Codex drops. Rule count 442 -> 443 (closes #1289).

### Fixed
- **`AGENTS.override.md` was not recognized as an instruction file**. Codex checks it before `AGENTS.md` in each directory, but `is_instruction_file()` did not list it, so it was invisible to every cross-platform rule. This also made the `AGENTS.override.md` handling added to XP-007 in the previous release unreachable in practice.

Comment on lines +21 to +26

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.

Two issues in this block.

  1. The "unreachable" claim is wrong - "This also made the AGENTS.override.md handling added to XP-007 in the previous release unreachable in practice." XP-007 is a per-file validator dispatched from detect_file_type(), and crates/agnix-core/src/file_types/detection.rs:352 has mapped AGENTS.override.md -> FileType::ClaudeMd since before this PR (that file is untouched by this diff). is_instruction_file() has only two non-test callers, both in pipeline.rs, and both feed project-level path collection only - it never gated XP-007. Verified on this branch: a 40 KB AGENTS.override.md produces XP-007 (plus AGM-002/003/004, CDX-003, XP-002), which is the same path that existed on main. The is_instruction_file() addition is still a genuine fix for the XP-004/005/006/XP-009 side; it just was not what was blocking XP-007.

  2. Duplicate ### Added heading - [Unreleased] now has two ### Added sections (this one and the pre-existing args-on-command-hooks entry immediately below). Merge into the single existing section per keep-a-changelog.

### Added
- **`args` on command hooks**. The exec-form field - "When present, `command` is resolved as an executable and spawned directly with `args` as the argument vector, with no shell involved" - was absent from the schema, so it was silently ignored and two rules could not tell exec form from shell form.

Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ editors/
├── vscode/ # VS Code extension
├── jetbrains/ # JetBrains IDE plugin
└── zed/ # Zed extension
knowledge-base/ # 442 rules, 75+ sources, rules.json
knowledge-base/ # 443 rules, 75+ sources, rules.json

tests/fixtures/ # Test cases by category
```
Expand Down Expand Up @@ -178,7 +178,7 @@ cargo run --bin agnix-mcp # Run MCP server

## Rules Reference

442 rules defined in `knowledge-base/rules.json` (source of truth)
443 rules defined in `knowledge-base/rules.json` (source of truth)


Human-readable docs: `knowledge-base/VALIDATION-RULES.md`
Expand All @@ -190,7 +190,7 @@ Format: `[CATEGORY]-[NUMBER]` (AS-004, CC-HK-001, etc.)
## Current State

- v0.37.3 - Production-ready with full validation pipeline
- 442 validation rules across 40 validators
- 443 validation rules across 40 validators

- 4200+ passing tests
- LSP + MCP servers with VS Code extension
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</p>
</div>

<p align="center">Catch broken agent configs before your AI tools silently ignore them.<br>442 rules across Claude Code, Codex CLI, OpenCode, Cursor, Copilot, and more -<br>validating CLAUDE.md, SKILL.md, hooks, MCP configs, and other agent files.</p>
<p align="center">Catch broken agent configs before your AI tools silently ignore them.<br>443 rules across Claude Code, Codex CLI, OpenCode, Cursor, Copilot, and more -<br>validating CLAUDE.md, SKILL.md, hooks, MCP configs, and other agent files.</p>

<p align="center"><strong>Auto-fix</strong> | <strong><a href="https://github.com/marketplace/actions/agnix-ci">GitHub Action</a></strong> | <strong><a href="https://marketplace.visualstudio.com/items?itemName=avifenesh.agnix">VS Code</a> + <a href="https://plugins.jetbrains.com/plugin/30087-agnix">JetBrains</a> + <a href="https://github.com/agent-sh/agnix.nvim">Neovim</a> + <a href="https://zed.dev/extensions?query=agnix">Zed</a></strong></p>

Expand All @@ -37,7 +37,7 @@

**Bad patterns get amplified.** AI assistants don't ignore wrong configs - they [learn from them](https://www.augmentcode.com/guides/enterprise-coding-standards-12-rules-for-ai-ready-teams).

agnix validates all of it - 442 rules sourced from official specs, academic research, and real-world breakage patterns. Auto-fix included.
agnix validates all of it - 443 rules sourced from official specs, academic research, and real-world breakage patterns. Auto-fix included.

> **Want to try it first?** [Open the playground](https://agent-sh.github.io/agnix/playground) - paste any agent config, see diagnostics instantly. No install, runs in your browser.

Expand Down
2 changes: 1 addition & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
| Claude Output Styles | .claude/output-styles/*.md | 6 |
| Claude Settings | .claude/settings.json | 20 |
| Prompt Engineering | CLAUDE.md, AGENTS.md | 6 |
| Cross-Platform | AGENTS.md | 9 |
| Cross-Platform | AGENTS.md | 10 |
| MCP | tool definitions | 26 |
| XML | all .md files | 3 |
| References | @imports | 4 |
Expand Down
3 changes: 3 additions & 0 deletions crates/agnix-cli/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,9 @@ rules:
message: 'CLAUDE.md contains Claude-specific feature ''%{feature}'' that Cursor cannot interpret: %{description}'
suggestion: Move Cursor-compatible instructions to .cursor/rules/ or guard Claude-specific content under a '## Claude
Code' section header
xp_009:
message: 'Codex instruction chain reaches %{bytes} bytes across %{count} file(s), past the %{limit}-byte project_doc_max_bytes default. Codex stops appending once the combined size hits the limit, so this file and any deeper ones are dropped.'
suggestion: Trim the chain, or raise project_doc_max_bytes in the Codex config (e.g. project_doc_max_bytes = 65536)
cop_001:
message_empty: Copilot instruction file is empty
message_no_content: Copilot instruction file has no content after frontmatter
Expand Down
3 changes: 3 additions & 0 deletions crates/agnix-core/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,9 @@ rules:
message: 'CLAUDE.md contains Claude-specific feature ''%{feature}'' that Cursor cannot interpret: %{description}'
suggestion: Move Cursor-compatible instructions to .cursor/rules/ or guard Claude-specific content under a '## Claude
Code' section header
xp_009:
message: 'Codex instruction chain reaches %{bytes} bytes across %{count} file(s), past the %{limit}-byte project_doc_max_bytes default. Codex stops appending once the combined size hits the limit, so this file and any deeper ones are dropped.'
suggestion: Trim the chain, or raise project_doc_max_bytes in the Codex config (e.g. project_doc_max_bytes = 65536)
cop_001:
message_empty: Copilot instruction file is empty
message_no_content: Copilot instruction file has no content after frontmatter
Expand Down
197 changes: 196 additions & 1 deletion crates/agnix-core/src/rules/project_level.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
//! Project-level cross-file validation rules (AGM-006, XP-004/005/006, VER-001).
//! Project-level cross-file validation rules (AGM-006, XP-004/005/006, XP-009,
//! VER-001).
// All items in this module require the filesystem feature. The file-level inner
// attribute avoids repeating #[cfg(feature = "filesystem")] on each import and
// function - unlike pipeline.rs, this file has no non-filesystem items.
#![cfg(feature = "filesystem")]

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use rust_i18n::t;
Expand All @@ -13,6 +15,7 @@ use crate::diagnostics::Diagnostic;
use crate::file_utils;
use crate::parsers::frontmatter::normalize_line_endings;
use crate::schemas;
use crate::schemas::cross_platform::CODEX_BYTE_LIMIT;

/// Join an iterator of paths into a comma-separated string.
///
Expand Down Expand Up @@ -148,6 +151,30 @@ pub(crate) fn run_project_level_checks(
}
}

// Drop any `AGENTS.md` shadowed by an `AGENTS.override.md` in the same
// directory. Codex "includes at most one file per directory", checking
// the override first, so the shadowed file is never loaded - the doc's
// own tree labels it "Ignored because an override exists". Comparing the
// two as peers reported a conflict between a file and the thing whose
// whole purpose is to differ from it.
let shadowed: std::collections::BTreeSet<PathBuf> = file_contents
.iter()
.filter(|(path, _)| {
// Case-insensitive, matching `is_instruction_file()`. The three
// places that name this file have to agree: that helper collects
// it case-insensitively, so an exact-match filter here left a
// lowercase `agents.override.md` shadowing nothing while still
// being treated as an instruction file.
path.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.eq_ignore_ascii_case("AGENTS.override.md"))
})
.filter_map(|(path, _)| path.parent().map(|dir| dir.join("AGENTS.md")))
.collect();
if !shadowed.is_empty() {
file_contents.retain(|(path, _)| !shadowed.contains(path));
}
Comment on lines +154 to +176

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.

The shadowing fix works - verified - but it is case-sensitive while the list it filters is not.

Confirmed working: root AGENTS.md "npm run build" + AGENTS.override.md "pnpm build" now produces no XP-004/005/006, and a genuine CLAUDE.md-vs-override conflict still fires. So this is an exclusion, not a disablement, as claimed.

Three follow-ups, all in the same class - this PR now has three independent places that decide "is this an override / is this shadowed", and they disagree:

  1. Here (== Some("AGENTS.override.md")) - exact match.
  2. codex_instruction_chains precedence - exact match.
  3. is_instruction_file() (schemas/cross_platform.rs:1011) - eq_ignore_ascii_case("agents.override.md").

So a lowercase agents.override.md is collected as an instruction file but shadows nothing and is absent from the chain. Measured on a fixture with agents.override.md + a 40 KB AGENTS.md: the shadowed 40 KB file is still summed and XP-009 fires on it. Pick one convention across all three - exact match everywhere (and drop the eq_ignore_ascii_case for this name) is the simpler resolution and matches XP-007.

  1. XP-007 does not share this shadowing model. Verified: small AGENTS.override.md + 40 KB shadowed AGENTS.md still yields XP-007 AGENTS.md, for bytes Codex never reads - while xp009_counts_one_file_per_directory_preferring_override in this same PR asserts those bytes "must not count". Two rules in one PR now hold opposite positions on the same file. Not fixable from a per-file validator, but it should be stated in the XP-007 docs rather than left to be discovered.

  2. Ordering nit with teeth: the XP-004 read-error diagnostic at line 138 is emitted for the shadowed AGENTS.md before this retain drops it, so an unreadable shadowed file still produces a diagnostic about a file Codex never opens.

Fix this →


// XP-004/005/006: each detector runs on a per-rule filtered subset
// of `file_contents`. A file whose `[[overrides]]` disable the rule
// is removed from that rule's candidate set up front, so the
Expand Down Expand Up @@ -307,9 +334,177 @@ pub(crate) fn run_project_level_checks(
}
}

// XP-009: the Codex instruction chain exceeds `project_doc_max_bytes`.
//
// XP-007 checks each AGENTS.md against the 32 KiB default in isolation, but
// the documented cap is cumulative: Codex "stops adding files once the
// combined size reaches the limit defined by `project_doc_max_bytes`". A
// project split across several mid-size files is therefore truncated with no
// per-file diagnostic - the gap recorded in the XP-007 rule docs.
//
// Modelled on the documented discovery rules: start at the project root,
// walk down, take at most one file per directory in the order
// `AGENTS.override.md`, `AGENTS.md`, then any configured
// `project_doc_fallback_filenames`. Names outside that set "are ignored for
// instruction discovery" and so do not count.
if config.is_rule_enabled("XP-009") {
// One chain per leaf directory, not one total for the whole tree.
//
// A Codex chain is a single root-to-cwd path: two sibling packages are
// never concatenated. Summing every discovered file into one running
// total blamed a sibling for bytes it does not share - and it scaled the
// wrong way, since N packages would eventually cross the cap no matter
// how small each one is. It also contradicted AGM-006 above, which
// recommends splitting across nested directories precisely to stay under
// this cap.
let chains = codex_instruction_chains(instruction_file_paths, root_dir);

// Attribute each over-cap chain to the *largest* file in it, not to the
// file where the running total happens to cross.
//
// Those differ, and the difference matters: a 30 KB root with twenty 5 KB
// packages crosses inside each package, so blaming the crossing file
// produced twenty diagnostics against twenty 5 KB files for a problem
// that only trimming the 30 KB root fixes. Trimming any one of them
// removes 5 KB from a chain that needs 30 KB removed, and the suggestion
// points at the file it names. Attributing to the biggest contributor
// names the one edit that resolves every affected chain, and collapses
// those twenty reports into one.
let mut reported: BTreeMap<PathBuf, (usize, usize)> = BTreeMap::new();

for chain in &chains {
let mut sizes: Vec<(PathBuf, usize)> = Vec::with_capacity(chain.len());
let mut total = 0usize;
for path in chain {
let Ok(content) = config.fs().read_to_string(path) else {
continue;
};
total += content.len();
sizes.push((path.clone(), content.len()));
}

if total <= CODEX_BYTE_LIMIT {
continue;
}

// Largest first, then shallowest, so the choice is deterministic when
// two files tie.
let Some((culprit, _)) = sizes
.iter()
.max_by_key(|(path, size)| (*size, std::cmp::Reverse(path.components().count())))
else {
continue;
};

reported
.entry(culprit.clone())
.and_modify(|entry| {
if total > entry.0 {
*entry = (total, chain.len());
}
})
.or_insert((total, chain.len()));
}

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.

New finding (not in the previous pass): XP-009 double-reports every file XP-007 already flags.

A chain of length 1 whose single file is over the cap satisfies total > CODEX_BYTE_LIMIT on the first iteration, so XP-009 fires on the same file XP-007 just flagged, with a differently-worded message. Verified on this branch with a single 40 KB root AGENTS.md:

XP-007  AGENTS.md :: AGENTS.md exceeds Codex CLI byte limit (40960 bytes, max 32768)
XP-009  AGENTS.md :: Codex instruction chain reaches 40960 bytes across 1 file(s), ...

Two diagnostics, one file, one cause. "instruction chain ... across 1 file(s)" also reads wrong - there is no chain. Every project that today gets a single XP-007 gains a duplicate on this PR, which is a real regression in output noise for the most common oversized-AGENTS.md case, and it is not covered by any test here (xp009_flags_cumulative_chain_over_the_cap asserts XP-007 is silent, i.e. only the disjoint case).

The gap XP-009 exists to close is chains where no single file is over. Suggest skipping when the crossing file is itself over the limit - XP-007 owns that - so the two rules partition the space instead of overlapping:

// XP-007 already reports a single file over the cap; XP-009 covers only
// the cumulative case no per-file check can see.
if total > CODEX_BYTE_LIMIT && truncated_at.is_none() && size <= CODEX_BYTE_LIMIT {
    truncated_at = Some((path.clone(), total));
}

(and pin it with a test asserting exactly one diagnostic for the single-oversized-file case).

Fix this →


for (report_path, (running_total, chain_len)) in reported {
if config.for_path(&report_path).is_rule_enabled("XP-009") {
diagnostics.push(
Diagnostic::warning(

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.

Minor, new this pass: column convention. This is Diagnostic::warning(report_path, 1, 1, ...). Every other diagnostic emitted from this module uses column 0 - AGM-006 at line 91, XP-004 at 138, XP-005 at 181/224, XP-006 at 269, VER-001 at 306. Confirmed in the JSON output of a probe run: XP-009 lands at "column": 1 while AGM-006 on the same file lands at "column": 1 in the CLI's 1-based rendering of 0, i.e. the two are actually off by one against each other in the LSP's 0-based character positions.

1, 0 matches the module; 1, 1 matches XP-007 in the per-file validator. Since this rule lives here and is merged into the LSP's project-level diagnostic cache (agnix-lsp/src/backend.rs:257), 0 is the consistent choice.

While in this block, the two items already raised still apply: the count = chain.len() / bytes = running_total mismatch (measured "reaches 40960 bytes across 3 file(s)" on a 20+20+5 KB chain, where 40960 is two files), and the missing size <= CODEX_BYTE_LIMIT guard that makes this fire alongside XP-007 on every single oversized AGENTS.md.

report_path,
1,
1,
"XP-009",
t!(
"rules.xp_009.message",
bytes = running_total,
limit = CODEX_BYTE_LIMIT,
count = chain_len
),
Comment on lines +417 to +422

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.

Correctness: bytes and count describe different sets, so the message can be arithmetically impossible.

running_total is the sum up to and including the crossing file; chain.len() is the length of the whole chain. When the crossing happens before the end, the message attributes a partial byte total to every file.

Verified on this branch with root 20 KB + api/ 20 KB + api/v2/ 5 KB:

file = api/AGENTS.md
"Codex instruction chain reaches 40960 bytes across 3 file(s) ..."

40960 is 2 files, not 3 - and the third file's 5 KB is precisely the content Codex drops. Either count the files actually summed, or report the full chain total alongside the crossing point:

truncated_at = Some((path.clone(), total, idx + 1)); // files counted so far

A test asserting the reported file and the numbers in the message would have caught this - see the top-level comment on test coverage.

)
.with_suggestion(t!("rules.xp_009.suggestion")),
);
}
}
}

diagnostics
}

/// Build every Codex instruction chain for XP-009, one per leaf directory.
///
/// A chain is a single root-to-directory path: "Starting at the project root
/// (typically the Git root), Codex walks down to your current working
/// directory", taking at most one file per directory in the order
/// `AGENTS.override.md`, then `AGENTS.md`. Sibling subtrees are separate
/// chains and are never concatenated, so each is summed on its own.
///
/// Only files the project walk already collected are considered, so this adds no
/// filesystem traversal. Each chain is returned root-first, the order Codex
/// concatenates in.
fn codex_instruction_chains(
instruction_file_paths: &[PathBuf],
root_dir: &Path,
) -> Vec<Vec<PathBuf>> {
// `AGENTS.override.md` first, then `AGENTS.md`. Codex also honors any
// `project_doc_fallback_filenames`, but those live in the user's Codex
// `config.toml` rather than anything agnix reads, so the chain is built from
// the two default names. A project relying on fallbacks would have a longer
// real chain, which makes this conservative: it can under-report, never
// over-report.
let precedence: [&str; 2] = ["AGENTS.override.md", "AGENTS.md"];

// One entry per directory, keeping only the highest-precedence name there.
let mut by_dir: BTreeMap<PathBuf, PathBuf> = BTreeMap::new();
for path in instruction_file_paths {
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
// Case-insensitive for the same reason as the shadow filter above.
let Some(rank) = precedence.iter().position(|p| p.eq_ignore_ascii_case(name)) else {
continue;
};
let Some(dir) = path.parent() else { continue };
if !dir.starts_with(root_dir) {
continue;
}
by_dir
.entry(dir.to_path_buf())
.and_modify(|existing| {
let existing_rank = existing
.file_name()
.and_then(|n| n.to_str())
.and_then(|n| precedence.iter().position(|p| p.eq_ignore_ascii_case(n)))
.unwrap_or(usize::MAX);
if rank < existing_rank {
*existing = path.clone();
}
})
.or_insert_with(|| path.clone());
}

// A leaf is a directory with no deeper instruction-bearing descendant. Every
// leaf yields one chain: itself plus each ancestor that holds a file.
let dirs: Vec<&PathBuf> = by_dir.keys().collect();
dirs.iter()
.filter(|dir| {
!dirs
.iter()
.any(|other| other != *dir && other.starts_with(dir.as_path()))
})
.map(|leaf| {
let mut chain: Vec<PathBuf> = by_dir
.iter()
.filter(|(dir, _)| leaf.starts_with(dir.as_path()))
.map(|(_, file)| file.clone())
.collect();
// BTreeMap iteration is shallow-to-deep for nested paths, matching
// root-down concatenation; sort defensively on depth anyway.
chain.sort_by_key(|p| p.components().count());
chain
})
.collect()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
4 changes: 4 additions & 0 deletions crates/agnix-core/src/schemas/cross_platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,10 @@ pub fn is_instruction_file(path: &Path) -> bool {
// Direct filename matches (case-insensitive)
if file_name.eq_ignore_ascii_case("claude.md")
|| file_name.eq_ignore_ascii_case("agents.md")
// Codex checks `AGENTS.override.md` before `AGENTS.md` in each
// directory, so it is part of the instruction set and counts toward
// `project_doc_max_bytes` (XP-007, XP-009).
|| file_name.eq_ignore_ascii_case("agents.override.md")
Comment on lines 1006 to +1011

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 one-line allowlist change has three knock-on effects, none of them tested.

  1. New false positives in XP-004/005/006. is_instruction_file() has exactly two non-test callers (pipeline.rs:720 and pipeline.rs:877), both feeding instruction_file_paths - which drives XP-004/005/006 as well as the new XP-009. Adding AGENTS.override.md means an override file and the AGENTS.md it shadows are now compared as two conflicting layers. Verified on this branch with a root AGENTS.md saying "Use npm run build" and an AGENTS.override.md saying "Use pnpm build":

    XP-004  AGENTS.override.md uses pnpm but AGENTS.md uses npm for build commands
    

    That is the file doing its job. XP-009 correctly excludes the shadowed file from the chain (line 830 of project_level.rs); XP-004/005/006 should apply the same one-file-per-directory shadowing, or the override pair should be excluded from conflict detection.

  2. No unit test for the new entry. test_is_instruction_file is right here at line ~2044 and asserts CLAUDE.md, AGENTS.md, .clinerules, etc. AGENTS.override.md was not added to it, so the allowlist entry is only exercised indirectly through one XP-009 integration test.

  3. The same list is still missing its siblings. It carries gemini.local.md but neither claude.local.md nor agents.local.md, even though CLAUDE.local.md is a first-class layer elsewhere in this file (XP-008 matches "CLAUDE.md" | "CLAUDE.local.md") and detection.rs:129 treats AGENTS.md | AGENTS.local.md | AGENTS.override.md as one set. Pre-existing, but it is the same list and the same class of gap - worth closing in one pass or documenting why .local variants are deliberately out of the cross-layer analysis.

Separately: the CHANGELOG's claim that this made XP-007's override handling "unreachable in practice" does not hold - see the top-level comment.

|| file_name.eq_ignore_ascii_case("gemini.md")
|| file_name.eq_ignore_ascii_case("gemini.local.md")
|| file_name.eq_ignore_ascii_case(".clinerules")
Expand Down
Loading