code-agnostic 0.5.0__tar.gz → 0.6.0__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 (172) hide show
  1. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/PKG-INFO +1 -1
  2. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/__init__.py +1 -1
  3. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/apps_service.py +21 -10
  4. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/claude/service.py +0 -7
  5. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/codex/service.py +18 -6
  6. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/common/interfaces/service.py +26 -3
  7. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/copilot/service.py +1 -5
  8. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/cursor/service.py +1 -3
  9. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/opencode/service.py +5 -6
  10. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/executor.py +8 -6
  11. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/models.py +1 -1
  12. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/planner.py +1 -1
  13. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic.egg-info/PKG-INFO +1 -1
  14. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic.egg-info/SOURCES.txt +1 -0
  15. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/pyproject.toml +1 -1
  16. code_agnostic-0.6.0/tests/test_cli_apply_codex_agent_cleanup.py +71 -0
  17. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/LICENSE +0 -0
  18. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/README.md +0 -0
  19. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/__main__.py +0 -0
  20. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/agents/__init__.py +0 -0
  21. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/agents/claude.py +0 -0
  22. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/agents/codex.py +0 -0
  23. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/agents/compilers.py +0 -0
  24. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/agents/copilot.py +0 -0
  25. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/agents/models.py +0 -0
  26. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/agents/opencode.py +0 -0
  27. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/agents/parser.py +0 -0
  28. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/__init__.py +0 -0
  29. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/app_id.py +0 -0
  30. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/claude/__init__.py +0 -0
  31. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/claude/config_repository.py +0 -0
  32. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/claude/mapper.py +0 -0
  33. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/codex/__init__.py +0 -0
  34. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/codex/config_repository.py +0 -0
  35. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/codex/mapper.py +0 -0
  36. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/codex/schema.json +0 -0
  37. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/codex/schema_repository.py +0 -0
  38. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/common/__init__.py +0 -0
  39. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/common/compiled_planning.py +0 -0
  40. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/common/framework.py +0 -0
  41. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  42. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  43. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
  44. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/common/loader.py +0 -0
  45. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/common/models.py +0 -0
  46. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/common/schema.py +0 -0
  47. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/common/symlink_planning.py +0 -0
  48. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/common/utils.py +0 -0
  49. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/copilot/__init__.py +0 -0
  50. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/copilot/config_repository.py +0 -0
  51. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/copilot/mapper.py +0 -0
  52. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/cursor/__init__.py +0 -0
  53. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/cursor/config_repository.py +0 -0
  54. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/cursor/mapper.py +0 -0
  55. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/cursor/schema.json +0 -0
  56. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  57. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/opencode/__init__.py +0 -0
  58. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/opencode/config_repository.py +0 -0
  59. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/opencode/mapper.py +0 -0
  60. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/opencode/schema.json +0 -0
  61. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  62. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/__init__.py +0 -0
  63. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/aliases.py +0 -0
  64. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/commands/__init__.py +0 -0
  65. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/commands/agents.py +0 -0
  66. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/commands/apply.py +0 -0
  67. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/commands/apps.py +0 -0
  68. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
  69. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/commands/import_.py +0 -0
  70. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/commands/mcp.py +0 -0
  71. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/commands/plan.py +0 -0
  72. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/commands/projects.py +0 -0
  73. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/commands/restore.py +0 -0
  74. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/commands/rules.py +0 -0
  75. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/commands/skills.py +0 -0
  76. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/commands/status.py +0 -0
  77. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/commands/validate.py +0 -0
  78. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/commands/workspaces.py +0 -0
  79. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/helpers.py +0 -0
  80. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/cli/options.py +0 -0
  81. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/constants.py +0 -0
  82. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/core/__init__.py +0 -0
  83. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/core/project_repository.py +0 -0
  84. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/core/repository.py +0 -0
  85. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/core/workspace_repository.py +0 -0
  86. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/errors.py +0 -0
  87. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/generated_artifacts.py +0 -0
  88. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/git_exclude_service.py +0 -0
  89. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/imports/__init__.py +0 -0
  90. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/imports/adapters.py +0 -0
  91. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/imports/filesystem.py +0 -0
  92. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/imports/models.py +0 -0
  93. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/imports/service.py +0 -0
  94. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/lossiness.py +0 -0
  95. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/mcp_service.py +0 -0
  96. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/project_artifacts.py +0 -0
  97. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/rules/__init__.py +0 -0
  98. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/rules/compilers.py +0 -0
  99. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/rules/models.py +0 -0
  100. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/rules/parser.py +0 -0
  101. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/rules/repository.py +0 -0
  102. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/skills/__init__.py +0 -0
  103. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/skills/compilers.py +0 -0
  104. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/skills/install_sources.py +0 -0
  105. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/skills/models.py +0 -0
  106. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/skills/parser.py +0 -0
  107. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/spec/__init__.py +0 -0
  108. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/spec/loaders.py +0 -0
  109. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  110. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
  111. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
  112. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  113. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  114. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/status.py +0 -0
  115. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/tui/__init__.py +0 -0
  116. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/tui/enums.py +0 -0
  117. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/tui/import_selector.py +0 -0
  118. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/tui/renderers.py +0 -0
  119. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/tui/sections.py +0 -0
  120. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/tui/tables.py +0 -0
  121. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/utils.py +0 -0
  122. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/validation.py +0 -0
  123. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/workspace_artifacts.py +0 -0
  124. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic/workspaces.py +0 -0
  125. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic.egg-info/dependency_links.txt +0 -0
  126. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic.egg-info/entry_points.txt +0 -0
  127. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic.egg-info/requires.txt +0 -0
  128. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/code_agnostic.egg-info/top_level.txt +0 -0
  129. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/setup.cfg +0 -0
  130. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_agents.py +0 -0
  131. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_aliases.py +0 -0
  132. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_apply_apps.py +0 -0
  133. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_apply_codex.py +0 -0
  134. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_apply_cursor.py +0 -0
  135. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_apply_mcp_cleanup.py +0 -0
  136. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_apply_target.py +0 -0
  137. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_apps.py +0 -0
  138. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_disable_cleanup.py +0 -0
  139. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_explain_lossiness.py +0 -0
  140. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_flags.py +0 -0
  141. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_git_exclude.py +0 -0
  142. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_import.py +0 -0
  143. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_import_interactive.py +0 -0
  144. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_mcp.py +0 -0
  145. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_module_organization.py +0 -0
  146. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_plan.py +0 -0
  147. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_projects.py +0 -0
  148. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_restore.py +0 -0
  149. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_rules.py +0 -0
  150. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_skills.py +0 -0
  151. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_status.py +0 -0
  152. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_validate.py +0 -0
  153. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_workspace_resolution.py +0 -0
  154. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_cli_workspaces.py +0 -0
  155. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_common_mcp_to_dto.py +0 -0
  156. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_common_repository.py +0 -0
  157. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_compiled_planning.py +0 -0
  158. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_dto_to_common_mcp.py +0 -0
  159. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_git_exclude_service.py +0 -0
  160. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_mcp_service.py +0 -0
  161. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_planner_executor.py +0 -0
  162. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_planner_rules.py +0 -0
  163. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_project_config_sync.py +0 -0
  164. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_skill_install_sources.py +0 -0
  165. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_symlink_planning.py +0 -0
  166. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_sync_plan_model.py +0 -0
  167. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_transactional_executor.py +0 -0
  168. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_utils.py +0 -0
  169. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_version.py +0 -0
  170. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_workspace_config_sync.py +0 -0
  171. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_workspace_repo_status.py +0 -0
  172. {code_agnostic-0.5.0 → code_agnostic-0.6.0}/tests/test_workspaces.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-agnostic
