Skip to content

fix(sync): dedupe agent files sharing a name across .md and .agent.md - #471

Merged
christso merged 4 commits into
EntityProcess:mainfrom
Vic-Wang-WTG:VWG/WI01075864_Deduplicate_copilot_agents
Sep 12, 2026
Merged

fix(sync): dedupe agent files sharing a name across .md and .agent.md#471
christso merged 4 commits into
EntityProcess:mainfrom
Vic-Wang-WTG:VWG/WI01075864_Deduplicate_copilot_agents

Conversation

@Vic-Wang-WTG

@Vic-Wang-WTG Vic-Wang-WTG commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Source

Reported here (internal, cited for reference only): https://teams.microsoft.com/l/message/19:d29d75b3931b4302b9bd9d968d55adcf@thread.skype/1787648455643?tenantId=8b493985-e1b4-4b95-ade6-98acafdbdb01&groupId=188ba793-8136-4b10-b984-10097d7bb40c&parentMessageId=1787648455643&teamName=Development%20Customs%20Team&channelName=Customs%20Specifications%20(Specs-as-Code)&createdTime=1787648455643

Problem

A synced workspace's .github/agents/ directory can contain two files for the same logical agent, for example cw-reviewer.md and cw-reviewer.agent.md, both declaring name: cw-reviewer.

This happens when a plugin ships both:

  • a portable agents/<name>.md definition; and
  • a GitHub-format .github/agents/<name>.agent.md definition.

For Copilot these routes converge on .github/agents/. VS Code versions that discover both Markdown files show the logical agent twice. The GitHub-format .agent.md file was also not tracked individually because .github copying previously returned one aggregate result.

Fix

  • Build one immutable agent-output plan before concurrent plugin copies.
  • Resolve every plugin's actual client mappings independently; Claude receives only its portable .md, while Copilot/compatible VS Code routes can receive both formats.
  • Use configuration order for cross-plugin conflicts: the first configured valid provider owns a destination/logical name, and later providers are skipped with a warning.
  • Copy top-level GitHub agent definitions individually while preserving nested files and companion assets through the aggregate .github copy.
  • Dedupe only a successful same-plugin pair consisting of a plain portable .md and a GitHub-route .agent.md with the same frontmatter name in the same physical directory.
  • Preserve and track the portable definition when the preferred GitHub-format copy fails.
  • Track GitHub-format-only agents individually so later plugin removal purges them.
  • Treat .md to .agent.md replacement as one logical agent during deletion reporting; later removal reports reviewer, not reviewer.agent.
  • Preserve dry-run parity without reading or modifying destination files.

Scope

This dedupes representations within the same Copilot/VS Code agent directory. It does not collapse definitions across .claude/agents/ and .github/agents/; those are distinct client destinations.

Verification

Automated:

  • bun run build
  • bun run typecheck
  • bun run lint
  • Focused agent regression suite: 57 passed, 0 failed
  • Full suite: bun test --timeout 10000 — 1527 passed, 6 skipped, 0 failed

Built-CLI smoke test:

HOME=/tmp/allagents-pr471-smoke/home \
  bun dist/index.js workspace sync --verbose

HOME=/tmp/allagents-pr471-smoke/home \
  bun dist/index.js workspace sync --dry-run --verbose

The fixture used one local plugin with agents/reviewer.md and .github/agents/reviewer.agent.md, targeting Claude and Copilot. Verified:

  • .claude/agents/reviewer.md remains present and tracked.
  • .github/agents/reviewer.md is removed after successful dedupe.
  • .github/agents/reviewer.agent.md remains present and tracked.
  • CLI reports one Claude agent and one Copilot agent.
  • Fresh dry-run reports the same planned dedupe and writes neither files nor sync state.
  • After the plugin drops its portable source, the GitHub-format file remains tracked.
  • After plugin removal, the GitHub-format file is purged and reported as Deleted: agent 'reviewer'.

Plugins ship two files for one logical agent: a portable <name>.md
(root agents/) and a GitHub Copilot native <name>.agent.md
(.github/agents/). Both land in the same agentsPath directory for
copilot/vscode because each copy phase matches on the destination
filename, not on the agent's identity, so Copilot Chat builds that
still load plain .md files there show the agent twice.

Match by frontmatter name (not filename), keep .agent.md, remove the
.md twin, and track the survivor in sync-state so future purges don't
orphan it. Reported in sync messages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-11T08:36:15.932587Z 824b26b PR opened
🔒 Security Review Completed 2026-09-11T08:54:26.716961Z 824b26b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 824b26bf65

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/core/transform.ts Outdated

