Skip to content

feat(rules): compose multiple root rules safely - #2395

Merged
dyoshikawa merged 8 commits into
dyoshikawa:mainfrom
rudironsoni:fix/multiple-root-rulesync-rules-found-for-target
Jul 29, 2026
Merged

feat(rules): compose multiple root rules safely#2395
dyoshikawa merged 8 commits into
dyoshikawa:mainfrom
rudironsoni:fix/multiple-root-rulesync-rules-found-for-target

Conversation

@rudironsoni

@rudironsoni rudironsoni commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • allow multiple matching root: true rules in project and global modes
  • compose compatible root and single-file outputs while preserving deterministic source order
  • preserve distinct target-native paths for modular targets such as Cursor, AugmentCode, and Takt
  • reject unsafe same-path collisions involving a source root: true rule before independently rendered metadata can be corrupted
  • warn and preserve existing last-writer behavior for pre-existing non-root exact-path and case-insensitive collisions
  • use one explicit collision policy while retaining each generated rule's source provenance
  • check collisions after conventions, MCP files, and root mirrors are added
  • document lexicographic source-path ordering and synchronize the documentation skill mirror
  • cover every supported rules target plus focused project and global E2E composition cases

Behavior change

Multiple root fragments targeting the same compatible output are composed with a blank line between fragments. Explicitly supported plain-Markdown modular outputs are also composed. Unsafe same-path collisions involving a source root: true rule fail before writing and report the generated path and originating .rulesync/rules/*.md files.

Pre-existing collisions between non-root modular rules remain backward compatible: Rulesync retains both generated outputs and warns that the last write wins wherever exact or case-insensitive paths collide.

Test plan

  • mise exec -- pnpm exec vitest run src/features/rules/rules-processor.test.ts --silent=false
  • NODE_ENV=e2e mise exec -- pnpm exec vitest run --config vitest.e2e.config.ts src/e2e/e2e-rules.spec.ts --silent=false
  • mise exec -- pnpm cicheck
  • git diff --check

Closes #2393

Copilot AI review requested due to automatic review settings July 26, 2026 17:08

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.

@rudironsoni rudironsoni changed the title feat(rules): compose multiple global root rules feat(rules): compose multiple root rules by output path Jul 26, 2026
Copilot AI review requested due to automatic review settings July 26, 2026 17:50

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.

Copilot AI review requested due to automatic review settings July 26, 2026 18:30

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.

@rudironsoni rudironsoni changed the title feat(rules): compose multiple root rules by output path feat(rules): compose multiple root rules safely Jul 26, 2026

@dyoshikawa dyoshikawa left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Before the code: #2393 was filed by the same account as this PR and nobody else has commented on it, so the acceptance criteria in it are your design rather than something we agreed on. Removing the one-root-per-target restriction is a deliberate guardrail that came out of #1247, and this PR also turns two previously-silent situations into hard failures. That is a product decision I need to make explicitly, so please treat the direction as open even though the implementation is solid. Happy to discuss it on the issue.

On the implementation itself, the core is genuinely good. I verified the premise (main really does throw on two matching root rules), and I looked hard at the test diff since that is where things usually go wrong: the six removed lines are the two rejects.toThrow assertions, and they were replaced with stronger positive assertions on count, bodies and order, not softened. The e2e matrix tests are untouched, the docs mirrors are in sync, and I confirmed that root composition cannot produce a file with frontmatter in the middle — every output that can land in a merge group is frontmatter-free, and every frontmatter-bearing output is isRoot() === false and therefore hits the reject path. Nice invariant.

The blocker is the case-only collision check. It is not scoped to groups that are actually merging, so it fires on ordinary unrelated rules and takes down the whole generate run. Details inline. Beyond that there is a trim regression for folding targets, an error message that cannot be traced back to source files, and a couple of smaller things.

One more that is not in the diff but becomes visible because of it: applyRootRuleSections injects the reference section and Additional Conventions into toolRules.find(rule => rule.isRoot()), i.e. only the first root. Fixed-root targets are fine since they end up with one file, but on cursor/augmentcode/takt with multiple surviving roots those sections now land in whichever rule sorts first by filename rather than in the overview. On cursor that can mean the memory-file references end up in a rule that is not always applied. Worth handling or at least documenting.

CI is green on all five jobs and I found nothing concerning security-wise: no network calls, no env/credential access, no CI or dependency changes.

Comment thread src/features/rules/rules-processor.ts Outdated
Comment thread src/features/rules/rules-processor.ts
Comment thread src/features/rules/rules-processor.ts Outdated
Comment thread src/e2e/e2e-rules.spec.ts Outdated
Comment thread docs/reference/file-formats.md Outdated
Comment thread src/features/rules/rules-processor.ts Outdated
@dyoshikawa

Copy link
Copy Markdown
Owner

Correction to my review: scratch the paragraph about applyRootRuleSections landing the reference section in the wrong file. I checked and it does not happen — cursor, augmentcode, aiassistant, takt and devin all set root: false in fromRulesyncRule, so applyRootRuleSections hits its early return on those targets and injects nothing, identically on main and on this branch. Fixed-root targets have already been merged to a single file by then. Nothing for you to do there.

It is latent rather than active, though: if a target ever emitted two isRoot() === true rules on distinct paths, that find() would silently pick one, and this PR is what makes multiple roots reachable. A short comment noting the assumption would be worth having.

The rest of the review stands.

Copilot AI review requested due to automatic review settings July 27, 2026 09:12

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.

@rudironsoni

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review. I agree that the case-only check was too broad and that its policy had not been established. I removed it, so unrelated outputs such as API.md and api.md retain their existing behavior. The narrower exact-path guard remains because independently rendered modular files resolving to the same path were already subject to silent last-writer-wins content loss. That error now identifies the original .rulesync/rules/*.md sources.

I also restored trimming for singleton folding groups, separated the global composition coverage from the target matrix, documented the lexicographic source-path ordering, changed the helper to an object argument, and documented the single-surviving-root invariant in applyRootRuleSections.

The focused processor suite passes 106 tests, the rules E2E suite passes 120 tests, and the full pnpm cicheck passes.

On the product decision, I agree that #2393 should not be treated as previously accepted direction. I do think multiple matching roots are the better supported behavior under this narrower policy.

The restriction retained by #1247 validates source cardinality before target conversion. That was a useful guardrail when Rulesync could not distinguish whether two roots would conflict, but the target adapters already contain the information needed to make that decision at the actual output boundary. This change generalizes #1247's target-aware behavior into output-aware behavior:

  • Fixed-root targets compose rendered fragments into their one native root file.
  • Modular targets preserve distinct native files.
  • Multiple modular rules resolving to the same exact output path fail explicitly instead of losing content.
  • Single-root configurations and unrelated case-only paths keep their existing behavior.

This lets users split always-on instructions into independently maintained source files without forcing them to concatenate those sources manually for single-file tools. It also preserves each target's native model instead of imposing one source-file cardinality rule across every adapter. Ordering is deterministic and documented, and the collision rule now covers only the concrete content-loss case.

With that narrowed scope, would you be comfortable treating multiple matching roots as supported behavior?

@rudironsoni
rudironsoni force-pushed the fix/multiple-root-rulesync-rules-found-for-target branch from 8d4596a to 4ab1793 Compare July 27, 2026 11:16

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.

@dyoshikawa dyoshikawa left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for the rework — the case-only check is gone, the folding-target trim regression is fixed, and the error message now names the originating .rulesync/rules/*.md files. Those all look right, and I re-checked my own earlier claim about applyRootRuleSections: the "at most one root survives" comment does hold, since every rule class derives its root path from a fixed per-scope constant.

Two things still block this, both in mergeRulesByOutputPath.

The reject condition is !rootRule && !mergeNonRootRules, so a group that happens to contain a root rule is merged unconditionally, even on targets that cannot compose modular files. Kiro in global mode is the concrete case: the global root is .kiro/steering/product.md and non-root steering files land in the same directory, so a non-root rule named product.md gets concatenated onto the root and the generated file ends up with an inclusion: frontmatter block sitting in the middle of it. That is exactly the malformed output this PR sets out to prevent, and it is new — on main the same config produced one intact file via last-writer-wins. kiro-cli and kiro-ide share the class.

In the other direction the reject is too broad. Two non-root agentsmd rules sharing an agentsmd.subprojectPath produce plain frontmatter-less Markdown that composes perfectly well, but they now throw, and the message says the target "does not support composing modular rule files", which is not true for that target. The same applies to amp, factorydroid, kilo and opencode. And since generateRulesCore has no per-target try/catch, one devin kebab-case collision (CodingGuidelines.md + coding_guidelines.md — plausible in a real repo, and the shape your own test uses) fails the whole generate run including every other target and feature. Deciding by "does this output carry frontmatter" rather than "is there a root in the group" would fix both directions at once.

The new ordering contract in the docs also does not match loadRulesyncFiles, which concatenates [...discoveredFiles, ...discoveredCuratedFiles] — see the inline notes.

Smaller things, no need to block on them but worth a pass: it.each(["devin", "antigravity-ide"] as const) uses $toolTarget in the title, which only expands for object arrays, so both cases print the same name; the four new tests around lines 1287-1443 sit under the global-mode describe but do not pass global; and the group.length === 1 branch produces the same result as the general merge path below it, so it can collapse to just the !mergeNonRootRules early-continue.

One process note rather than a code one: #2393 is your own issue, so the "allow multiple roots" direction has not actually been agreed on our side yet. I'm fine with it in principle given the fail-closed framing, but it is a behavior change for existing configs that previously generated silently, so it needs a release-note callout.

Comment thread src/features/rules/rules-processor.ts Outdated
Comment thread src/features/rules/rules-processor.ts Outdated
Comment thread docs/reference/file-formats.md Outdated
Comment thread docs/guide/global-mode.md Outdated
Copilot AI review requested due to automatic review settings July 27, 2026 14:42
@rudironsoni
rudironsoni force-pushed the fix/multiple-root-rulesync-rules-found-for-target branch from 4ab1793 to d7fd3b3 Compare July 27, 2026 14:42

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.

@rudironsoni

Copy link
Copy Markdown
Contributor Author

@dyoshikawa do not merge, let's test this thoroughly and assert the intended product decision first!

@dyoshikawa dyoshikawa left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I ran another pass over this, and I want to answer your "assert the intended product decision first" directly rather than merge it.

The composition half is sound. I re-derived the invariant your new comment at applyRootRuleSections claims and it holds across every adapter: buildToolRuleParamsDefault always routes root: true to the target's fixed root path, so a target can only ever have one root group, and every frontmatter-emitting adapter writes plain Markdown on its root branch — so concatenating root fragments can't inject a --- block mid-body. Ordering is genuinely deterministic (findFilesByGlobs sorts, then local before .curated/), which matches what you wrote in the docs. And I checked the test diff against main: the only two assertions you removed are the ones for the error this PR intentionally retires, and their replacements assert more than the originals did. No silent weakening, nothing suspicious anywhere in the diff.

What I'm not ready to sign off on is the second half. The hard error for modular same-path collisions is a separate decision from "multiple roots may compose", and it's the part #2393 never asked for. Removing the old Multiple root rulesync rules found guard newly exposes exactly two cases (takt's takt.name override, global kiro's fixed product.md); the non-root collisions the new error covers were already reachable on main, where they warned-or-overwrote rather than aborted. So this PR is converting a pre-existing silent-overwrite path into a generate-stopping exception, for targets that had nothing to do with the root-composition feature. That's defensible — an error beats losing a rule file — but it's my call to make, not something to fold in, and right now it's coupled to the feature so I can't take one without the other. I'd rather see the collision policy split out into its own issue and PR.

Two more things worth settling before either half lands. The guard is case-sensitive, so the most likely real-world collision slips through it, and the new test pins that silence as spec — details on the line. And it runs mid-way through convertRulesyncFilesToToolFiles, before appendSeparateConventionsRule and the root-mirror pushes, so it doesn't actually cover everything that reaches the writer; a check over the final file list would be both simpler and complete, and would let the merge function stop throwing.

Smaller stuff: the new mergeRulesByOutputPath JSDoc drops the tool-specific rationale the old comment carried; .rulesync/rules/overview.md:2 still has the old "less than or equal to one file" wording that you updated in both file-formats.md copies, and that file is what people copy as a template; and the docs now promise local-before-.curated/ ordering with no test pinning it. Nothing blocking, but the overview.md one is a real inconsistency now.

So: holding as you asked. Once we've settled whether the collision error ships here or separately, the rest is close.

Comment thread src/features/rules/rules-processor.ts Outdated
Comment thread src/features/rules/rules-processor.test.ts Outdated
Comment thread src/features/rules/rules-processor.ts Outdated
Comment thread src/features/rules/rules-processor.ts Outdated
Comment thread src/features/rules/rules-processor.ts Outdated
Comment thread src/features/rules/rules-processor.test.ts Outdated
Copilot AI review requested due to automatic review settings July 28, 2026 12:21
@rudironsoni
rudironsoni force-pushed the fix/multiple-root-rulesync-rules-found-for-target branch from d7fd3b3 to ee1a42a Compare July 28, 2026 12:21

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.

@rudironsoni

Copy link
Copy Markdown
Contributor Author

Implemented the requested product-policy split in ee1a42a9.

Unsafe same-path groups involving a source root: true rule still fail. Explicitly safe plain-Markdown outputs compose. Pre-existing non-root exact-path and case-insensitive collisions now warn and continue with last-writer behavior. Collision detection runs over the final generated list, the factory uses one collision policy, and source provenance stays paired through conversion.

I also aligned the template, canonical docs, generated skill mirrors, PR body, test placement, five-adapter composition matrix, case-warning coverage, and local-before-curated ordering coverage.

Validation after rebasing onto current upstream/main:

  • processor suite: 118 passed
  • rules E2E suite: 121 passed
  • pnpm cicheck: 342 files and 7,778 tests passed, with all content checks green
  • git diff --check: passed

The broader decision to hard-fail ordinary modular collisions is intentionally deferred.

@dyoshikawa dyoshikawa left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for the rework — I went back through all six points from the last round and they are genuinely addressed. The hard error is scoped to groups involving a source root: true rule now, so the devin/copilot/.curated/ collisions that used to abort warn and keep generating; the case-insensitive pass runs over the final file list after conventions, MCP files and the root mirrors, and the test asserts the warning instead of asserting silence; the two booleans collapsed into one collisionPolicy; the provenance Map is gone; the JSDoc carries the dcode/Warp rationale again; the composition case covers all five re-routed targets; and the project-mode cases moved out of the global describe.

On the product decision I was holding on: scoping the throw to root-involved collisions and warning for the rest is exactly where I said I wanted it, so I am no longer asking to split the collision policy into its own PR. Consider that settled.

I also re-derived the composition invariant rather than take it on trust — I swept every target in toolRuleFactories with two root: true rules, in both project and global scope, and checked each generated file for a frontmatter block appearing mid-body. Nothing malformed, nothing throws. That half is solid.

What blocks it is the branch being stale, not the design. This is 44 commits behind main (merge-base 04d8e5d) and GitHub reports it as conflicting. I merged current main into a scratch worktree — only the two file-formats.md copies conflicted, the source auto-merged cleanly — and one of the new compose targets breaks in exactly the way this PR sets out to prevent. Details on the line.

So: please rebase on main and re-check the amp case, with a regression test as part of the fix — pnpm cicheck does not catch it as things stand (tsc clean, all 7816 unit tests green on the merged tree). The .curated/ note is smaller and I am happy to take it separately if you prefer.

extension: "md",
supportsGlobal: true,
ruleDiscoveryMode: "toon",
collisionPolicy: "compose",

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This is the blocker, and it only appears after a rebase.

Since this branch's merge-base, main taught AmpRule.fromRulesyncRule to prepend a globs: YAML block to non-root files — Amp's native conditional-load gate, from #2410. So amp is no longer a plain-Markdown adapter, and compose ends up concatenating two frontmatter-bearing bodies.

I merged current main into a scratch worktree and ran it. Two non-root rules that both carry agentsmd: { subprojectPath: "packages/app" } and non-empty globs produce a single packages/app/AGENTS.md:

---
globs:
  - packages/app/**/*.ts
