code-agnostic 0.3.7__tar.gz → 0.3.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/PKG-INFO +26 -7
  2. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/README.md +25 -6
  3. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/__init__.py +1 -1
  4. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/codex/schema.json +8 -0
  5. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/opencode/schema.json +1 -10
  6. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/commands/workspaces.py +3 -3
  7. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/imports/service.py +5 -1
  8. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/lossiness.py +30 -0
  9. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/status.py +63 -2
  10. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/tui/renderers.py +60 -2
  11. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/utils.py +2 -2
  12. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic.egg-info/PKG-INFO +26 -7
  13. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/pyproject.toml +1 -1
  14. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_aliases.py +1 -1
  15. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_explain_lossiness.py +32 -0
  16. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_flags.py +1 -1
  17. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_import.py +25 -0
  18. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_status.py +44 -0
  19. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_workspaces.py +2 -1
  20. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_utils.py +45 -0
  21. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/LICENSE +0 -0
  22. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/__main__.py +0 -0
  23. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/agents/__init__.py +0 -0
  24. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/agents/claude.py +0 -0
  25. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/agents/codex.py +0 -0
  26. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/agents/compilers.py +0 -0
  27. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/agents/models.py +0 -0
  28. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/agents/opencode.py +0 -0
  29. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/agents/parser.py +0 -0
  30. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/__init__.py +0 -0
  31. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/app_id.py +0 -0
  32. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/apps_service.py +0 -0
  33. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/claude/__init__.py +0 -0
  34. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/claude/config_repository.py +0 -0
  35. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/claude/mapper.py +0 -0
  36. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/claude/service.py +0 -0
  37. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/codex/__init__.py +0 -0
  38. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/codex/config_repository.py +0 -0
  39. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/codex/mapper.py +0 -0
  40. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/codex/schema_repository.py +0 -0
  41. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/codex/service.py +0 -0
  42. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/common/__init__.py +0 -0
  43. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/common/compiled_planning.py +0 -0
  44. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/common/framework.py +0 -0
  45. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  46. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  47. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
  48. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/common/interfaces/service.py +0 -0
  49. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/common/loader.py +0 -0
  50. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/common/models.py +0 -0
  51. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/common/schema.py +0 -0
  52. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/common/symlink_planning.py +0 -0
  53. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/common/utils.py +0 -0
  54. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/cursor/__init__.py +0 -0
  55. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/cursor/config_repository.py +0 -0
  56. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/cursor/mapper.py +0 -0
  57. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/cursor/schema.json +0 -0
  58. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  59. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/cursor/service.py +0 -0
  60. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/opencode/__init__.py +0 -0
  61. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/opencode/config_repository.py +0 -0
  62. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/opencode/mapper.py +0 -0
  63. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  64. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/apps/opencode/service.py +0 -0
  65. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/__init__.py +0 -0
  66. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/aliases.py +0 -0
  67. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/commands/__init__.py +0 -0
  68. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/commands/agents.py +0 -0
  69. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/commands/apply.py +0 -0
  70. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/commands/apps.py +0 -0
  71. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
  72. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/commands/import_.py +0 -0
  73. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/commands/mcp.py +0 -0
  74. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/commands/plan.py +0 -0
  75. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/commands/restore.py +0 -0
  76. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/commands/rules.py +0 -0
  77. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/commands/skills.py +0 -0
  78. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/commands/status.py +0 -0
  79. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/commands/validate.py +0 -0
  80. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/helpers.py +0 -0
  81. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/cli/options.py +0 -0
  82. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/constants.py +0 -0
  83. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/core/__init__.py +0 -0
  84. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/core/repository.py +0 -0
  85. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/core/workspace_repository.py +0 -0
  86. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/errors.py +0 -0
  87. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/executor.py +0 -0
  88. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/git_exclude_service.py +0 -0
  89. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/imports/__init__.py +0 -0
  90. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/imports/adapters.py +0 -0
  91. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/imports/filesystem.py +0 -0
  92. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/imports/models.py +0 -0
  93. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/mcp_service.py +0 -0
  94. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/models.py +0 -0
  95. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/planner.py +0 -0
  96. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/rules/__init__.py +0 -0
  97. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/rules/compilers.py +0 -0
  98. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/rules/models.py +0 -0
  99. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/rules/parser.py +0 -0
  100. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/rules/repository.py +0 -0
  101. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/skills/__init__.py +0 -0
  102. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/skills/compilers.py +0 -0
  103. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/skills/models.py +0 -0
  104. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/skills/parser.py +0 -0
  105. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/spec/__init__.py +0 -0
  106. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/spec/loaders.py +0 -0
  107. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  108. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
  109. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
  110. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  111. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  112. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/tui/__init__.py +0 -0
  113. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/tui/enums.py +0 -0
  114. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/tui/import_selector.py +0 -0
  115. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/tui/sections.py +0 -0
  116. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/tui/tables.py +0 -0
  117. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/validation.py +0 -0
  118. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic/workspaces.py +0 -0
  119. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic.egg-info/SOURCES.txt +0 -0
  120. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic.egg-info/dependency_links.txt +0 -0
  121. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic.egg-info/entry_points.txt +0 -0
  122. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic.egg-info/requires.txt +0 -0
  123. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/code_agnostic.egg-info/top_level.txt +0 -0
  124. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/setup.cfg +0 -0
  125. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_agents.py +0 -0
  126. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_apply_apps.py +0 -0
  127. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_apply_codex.py +0 -0
  128. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_apply_cursor.py +0 -0
  129. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_apply_target.py +0 -0
  130. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_apps.py +0 -0
  131. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_git_exclude.py +0 -0
  132. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_import_interactive.py +0 -0
  133. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_mcp.py +0 -0
  134. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_module_organization.py +0 -0
  135. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_plan.py +0 -0
  136. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_restore.py +0 -0
  137. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_rules.py +0 -0
  138. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_skills.py +0 -0
  139. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_validate.py +0 -0
  140. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_cli_workspace_resolution.py +0 -0
  141. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_common_mcp_to_dto.py +0 -0
  142. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_common_repository.py +0 -0
  143. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_compiled_planning.py +0 -0
  144. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_dto_to_common_mcp.py +0 -0
  145. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_git_exclude_service.py +0 -0
  146. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_mcp_service.py +0 -0
  147. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_planner_executor.py +0 -0
  148. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_planner_rules.py +0 -0
  149. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_symlink_planning.py +0 -0
  150. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_sync_plan_model.py +0 -0
  151. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_transactional_executor.py +0 -0
  152. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_version.py +0 -0
  153. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_workspace_config_sync.py +0 -0
  154. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_workspace_repo_status.py +0 -0
  155. {code_agnostic-0.3.7 → code_agnostic-0.3.9}/tests/test_workspaces.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-agnostic
