code-agnostic 0.3.3__tar.gz → 0.3.4__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 (149) hide show
  1. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/PKG-INFO +5 -5
  2. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/README.md +4 -4
  3. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/codex/schema.json +18 -1
  4. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/codex/service.py +7 -1
  5. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/common/compiled_planning.py +12 -0
  6. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/common/interfaces/service.py +23 -1
  7. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/common/utils.py +46 -0
  8. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/cursor/service.py +2 -0
  9. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/opencode/config_repository.py +5 -3
  10. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/opencode/service.py +28 -2
  11. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/constants.py +6 -0
  12. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/git_exclude_service.py +6 -1
  13. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/planner.py +124 -90
  14. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/utils.py +14 -0
  15. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic.egg-info/PKG-INFO +5 -5
  16. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/pyproject.toml +1 -1
  17. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_apply_apps.py +169 -0
  18. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_apply_target.py +51 -0
  19. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_workspaces.py +6 -3
  20. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_common_mcp_to_dto.py +64 -1
  21. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_git_exclude_service.py +6 -1
  22. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_planner_executor.py +40 -0
  23. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_transactional_executor.py +51 -0
  24. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_workspace_config_sync.py +201 -9
  25. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/LICENSE +0 -0
  26. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/__init__.py +0 -0
  27. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/__main__.py +0 -0
  28. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/agents/__init__.py +0 -0
  29. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/agents/codex.py +0 -0
  30. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/agents/compilers.py +0 -0
  31. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/agents/models.py +0 -0
  32. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/agents/opencode.py +0 -0
  33. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/agents/parser.py +0 -0
  34. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/__init__.py +0 -0
  35. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/app_id.py +0 -0
  36. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/apps_service.py +0 -0
  37. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/codex/__init__.py +0 -0
  38. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/codex/config_repository.py +0 -0
  39. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/codex/mapper.py +0 -0
  40. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/codex/schema_repository.py +0 -0
  41. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/common/__init__.py +0 -0
  42. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/common/framework.py +0 -0
  43. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  44. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  45. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
  46. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/common/loader.py +0 -0
  47. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/common/models.py +0 -0
  48. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/common/schema.py +0 -0
  49. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/common/symlink_planning.py +0 -0
  50. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/cursor/__init__.py +0 -0
  51. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/cursor/config_repository.py +0 -0
  52. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/cursor/mapper.py +0 -0
  53. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/cursor/schema.json +0 -0
  54. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  55. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/opencode/__init__.py +0 -0
  56. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/opencode/mapper.py +0 -0
  57. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/opencode/schema.json +0 -0
  58. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  59. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/__init__.py +0 -0
  60. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/aliases.py +0 -0
  61. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/commands/__init__.py +0 -0
  62. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/commands/agents.py +0 -0
  63. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/commands/apply.py +0 -0
  64. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/commands/apps.py +0 -0
  65. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
  66. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/commands/import_.py +0 -0
  67. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/commands/mcp.py +0 -0
  68. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/commands/plan.py +0 -0
  69. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/commands/restore.py +0 -0
  70. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/commands/rules.py +0 -0
  71. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/commands/skills.py +0 -0
  72. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/commands/status.py +0 -0
  73. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/commands/validate.py +0 -0
  74. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/commands/workspaces.py +0 -0
  75. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/helpers.py +0 -0
  76. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/cli/options.py +0 -0
  77. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/core/__init__.py +0 -0
  78. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/core/repository.py +0 -0
  79. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/core/workspace_repository.py +0 -0
  80. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/errors.py +0 -0
  81. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/executor.py +0 -0
  82. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/imports/__init__.py +0 -0
  83. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/imports/adapters.py +0 -0
  84. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/imports/filesystem.py +0 -0
  85. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/imports/models.py +0 -0
  86. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/imports/service.py +0 -0
  87. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/lossiness.py +0 -0
  88. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/mcp_service.py +0 -0
  89. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/models.py +0 -0
  90. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/rules/__init__.py +0 -0
  91. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/rules/compilers.py +0 -0
  92. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/rules/models.py +0 -0
  93. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/rules/parser.py +0 -0
  94. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/rules/repository.py +0 -0
  95. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/skills/__init__.py +0 -0
  96. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/skills/compilers.py +0 -0
  97. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/skills/models.py +0 -0
  98. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/skills/parser.py +0 -0
  99. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/spec/__init__.py +0 -0
  100. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/spec/loaders.py +0 -0
  101. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  102. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
  103. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
  104. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  105. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  106. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/status.py +0 -0
  107. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/tui/__init__.py +0 -0
  108. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/tui/enums.py +0 -0
  109. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/tui/import_selector.py +0 -0
  110. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/tui/renderers.py +0 -0
  111. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/tui/sections.py +0 -0
  112. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/tui/tables.py +0 -0
  113. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/validation.py +0 -0
  114. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic/workspaces.py +0 -0
  115. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic.egg-info/SOURCES.txt +0 -0
  116. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic.egg-info/dependency_links.txt +0 -0
  117. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic.egg-info/entry_points.txt +0 -0
  118. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic.egg-info/requires.txt +0 -0
  119. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/code_agnostic.egg-info/top_level.txt +0 -0
  120. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/setup.cfg +0 -0
  121. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_agents.py +0 -0
  122. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_aliases.py +0 -0
  123. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_apply_codex.py +0 -0
  124. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_apply_cursor.py +0 -0
  125. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_apps.py +0 -0
  126. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_explain_lossiness.py +0 -0
  127. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_flags.py +0 -0
  128. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_git_exclude.py +0 -0
  129. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_import.py +0 -0
  130. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_import_interactive.py +0 -0
  131. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_mcp.py +0 -0
  132. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_module_organization.py +0 -0
  133. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_plan.py +0 -0
  134. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_restore.py +0 -0
  135. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_rules.py +0 -0
  136. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_skills.py +0 -0
  137. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_status.py +0 -0
  138. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_validate.py +0 -0
  139. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_cli_workspace_resolution.py +0 -0
  140. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_common_repository.py +0 -0
  141. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_compiled_planning.py +0 -0
  142. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_dto_to_common_mcp.py +0 -0
  143. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_mcp_service.py +0 -0
  144. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_planner_rules.py +0 -0
  145. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_symlink_planning.py +0 -0
  146. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_sync_plan_model.py +0 -0
  147. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_utils.py +0 -0
  148. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/tests/test_workspace_repo_status.py +0 -0
  149. {code_agnostic-0.3.3 → code_agnostic-0.3.4}/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.3
