Skip to content

fix: A-tier spec drift for Cursor, Copilot and Cline (#1288) - #1293

Merged
avifenesh merged 2 commits into
mainfrom
fix/a-tier-spec-drift-1288
Jul 31, 2026
Merged

fix: A-tier spec drift for Cursor, Copilot and Cline (#1288)#1293
avifenesh merged 2 commits into
mainfrom
fix/a-tier-spec-drift-1288

Conversation

@avifenesh

Copy link
Copy Markdown
Collaborator

Closes #1288 — the six A-tier sources that had never been audited, and whose baselines were deliberately left stale in #1287 for exactly that reason.

Nine false positives, each reproduced against the doc's own example before fixing and re-verified after.

Copilot

COP-005 rejected the documented value and autofixed it wrong.

"add the excludeAgent keyword to the frontmatter block. Use either "code-review" or "cloud-agent"."

agnix listed coding-agent, which was never upstream. So the doc's own value errored — and worse, the unsafe autofix rewrote a correct value into the invalid one via find_closest_value.

56 occurrences across 22 files. Fixing only the const would have left every user-facing message wrong, so this covers the validator, all 12 locale files, both rules.json copies, and the knowledge-base docs. coding-agent is retained as a deprecated alias — users who followed agnix's own wrong advice shouldn't break — ordered last so the fixer prefers the documented spelling.

COP-003 rejected comma-separated applyTo.

"You can specify multiple patterns by separating them with commas." — with applyTo: "**/*.ts,**/*.tsx" as the example

The whole string went to glob::Pattern. The notable part: a correct brace/bracket-aware splitter already existed with 18 passing tests, marked #[allow(dead_code)] // reserved for future use and never called. The fix was wiring, not new logic.

Cursor

CUR-004 — same comma bug, same upstream wording ("Separate multiple patterns with commas"). Worth calling out why it survived: it was intermittently wrong. The doc's literal row docs/**/*.md, docs/**/*.mdx happens to parse as one pattern, while src/**, tests/** does not — so a fixture built from the doc's own example would have passed.

CUR-010 required version — documented as | version | number | 1 | Config schema version |, and several of the doc's examples omit it. Now optional, and typed number rather than integer, so 1.0 is valid.

CUR-012 required command on prompt hooks. The doc's prompt example has only type, prompt, timeout — "Prompt hooks use an LLM to evaluate a natural language condition". CUR-018 already checked prompt for those.

CUR-016 required install. I fetched the published schema rather than trusting the page:

root required: NONE          root unevaluatedProperties: False
def common: required=NONE    def container: required=[]
common props: install, name, ports, repositoryDependencies, start, terminals, user
'update' present? False
terminals items oneOf[0] type=array required=['command']
                oneOf[1] type=object required=['command']

So: nothing is required, the setup page's snapshot example legitimately omits install, terminal entries need only command (both oneOf branches), and update is not in the schema — which sets unevaluatedProperties: false, making it invalid. It's now reported as renamed to install rather than silently accepted.

Timeout units — two suggestions said milliseconds; the doc says "Execution timeout in seconds".

Cross-tool

CC-MEM-014 fired on Cursor .mdc files, reporting them as CLAUDE.md has N non-empty lines, exceeding the recommended 200 line limit — wrong filename, wrong threshold (Cursor documents 500), wrong tool attribution. ClaudeMdValidator is registered for FileType::CursorRule for its generic prose checks; the Claude-specific line rule is now gated on the file actually being a Claude memory file.

18 rules had redirecting evidence URLs — Copilot 301s to /how-tos/provide-context, Cline 308s to /customization/cline-rules. Both verified with curl and refreshed in rules.json and spec-baselines.json, so those two sentinel sources can match again.

Where the two audits disagreed

One auditor reported comma-globs as a five-tool class including Cline and Claude rules. I checked each upstream doc separately rather than applying it as a blanket change:

tool field verdict
Cursor globs scalar string comma-separated — fixed
Copilot applyTo string comma-separated — fixed
Cline paths YAML array not comma-separated; scalar form already errors — no change
Claude rules paths YAML array array form documented — no change

Splitting on commas for Cline or Claude would have been wrong. The docs settled it per tool.

Deliberately not done

User-global paths (~/.copilot/, ~/Documents/Cline/Rules, ~/.agents/AGENTS.md) are outside the project tree agnix walks. Adding rules there is a scope decision, not a drift fix — worth a separate issue if wanted.

Baseline hashes stay stale until this merges; re-cutting them is a separate reviewable change, same as #1287.

Verification

5060+ tests / 37 binaries green · eval 61/61 · self-lint clean · cargo fmt --check and cargo clippy --all-targets -- -D warnings clean · bookkeeping, locale sync and rule counts in sync at 442 · actionlint + shellcheck clean.

Every fix reproduced before and re-verified after; new tests assert both directions (documented value accepted and the genuinely invalid one still rejected).

Closes #1288. Audited the six A-tier sources that had never been reviewed
and whose baselines were left stale in #1287 for exactly that reason. Nine
false positives, each reproduced against the doc's own example before
fixing and re-verified after.

Copilot:

- COP-005 listed `coding-agent`, which was never an upstream value. The
  documented pair is `code-review` / `cloud-agent`, so the doc's own value
  errored - and the unsafe autofix rewrote a correct value into the invalid
  one via find_closest_value. `coding-agent` stays as a deprecated alias so
  configs written against agnix's own wrong advice keep working, ordered
  last so the fixer prefers the documented spelling. 56 occurrences across
  22 files: fixing only the const would have left every user-facing message
  wrong.
- COP-003 rejected comma-separated `applyTo`, which the doc documents with
  `"**/*.ts,**/*.tsx"` as its example. A correct brace/bracket-aware
  splitter already existed with 18 passing tests, marked
  `#[allow(dead_code)] // reserved for future use` and never called. The fix
  was wiring, not new logic.

Cursor:

- CUR-004 had the same comma bug. Notable because it was *intermittently*
  wrong: the doc's literal row parses as one pattern while `src/**, tests/**`
  does not, so a fixture built from the doc's own example would have passed.
- CUR-010 required `version`, documented with a default of 1 and omitted by
  several of the doc's examples. Now optional, and typed number rather than
  integer.
- CUR-012 required `command` on every hook entry, so the documented prompt
  hook - `type`, `prompt`, `timeout`, no `command` - errored. CUR-018 already
  covers `prompt` for those.
- CUR-016 required `install`. Fetched the published schema: no `required`
  array in `definitions.common`, `required: []` in `definitions.container`,
  and the setup page's snapshot example omits it. Terminal entries now need
  only `command`, the array `oneOf` branch is accepted, and `update` - absent
  from a schema that sets unevaluatedProperties: false - is reported as
  renamed rather than blessed.
- Two timeout suggestions said milliseconds; the doc says seconds.

Cross-tool:

- CC-MEM-014 fired on `.cursor/rules/*.mdc`, reporting a Cursor file as
  "CLAUDE.md" against Claude's 200-line limit where Cursor documents 500.
  ClaudeMdValidator is registered for CursorRule for its generic prose
  checks; the Claude-specific line rule is now gated on the file actually
  being a Claude memory file.
- 18 rules carried evidence URLs that redirect (Copilot 301, Cline 308).
  Refreshed in rules.json and spec-baselines.json so those two sentinel
  sources can match again.

Cline needed no rule change: `paths` is documented as a YAML array and the
scalar form is already reported, so comma-splitting there would be wrong -
the two audits disagreed on this and the docs settled it per tool rather
than as a blanket change.

Deliberately not done: user-global paths (`~/.copilot/`, `~/Documents/Cline/
Rules`, `~/.agents/AGENTS.md`) are outside the project tree agnix walks, so
adding rules there is a scope decision rather than a drift fix.
Copilot AI review requested due to automatic review settings July 31, 2026 14:37

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Claude finished @avifenesh's task in 12m 13s —— View job


Review: A-tier spec drift (#1293)

Nine claimed false positives; I re-fetched every upstream source rather than taking the PR body's quotes on trust, and re-ran the affected paths against the built CLI. Seven of the nine are correct and well-evidenced. Two changes are wrong in a way that reintroduces the exact class of bug being fixed, and one has no test at all.

  • Read full diff (48 files)
  • Verify validator correctness against upstream (COP-003/005, CUR-004/010/012/016, CC-MEM-014)
  • Re-fetch cited sources (Copilot instructions page, Cursor rules/hooks pages, published environment schema)
  • Check test bidirectionality
  • Check bookkeeping parity
  • Build + run tests (cargo test --workspace: all green, 0 failures; sync-rule-bookkeeping.js --check: in sync)

Findings

1. COP-005: the unsafe autofix can still write coding-agent (inline) - CONFIRMED, reproduced

VALID_AGENTS (copilot.rs:1309) doubles as the acceptance list and the fix-candidate list, and find_closest_value falls back to substring matching. excludeAgent: "coding"--fix-unsafe rewrites it to "coding-agent" while the message on the very same diagnostic says the valid values are code-review/cloud-agent. That is the PR's own headline bug, reached through a different input. The "ordered last" comment only settles ties. Split into DOCUMENTED_AGENTS (fix candidates) and ACCEPTED_AGENTS (input tolerance). None of the three new COP-005 tests assert the replacement text, which is why this survived.

Related, softer: the alias is accepted with no diagnostic at all, so a user who followed agnix's old advice ships a file Copilot won't honour and never hears about it - and rules.json cites no source for the value. A LOW deprecation notice would keep them working and give the leniency an evidence trail.

2. CUR-016 terminals: the oneOf was misread, and the two branches are now inverted (inline) - CONFIRMED against the published schema + CLI

I fetched cursor.com/schemas/environment.schema.json: the oneOf is on the terminals value - branch 1 is an array of objects each requiring command, branch 2 is a single object requiring command. There is no array-of-arrays branch. Result on this branch:

input schema agnix after this PR
{"terminals":[[{"name":"x"}]]} invalid silently clean
{"terminals":{"command":"npm run dev"}} valid error: 'terminals' must be an array, got object

So a false negative was added and a false positive of exactly the class this PR targets was left in place. The name/description relaxation itself is right. Same misreading is repeated in the code comment (cursor.rs:874-877, 893-895), the CHANGELOG (the array branch is accepted) and VALIDATION-RULES.md CUR-016 (the oneOf also permits an array branch) - all four need the same correction. No test covers either form.

3. CC-MEM-014 gating has no test, and introduces a second definition of "Claude memory file" (inline) - CONFIRMED by reading

Nothing asserts a >200-line .mdc is now exempt, and nothing asserts CLAUDE.local.md still fires; the three existing tests all pass Path::new("CLAUDE.md") and passed before the change too (CLAUDE.md rule 7). The gate is also derived independently of the is_claude_md computed 200 lines earlier - it agrees only because of the early return, since detect_file_type maps AGENTS.md/AGENTS.local.md/AGENTS.override.md to FileType::ClaudeMd too. Reuse is_claude_md.

4. Same class, not covered: CC-MEM-009 - PLAUSIBLE, by reading

CC-MEM-009 (applies_to: claude-code, source code.claude.com/docs/en/memory) also runs on .cursor/rules/*.mdc and .cursorrules via the same registration, applying Claude's token budget to Cursor files. Its message is tool-neutral (File exceeds recommended token limit), so it doesn't misattribute the way CC-MEM-014 did - but by this PR's own reasoning it is the same defect. Either gate it alongside CC-MEM-014 or say why the budget is generic. CC-MEM-004/005/006/007/008/010 are genuinely tool-neutral prose/IO checks - no change needed there.

5. Nits (behavioural only where noted)

  • Dead locale keys: rules.cur_016.missing_install and rules.cur_016.update are now unreferenced in all four en.yml copies. check-locale-sync.sh only diffs files across crates, so nothing catches them.
  • install match (cursor.rs:774-795) now has None => {} plus _ => {}; the adjacent start check two lines below uses if let ... && .... Collapse for consistency.
  • is_prompt_hook (cursor.rs:384) and is_prompt_type (cursor.rs:495) compute the same predicate in one loop.
  • CUR-017's touched suggestions are hardcoded English ("Set 'timeout' to a positive number of seconds.") while everything around them uses t!. Since the lines were edited anyway, moving them into locales/ keeps i18n complete.
  • terminals[%{index}] is emitted with index + 1, so the first element reports as terminals[1]. Pre-existing, but the message text was rewritten here.
  • GlobsField::patterns() kept its old summary line above the new doc block, so rustdoc now reads "Get all glob patterns as a vector Individual glob patterns."
  • CUR-016's update is emitted at warning level for a rule whose rules.json severity is HIGH and a value that is schema-invalid. Deliberate?

What I verified as correct

Upstream re-fetched today, all four sources back the PR's claims verbatim:

  • Copilot instructions page: excludeAgent - "Use either "code-review" or "cloud-agent"", exactly two values, no coding-agent; applyTo - "You can specify multiple patterns by separating them with commas" with "**/*.ts,**/*.tsx". COP-005 and COP-003 diagnoses are right.
  • Cursor rules page: "Separate multiple patterns with commas", example docs/**/*.md, docs/**/*.mdx. CUR-004 is right, including the observation about why it was intermittent.
  • Cursor hooks page: version is "Type: number, Default: 1"; the prompt-hook example carries only type/prompt/timeout (+ optional model); timeout is "Execution timeout in seconds". CUR-010, CUR-012 and the two unit-string fixes are right. One caveat on the PR body's wording: the page's examples do consistently include version - the support for it being optional is the documented default, not the examples. CUR-018 does cover prompt hooks missing prompt (warning-level, contains_key only, so prompt: "" slips through - pre-existing).
  • Published environment schema: no required anywhere, install optional, update absent, root unevaluatedProperties: false, and snapshot/build are defined - so the new snapshot test is consistent with treating update as invalid.

Also checked:

  • The splitter wiring is the right fix, not new logic; brace/bracket depth handling is correct, and both COP-003 and CUR-004 have accept-and-still-reject pairs naming the bad segment.
  • The per-tool comma table holds: CLN-004 does already reject scalar paths, so leaving Cline and Claude rules alone was the correct call.
  • No live coding-agent reference survives outside the CHANGELOG and archived versioned_docs/; all 12 locale copies and both rules.json mirrors are consistent.
  • es.yml/zh-CN.yml not carrying the new cur_010/cur_016 keys is fine - i18n!("locales", fallback = "en").
  • cargo test --workspace green (4061 in agnix-core lib + the rest, 0 failures); node scripts/sync-rule-bookkeeping.js --check reports everything in sync at 442.

Not verified: the two redirect claims (301/308) - I did not re-run curl against the old URLs, though both new URLs fetch as live pages with the quoted content. The stale baseline hashes are as the PR describes and will keep the sentinel check red until re-cut.

Findings 1 and 2 are the blocking ones; 3 is required by CLAUDE.md rule 7. The rest of the diff is accurate work with unusually good evidence discipline.
| Branch fix/a-tier-spec-drift-1288

Comment thread crates/agnix-core/src/rules/copilot.rs Outdated
// here only so configs written against agnix's own previous
// wrong advice keep validating, and it is deliberately last
// so `find_closest_value` prefers the documented spelling.
const VALID_AGENTS: &[&str] = &["code-review", "cloud-agent", "coding-agent"];

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 unsafe autofix can still write coding-agent - the same failure mode this PR is fixing, just reached by a different input.

VALID_AGENTS is used both as the acceptance list here and as the fix-candidate list at line 1331. find_closest_value falls back to a substring match, so any invalid value that matches only the deprecated alias gets rewritten to it. The "ordered last" comment only decides ties.

Reproduced on this branch with the built CLI:

$ cat .github/instructions/x.instructions.md
excludeAgent: "coding"

$ agnix . --fix-unsafe
error: Invalid excludeAgent value 'coding'. Valid values: 'code-review', 'cloud-agent' [fixable]
  Fixed: - Change excludeAgent to 'coding-agent'

$ cat .github/instructions/x.instructions.md
excludeAgent: "coding-agent"

So the message names the two documented values and the fix then writes the third, undocumented one. Splitting the lists fixes it:

/// Documented upstream - the only values the fixer may produce.
const DOCUMENTED_AGENTS: &[&str] = &["code-review", "cloud-agent"];
/// Additionally tolerated on input: written against agnix's own previous wrong advice.
const ACCEPTED_AGENTS: &[&str] = &["code-review", "cloud-agent", "coding-agent"];

then ACCEPTED_AGENTS.contains(...) at the check and find_closest_value(value, DOCUMENTED_AGENTS) at the fix. A test asserting the replacement text (not just that a diagnostic exists) would have caught this - the three new COP-005 tests all assert presence/absence only.

Separately: accepting coding-agent silently means a user who followed the old advice gets no signal that Copilot won't honour their file, and rules.json cites no source for the value. A LOW/INFO deprecation diagnostic would keep them working and tell them.

Comment thread crates/agnix-core/src/rules/cursor.rs Outdated
Comment on lines +892 to +895
} else if terminal.as_array().is_none() {
// The `oneOf`'s other branch is an array of
// command-bearing objects, so a nested list is valid
// even though it reads oddly.

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 oneOf is on the terminals value, not on its items - so this branch accepts what the schema rejects, while the form the schema accepts is still rejected at line 909.

I fetched cursor.com/schemas/environment.schema.json to check:

  • branch 1: terminals is an array of objects, each requiring command
  • branch 2: terminals is a single object requiring command

There is no "array of arrays" branch. Both directions verified against the built CLI on this branch:

// invalid per schema - agnix reports nothing at all
{ "terminals": [ [ { "name": "no-command-here" } ] ] }

// valid per schema (branch 2) - agnix: error "Field 'terminals' must be an array, got object"
{ "install": "npm ci", "terminals": { "command": "npm run dev", "name": "dev" } }

The name/description relaxation above is right; this escape hatch is not. Suggested shape: drop the as_array() exemption (a nested array is terminal_not_object), and handle JsonValue::Object at line 909 by running the same command check instead of erroring. The comments at lines 874-877 and 893-895, the CHANGELOG line ("the array branch is accepted") and VALIDATION-RULES.md CUR-016 ("the oneOf also permits an array branch") all repeat the same misreading and need the same correction.

Neither form has a test - test_cur_016_terminal_name_is_optional and test_cur_016_terminal_command_still_required both use the array-of-objects branch only.

// file was reported as "CLAUDE.md has N lines" against Claude's limit -
// wrong filename, wrong threshold (Cursor documents 500), wrong tool.
// Gated on the file actually being a Claude memory file.
let is_claude_memory = matches!(crate::detect_file_type(path), crate::FileType::ClaudeMd);

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 gate is correct today, but it is the second, differently-derived notion of "Claude memory file" in this one function.

Line 69 already computed is_claude_md = matches!(filename, "CLAUDE.md" | "CLAUDE.local.md"), and the early return at line 71 is the only reason this new gate agrees with it: detect_file_type also maps AGENTS.md, AGENTS.local.md and AGENTS.override.md to FileType::ClaudeMd (file_types/detection.rs:351), and the comment at line 67 says CC-MEM rules deliberately skip AGENTS.*. If line 69 ever gains a filename, CC-MEM-014's scope changes silently in the opposite direction. if is_claude_md && config.is_rule_enabled(...) says the same thing with no second source of truth and no path re-parse.

More important: there is no test for this change in either direction. Nothing asserts that a .cursor/rules/*.mdc over 200 lines no longer reports CC-MEM-014, and nothing asserts CLAUDE.local.md still does - the three existing tests (lines 1099-1153) all pass Path::new("CLAUDE.md"), which was passing before the change too. Per CLAUDE.md rule 7 this needs the pair.

…als (#1288)

Review found the COP-005 fix reintroduced its own bug through a different
input, and a real gap in the terminals branch.

VALID_AGENTS doubled as the acceptance list and the fix-candidate list, and
find_closest_value also does substring matching. So `excludeAgent: "coding"`
was "fixed" to `coding-agent` while the message on the same diagnostic said
the valid values are code-review/cloud-agent - exactly the bug this rule was
corrected for. Reproduced with --fix-unsafe before fixing. Split into
DOCUMENTED_AGENTS (fix candidates) and ACCEPTED_AGENTS (input tolerance);
`coding` now reports without a fix rather than being guessed at, which is the
right outcome for an input that matches no documented value.

The three new COP-005 tests all asserted that a diagnostic fired and none
asserted its replacement text, which is why this survived. Added a test that
checks the fix text directly.

The legacy spelling is also now reported at info level instead of accepted
silently. A user who followed agnix's old advice was shipping a file Copilot
will not honor and hearing nothing about it.

Terminals: the reviewer read the `oneOf` as sitting on the `terminals` value
rather than on `items`. I re-fetched the schema and the original reading was
right - branch 1 is an array of command-bearing objects, branch 2 is one such
object - so the branches are not inverted. But accepting the array branch
without checking inside was a real gap: `[[{"name":"x"}]]` passed with no
`command`. Its contents are now validated, with a test for both directions
and one for the scalar case that matches no branch.
Copilot AI review requested due to automatic review settings July 31, 2026 15:01

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@avifenesh

Copy link
Copy Markdown
Collaborator Author

Fixed in 6750bf1. One of your two "wrong" findings was right and one wasn't — details below, with the measurement for each.

1, the autofix could still write coding-agent. Correct, and it's the PR's own headline bug through a different input. Reproduced with --fix-unsafe:

before:  excludeAgent: "coding"  ->  autofix wrote "coding-agent"
         (while the same diagnostic said: Valid values: 'code-review', 'cloud-agent')
after:   excludeAgent: "coding"  ->  reported, no fix offered

Split into DOCUMENTED_AGENTS (fix candidates) and ACCEPTED_AGENTS (input tolerance), exactly as you suggested. coding now matches no documented value so no fix is guessed — the right outcome.

Your diagnosis of why it survived was the useful part: all three new COP-005 tests asserted a diagnostic fired, none asserted the replacement text. Added a test that checks the fix text directly.

Also took the softer point — the legacy spelling now reports at info level instead of being accepted silently. A user who followed agnix's old advice was shipping a file Copilot won't honor and hearing nothing.

2, terminals oneOf — I don't think this one holds. I re-fetched the schema and dumped the structure:

"terminals": { "type": "array", "items": { "oneOf": [
  { "type": "array", "items": { "type":"object", "required":["command"] } },
  { "type": "object", "required": ["command"] } ] } }

The oneOf is on items, not on the terminals value, and branch 1 is an array of command-bearing objects. So the branches aren't inverted and there is an array-of-arrays branch.

But you found a real gap next to it: I accepted the array branch without checking inside, so [[{"name":"x"}]] passed with no command. Now validated, with tests for both directions plus the scalar case that matches no branch.

If you're reading the schema differently, point me at the path — I'm working from cursor.com/schemas/environment.schema.json fetched today, and I'd rather be corrected than leave a wrong reading in a comment.

Verified: 37 binaries green, eval 61/61, fmt + clippy -D warnings clean, bookkeeping and locales in sync.

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Claude finished @avifenesh's task in 4m 38s —— View job


Review: follow-up push 6750bf10

  • Read the new commit diff
  • Re-fetch cursor.com/schemas/environment.schema.json and settle the oneOf disagreement
  • Verify the COP-005 list split and the fix-text test
  • Re-check the two findings the push did not touch (CC-MEM-014 test gap, CC-MEM-009)
  • Sweep for the same class elsewhere (locale strings, docs, dead keys)
  • cargo test --workspace (0 failures), cargo clippy --all-targets -- -D warnings (clean), sync-rule-bookkeeping.js --check (in sync at 442)

You were right about the oneOf, I was wrong

I re-fetched cursor.com/schemas/environment.schema.json myself rather than take either reading on trust. The oneOf is on terminals.items, not on the terminals value:

"terminals": { "type": "array", "items": { "oneOf": [
  { "type": "array",  "items": { "type": "object", "required": ["command"] } },
  { "type": "object", "required": ["command"] } ] } }

So terminals itself is "type": "array", the array-of-arrays branch is real, and {"terminals": {"command": "..."}} is invalid — agnix's must be an array, got object was correct all along. My "the branches are inverted" claim and the two table rows under it were wrong, and the corrections I asked for in the CHANGELOG and VALIDATION-RULES.md CUR-016 (the oneOf also permits an array branch) were unnecessary — that wording is accurate. Nothing there needs changing.

The part that did hold — accepting the array branch without looking inside — is fixed at cursor.rs:892-915, with all three directions tested: nested-with-command accepted, nested-without rejected, scalar rejected. That closes it.

COP-005 is correct now. DOCUMENTED_AGENTS / ACCEPTED_AGENTS split, find_closest_value fed only documented values, and test_cop_005_autofix_never_writes_an_undocumented_value asserts the replacement text rather than mere presence — which is what let the bug through before. The info-level legacy diagnostic is a better answer than silent acceptance.


Still open from the last review

A. CC-MEM-014 has no test in either direction (finding 3, untouched by this push) — CONFIRMED by reading claude_md.rs:1097-1153

All three CC-MEM-014 tests pass Path::new("CLAUDE.md") and passed identically before the gate existed. Nothing asserts a >200-line .cursor/rules/*.mdc is now exempt, and nothing asserts CLAUDE.local.md still fires. CLAUDE.md rule 7 wants the pair. (The gate itself I re-verified as behaviourally correct today — the early return at line 71 keeps AGENTS.* out — so this is purely the missing test.) Fix this →

B. CC-MEM-009 is the same defect, uncorrected (finding 4) — CONFIRMED

claude_md.rs:99-117 runs unconditionally for FileType::CursorRule, and rules.json:2320-2332 gives it applies_to.tool: claude-code with source code.claude.com/docs/en/memory — identical provenance to CC-MEM-014. Its message is tool-neutral so it doesn't misattribute, but it applies Claude's token budget to Cursor files. Either gate it next to CC-MEM-014 or state in the CHANGELOG why the budget is generic. CC-MEM-004/005/006/007/008/010 are genuinely tool-neutral — no change needed there.


New, and the same class as the fix (sweep result)

C. rules.cur_016.message still describes the pre-fix spec — CONFIRMED, live code path

message: .cursor/environment.json must be an object with install (required) and optional start, update, build, terminals

It is emitted at cursor.rs:766 whenever the root isn't an object, so it's user-facing. It states install is required — the false positive this PR removed — and lists update among the valid optional fields, which the same PR now reports as schema-invalid (update_renamed). Present in all four copies: locales/en.yml:502, crates/agnix-{cli,core,lsp}/locales/en.yml:502. es.yml/zh-CN.yml have no cur_016 block, so the fallback carries it too.

While in that block, two now-dead keys: missing_install (line 505) and update (line 509) have no remaining t! reference — the only live update-related keys are update_renamed/update_renamed_suggestion. check-locale-sync.sh only diffs the copies against each other, so nothing flags them. Same four files. Fix this →


Nits (behavioural, low)

  • The new legacy-agent info diagnostic (copilot.rs:1318) is anchored at parsed.start_line + 1, while the error branch two lines below searches parsed.raw for the actual excludeAgent line. A user with a multi-key frontmatter gets the info pointed at the fence, not the field.
  • Inside the nested-array branch, each bad inner entry pushes a diagnostic labelled with the outer index + 1, so [[{"a":1},{"b":2}]] yields two byte-identical diagnostics on the same line. An inner index in the message, or one diagnostic per outer entry, reads better.
  • Carried over, unchanged: terminals[%{index}] uses index + 1 so the first entry reports as terminals[1]; is_prompt_hook/is_prompt_type duplicate one predicate; CUR-017's suggestions are hardcoded English; GlobsField::patterns()'s doc comment now reads as two run-on sentences.

Verified clean

cargo test --workspace — 0 failures across all binaries. cargo clippy --all-targets -- -D warnings — clean. node scripts/sync-rule-bookkeeping.js --check — rule count tables and all bookkeeping in sync at 442. The seven findings I confirmed correct last round are unchanged and still correct; the eighth (terminals) I retract as described above.

Blocking, in my read: A (CLAUDE.md rule 7). B and C are judgment calls, but C is a user-facing message asserting the exact behaviour this PR removed, so it would be odd to ship the fix with it.
| Branch fix/a-tier-spec-drift-1288

@avifenesh
avifenesh merged commit 6ffaaa4 into main Jul 31, 2026
15 checks passed
@avifenesh
avifenesh deleted the fix/a-tier-spec-drift-1288 branch July 31, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Audit the six A-tier spec-drift sources (Cursor, Copilot, Cline)

2 participants