3
- Version: 0.3.7
3
+ Version: 0.3.9
4
4
  Summary: Centralized hub for LLM coding config: MCP, skills, rules, and agents.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -119,6 +119,10 @@ code-agnostic apply
119
119
  | Import from | yes | yes | yes | yes |
120
120
  | Interactive import (TUI) | yes | yes | yes | yes |
121
121
 
122
+ `yes` means the resource type is synced for that editor. Some metadata is still
123
+ target-specific or lossy; run `code-agnostic explain-lossiness` to see fields
124
+ that are omitted or rejected for a selected target.
125
+
122
126
  Cursor workspace propagation is intentionally disabled to avoid duplicate MCP initialization in multi-root workspaces: https://forum.cursor.com/t/mcp-multi-root-workspace-causes-duplicate-mcp-server-initialization-4x-createclient-actions/144003
123
127
 
124
128
  OpenCode workspace configs include the shared workspace `AGENTS.md` natively via `instructions`, so repos under the workspace get both repo-local and shared workspace instructions. Codex repos receive workspace instructions through a generated `AGENTS.override.md`, which is added to each repo's `.git/info/exclude`. Claude Code receives workspace instructions through generated `CLAUDE.local.md` files, never by editing committed `CLAUDE.md`.
@@ -160,19 +164,31 @@ Env vars without a value (`--env GITHUB_TOKEN`) are stored as `${GITHUB_TOKEN}`
160
164
 
161
165
  ### Rules with metadata
162
166
 
163
- Rules live in `rules/` as markdown files with optional YAML frontmatter:
167
+ New rules should use bundle directories with schema-validated metadata and a
168
+ separate prompt body:
164
169
 
165
- ```markdown
166
- ---
170
+ ```text
171
+ rules/python-style/
172
+ ├── meta.yaml
173
+ └── prompt.md
174
+ ```
175
+
176
+ ```yaml
177
+ # rules/python-style/meta.yaml
178
+ spec_version: v1
179
+ kind: rule
167
180
  description: "Python coding standards"
168
181
  globs: ["*.py"]
169
182
  always_apply: false
170
- ---
183
+ ```
171
184
 
185
+ ```markdown
186
+ <!-- rules/python-style/prompt.md -->
172
187
  Always use type hints. Prefer dataclasses over dicts.
173
188
  ```
174
189
 
175
190
  Cross-compiled per editor: Cursor gets `.mdc` files with native frontmatter, OpenCode/Codex get `AGENTS.md` sections.
191
+ Legacy single-file rule markdown with YAML frontmatter remains supported for migration.
176
192
 
177
193
  ```bash