3
+ Version: 0.3.4
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
@@ -102,7 +102,7 @@ code-agnostic apply
102
102
  | Agents sync | yes | yes | yes |
103
103
  | Workspace root `AGENTS.md` link | yes | yes | yes |
104
104
  | Native repo config include for workspace `AGENTS.md` | yes | -- | -- |
105
- | Repo/subdir gets shared workspace `AGENTS.md` today | yes | -- | -- |
105
+ | Repo/subdir gets shared workspace `AGENTS.md` today | yes | -- | yes |
106
106
  | Root-level `AGENTS.md` discovery only | -- | yes | yes |
107
107
  | Workspace propagation | yes | -- | yes |
108
108
  | Import from | yes | yes | yes |
@@ -110,9 +110,9 @@ code-agnostic apply
110
110
 
111
111
  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
112
112
 
113
- 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.
113
+ 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`.
114
114
 
115
- Cursor documents `AGENTS.md` as a root-level project file. Codex documents `AGENTS.md` discovery, but not a native config include for an extra workspace file. In practice that means subdirectories and repos opened below the workspace root will not reliably get the shared workspace `AGENTS.md` today for Cursor or Codex.
115
+ Cursor documents `AGENTS.md` as a root-level project file. Codex documents `AGENTS.md` discovery, but not a native config include for an extra workspace file. In practice that means subdirectories and repos opened below the workspace root will not reliably get the shared workspace `AGENTS.md` today for Cursor.
116
116
 
117
117
  ## Features
118
118
 
@@ -171,7 +171,7 @@ code-agnostic agents list
171
171
 
172
172
  ### Workspaces
173
173
 
174
- Register workspace directories. Workspace rules are compiled into a canonical `AGENTS.md` and symlinked to the workspace root. Repos keep their own repo-specific `AGENTS.md`. OpenCode workspace configs also reference the shared workspace file through `instructions`, so a repo can load both its own `AGENTS.md` and the workspace-level one. Repo-local app config, skills, and agents are propagated for OpenCode and Codex.
174
+ Register workspace directories. Workspace rules are compiled into a canonical `AGENTS.md` at the workspace root. Repos keep their own repo-specific `AGENTS.md`; Codex receives the workspace rules through generated, git-excluded `AGENTS.override.md` files, while OpenCode workspace configs reference the shared workspace file through `instructions`. Repo-local app config, skills, and agents are propagated for OpenCode and Codex.
175
175
 
176
176
  `.cursor` workspace propagation is intentionally disabled to avoid duplicate MCP initialization when opening multi-root workspaces in Cursor (related issue: https://forum.cursor.com/t/mcp-multi-root-workspace-causes-duplicate-mcp-server-initialization-4x-createclient-actions/144003).
177
177
 
@@ -79,7 +79,7 @@ code-agnostic apply
79
79
  | Agents sync | yes | yes | yes |
80
80
  | Workspace root `AGENTS.md` link | yes | yes | yes |
81
81
  | Native repo config include for workspace `AGENTS.md` | yes | -- | -- |
82
- | Repo/subdir gets shared workspace `AGENTS.md` today | yes | -- | -- |
82
+ | Repo/subdir gets shared workspace `AGENTS.md` today | yes | -- | yes |
83
83
  | Root-level `AGENTS.md` discovery only | -- | yes | yes |
84
84
  | Workspace propagation | yes | -- | yes |
85
85
  | Import from | yes | yes | yes |
@@ -87,9 +87,9 @@ code-agnostic apply
87
87
 
88
88
  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
89
89
 
90
- 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.
90
+ 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`.
91
91
 
