code-agnostic 0.5.1__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.1 → code_agnostic-0.6.0}/PKG-INFO +1 -1
  2. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/__init__.py +1 -1
  3. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/apps_service.py +21 -10
  4. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/codex/service.py +17 -5
  5. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/common/interfaces/service.py +8 -1
  6. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/opencode/service.py +4 -1
  7. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/executor.py +8 -6
  8. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/models.py +1 -1
  9. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/planner.py +1 -1
  10. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic.egg-info/PKG-INFO +1 -1
  11. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic.egg-info/SOURCES.txt +1 -0
  12. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/pyproject.toml +1 -1
  13. code_agnostic-0.6.0/tests/test_cli_apply_codex_agent_cleanup.py +71 -0
  14. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/LICENSE +0 -0
  15. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/README.md +0 -0
  16. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/__main__.py +0 -0
  17. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/agents/__init__.py +0 -0
  18. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/agents/claude.py +0 -0
  19. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/agents/codex.py +0 -0
  20. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/agents/compilers.py +0 -0
  21. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/agents/copilot.py +0 -0
  22. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/agents/models.py +0 -0
  23. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/agents/opencode.py +0 -0
  24. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/agents/parser.py +0 -0
  25. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/__init__.py +0 -0
  26. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/app_id.py +0 -0
  27. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/claude/__init__.py +0 -0
  28. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/claude/config_repository.py +0 -0
  29. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/claude/mapper.py +0 -0
  30. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/claude/service.py +0 -0
  31. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/codex/__init__.py +0 -0
  32. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/codex/config_repository.py +0 -0
  33. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/codex/mapper.py +0 -0
  34. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/codex/schema.json +0 -0
  35. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/codex/schema_repository.py +0 -0
  36. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/common/__init__.py +0 -0
  37. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/common/compiled_planning.py +0 -0
  38. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/common/framework.py +0 -0
  39. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  40. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  41. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
  42. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/common/loader.py +0 -0
  43. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/common/models.py +0 -0
  44. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/common/schema.py +0 -0
  45. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/common/symlink_planning.py +0 -0
  46. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/common/utils.py +0 -0
  47. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/copilot/__init__.py +0 -0
  48. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/copilot/config_repository.py +0 -0
  49. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/copilot/mapper.py +0 -0
  50. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/copilot/service.py +0 -0
  51. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/cursor/__init__.py +0 -0
  52. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/cursor/config_repository.py +0 -0
  53. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/cursor/mapper.py +0 -0
  54. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/cursor/schema.json +0 -0
  55. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  56. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/cursor/service.py +0 -0
  57. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/opencode/__init__.py +0 -0
  58. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/opencode/config_repository.py +0 -0
  59. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/opencode/mapper.py +0 -0
  60. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/opencode/schema.json +0 -0
  61. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  62. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/__init__.py +0 -0
  63. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/aliases.py +0 -0
  64. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/commands/__init__.py +0 -0
  65. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/commands/agents.py +0 -0
  66. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/commands/apply.py +0 -0
  67. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/commands/apps.py +0 -0
  68. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
  69. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/commands/import_.py +0 -0
  70. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/commands/mcp.py +0 -0
  71. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/commands/plan.py +0 -0
  72. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/commands/projects.py +0 -0
  73. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/commands/restore.py +0 -0
  74. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/commands/rules.py +0 -0
  75. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/commands/skills.py +0 -0
  76. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/commands/status.py +0 -0
  77. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/commands/validate.py +0 -0
  78. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/commands/workspaces.py +0 -0
  79. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/helpers.py +0 -0
  80. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/cli/options.py +0 -0
  81. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/constants.py +0 -0
  82. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/core/__init__.py +0 -0
  83. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/core/project_repository.py +0 -0
  84. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/core/repository.py +0 -0
  85. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/core/workspace_repository.py +0 -0
  86. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/errors.py +0 -0
  87. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/generated_artifacts.py +0 -0
  88. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/git_exclude_service.py +0 -0
  89. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/imports/__init__.py +0 -0
  90. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/imports/adapters.py +0 -0
  91. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/imports/filesystem.py +0 -0
  92. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/imports/models.py +0 -0
  93. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/imports/service.py +0 -0
  94. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/lossiness.py +0 -0
  95. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/mcp_service.py +0 -0
  96. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/project_artifacts.py +0 -0
  97. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/rules/__init__.py +0 -0
  98. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/rules/compilers.py +0 -0
  99. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/rules/models.py +0 -0
  100. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/rules/parser.py +0 -0
  101. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/rules/repository.py +0 -0
  102. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/skills/__init__.py +0 -0
  103. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/skills/compilers.py +0 -0
  104. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/skills/install_sources.py +0 -0
  105. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/skills/models.py +0 -0
  106. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/skills/parser.py +0 -0
  107. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/spec/__init__.py +0 -0
  108. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/spec/loaders.py +0 -0
  109. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  110. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
  111. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
  112. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  113. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  114. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/status.py +0 -0
  115. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/tui/__init__.py +0 -0
  116. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/tui/enums.py +0 -0
  117. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/tui/import_selector.py +0 -0
  118. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/tui/renderers.py +0 -0
  119. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/tui/sections.py +0 -0
  120. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/tui/tables.py +0 -0
  121. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/utils.py +0 -0
  122. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/validation.py +0 -0
  123. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/workspace_artifacts.py +0 -0
  124. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic/workspaces.py +0 -0
  125. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic.egg-info/dependency_links.txt +0 -0
  126. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic.egg-info/entry_points.txt +0 -0
  127. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic.egg-info/requires.txt +0 -0
  128. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/code_agnostic.egg-info/top_level.txt +0 -0
  129. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/setup.cfg +0 -0
  130. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_agents.py +0 -0
  131. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_aliases.py +0 -0
  132. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_apply_apps.py +0 -0
  133. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_apply_codex.py +0 -0
  134. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_apply_cursor.py +0 -0
  135. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_apply_mcp_cleanup.py +0 -0
  136. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_apply_target.py +0 -0
  137. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_apps.py +0 -0
  138. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_disable_cleanup.py +0 -0
  139. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_explain_lossiness.py +0 -0
  140. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_flags.py +0 -0
  141. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_git_exclude.py +0 -0
  142. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_import.py +0 -0
  143. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_import_interactive.py +0 -0
  144. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_mcp.py +0 -0
  145. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_module_organization.py +0 -0
  146. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_plan.py +0 -0
  147. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_projects.py +0 -0
  148. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_restore.py +0 -0
  149. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_rules.py +0 -0
  150. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_skills.py +0 -0
  151. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_status.py +0 -0
  152. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_validate.py +0 -0
  153. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_workspace_resolution.py +0 -0
  154. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_cli_workspaces.py +0 -0
  155. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_common_mcp_to_dto.py +0 -0
  156. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_common_repository.py +0 -0
  157. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_compiled_planning.py +0 -0
  158. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_dto_to_common_mcp.py +0 -0
  159. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_git_exclude_service.py +0 -0
  160. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_mcp_service.py +0 -0
  161. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_planner_executor.py +0 -0
  162. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_planner_rules.py +0 -0
  163. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_project_config_sync.py +0 -0
  164. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_skill_install_sources.py +0 -0
  165. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_symlink_planning.py +0 -0
  166. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_sync_plan_model.py +0 -0
  167. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_transactional_executor.py +0 -0
  168. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_utils.py +0 -0
  169. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_version.py +0 -0
  170. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_workspace_config_sync.py +0 -0
  171. {code_agnostic-0.5.1 → code_agnostic-0.6.0}/tests/test_workspace_repo_status.py +0 -0
  172. {code_agnostic-0.5.1 → 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.1
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.1"
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()
@@ -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,
@@ -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(
@@ -270,6 +270,7 @@ class IAppConfigService(ABC):
270
270
  common_servers: dict[str, MCPServerDTO],
271
271
  agent_sources: list[Path] | None = None,
272
272
  previously_managed: set[str] | None = None,
273
+ previously_managed_agents: set[str] | None = None,
273
274
  *,
274
275
  replace_mcp: bool = False,
275
276
  ) -> Action:
@@ -296,7 +297,9 @@ class IAppConfigService(ABC):
296
297
  # User-shared global config: track which servers we own so a later
297
298
  # source removal can be pruned without touching the user's servers.
298
299
  action.scope = app_scope(self.app_id, "mcp")
299
- action.mcp_managed = sorted(desired_mcp)
300
+ action.managed_entries = {
301
+ app_scope(self.app_id, "mcp"): sorted(desired_mcp)
302
+ }
300
303
  return action
301
304
 
302
305
  def build_plan(
@@ -313,6 +316,9 @@ class IAppConfigService(ABC):
313
316
  previously_managed_mcp = self._load_state_names(
314
317
  managed_mcp_group, app_scope(self.app_id, "mcp")
315
318
  )
319
+ previously_managed_agents = self._load_state_names(
320
+ managed_mcp_group, app_scope(self.app_id, "agents_registry")
321
+ )
316
322
 
317
323
  skill_actions, skill_skipped = self._build_compiled_group(
318
324
  sources=source_repository.list_skill_sources(),
@@ -341,6 +347,7 @@ class IAppConfigService(ABC):
341
347
  common_servers,
342
348
  agent_sources=source_repository.list_agent_sources(),
343
349
  previously_managed=previously_managed_mcp,
350
+ previously_managed_agents=previously_managed_agents,
344
351
  ),
345
352
  *skill_actions,
346
353
  *agent_actions,
@@ -137,6 +137,7 @@ class OpenCodeConfigService(RegisteredAppConfigService):
137
137
  common_servers: dict[str, MCPServerDTO],
138
138
  agent_sources: list[Path] | None = None,
139
139
  previously_managed: set[str] | None = None,
140
+ previously_managed_agents: set[str] | None = None,
140
141
  *,
141
142
  replace_mcp: bool = False,
142
143
  ) -> Action:
@@ -174,7 +175,9 @@ class OpenCodeConfigService(RegisteredAppConfigService):
174
175
  )
175
176
  if not replace_mcp:
176
177
  action.scope = app_scope(self.app_id, "mcp")
177
- action.mcp_managed = sorted(desired_mcp)
178
+ action.managed_entries = {
179
+ app_scope(self.app_id, "mcp"): sorted(desired_mcp)
180
+ }
178
181
  return action
179
182
 
180
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.1
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.1"
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