178
194
  code-agnostic rules list
@@ -181,7 +197,10 @@ code-agnostic rules remove --name python-style
181
197
 
182
198
  ### Skills and agents
183
199
 
184
- Canonical YAML frontmatter format, cross-compiled per editor. Install or edit skills in the `code-agnostic` source of truth, then run `plan` / `apply`; do not hand-copy generated skills into `.codex`, `.cursor`, or OpenCode directories.
200
+ Use bundle directories for new skills and agents, then let `code-agnostic`
201
+ cross-compile them per editor. Install or edit skills in the `code-agnostic`
202
+ source of truth, then run `plan` / `apply`; do not hand-copy generated skills
203
+ into `.codex`, `.cursor`, `.agents`, or OpenCode directories.
185
204
 
186
205
  ```bash
187
206
  code-agnostic skills list
@@ -262,7 +281,7 @@ The compiler migration is documented in:
262
281
 
263
282
  - [x] Plan/apply/status sync engine
264
283
  - [x] MCP server sync across editors
265
- - [x] Skills and agents sync (symlink-based)
284
+ - [x] Skills and agents sync across editors
266
285
  - [x] Workspace propagation into git repos
267
286
  - [x] Import from existing editor configs
268
287
  - [x] Consistent CLI with named flags and aliases
@@ -96,6 +96,10 @@ code-agnostic apply
96
96
  | Import from | yes | yes | yes | yes |
97
97
  | Interactive import (TUI) | yes | yes | yes | yes |
98
98
 
99
+ `yes` means the resource type is synced for that editor. Some metadata is still
100
+ target-specific or lossy; run `code-agnostic explain-lossiness` to see fields
101
+ that are omitted or rejected for a selected target.
102
+
99
103
  Cursor workspace propagation is intentionally disabled to avoid duplicate MCP initialization in multi-root workspaces: https://forum.cursor.com/t/mcp-multi-root-workspace-causes-duplicate-mcp-server-initialization-4x-createclient-actions/144003
100
104
 
101
105
  OpenCode workspace configs include the shared workspace `AGENTS.md` natively via `instructions`, so repos under the workspace get both repo-local and shared workspace instructions. Codex repos receive workspace instructions through a generated `AGENTS.override.md`, which is added to each repo's `.git/info/exclude`. Claude Code receives workspace instructions through generated `CLAUDE.local.md` files, never by editing committed `CLAUDE.md`.
@@ -137,19 +141,31 @@ Env vars without a value (`--env GITHUB_TOKEN`) are stored as `${GITHUB_TOKEN}`
137
141
 
138
142
  ### Rules with metadata
139
143
 
140
- Rules live in `rules/` as markdown files with optional YAML frontmatter:
144
+ New rules should use bundle directories with schema-validated metadata and a
145
+ separate prompt body:
141
146
 
142
- ```markdown
143
- ---
147
+ ```text
148
+ rules/python-style/
149
+ ├── meta.yaml
150
+ └── prompt.md
151
+ ```
152
+
153
+ ```yaml
154
+ # rules/python-style/meta.yaml
155
+ spec_version: v1
156
+ kind: rule
144
157
  description: "Python coding standards"
145
158
  globs: ["*.py"]
146
159
  always_apply: false
147
- ---
160
+ ```
148
161
 
162
+ ```markdown
163
+ <!-- rules/python-style/prompt.md -->
149
164
  Always use type hints. Prefer dataclasses over dicts.
150
165
  ```
151
166
 
152
167
  Cross-compiled per editor: Cursor gets `.mdc` files with native frontmatter, OpenCode/Codex get `AGENTS.md` sections.
168
+ Legacy single-file rule markdown with YAML frontmatter remains supported for migration.
153
169
 
154
170
  ```bash
155
171
  code-agnostic rules list
@@ -158,7 +174,10 @@ code-agnostic rules remove --name python-style
158
174
 
159
175
  ### Skills and agents
160
176
 
161
- Canonical YAML frontmatter format, cross-compiled per editor. Install or edit skills in the `code-agnostic` source of truth, then run `plan` / `apply`; do not hand-copy generated skills into `.codex`, `.cursor`, or OpenCode directories.
177
+ Use bundle directories for new skills and agents, then let `code-agnostic`
178
+ cross-compile them per editor. Install or edit skills in the `code-agnostic`
179
+ source of truth, then run `plan` / `apply`; do not hand-copy generated skills
180
+ into `.codex`, `.cursor`, `.agents`, or OpenCode directories.
162
181
 
163
182
  ```bash