3
- Version: 0.5.0
3
+ Version: 0.6.0
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
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.6.0"
@@ -201,14 +201,11 @@ class AppsService:
201
201
  def _cleanup_global_mcp(
202
202
  self, app_id: AppId, managed_mcp: dict[str, Any]
203
203
  ) -> list[Action]:
204
- scope = app_scope(app_id, "mcp")
205
- raw = managed_mcp.get(scope, [])
206
- managed = (
207
- {item for item in raw if isinstance(item, str)}
208
- if isinstance(raw, list)
209
- else set()
204
+ managed = self._names_for_scope(managed_mcp, app_scope(app_id, "mcp"))
205
+ managed_agents = self._names_for_scope(
206
+ managed_mcp, app_scope(app_id, "agents_registry")
210
207
  )
211
- if not managed:
208
+ if not managed and not managed_agents:
212
209
  return []
213
210
  try:
214
211
  service = create_registered_app_service(app_id)
@@ -216,9 +213,23 @@ class AppsService:
216
213
  return []
217
214
  if not service.repository.config_path.exists():
218
215
  return []
219
- # Empty desired + our previously-managed names → prune only our servers,
220
- # keep the user's, and clear managed_mcp state (mcp_managed becomes []).
221
- return [service.build_action({}, previously_managed=managed)]
216
+ # Empty desired + our previously-managed names → prune only what we wrote
217
+ # (MCP servers and agent-registry entries), keep the user's, and clear the
218
+ # ownership state (managed_entries becomes empty).
219
+ return [
220
+ service.build_action(
221
+ {},
222
+ previously_managed=managed,
223
+ previously_managed_agents=managed_agents,
224
+ )
225
+ ]
226
+
227
+ @staticmethod
228
+ def _names_for_scope(group: dict[str, Any], scope: str) -> set[str]:
229
+ raw = group.get(scope, [])
230
+ if not isinstance(raw, list):
231
+ return set()
232
+ return {item for item in raw if isinstance(item, str)}
222
233
 
223
234
  def list_status_rows(self) -> list[AppStatusRow]:
224
235
  apps = self.load_apps()
@@ -12,7 +12,6 @@ from code_agnostic.apps.common.framework import RegisteredAppConfigService
12
12
  from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
13
13
  from code_agnostic.apps.common.interfaces.repositories import IAppConfigRepository
14
14
  from code_agnostic.apps.common.models import MCPServerDTO
15
- from code_agnostic.errors import InvalidConfigSchemaError
16
15
  from code_agnostic.models import Action, ActionKind, ActionStatus
17
16
  from code_agnostic.skills.compilers import ClaudeSkillCompiler
18
17
  from code_agnostic.skills.parser import parse_skill
@@ -57,12 +56,6 @@ class ClaudeConfigService(RegisteredAppConfigService):
57
56
  def mcp_config_key(self) -> str:
58
57
  return "mcpServers"
59
58
 
60
- def validate_config(self, payload: Any) -> None:
61
- if not isinstance(payload, dict):
62
- raise InvalidConfigSchemaError(
63
- self.repository.config_path, "must be a JSON object"
64
- )
65
-
66
59
  def build_action_payload(self, payload: dict[str, Any]) -> Any:
67
60
  return payload
68
61
 
@@ -16,6 +16,7 @@ from code_agnostic.apps.codex.config_repository import CodexConfigRepository
16
16
  from code_agnostic.apps.codex.mapper import CodexMCPMapper
17
17
  from code_agnostic.apps.codex.schema_repository import CodexSchemaRepository
18
18
  from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
19
+ from code_agnostic.apps.common.utils import apply_mcp_servers
19
20
  from code_agnostic.apps.common.interfaces.repositories import (
20
21
  IAppConfigRepository,
21
22
  ISchemaRepository,
@@ -88,7 +89,7 @@ class CodexConfigService(RegisteredAppConfigService):
88
89
  def mcp_config_key(self) -> str:
89
90
  return "mcp_servers"
90
91
 
91
- def validate_config(self, payload: Any) -> None:
92
+ def _validate_schema(self, payload: dict[str, Any]) -> None:
92
93
  error = next(iter(self._validator.iter_errors(payload)), None)
93
94
  if error is not None:
94
95
  raise InvalidConfigSchemaError(
@@ -118,6 +119,7 @@ class CodexConfigService(RegisteredAppConfigService):
118
119
  common_servers: dict[str, MCPServerDTO],
119
120
  agent_sources: list[Path] | None = None,
120
121
  previously_managed: set[str] | None = None,
122
+ previously_managed_agents: set[str] | None = None,
121
123
  *,
122
124
  replace_mcp: bool = False,
123
125
  ) -> Action:
@@ -144,10 +146,17 @@ class CodexConfigService(RegisteredAppConfigService):
144
146
  self.set_mcp_payload(
145
147
  merged, desired_mcp, previously_managed, replace=replace_mcp
146
148
  )
147
- if agent_sources:
148
- merged["agents"] = self._merge_agents_payload(
149
- merged.get("agents"),
150
- self._build_agent_registry(agent_sources),
149
+ registry = self._build_agent_registry(agent_sources) if agent_sources else {}
150
+ if replace_mcp:
151
+ if registry:
152
+ merged["agents"] = self._merge_agents_payload(
153
+ merged.get("agents"), registry
154
+ )
155
+ elif registry or previously_managed_agents:
156
+ # Ownership-aware: prune agent-registry entries we previously wrote
157
+ # that are gone from the source, keep base-config/user agents.
158
+ merged["agents"] = apply_mcp_servers(
159
+ merged.get("agents"), registry, previously_managed_agents
151
160
  )
152
161
  self.validate_config(merged)
153
162
 
@@ -161,7 +170,10 @@ class CodexConfigService(RegisteredAppConfigService):
161
170
  )
162
171
  if not replace_mcp:
163
172
  action.scope = app_scope(self.app_id, "mcp")
164
- action.mcp_managed = sorted(desired_mcp)
173
+ action.managed_entries = {
174
+ app_scope(self.app_id, "mcp"): sorted(desired_mcp),
175
+ app_scope(self.app_id, "agents_registry"): sorted(registry),
176
+ }
165
177
  return action
166
178
 
167
179
  def _merge_agents_payload(
@@ -12,6 +12,7 @@ from code_agnostic.apps.common.interfaces.repositories import IAppConfigReposito
12
12
  from code_agnostic.apps.common.interfaces.repositories import ISourceRepository
13
13
  from code_agnostic.apps.common.models import MCPServerDTO
14
14
  from code_agnostic.apps.common.utils import apply_mcp_servers
15
+ from code_agnostic.errors import InvalidConfigSchemaError
15
16
  from code_agnostic.apps.common.symlink_planning import (
16
17
  load_state_links,
17
18
  load_state_paths,
@@ -59,9 +60,24 @@ class IAppConfigService(ABC):
59
60
  """Native config key that holds the MCP server map for this editor."""
60
61
  raise NotImplementedError
61
62
 
62
- @abstractmethod
63
63
  def validate_config(self, payload: Any) -> None:
64
- raise NotImplementedError
64
+ """Single validation contract for every editor config.
65
+
66
+ Empty/absent config is always valid; anything present must be a JSON
67
+ object; schema-backed editors add their own checks via
68
+ ``_validate_schema``.
69
+ """
70
+ if payload is None or payload == {}:
71
+ return
72
+ if not isinstance(payload, dict):
73
+ raise InvalidConfigSchemaError(
74
+ self.repository.config_path, "must be a JSON object"
75
+ )
76
+ self._validate_schema(payload)
77
+
78
+ def _validate_schema(self, payload: dict[str, Any]) -> None:
79
+ """Editor-specific validation hook. Default: no schema."""
80
+ return
65
81
 
66
82
  @abstractmethod
67
83
  def build_action_payload(self, payload: dict[str, Any]) -> Any:
@@ -254,6 +270,7 @@ class IAppConfigService(ABC):
254
270
  common_servers: dict[str, MCPServerDTO],
255
271
  agent_sources: list[Path] | None = None,
256
272
  previously_managed: set[str] | None = None,
273
+ previously_managed_agents: set[str] | None = None,
257
274
  *,
258
275
  replace_mcp: bool = False,
259
276
  ) -> Action:
@@ -280,7 +297,9 @@ class IAppConfigService(ABC):
280
297
  # User-shared global config: track which servers we own so a later
281
298
  # source removal can be pruned without touching the user's servers.
282
299
  action.scope = app_scope(self.app_id, "mcp")
283
- action.mcp_managed = sorted(desired_mcp)
300
+ action.managed_entries = {
301
+ app_scope(self.app_id, "mcp"): sorted(desired_mcp)
302
+ }
284
303
  return action
285
304
 
286
305
  def build_plan(
@@ -297,6 +316,9 @@ class IAppConfigService(ABC):
297
316
  previously_managed_mcp = self._load_state_names(
298
317
  managed_mcp_group, app_scope(self.app_id, "mcp")
299
318
  )
319
+ previously_managed_agents = self._load_state_names(
320
+ managed_mcp_group, app_scope(self.app_id, "agents_registry")
321
+ )
300
322
 
301
323
  skill_actions, skill_skipped = self._build_compiled_group(
302
324
  sources=source_repository.list_skill_sources(),
@@ -325,6 +347,7 @@ class IAppConfigService(ABC):
325
347
  common_servers,
326
348
  agent_sources=source_repository.list_agent_sources(),
327
349
  previously_managed=previously_managed_mcp,
350
+ previously_managed_agents=previously_managed_agents,
328
351
  ),
329
352
  *skill_actions,
330
353
  *agent_actions,
@@ -55,11 +55,7 @@ class CopilotConfigService(RegisteredAppConfigService):
55
55
  def mcp_config_key(self) -> str:
56
56
  return "mcpServers"
57
57
 
58
- def validate_config(self, payload: Any) -> None:
59
- if not isinstance(payload, dict):
60
- raise InvalidConfigSchemaError(
61
- self.repository.config_path, "must be a JSON object"
62
- )
58
+ def _validate_schema(self, payload: dict[str, Any]) -> None:
63
59
  mcp_servers = payload.get("mcpServers")
64
60
  if mcp_servers is not None and not isinstance(mcp_servers, dict):
65
61
  raise InvalidConfigSchemaError(
@@ -68,9 +68,7 @@ class CursorConfigService(RegisteredAppConfigService):
68
68
  def mcp_config_key(self) -> str:
69
69
  return "mcpServers"
70
70
 
71
- def validate_config(self, payload: Any) -> None:
72
- if payload == {}:
73
- return
71
+ def _validate_schema(self, payload: dict[str, Any]) -> None:
74
72
  error = next(iter(self._validator.iter_errors(payload)), None)
75
73
  if error is not None:
76
74
  raise InvalidConfigSchemaError(
@@ -104,11 +104,7 @@ class OpenCodeConfigService(RegisteredAppConfigService):
104
104
  def mcp_config_key(self) -> str:
105
105
  return "mcp"
106
106
 
107
- def validate_config(self, payload: Any) -> None:
108
- if not isinstance(payload, dict):
109
- raise InvalidConfigSchemaError(
110
- self.repository.config_path, "must be a JSON object"
111
- )
107
+ def _validate_schema(self, payload: dict[str, Any]) -> None:
112
108
  for error in self._validator.iter_errors(payload):
113
109
  if _is_unknown_provider_model_enum_error(error):
114
110
  continue
@@ -141,6 +137,7 @@ class OpenCodeConfigService(RegisteredAppConfigService):
141
137
  common_servers: dict[str, MCPServerDTO],
142
138
  agent_sources: list[Path] | None = None,
143
139
  previously_managed: set[str] | None = None,
140
+ previously_managed_agents: set[str] | None = None,
144
141
  *,
145
142
  replace_mcp: bool = False,
146
143
  ) -> Action:
@@ -178,7 +175,9 @@ class OpenCodeConfigService(RegisteredAppConfigService):
178
175
  )
179
176
  if not replace_mcp:
180
177
  action.scope = app_scope(self.app_id, "mcp")
181
- action.mcp_managed = sorted(desired_mcp)
178
+ action.managed_entries = {
179
+ app_scope(self.app_id, "mcp"): sorted(desired_mcp)
180
+ }
182
181
  return action
183
182
 
184
183
  def _validate_common_mcp(self, common_servers: dict[str, MCPServerDTO]) -> None:
@@ -770,12 +770,14 @@ class SyncExecutor:
770
770
  if action.scope is None:
771
771
  continue
772
772
 
773
- if action.mcp_managed is not None:
774
- # User-shared MCP config: track the server names we own, never the
775
- # shared config file itself (so cleanup prunes our servers only).
776
- global_mcp_touched.add(action.scope)
777
- if action.mcp_managed:
778
- global_mcp[action.scope] = sorted(set(action.mcp_managed))
773
+ if action.managed_entries is not None:
774
+ # User-shared config: track the named entries we own (MCP servers,
775
+ # agent-registry names, …) by scope never the shared config file
776
+ # itself, so cleanup prunes only what we wrote.
777
+ for entry_scope, names in action.managed_entries.items():
778
+ global_mcp_touched.add(entry_scope)
779
+ if names:
780
+ global_mcp[entry_scope] = sorted(set(names))
779
781
  continue
780
782
 
781
783
  if action.workspace is not None:
@@ -74,7 +74,7 @@ class Action:
74
74
  scope: str | None = None
75
75
  workspace: str | None = None
76
76
  project: str | None = None
77
- mcp_managed: list[str] | None = None
77
+ managed_entries: dict[str, list[str]] | None = None
78
78
 
79
79
 
80
80
  @dataclass
@@ -291,7 +291,7 @@ class SyncPlanner:
291
291
  # Carry global MCP ownership tracking onto the merged Claude action so
292
292
  # top-level `mcpServers` orphans still get pruned on later applies.
293
293
  project_action.scope = existing_action.scope
294
- project_action.mcp_managed = existing_action.mcp_managed
294
+ project_action.managed_entries = existing_action.managed_entries
295
295
  actions = [
296
296
  action
297
297
  for action in plan.actions
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-agnostic
3
- Version: 0.5.0
3
+ Version: 0.6.0
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
@@ -128,6 +128,7 @@ tests/test_cli_agents.py
128
128
  tests/test_cli_aliases.py
129
129
  tests/test_cli_apply_apps.py
130
130
  tests/test_cli_apply_codex.py
131
+ tests/test_cli_apply_codex_agent_cleanup.py
131
132
  tests/test_cli_apply_cursor.py
132
133
  tests/test_cli_apply_mcp_cleanup.py
133
134
  tests/test_cli_apply_target.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code-agnostic"
7
- version = "0.5.0"
7
+ version = "0.6.0"
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"
@@ -0,0 +1,71 @@
1
+ """Codex agent-registry ownership: an agent removed from the source is pruned
2
+ from `[agents.*]` in ~/.codex/config.toml on the next apply, while base-config
3
+ agent settings and user-added entries survive."""
4
+
5
+ from pathlib import Path
6
+
7
+ from code_agnostic.__main__ import cli
8
+
9
+ try:
10
+ import tomllib
11
+ except ModuleNotFoundError: # pragma: no cover
12
+ import tomli as tomllib # type: ignore
13
+
14
+
15
+ def _write_agent(core_root: Path, name: str) -> None:
16
+ agent = core_root / "agents" / f"{name}.md"
17
+ agent.parent.mkdir(parents=True, exist_ok=True)
18
+ agent.write_text(
19
+ f"---\nname: {name}\ndescription: {name} specialist\n---\n\nDo {name}.\n",
20
+ encoding="utf-8",
21
+ )
22
+
23
+
24
+ def test_apply_codex_prunes_removed_agent_from_registry(
25
+ minimal_shared_config: Path,
26
+ core_root: Path,
27
+ tmp_path: Path,
28
+ cli_runner,
29
+ enable_app,
30
+ ) -> None:
31
+ enable_app("codex")
32
+ # Base-config agent settings + a user-managed agent entry must be preserved.
33
+ (core_root / "config" / "codex.base.json").write_text(
34
+ '{"agents": {"max_depth": 2}}', encoding="utf-8"
35
+ )
36
+ (tmp_path / ".codex").mkdir(parents=True, exist_ok=True)
37
+ (tmp_path / ".codex" / "config.toml").write_text(
38
+ '[agents.mine]\ndescription = "user agent"\nconfig_file = "agents/mine.toml"\n',
39
+ encoding="utf-8",
40
+ )
41
+ _write_agent(core_root, "planner")
42
+ _write_agent(core_root, "reviewer")
43
+
44
+ assert cli_runner.invoke(cli, ["apply", "-a", "codex"]).exit_code == 0
45
+ cfg = tomllib.loads(
46
+ (tmp_path / ".codex" / "config.toml").read_text(encoding="utf-8")
47
+ )
48
+ assert cfg["agents"]["max_depth"] == 2
49
+ assert set(cfg["agents"]) >= {"planner", "reviewer", "mine", "max_depth"}
50
+
51
+ state_path = core_root / ".sync-state.json"
52
+ import json
53
+
54
+ state = json.loads(state_path.read_text(encoding="utf-8"))
55
+ assert state["managed_mcp"]["app:codex:agents_registry"] == ["planner", "reviewer"]
56
+
57
+ # Remove reviewer from the source and re-apply.
58
+ (core_root / "agents" / "reviewer.md").unlink()
59
+ assert cli_runner.invoke(cli, ["apply", "-a", "codex"]).exit_code == 0
60
+
61
+ cfg = tomllib.loads(
62
+ (tmp_path / ".codex" / "config.toml").read_text(encoding="utf-8")
63
+ )
64
+ agents = cfg["agents"]
65
+ assert "reviewer" not in agents, "our removed agent must be pruned from registry"
66
+ assert "planner" in agents
67
+ assert agents["mine"]["description"] == "user agent", "user entry preserved"
68
+ assert agents["max_depth"] == 2, "base-config agent settings preserved"
69
+
70
+ state = json.loads(state_path.read_text(encoding="utf-8"))
71
+ assert state["managed_mcp"]["app:codex:agents_registry"] == ["planner"]
File without changes
File without changes
File without changes