92
- Cursor documents `AGENTS.md` as a root-level project file. Codex documents `AGENTS.md` discovery, but not a native config include for an extra workspace file. In practice that means subdirectories and repos opened below the workspace root will not reliably get the shared workspace `AGENTS.md` today for Cursor or Codex.
92
+ Cursor documents `AGENTS.md` as a root-level project file. Codex documents `AGENTS.md` discovery, but not a native config include for an extra workspace file. In practice that means subdirectories and repos opened below the workspace root will not reliably get the shared workspace `AGENTS.md` today for Cursor.
93
93
 
94
94
  ## Features
95
95
 
@@ -148,7 +148,7 @@ code-agnostic agents list
148
148
 
149
149
  ### Workspaces
150
150
 
151
- Register workspace directories. Workspace rules are compiled into a canonical `AGENTS.md` and symlinked to the workspace root. Repos keep their own repo-specific `AGENTS.md`. OpenCode workspace configs also reference the shared workspace file through `instructions`, so a repo can load both its own `AGENTS.md` and the workspace-level one. Repo-local app config, skills, and agents are propagated for OpenCode and Codex.
151
+ Register workspace directories. Workspace rules are compiled into a canonical `AGENTS.md` at the workspace root. Repos keep their own repo-specific `AGENTS.md`; Codex receives the workspace rules through generated, git-excluded `AGENTS.override.md` files, while OpenCode workspace configs reference the shared workspace file through `instructions`. Repo-local app config, skills, and agents are propagated for OpenCode and Codex.
152
152
 
153
153
  `.cursor` workspace propagation is intentionally disabled to avoid duplicate MCP initialization when opening multi-root workspaces in Cursor (related issue: https://forum.cursor.com/t/mcp-multi-root-workspace-causes-duplicate-mcp-server-initialization-4x-createclient-actions/144003).
154
154
 
@@ -56,32 +56,49 @@
56
56
  },
57
57
  "description": "Per-tool approval settings for a single MCP server tool."