164
183
  code-agnostic skills list
@@ -239,7 +258,7 @@ The compiler migration is documented in:
239
258
 
240
259
  - [x] Plan/apply/status sync engine
241
260
  - [x] MCP server sync across editors
242
- - [x] Skills and agents sync (symlink-based)
261
+ - [x] Skills and agents sync across editors
243
262
  - [x] Workspace propagation into git repos
244
263
  - [x] Import from existing editor configs
245
264
  - [x] Consistent CLI with named flags and aliases
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.3.7"
3
+ __version__ = "0.3.9"
@@ -102,6 +102,14 @@
102
102
  "additionalProperties": false,
103
103
  "description": "Config values for a single app/connector.",
104
104
  "properties": {
105
+ "approvals_reviewer": {
106
+ "allOf": [
107
+ {
108
+ "$ref": "#/definitions/ApprovalsReviewer"
109
+ }
110
+ ],
111
+ "description": "Reviewer for approval prompts from this app, overriding the thread default."
112
+ },
105
113
  "default_tools_approval_mode": {
106
114
  "allOf": [
107
115
  {
@@ -57,7 +57,7 @@
57
57
  },
58
58
  "branch": {
59
59
  "type": "string",
60
- "description": "Branch or ref Scout should clone and inspect"
60
+ "description": "Branch or ref to clone and inspect"
61
61
  }
62
62
  },
63
63
  "required": [
@@ -154,12 +154,6 @@
154
154
  "websearch": {
155
155
  "$ref": "#/$defs/PermissionActionConfig"
156
156
  },
157
- "repo_clone": {
158
- "$ref": "#/$defs/PermissionRuleConfig"
159
- },
160
- "repo_overview": {
161
- "$ref": "#/$defs/PermissionRuleConfig"
162
- },
163
157
  "lsp": {
164
158
  "$ref": "#/$defs/PermissionRuleConfig"
165
159
  },
@@ -961,9 +955,6 @@
961
955
  "explore": {
962
956
  "$ref": "#/$defs/AgentConfig"
963
957
  },
964
- "scout": {
965
- "$ref": "#/$defs/AgentConfig"
966
- },
967
958
  "title": {
968
959
  "$ref": "#/$defs/AgentConfig"
969
960
  },
@@ -15,7 +15,7 @@ from code_agnostic.tui import SyncConsoleUI
15
15
  from code_agnostic.workspaces import WorkspaceService
16
16
 
17
17
 
18
- @click.group(help="Manage workspace roots for repo rule propagation.")
18
+ @click.group(help="Manage workspace roots for repo-local sync.")
19
19
  def workspaces() -> None:
20
20
  pass
21
21
 
@@ -39,8 +39,8 @@ def workspaces_add(obj: dict[str, str], name: str, path: Path) -> None:
39
39
  ui.render_workspace_saved(name, str(path.expanduser().resolve()))
40
40
 
41
41
 
42
- @workspaces.command("remove", help="Remove a workspace from config by name.")
43
- @click.option("--name", required=True, help="Workspace name to remove.")
42
+ @workspaces.command("remove", help="Unregister a workspace by name.")
43
+ @click.option("--name", required=True, help="Workspace name to unregister.")
44
44
  @click.pass_obj
45
45
  def workspaces_remove(obj: dict[str, str], name: str) -> None:
46
46
  ui = SyncConsoleUI(Console())
@@ -292,7 +292,11 @@ class ImportService:
292
292
  else:
293
293
  skipped.append(f"MCP server skipped due to conflict: {name}")
294
294
 
295
- normalized_payload = {"mcpServers": dto_to_common_mcp(merged)}
295
+ normalized_payload = {}
296
+ existing_schema = existing_payload.get("$schema")
297
+ if isinstance(existing_schema, str) and existing_schema:
298
+ normalized_payload["$schema"] = existing_schema
299
+ normalized_payload["mcpServers"] = dto_to_common_mcp(merged)
296
300
  actions.append(
297
301
  ImportAction(
298
302
  section=ImportSection.MCP,
@@ -211,6 +211,36 @@ class LossinessExplainer:
211
211
  reason="target does not support agent sandbox_mode",
212
212
  )
213
213
  )
214
+ if agent.metadata.tools.read is not True:
215
+ findings.extend(
216
+ self._findings_for_targets(
217
+ resource_path=resource_path,
218
+ property_name="tools.read",
219
+ targets=("codex",),
220
+ app=app,
221
+ reason="target does not support agent read permissions",
222
+ )
223
+ )
224
+ if agent.metadata.tools.write is not True:
225
+ findings.extend(
226
+ self._findings_for_targets(
227
+ resource_path=resource_path,
228
+ property_name="tools.write",
229
+ targets=("codex",),
230
+ app=app,
231
+ reason="target does not support agent write permissions",
232
+ )
233
+ )
234
+ if agent.metadata.tools.mcp:
235
+ findings.extend(
236
+ self._findings_for_targets(
237
+ resource_path=resource_path,
238
+ property_name="tools.mcp",
239
+ targets=("codex",),
240
+ app=app,
241
+ reason="target does not support agent MCP permissions",
242
+ )
243
+ )
214
244
 
215
245
  return findings
216
246
 
@@ -12,11 +12,15 @@ from code_agnostic.constants import (
12
12
  )
13
13
  from code_agnostic.core.workspace_repository import WorkspaceConfigRepository
14
14
  from code_agnostic.models import (
15
+ Action,
16
+ ActionStatus,
15
17
  RepoSyncStatus,
16
18
  WorkspaceRepoStatusRow,
17
19
  WorkspaceStatusRow,
18
20
  WorkspaceSyncStatus,
19
21
  )
22
+ from code_agnostic.planner import SyncPlanner
23
+ from code_agnostic.utils import is_under
20
24
  from code_agnostic.workspaces import WorkspaceService
21
25
 
22
26
 
@@ -30,6 +34,7 @@ class StatusService:
30
34
  app_services: list[IAppConfigService] | None = None,
31
35
  ) -> list[WorkspaceStatusRow]:
32
36
  status_rows: list[WorkspaceStatusRow] = []
37
+ workspace_actions = self._workspace_actions(source_repo, app_services)
33
38
 
34
39
  for workspace in source_repo.load_workspaces():
35
40
  workspace_name = workspace["name"]
@@ -75,7 +80,18 @@ class StatusService:
75
80
  app_metas.append(meta)
76
81
 
77
82
  repo_rows = [
78
- self._repo_sync_status(repo, ws_source, app_metas) for repo in repos
83
+ self._repo_sync_status(
84
+ repo,
85
+ ws_source,
86
+ app_metas,
87
+ workspace_actions=[
88
+ action
89
+ for action in workspace_actions
90
+ if action.workspace == workspace_name
91
+ and is_under(action.path, repo)
92
+ ],
93
+ )
94
+ for repo in repos
79
95
  ]
80
96
 
81
97
  detail = "all git repos synced"
@@ -98,14 +114,32 @@ class StatusService:
98
114
 
99
115
  return status_rows
100
116
 
117
+ @staticmethod
118
+ def _workspace_actions(
119
+ source_repo: ISourceRepository,
120
+ app_services: list[IAppConfigService] | None,
121
+ ) -> list[Action]:
122
+ if not app_services:
123
+ return []
124
+ plan = SyncPlanner(core=source_repo, app_services=app_services).build()
125
+ return [action for action in plan.actions if action.workspace is not None]
126
+
101
127
  @staticmethod
102
128
  def _repo_sync_status(
103
129
  repo_path: Path,
104
130
  ws_source: WorkspaceConfigRepository,
105
131
  app_metas: list[AppMetadata] | None = None,
132
+ workspace_actions: list[Action] | None = None,
106
133
  ) -> WorkspaceRepoStatusRow:
107
134
  issues: list[str] = []
108
135
 
136
+ if workspace_actions is not None:
137
+ for action in workspace_actions:
138
+ if action.status == ActionStatus.NOOP:
139
+ continue
140
+ issues.append(StatusService._repo_action_issue(action, repo_path))
141
+ return StatusService._repo_status_row(repo_path, issues)
142
+
109
143
  # Check workspace-managed config files in repo project dirs.
110
144
  # Workspace rendering creates regular files (not symlinks) in
111
145
  # ws_source.root/<project_dir_name>/..., and repos get the same.
@@ -165,19 +199,46 @@ class StatusService:
165
199
  f"missing or mismatched {meta.app_id.value} agents link"
166
200
  )
167
201
 
202
+ return StatusService._repo_status_row(repo_path, issues)
203
+
204
+ @staticmethod
205
+ def _repo_status_row(repo_path: Path, issues: list[str]) -> WorkspaceRepoStatusRow:
168
206
  if not issues:
169
207
  return WorkspaceRepoStatusRow(
170
208
  repo=repo_path.name,
171
209
  status=RepoSyncStatus.SYNCED,
172
210
  detail="linked",
173
211
  )
174
-
175
212
  return WorkspaceRepoStatusRow(
176
213
  repo=repo_path.name,
177
214
  status=RepoSyncStatus.NEEDS_SYNC,
178
215
  detail="; ".join(issues),
179
216
  )
180
217
 
218
+ @staticmethod
219
+ def _repo_action_issue(action: Action, repo_path: Path) -> str:
220
+ rel_path = StatusService._relative_repo_path(action.path, repo_path)
221
+ if action.status == ActionStatus.CREATE:
222
+ return f"missing {rel_path}"
223
+ if action.status in {ActionStatus.UPDATE, ActionStatus.FIX}:
224
+ return f"mismatched {rel_path}"
225
+ if action.status == ActionStatus.CONFLICT:
226
+ return f"conflict at {rel_path}"
227
+ if action.status == ActionStatus.REMOVE:
228
+ return f"stale {rel_path}"
229
+ return f"{action.status.value} {rel_path}"
230
+
231
+ @staticmethod
232
+ def _relative_repo_path(path: Path, repo_path: Path) -> str:
233
+ try:
234
+ return (
235
+ path.resolve(strict=False)
236
+ .relative_to(repo_path.resolve(strict=False))
237
+ .as_posix()
238
+ )
239
+ except ValueError:
240
+ return path.as_posix()
241
+
181
242
 
182
243
  def _claude_project_mcp_exists(repo_path: Path) -> bool:
183
244
  payload, error = read_json_safe(Path.home() / CLAUDE_CONFIG_FILENAME)
@@ -1,6 +1,10 @@
1
1
  from rich.console import Console
2
2
 
3
- from code_agnostic.imports.models import ImportApplyResult, ImportPlan
3
+ from code_agnostic.imports.models import (
4
+ ImportActionStatus,
5
+ ImportApplyResult,
6
+ ImportPlan,
7
+ )
4
8
  from code_agnostic.models import (
5
9
  AppStatusRow,
6
10
  EditorStatusRow,
@@ -148,7 +152,7 @@ class SyncConsoleUI:
148
152
  def render_workspace_saved(
149
153
  self, name: str, path: str, removed: bool = False
150
154
  ) -> None:
151
- verb = "removed" if removed else "added"
155
+ verb = "unregistered" if removed else "added"
152
156
  border_style = UIStyle.YELLOW.value if removed else UIStyle.GREEN.value
153
157
  self.console.print(
154
158
  UISection.note(
@@ -330,6 +334,60 @@ class SyncConsoleUI:
330
334
  UISection.note("skipped", skipped_text, style=UIStyle.YELLOW.value)
331
335
  )
332
336
 
337
+ next_steps = self._import_next_steps(plan, mode)
338
+ if next_steps:
339
+ self.console.print(
340
+ UISection.note("next", next_steps, style=UIStyle.DIM.value)
341
+ )
342
+
343
+ @staticmethod
344
+ def _import_next_steps(plan: ImportPlan, mode: str) -> str | None:
345
+ parts = mode.split(":", 2)
346
+ if len(parts) != 3:
347
+ return None
348
+
349
+ _, command, target = parts
350
+ target_flag = f" -a {target}" if target else ""
351
+ conflict_items = [*plan.errors, *plan.skipped]
352
+ has_conflict = any("conflict" in item.lower() for item in conflict_items)
353
+
354
+ if command == "apply":
355
+ return None
356
+
357
+ if plan.errors:
358
+ lines = [
359
+ "Fix the errors above, then rerun the import preview.",
360
+ f"- code-agnostic import plan{target_flag}",
361
+ ]
362
+ if has_conflict:
363
+ lines.append(
364
+ "For conflicts, choose --on-conflict overwrite or --on-conflict fail."
365
+ )
366
+ return "\n".join(lines)
367
+
368
+ has_writes = any(
369
+ action.status in {ImportActionStatus.CREATE, ImportActionStatus.UPDATE}
370
+ for action in plan.actions
371
+ )
372
+ if has_writes:
373
+ lines = [
374
+ "Review the imported items. If they match what you expect, write them to the hub.",
375
+ f"- code-agnostic import apply{target_flag}",
376
+ ]
377
+ if has_conflict:
378
+ lines.append(
379
+ "Skipped conflicts will stay unchanged unless you rerun with --on-conflict overwrite."
380
+ )
381
+ return "\n".join(lines)
382
+
383
+ if has_conflict:
384
+ return (
385
+ "Skipped conflicts were left unchanged.\n"
386
+ f"- code-agnostic import plan{target_flag} --on-conflict overwrite"
387
+ )
388
+
389
+ return "No import changes needed.\n- code-agnostic validate"
390
+
333
391
  def render_import_apply_result(self, result: ImportApplyResult) -> None:
334
392
  self.render_apply_result(
335
393
  applied=result.applied,
@@ -21,10 +21,10 @@ def read_json_safe(path: Path) -> tuple[Any | None, str | None]:
21
21
 
22
22
 
23
23
  def write_json(path: Path, payload: Any) -> None:
24
+ rendered = json.dumps(payload, indent=2, sort_keys=False) + "\n"
24
25
  path.parent.mkdir(parents=True, exist_ok=True)
25
26
  with path.open("w", encoding="utf-8") as handle:
26
- json.dump(payload, handle, indent=2, sort_keys=False)
27
- handle.write("\n")
27
+ handle.write(rendered)
28
28
 
29
29
 
30
30
  def merge_dict_overlay(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-agnostic
3
- Version: 0.3.7
3
+ Version: 0.3.9
4
4
  Summary: Centralized hub for LLM coding config: MCP, skills, rules, and agents.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -119,6 +119,10 @@ code-agnostic apply
119
119
  | Import from | yes | yes | yes | yes |
120
120
  | Interactive import (TUI) | yes | yes | yes | yes |
121
121
 
122
+ `yes` means the resource type is synced for that editor. Some metadata is still
123
+ target-specific or lossy; run `code-agnostic explain-lossiness` to see fields
124
+ that are omitted or rejected for a selected target.
125
+
122
126
  Cursor workspace propagation is intentionally disabled to avoid duplicate MCP initialization in multi-root workspaces: https://forum.cursor.com/t/mcp-multi-root-workspace-causes-duplicate-mcp-server-initialization-4x-createclient-actions/144003
123
127
 
124
128
  OpenCode workspace configs include the shared workspace `AGENTS.md` natively via `instructions`, so repos under the workspace get both repo-local and shared workspace instructions. Codex repos receive workspace instructions through a generated `AGENTS.override.md`, which is added to each repo's `.git/info/exclude`. Claude Code receives workspace instructions through generated `CLAUDE.local.md` files, never by editing committed `CLAUDE.md`.
@@ -160,19 +164,31 @@ Env vars without a value (`--env GITHUB_TOKEN`) are stored as `${GITHUB_TOKEN}`
160
164
 
161
165
  ### Rules with metadata
162
166
 
163
- Rules live in `rules/` as markdown files with optional YAML frontmatter:
167
+ New rules should use bundle directories with schema-validated metadata and a
168
+ separate prompt body:
164
169
 
165
- ```markdown
166
- ---
170
+ ```text
171
+ rules/python-style/
172
+ ├── meta.yaml
173
+ └── prompt.md
174
+ ```
175
+
176
+ ```yaml
177
+ # rules/python-style/meta.yaml
178
+ spec_version: v1
179
+ kind: rule
167
180
  description: "Python coding standards"
168
181
  globs: ["*.py"]
169
182
  always_apply: false
170
- ---
183
+ ```
171
184
 
185
+ ```markdown
186
+ <!-- rules/python-style/prompt.md -->
172
187
  Always use type hints. Prefer dataclasses over dicts.
173
188
  ```
174
189
 
175
190
  Cross-compiled per editor: Cursor gets `.mdc` files with native frontmatter, OpenCode/Codex get `AGENTS.md` sections.
191
+ Legacy single-file rule markdown with YAML frontmatter remains supported for migration.
176
192
 
177
193
  ```bash
178
194
  code-agnostic rules list
@@ -181,7 +197,10 @@ code-agnostic rules remove --name python-style
181
197
 
182
198
  ### Skills and agents
183
199
 
184
- Canonical YAML frontmatter format, cross-compiled per editor. Install or edit skills in the `code-agnostic` source of truth, then run `plan` / `apply`; do not hand-copy generated skills into `.codex`, `.cursor`, or OpenCode directories.
200
+ Use bundle directories for new skills and agents, then let `code-agnostic`
201
+ cross-compile them per editor. Install or edit skills in the `code-agnostic`
202
+ source of truth, then run `plan` / `apply`; do not hand-copy generated skills
203
+ into `.codex`, `.cursor`, `.agents`, or OpenCode directories.
185
204
 
186
205
  ```bash
187
206
  code-agnostic skills list
@@ -262,7 +281,7 @@ The compiler migration is documented in:
262
281
 
263
282
  - [x] Plan/apply/status sync engine
264
283
  - [x] MCP server sync across editors
265
- - [x] Skills and agents sync (symlink-based)
284
+ - [x] Skills and agents sync across editors
266
285
  - [x] Workspace propagation into git repos
267
286
  - [x] Import from existing editor configs
268
287
  - [x] Consistent CLI with named flags and aliases
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code-agnostic"
7
- version = "0.3.7"
7
+ version = "0.3.9"
8
8
  description = "Centralized hub for LLM coding config: MCP, skills, rules, and agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -49,4 +49,4 @@ def test_workspace_alias_remove(
49
49
  cli_runner.invoke(cli, ["workspace", "add", "--name", "myws", "--path", str(ws)])
50
50
  result = cli_runner.invoke(cli, ["workspace", "remove", "--name", "myws"])
51
51
  assert result.exit_code == 0
52
- assert "Workspace removed" in result.output
52
+ assert "Workspace unregistered" in result.output
@@ -126,6 +126,38 @@ def test_explain_lossiness_reports_skill_tool_mappings(
126
126
  ]
127
127
 
128
128
 
129
+ def test_explain_lossiness_reports_codex_agent_tool_mappings(
130
+ minimal_shared_config: Path,
131
+ core_root: Path,
132
+ cli_runner,
133
+ ) -> None:
134
+ agent_dir = core_root / "agents" / "reviewer"
135
+ agent_dir.mkdir(parents=True)
136
+ (agent_dir / "meta.yaml").write_text(
137
+ "spec_version: v1\n"
138
+ "kind: agent\n"
139
+ "name: reviewer\n"
140
+ "tools:\n"
141
+ " read: false\n"
142
+ " write: false\n"
143
+ " mcp:\n"
144
+ " - server: github\n"
145
+ " tool: create_review\n",
146
+ encoding="utf-8",
147
+ )
148
+ (agent_dir / "prompt.md").write_text("Review code.\n", encoding="utf-8")
149
+
150
+ result = cli_runner.invoke(cli, ["explain-lossiness", "--app", "codex"])
151
+
152
+ assert result.exit_code == 0
153
+ assert result.output.splitlines() == [
154
+ "resource_path\tapp\tproperty\tstatus\treason",
155
+ "agents/reviewer\tcodex\ttools.mcp\tignored\ttarget does not support agent MCP permissions",
156
+ "agents/reviewer\tcodex\ttools.read\tignored\ttarget does not support agent read permissions",
157
+ "agents/reviewer\tcodex\ttools.write\tignored\ttarget does not support agent write permissions",
158
+ ]
159
+
160
+
129
161
  def test_explain_lossiness_reports_workspace_paths(
130
162
  minimal_shared_config: Path,
131
163
  core_root: Path,
@@ -99,7 +99,7 @@ def test_workspaces_remove_with_named_flag(
99
99
  cli_runner.invoke(cli, ["workspaces", "add", "--name", "myws", "--path", str(ws)])
100
100
  result = cli_runner.invoke(cli, ["workspaces", "remove", "--name", "myws"])
101
101
  assert result.exit_code == 0
102
- assert "Workspace removed" in result.output
102
+ assert "Workspace unregistered" in result.output
103
103
 
104
104
 
105
105
  def test_workspaces_git_exclude_with_workspace_flag(
@@ -60,6 +60,7 @@ def test_import_plan_codex_shows_sections(cli_runner, tmp_path: Path) -> None:
60
60
  assert "mcp" in result.output
61
61
  assert "skills" in result.output
62
62
  assert "agents" in result.output
63
+ assert "code-agnostic import apply -a codex" in result.output
63
64
 
64
65
 
65
66
  def test_import_apply_codex_imports_mcp_skills_and_agents(
@@ -257,6 +258,30 @@ def test_import_apply_conflict_policy_fail(cli_runner, tmp_path: Path) -> None:
257
258
  assert "conflict" in result.output.lower()
258
259
 
259
260
 
261
+ def test_import_plan_conflict_skip_shows_overwrite_next_step(
262
+ cli_runner, tmp_path: Path
263
+ ) -> None:
264
+ _write_codex_source(
265
+ tmp_path / ".codex",
266
+ {"demo": {"command": "uvx"}},
267
+ with_skill=False,
268
+ )
269
+ core_mcp_path = tmp_path / ".config" / "code-agnostic" / "config" / "mcp.base.json"
270
+ core_mcp_path.parent.mkdir(parents=True, exist_ok=True)
271
+ core_mcp_path.write_text(
272
+ json.dumps({"mcpServers": {"demo": {"url": "https://existing"}}}),
273
+ encoding="utf-8",
274
+ )
275
+
276
+ result = cli_runner.invoke(
277
+ cli, ["import", "plan", "-a", "codex", "--include", "mcp"]
278
+ )
279
+
280
+ assert result.exit_code == 0
281
+ assert "Skipped conflicts were left unchanged." in result.output
282
+ assert "code-agnostic import plan -a codex --on-conflict overwrite" in result.output
283
+
284
+
260
285
  def test_import_plan_default_view_shows_app_labels(cli_runner, tmp_path: Path) -> None:
261
286
  _write_codex_source(tmp_path / ".codex", {"demo": {"command": "uvx"}})
262
287