---
# first

---
globs:
  - packages/app/**/*.tsx
---
# second

That is the mid-body frontmatter this PR exists to prevent. Amp reads only the leading block, so the second rule's gate is silently dropped, its body gets loaded under the first rule's globs, and a stray --- block is left sitting in the text. On main today the same config produces two separate well-formed files and last-writer-wins on disk: one rule lost, but nothing corrupt. The rebase turns a lossy-but-valid outcome into a malformed one.

I swept every target in toolRuleFactories for this shape and amp is the only one affected — the other four compose targets and every fold/preserve target are fine. So either drop amp from compose, or make the compose path strip each fragment's frontmatter and re-emit one merged globs: gate. Either way it needs the test below.

);
});

it.each(["agentsmd", "amp", "factorydroid", "kilo", "opencode"] as const)(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This is why CI stays green on the amp problem: none of the rules here set globs, so nothing emits frontmatter and the composed output is plain Markdown for all five targets.

Please add a case where both rules carry non-empty globs. On this branch as-is it passes for all five; after a rebase onto main it fails for amp, which is exactly the signal you want.

});

describe("loadRulesyncFiles with curated rules", () => {
it("should compose local root fragments before curated root fragments", async () => {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Not blocking — happy to take this in a follow-up.

.curated/ is where rulesync add/fetch/install drop rules pulled from other repositories, and the guard this PR removes was the thing that made a third-party root fragment landing in CLAUDE.md/AGENTS.md impossible to miss: it aborted the run. Now it composes with no output at all. Root files are loaded unconditionally by every agent, so silently promoting remote content into one is worth a logger.warn when a .curated/ fragment is merged into a root output. formatRulePaths already gives you the wording.

Comment thread docs/reference/file-formats.md Outdated
...
```

Multiple files can set `root: true` for the same target in project and global modes. Rulesync renders each file through the target adapter, then combines compatible root or plain-Markdown single-file outputs in deterministic source-discovery order with one blank line between fragments. Local rules are ordered lexicographically by source file path and composed before non-overridden `.curated/` rules, which are also ordered lexicographically; filename prefixes such as `10-` and `20-` control composition order within each set. Targets that map source rules to distinct native paths keep those files separate. Explicitly supported plain-Markdown modular rules that normalize to the same output path are combined. Unsafe collisions involving a source `root: true` rule fail. Other exact or case-insensitive modular collisions remain separate and produce a warning that the last write wins wherever the filesystem treats their paths as the same.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

"Explicitly supported plain-Markdown modular rules" stops being accurate for amp once you rebase — see the collisionPolicy comment. Whichever way you resolve that, this sentence and its skills/rulesync/file-formats.md mirror need to follow.

@dyoshikawa dyoshikawa left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Two more from a second pass, both smaller than the amp one but worth folding into the same round.

const shouldCompose =
collisionPolicy === "fold" || collisionPolicy === "compose" || allGeneratedRulesAreRoots;

if (!shouldCompose && hasSourceRoot) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This throw and the amp problem share a root cause worth naming: the decision is structural (what policy the target carries) rather than content-based (whether the rendered bodies actually contain a --- block). That cuts both ways.

Too strict here: kiro in global mode with a non-root product.md that has no globs renders as plain Markdown — no inclusion: block, nothing to corrupt — and it still aborts with "cannot safely compose", which isn't true for that input. Too loose over at collisionPolicy: "compose", which is how amp slipped through the moment #2410 gave it frontmatter. A content check (skip composing any fragment whose rendered body starts with ---) would fix both at once, and would not silently break again the next time one of agentsmd/factorydroid/kilo/opencode gains a frontmatter branch.

One knock-on effect to weigh: generateRulesCore in src/lib/generate.ts has no try/catch around its per-target loop — unlike generateIgnoreCore, which does. So this exception aborts rules generation for every remaining target, after the earlier ones have already been written. The old Multiple root rulesync rules found guard had the same property, but it never fired for the kiro-global shape, so for those users this turns a working (if lossy) generate into a hard stop with no way forward but renaming a file. At minimum the message should say that.

Comment thread src/features/rules/rules-processor.ts Outdated
if (previous) {
const previousPath = join(previous.getRelativeDirPath(), previous.getRelativeFilePath());
this.logger.warn(
`Both ${describeSource(previous)} and ${describeSource(file)} generate to '${previousPath}' and '${path}' (compared case-insensitively, as on macOS and Windows); the last one wins wherever they collide.`,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

For the common case — an exact path collision, not a case-only one — previousPath and path are the same string, so this reads "generate to '.devin/rules/coding-guidelines.md' and '.devin/rules/coding-guidelines.md'". Worth branching on previousPath === path and printing one path there; the import site's import to <path> phrasing is a good model.

Separately, describeSource falls back to the output path for files that aren't in convertedRules — the conventions rule, MCP instruction files, root mirrors — so a single message can end up mixing source paths and output paths. Minor, but it makes the warning harder to act on.

dyoshikawa and others added 2 commits July 29, 2026 00:07
# Conflicts:
#	docs/reference/file-formats.md
#	skills/rulesync/file-formats.md
Since dyoshikawa#2410, Amp prepends a globs: frontmatter gate to non-root files, so composing two gated fragments buried the second gate mid-body where Amp never reads it. Composition safety is now decided by content: a colliding group only composes when every rendered fragment is plain Markdown; otherwise it falls back to preserve-and-warn, or fails when a source root rule is involved. Exact-path collision warnings also stop repeating the same path twice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 07:14

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 is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

@dyoshikawa

Copy link
Copy Markdown
Owner

@rudironsoni I've pushed two commits here (using the maintainer-edit permission) so this can land without another round trip — the design was in good shape and I didn't want to hold it on a staleness issue that grew while the review was running.

The merge commit brings the branch up to date with main; only the two file-formats.md mirrors conflicted (your root: comment rewording vs main's Roo Code addition — kept both).

The fix commit addresses the amp interaction I flagged: since #2410, amp prepends a globs: frontmatter gate to non-root files, so collisionPolicy: "compose" would have buried the second fragment's gate mid-body. Rather than special-casing amp, composition safety is now decided by content — a colliding group only composes when every rendered fragment is plain Markdown, and otherwise falls back to preserve-and-warn (or the root-collision error, unchanged). That also means the guard won't silently break again if another compose target gains a frontmatter branch later. Regression test added, plus the exact-path collision warning no longer prints the same path twice, and the docs paragraph now mentions the frontmatter carve-out.

Everything else from the review stands as written by you. Merging once CI is green.

@dyoshikawa
dyoshikawa merged commit 72f7b79 into dyoshikawa:main Jul 29, 2026
5 checks passed
@dyoshikawa

Copy link
Copy Markdown
Owner

@rudironsoni Thank you!

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.

Allow multiple root rules to compose safely in project and global modes

3 participants