58
58
  },
59
+ "McpServerOAuthConfig": {
60
+ "type": "object",
61
+ "additionalProperties": false,
62
+ "properties": {
63
+ "client_id": { "type": "string" }
64
+ },
65
+ "description": "OAuth client settings used when Codex launches an MCP OAuth flow."
66
+ },
59
67
  "RawMcpServerConfig": {
60
68
  "type": "object",
61
69
  "additionalProperties": false,
62
70
  "properties": {
63
71
  "args": { "type": "array", "items": { "type": "string" } },
64
- "bearer_token": { "type": "string" },
65
72
  "bearer_token_env_var": { "type": "string" },
66
73
  "command": { "type": "string" },
67
74
  "cwd": { "type": "string" },
75
+ "default_tools_approval_mode": {
76
+ "allOf": [{ "$ref": "#/definitions/AppToolApproval" }],
77
+ "default": null
78
+ },
68
79
  "disabled_tools": { "type": "array", "items": { "type": "string" } },
69
80
  "enabled": { "type": "boolean" },
70
81
  "enabled_tools": { "type": "array", "items": { "type": "string" } },
71
82
  "env": { "type": "object", "additionalProperties": { "type": "string" } },
72
83
  "env_http_headers": { "type": "object", "additionalProperties": { "type": "string" } },
73
84
  "env_vars": { "type": "array", "items": { "type": "string" } },
85
+ "experimental_environment": { "type": "string" },
74
86
  "http_headers": { "type": "object", "additionalProperties": { "type": "string" } },
75
87
  "name": {
76
88
  "type": "string",
77
89
  "default": null,
78
90
  "description": "Legacy display-name field accepted for backward compatibility."
79
91
  },
92
+ "oauth": {
93
+ "allOf": [{ "$ref": "#/definitions/McpServerOAuthConfig" }],
94
+ "default": null
95
+ },
80
96
  "oauth_resource": { "type": "string", "default": null },
81
97
  "required": { "type": "boolean" },
82
98
  "scopes": { "type": "array", "items": { "type": "string" } },
83
99
  "startup_timeout_ms": { "type": "integer", "minimum": 0 },
84
100
  "startup_timeout_sec": { "type": "number" },
101
+ "supports_parallel_tool_calls": { "type": "boolean" },
85
102
  "tool_timeout_sec": { "type": "number" },
86
103
  "tools": {
87
104
  "type": "object",
@@ -26,7 +26,7 @@ from code_agnostic.errors import (
26
26
  InvalidJsonFormatError,
27
27
  )
28
28
  from code_agnostic.models import Action, ActionKind, ActionStatus
29
- from code_agnostic.utils import read_json_safe
29
+ from code_agnostic.utils import merge_dict_overlay, read_json_safe
30
30
  from code_agnostic.skills.compilers import CodexSkillCompiler
31
31
  from code_agnostic.skills.parser import parse_skill
32
32
 
@@ -134,6 +134,10 @@ class CodexConfigService(RegisteredAppConfigService):
134
134
  merged.get("agents"), value
135
135
  )
136
136
  continue
137
+ current = merged.get(key)
138
+ if isinstance(current, dict) and isinstance(value, dict):
139
+ merged[key] = merge_dict_overlay(current, value)
140
+ continue
137
141
  merged[key] = deepcopy(value)
138
142
  self.set_mcp_payload(merged, desired_mcp)
139
143
  if agent_sources:
@@ -196,6 +200,7 @@ class CodexConfigService(RegisteredAppConfigService):
196
200
  compiler = CodexSkillCompiler()
197
201
  return self._plan_compiled_text_actions(
198
202
  sources=sources,
203
+ target_dir=target_dir,
199
204
  scope=scope,
200
205
  app=app,
201
206
  managed_paths=managed_paths,
@@ -244,6 +249,7 @@ class CodexConfigService(RegisteredAppConfigService):
244
249
 
245
250
  return self._plan_compiled_text_actions(
246
251
  sources=sources,
252
+ target_dir=target_dir,
247
253
  scope=scope,
248
254
  app=app,
249
255
  managed_paths=managed_paths,
@@ -3,6 +3,18 @@ from pathlib import Path
3
3
  from code_agnostic.models import Action, ActionKind, ActionStatus
4
4
 
5
5
 
6
+ def find_replaceable_symlink_ancestor(target: Path, managed_root: Path) -> Path | None:
7
+ current = target
8
+ while True:
9
+ if current.is_symlink() and (
10
+ current == managed_root or current.is_relative_to(managed_root)
11
+ ):
12
+ return current
13
+ if current.parent == current:
14
+ return None
15
+ current = current.parent
16
+
17
+
6
18
  def _symlink_ancestor_state(
7
19
  target: Path, removable_link_paths: set[Path]
8
20
  ) -> tuple[bool, bool]:
@@ -4,7 +4,10 @@ from pathlib import Path
4
4
  from typing import Any
5
5
 
6
6
  from code_agnostic.apps.app_id import AppId, app_scope
7
- from code_agnostic.apps.common.compiled_planning import plan_compiled_text_action
7
+ from code_agnostic.apps.common.compiled_planning import (
8
+ find_replaceable_symlink_ancestor,
9
+ plan_compiled_text_action,
10
+ )
8
11
  from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
9
12
  from code_agnostic.apps.common.interfaces.repositories import IAppConfigRepository
10
13
  from code_agnostic.apps.common.interfaces.repositories import ISourceRepository
@@ -99,6 +102,7 @@ class IAppConfigService(ABC):
99
102
  self,
100
103
  *,
101
104
  sources: list[Path],
105
+ target_dir: Path,
102
106
  scope: str,
103
107
  app: str,
104
108
  managed_paths: list[Path],
@@ -114,10 +118,28 @@ class IAppConfigService(ABC):
114
118
  actions: list[Action] = []
115
119
  desired_paths: list[Path] = []
116
120
  skipped: list[str] = []
121
+ scheduled_removals: set[Path] = set()
117
122
 
118
123
  for source in sources:
119
124
  target, payload = compile_source(source)
120
125
  desired_paths.append(target)
126
+ replaceable_symlink = find_replaceable_symlink_ancestor(target, target_dir)
127
+ if (
128
+ replaceable_symlink is not None
129
+ and replaceable_symlink not in scheduled_removals
130
+ ):
131
+ scheduled_removals.add(replaceable_symlink)
132
+ removable_link_set.add(replaceable_symlink.resolve(strict=False))
133
+ actions.append(
134
+ Action(
135
+ kind=ActionKind.REMOVE_SYMLINK,
136
+ path=replaceable_symlink,
137
+ status=ActionStatus.REMOVE,
138
+ detail=f"replace compiled {scope} symlink",
139
+ app=app,
140
+ scope=scope,
141
+ )
142
+ )
121
143
  action = plan_compiled_text_action(
122
144
  target=target,
123
145
  payload=payload,
@@ -1,6 +1,52 @@
1
1
  from typing import Any
2
2
 
3
+ from code_agnostic.apps.app_id import AppId, app_ids_by_capability
3
4
  from code_agnostic.apps.common.models import MCPAuthDTO, MCPServerDTO, MCPServerType
5
+ from code_agnostic.constants import (
6
+ MCP_APP_EXCLUDE_PREFIX,
7
+ MCP_APP_INCLUDE_PREFIX,
8
+ MCP_APP_TARGET_SEPARATOR,
9
+ )
10
+
11
+
12
+ _TARGETABLE_APP_NAMES = {
13
+ app_id.value for app_id in app_ids_by_capability(targetable=True)
14
+ }
15
+
16
+
17
+ def _split_app_targeted_key(key: str) -> tuple[str, str, str] | None:
18
+ if not key or key[0] not in (MCP_APP_INCLUDE_PREFIX, MCP_APP_EXCLUDE_PREFIX):
19
+ return None
20
+
21
+ marker = key[0]
22
+ remainder = key[1:]
23
+ app_name, separator, server_name = remainder.partition(MCP_APP_TARGET_SEPARATOR)
24
+ if (
25
+ separator != MCP_APP_TARGET_SEPARATOR
26
+ or not server_name
27
+ or app_name not in _TARGETABLE_APP_NAMES
28
+ ):
29
+ return None
30
+ return marker, app_name, server_name
31
+
32
+
33
+ def mcp_servers_for_app(
34
+ mcp_servers: dict[str, Any], app_id: AppId | str
35
+ ) -> dict[str, Any]:
36
+ target = app_id.value if isinstance(app_id, AppId) else app_id
37
+ filtered: dict[str, Any] = {}
38
+ for name, raw in mcp_servers.items():
39
+ targeted = _split_app_targeted_key(name)
40
+ if targeted is None:
41
+ filtered[name] = raw
42
+ continue
43
+
44
+ marker, app_name, server_name = targeted
45
+ if marker == MCP_APP_INCLUDE_PREFIX and app_name == target:
46
+ filtered[server_name] = raw
47
+ elif marker == MCP_APP_EXCLUDE_PREFIX and app_name != target:
48
+ filtered[server_name] = raw
49
+ return filtered
4
50
 
5
51
 
6
52
  def _coerce_timeout_ms(value: Any) -> int | None:
@@ -115,6 +115,7 @@ class CursorConfigService(RegisteredAppConfigService):
115
115
  compiler = CursorSkillCompiler()
116
116
  return self._plan_compiled_text_actions(
117
117
  sources=sources,
118
+ target_dir=target_dir,
118
119
  scope=scope,
119
120
  app=app,
120
121
  managed_paths=managed_paths,
@@ -147,6 +148,7 @@ class CursorConfigService(RegisteredAppConfigService):
147
148
  compiler = CursorAgentCompiler()
148
149
  return self._plan_compiled_text_actions(
149
150
  sources=sources,
151
+ target_dir=target_dir,
150
152
  scope=scope,
151
153
  app=app,
152
154
  managed_paths=managed_paths,
@@ -9,7 +9,7 @@ from code_agnostic.constants import (
9
9
  SKILLS_DIRNAME,
10
10
  )
11
11
  from code_agnostic.errors import InvalidConfigSchemaError, InvalidJsonFormatError
12
- from code_agnostic.utils import read_json_safe, write_json
12
+ from code_agnostic.utils import merge_dict_overlay, read_json_safe, write_json
13
13
 
14
14
 
15
15
  class OpenCodeConfigRepository(IAppConfigRepository):
@@ -69,6 +69,10 @@ class OpenCodeConfigRepository(IAppConfigRepository):
69
69
  for key, value in base.items():
70
70
  if key == "permission" and isinstance(value, list):
71
71
  continue
72
+ current = merged.get(key)
73
+ if isinstance(current, dict) and isinstance(value, dict):
74
+ merged[key] = merge_dict_overlay(current, value)
75
+ continue
72
76
  merged[key] = deepcopy(value)
73
77
  merged["mcp"] = deepcopy(mapped_mcp)
74
78
  return merged
@@ -101,5 +105,3 @@ class OpenCodeConfigRepository(IAppConfigRepository):
101
105
 
102
106
  if tools:
103
107
  merged["tools"] = tools
104
-
105
- merged.pop("permission", None)
@@ -26,6 +26,29 @@ from code_agnostic.skills.compilers import OpenCodeSkillCompiler
26
26
  from code_agnostic.skills.parser import parse_skill
27
27
 
28
28
 
29
+ def _is_unknown_provider_model_enum_error(error: Any) -> bool:
30
+ if error.validator != "enum":
31
+ return False
32
+ model = error.instance
33
+ if not isinstance(model, str) or "/" not in model:
34
+ return False
35
+
36
+ known_models = error.validator_value
37
+ if not isinstance(known_models, list):
38
+ return False
39
+
40
+ known_providers = {
41
+ item.split("/", 1)[0]
42
+ for item in known_models
43
+ if isinstance(item, str) and "/" in item
44
+ }
45
+ if not known_providers:
46
+ return False
47
+
48
+ provider = model.split("/", 1)[0]
49
+ return provider not in known_providers
50
+
51
+
29
52
  class OpenCodeConfigService(RegisteredAppConfigService):
30
53
  APP_ID = AppId.OPENCODE
31
54
  APP_LABEL = app_label(APP_ID)
@@ -82,8 +105,9 @@ class OpenCodeConfigService(RegisteredAppConfigService):
82
105
  raise InvalidConfigSchemaError(
83
106
  self.repository.config_path, "must be a JSON object"
84
107
  )
85
- error = next(iter(self._validator.iter_errors(payload)), None)
86
- if error is not None:
108
+ for error in self._validator.iter_errors(payload):
109
+ if _is_unknown_provider_model_enum_error(error):
110
+ continue
87
111
  raise InvalidConfigSchemaError(
88
112
  self.repository.config_path, format_schema_error(error)
89
113
  )
@@ -149,6 +173,7 @@ class OpenCodeConfigService(RegisteredAppConfigService):
149
173
  compiler = OpenCodeSkillCompiler()
150
174
  return self._plan_compiled_text_actions(
151
175
  sources=sources,
176
+ target_dir=target_dir,
152
177
  scope=scope,
153
178
  app=app,
154
179
  managed_paths=managed_paths,
@@ -181,6 +206,7 @@ class OpenCodeConfigService(RegisteredAppConfigService):
181
206
  compiler = OpenCodeAgentCompiler()
182
207
  return self._plan_compiled_text_actions(
183
208
  sources=sources,
209
+ target_dir=target_dir,
184
210
  scope=scope,
185
211
  app=app,
186
212
  managed_paths=managed_paths,
@@ -2,6 +2,7 @@ from typing import Final
2
2
 
3
3
 
4
4
  AGENTS_FILENAME: Final[str] = "AGENTS.md"
5
+ CODEX_AGENTS_OVERRIDE_FILENAME: Final[str] = "AGENTS.override.md"
5
6
  CLAUDE_FILENAME: Final[str] = "CLAUDE.md"
6
7
  GIT_DIRNAME: Final[str] = ".git"
7
8
  SYNC_STATE_FILENAME: Final[str] = ".sync-state.json"
@@ -20,6 +21,11 @@ OPENCODE_CONFIG_FILENAME: Final[str] = "opencode.json"
20
21
  CURSOR_CONFIG_FILENAME: Final[str] = "mcp.json"
21
22
  CODEX_CONFIG_FILENAME: Final[str] = "config.toml"
22
23
 
24
+ MCP_SERVERS_KEY: Final[str] = "mcpServers"
25
+ MCP_APP_INCLUDE_PREFIX: Final[str] = "@"
26
+ MCP_APP_EXCLUDE_PREFIX: Final[str] = "!"
27
+ MCP_APP_TARGET_SEPARATOR: Final[str] = "-"
28
+
23
29
  WORKSPACE_IGNORED_DIRS: Final[tuple[str, ...]] = (
24
30
  "node_modules",
25
31
  ".venv",
@@ -6,7 +6,11 @@ from pathlib import Path
6
6
  from typing import Any
7
7
 
8
8
  from code_agnostic.apps.app_id import app_metadata
9
- from code_agnostic.constants import AGENTS_FILENAME, CLAUDE_FILENAME
9
+ from code_agnostic.constants import (
10
+ AGENTS_FILENAME,
11
+ CLAUDE_FILENAME,
12
+ CODEX_AGENTS_OVERRIDE_FILENAME,
13
+ )
10
14
  from code_agnostic.core.repository import CoreRepository
11
15
  from code_agnostic.utils import read_json_safe, write_json
12
16
 
@@ -58,6 +62,7 @@ class GitExcludeService:
58
62
 
59
63
  defaults = [f".{app_name}" for app_name in workspace_apps] + [
60
64
  AGENTS_FILENAME,
65
+ CODEX_AGENTS_OVERRIDE_FILENAME,
61
66
  CLAUDE_FILENAME,
62
67
  ]
63
68
  return defaults + extras