for (const file of group.plainMdFiles) {
if (!dryRun) {
await unlink(join(dir, file));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restrict deduplication to AllAgents-owned agent files

When syncing any client with an agentsPath, this scan includes every existing file in that directory and unconditionally deletes matching plain .md files without checking copy results or prior ownership. For example, a Copilot workspace containing user-created foo.md and foo.agent.md with the same frontmatter name loses foo.md even when no plugin supplies either file; the survivor is then recorded as synced and can be deleted by the next selective purge. Only deduplicate variants known to have been produced or managed by AllAgents.

AGENTS.md reference: AGENTS.md:L7-L11

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 80f738b: dedupeAgentFilesByName no longer scans the whole destination directory. It now only considers filenames that the given plugins' own agents/ and .github/agents/ source directories ship this sync (same exclude/fileArtifacts gating as copyAgents/copyGitHubContent), reading frontmatter from those source files — never from an arbitrary file already sitting in the destination. Added a regression test ("never touches a file the given plugins do not ship, even if it collides on disk").

Comment thread src/core/sync.ts Outdated
Comment on lines +2503 to +2507
const agentDedupeRecords = await dedupeAgentFilesForClients(
workspacePath,
syncClients,
resolvedMappings,
dryRun,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Simulate planned agent files during dry runs

On a fresh workspace, dry-run plugin copies return planned results without materializing the destination files, but this call asks the dedupe helper to inspect only the current filesystem. A plugin providing both agents/foo.md and .github/agents/foo.agent.md therefore produces no Would dedupe message during --dry-run, while the equivalent real sync creates both and removes the plain file. Model the planned copies when calculating dry-run deduplication so the reported operations match the real command.

AGENTS.md reference: AGENTS.md:L7-L10

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 80f738b: frontmatter is now always read from the plugin's own source file (agents/.md or .github/agents/.agent.md), not the destination directory. Dry-run and a real sync now compute identical dedupe records regardless of whether the destination file has been materialized yet. Added a regression test ("reports what a fresh sync would dedupe even before any destination file exists").

Addresses Codex review on EntityProcess#471:

- P1: dedupeAgentFilesByName scanned the entire destination agentsPath
  directory, so a user-created (or otherwise unmanaged) .md/.agent.md
  pair with matching frontmatter name would get deleted even though no
  plugin provided it, and the survivor would then be tracked as synced
  and eligible for a future selective purge. It now only considers
  filenames that the given plugins' own agents/ and .github/agents/
  source directories actually ship this sync (respecting each plugin's
  exclude patterns and fileArtifacts gating, same as copyAgents/
  copyGitHubContent), reading frontmatter from those source files.

- P2: dry-run previously read frontmatter from the destination
  directory, which dry-run copies never materialize, so a fresh
  workspace's dry-run reported no dedup even though the following real
  sync would immediately remove a file. Reading from plugin sources
  instead means dry-run and a real sync now compute identical records.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@christso christso 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.

Requesting changes based on reproduced file-copy sync regressions.

Terminology used in the inline comments:

Term Meaning here
Portable agent definition agents/<name>.md, copied by copyAgents
GitHub-format agent definition .github/agents/<name>.agent.md, copied by copyGitHubContent for Copilot

Two blocking regressions are introduced by this PR: mixed Claude/Copilot sync deletes Claude's valid .md definition, and a failed GitHub-format file copy still causes the successfully copied portable definition to be deleted. I also found a narrower state-ownership gap after a plugin removes its portable duplicate, false deletion output during the path migration, and a non-blocking cross-plugin collision edge case. Please address the blocking findings and add syncWorkspace regression coverage; the ownership guarantee should be fixed or narrowed. For cross-plugin destination conflicts, use deterministic configuration order—first configured plugin wins, matching the existing MCP precedent—and use that same owner for copying and dedupe.

Comment thread src/core/sync.ts Outdated
Comment thread src/core/sync.ts
Comment thread src/core/transform.ts Outdated
Comment thread src/core/sync.ts Outdated
Comment thread src/core/transform.ts Outdated

@christso christso 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.

Maintainer repair b822937 addresses all requested changes. Verified focused agent regressions, full test suite, build, typecheck, lint, built-CLI mixed-client and dry-run flows, and green GitHub CI.

@christso
christso merged commit a28e1e6 into EntityProcess:main Sep 12, 2026
6 checks passed
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.

2 participants