code-agnostic 0.3.2__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 (156) hide show
  1. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/PKG-INFO +5 -5
  2. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/README.md +4 -4
  3. code_agnostic-0.3.4/code_agnostic/__main__.py +62 -0
  4. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/app_id.py +22 -4
  5. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/codex/config_repository.py +10 -4
  6. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/codex/schema.json +45 -1
  7. code_agnostic-0.3.4/code_agnostic/apps/codex/service.py +274 -0
  8. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/compiled_planning.py +12 -0
  9. code_agnostic-0.3.4/code_agnostic/apps/common/interfaces/service.py +284 -0
  10. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/utils.py +46 -0
  11. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/cursor/config_repository.py +10 -4
  12. code_agnostic-0.3.4/code_agnostic/apps/cursor/service.py +164 -0
  13. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/opencode/config_repository.py +13 -6
  14. code_agnostic-0.3.4/code_agnostic/apps/opencode/service.py +236 -0
  15. code_agnostic-0.3.4/code_agnostic/cli/__init__.py +29 -0
  16. code_agnostic-0.3.4/code_agnostic/cli/aliases.py +20 -0
  17. code_agnostic-0.3.4/code_agnostic/cli/commands/__init__.py +33 -0
  18. code_agnostic-0.3.4/code_agnostic/cli/commands/agents.py +47 -0
  19. code_agnostic-0.3.4/code_agnostic/cli/commands/apply.py +42 -0
  20. code_agnostic-0.3.4/code_agnostic/cli/commands/apps.py +45 -0
  21. code_agnostic-0.3.4/code_agnostic/cli/commands/explain_lossiness.py +45 -0
  22. code_agnostic-0.3.4/code_agnostic/cli/commands/import_.py +189 -0
  23. code_agnostic-0.3.4/code_agnostic/cli/commands/mcp.py +122 -0
  24. code_agnostic-0.3.4/code_agnostic/cli/commands/plan.py +30 -0
  25. code_agnostic-0.3.4/code_agnostic/cli/commands/restore.py +28 -0
  26. code_agnostic-0.3.4/code_agnostic/cli/commands/rules.py +46 -0
  27. code_agnostic-0.3.4/code_agnostic/cli/commands/skills.py +42 -0
  28. code_agnostic-0.3.4/code_agnostic/cli/commands/status.py +52 -0
  29. code_agnostic-0.3.4/code_agnostic/cli/commands/validate.py +30 -0
  30. code_agnostic-0.3.4/code_agnostic/cli/commands/workspaces.py +186 -0
  31. code_agnostic-0.3.4/code_agnostic/cli/helpers.py +87 -0
  32. code_agnostic-0.3.4/code_agnostic/cli/options.py +95 -0
  33. code_agnostic-0.3.4/code_agnostic/constants.py +32 -0
  34. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/core/repository.py +4 -3
  35. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/executor.py +24 -13
  36. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/git_exclude_service.py +6 -1
  37. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/planner.py +139 -90
  38. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/status.py +15 -19
  39. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/utils.py +14 -0
  40. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic.egg-info/PKG-INFO +5 -5
  41. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic.egg-info/SOURCES.txt +20 -0
  42. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/pyproject.toml +1 -1
  43. code_agnostic-0.3.4/tests/test_cli_apply_apps.py +574 -0
  44. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_apply_codex.py +14 -0
  45. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_apply_target.py +51 -0
  46. code_agnostic-0.3.4/tests/test_cli_module_organization.py +55 -0
  47. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_workspaces.py +7 -4
  48. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_common_mcp_to_dto.py +64 -1
  49. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_git_exclude_service.py +8 -3
  50. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_planner_executor.py +40 -0
  51. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_planner_rules.py +1 -2
  52. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_transactional_executor.py +51 -0
  53. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_workspace_config_sync.py +315 -27
  54. code_agnostic-0.3.4/tests/test_workspace_repo_status.py +240 -0
  55. code_agnostic-0.3.2/code_agnostic/__main__.py +0 -1071
  56. code_agnostic-0.3.2/code_agnostic/apps/codex/service.py +0 -365
  57. code_agnostic-0.3.2/code_agnostic/apps/common/interfaces/service.py +0 -84
  58. code_agnostic-0.3.2/code_agnostic/apps/cursor/service.py +0 -291
  59. code_agnostic-0.3.2/code_agnostic/apps/opencode/service.py +0 -353
  60. code_agnostic-0.3.2/code_agnostic/constants.py +0 -13
  61. code_agnostic-0.3.2/tests/test_cli_apply_apps.py +0 -256
  62. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/LICENSE +0 -0
  63. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/__init__.py +0 -0
  64. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/agents/__init__.py +0 -0
  65. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/agents/codex.py +0 -0
  66. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/agents/compilers.py +0 -0
  67. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/agents/models.py +0 -0
  68. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/agents/opencode.py +0 -0
  69. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/agents/parser.py +0 -0
  70. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/__init__.py +0 -0
  71. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/apps_service.py +0 -0
  72. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/codex/__init__.py +0 -0
  73. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/codex/mapper.py +0 -0
  74. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/codex/schema_repository.py +0 -0
  75. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/__init__.py +0 -0
  76. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/framework.py +0 -0
  77. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  78. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  79. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
  80. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/loader.py +0 -0
  81. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/models.py +0 -0
  82. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/schema.py +0 -0
  83. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/symlink_planning.py +0 -0
  84. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/cursor/__init__.py +0 -0
  85. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/cursor/mapper.py +0 -0
  86. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/cursor/schema.json +0 -0
  87. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  88. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/opencode/__init__.py +0 -0
  89. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/opencode/mapper.py +0 -0
  90. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/opencode/schema.json +0 -0
  91. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  92. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/core/__init__.py +0 -0
  93. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/core/workspace_repository.py +0 -0
  94. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/errors.py +0 -0
  95. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/imports/__init__.py +0 -0
  96. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/imports/adapters.py +0 -0
  97. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/imports/filesystem.py +0 -0
  98. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/imports/models.py +0 -0
  99. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/imports/service.py +0 -0
  100. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/lossiness.py +0 -0
  101. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/mcp_service.py +0 -0
  102. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/models.py +0 -0
  103. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/rules/__init__.py +0 -0
  104. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/rules/compilers.py +0 -0
  105. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/rules/models.py +0 -0
  106. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/rules/parser.py +0 -0
  107. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/rules/repository.py +0 -0
  108. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/skills/__init__.py +0 -0
  109. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/skills/compilers.py +0 -0
  110. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/skills/models.py +0 -0
  111. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/skills/parser.py +0 -0
  112. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/spec/__init__.py +0 -0
  113. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/spec/loaders.py +0 -0
  114. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  115. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
  116. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
  117. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  118. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  119. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/tui/__init__.py +0 -0
  120. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/tui/enums.py +0 -0
  121. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/tui/import_selector.py +0 -0
  122. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/tui/renderers.py +0 -0
  123. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/tui/sections.py +0 -0
  124. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/tui/tables.py +0 -0
  125. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/validation.py +0 -0
  126. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/workspaces.py +0 -0
  127. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic.egg-info/dependency_links.txt +0 -0
  128. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic.egg-info/entry_points.txt +0 -0
  129. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic.egg-info/requires.txt +0 -0
  130. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic.egg-info/top_level.txt +0 -0
  131. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/setup.cfg +0 -0
  132. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_agents.py +0 -0
  133. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_aliases.py +0 -0
  134. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_apply_cursor.py +0 -0
  135. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_apps.py +0 -0
  136. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_explain_lossiness.py +0 -0
  137. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_flags.py +0 -0
  138. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_git_exclude.py +0 -0
  139. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_import.py +0 -0
  140. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_import_interactive.py +0 -0
  141. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_mcp.py +0 -0
  142. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_plan.py +0 -0
  143. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_restore.py +0 -0
  144. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_rules.py +0 -0
  145. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_skills.py +0 -0
  146. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_status.py +0 -0
  147. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_validate.py +0 -0
  148. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_workspace_resolution.py +0 -0
  149. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_common_repository.py +0 -0
  150. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_compiled_planning.py +0 -0
  151. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_dto_to_common_mcp.py +0 -0
  152. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_mcp_service.py +0 -0
  153. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_symlink_planning.py +0 -0
  154. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_sync_plan_model.py +0 -0
  155. {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_utils.py +0 -0
  156. {code_agnostic-0.3.2 → 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.2
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
 
@@ -0,0 +1,62 @@
1
+ """CLI entrypoint - thin wrapper that wires command modules."""
2
+
3
+ import click
4
+
5
+ from code_agnostic.cli import AliasedGroup
6
+ from code_agnostic.cli.commands.agents import agents_group
7
+ from code_agnostic.cli.commands.apps import apps
8
+ from code_agnostic.cli.commands.apply import apply
9
+ from code_agnostic.cli.commands.explain_lossiness import explain_lossiness
10
+ from code_agnostic.cli.commands.import_ import import_group
11
+ from code_agnostic.cli.commands.mcp import mcp
12
+ from code_agnostic.cli.commands.plan import plan
13
+ from code_agnostic.cli.commands.restore import restore
14
+ from code_agnostic.cli.commands.rules import rules
15
+ from code_agnostic.cli.commands.skills import skills
16
+ from code_agnostic.cli.commands.status import status
17
+ from code_agnostic.cli.commands.validate import validate
18
+ from code_agnostic.cli.commands.workspaces import workspaces
19
+
20
+
21
+ @click.group(
22
+ cls=AliasedGroup,
23
+ context_settings={"help_option_names": ["-h", "--help"]},
24
+ )
25
+ @click.pass_context
26
+ def cli(ctx: click.Context) -> None:
27
+ """App-based config sync."""
28
+ ctx.obj = {}
29
+
30
+
31
+ # Register individual commands
32
+ cli.add_command(plan)
33
+ cli.add_command(apply)
34
+ cli.add_command(restore)
35
+ cli.add_command(status)
36
+ cli.add_command(validate)
37
+ cli.add_command(explain_lossiness)
38
+
39
+ # Register command groups
40
+ cli.add_command(apps)
41
+ cli.add_command(workspaces)
42
+ cli.add_command(rules)
43
+ cli.add_command(skills)
44
+ cli.add_command(agents_group)
45
+ cli.add_command(mcp)
46
+ cli.add_command(import_group)
47
+
48
+
49
+ def main() -> int:
50
+ try:
51
+ cli(standalone_mode=False)
52
+ except click.exceptions.Exit as exc:
53
+ code = exc.exit_code
54
+ return code if isinstance(code, int) else 1
55
+ except click.ClickException as exc:
56
+ exc.show()
57
+ return 2
58
+ return 0
59
+
60
+
61
+ if __name__ == "__main__":
62
+ raise SystemExit(main())
@@ -1,6 +1,15 @@
1
1
  from enum import Enum
2
2
  from dataclasses import dataclass
3
3
 
4
+ from code_agnostic.constants import (
5
+ CODEX_CONFIG_FILENAME,
6
+ CODEX_PROJECT_DIRNAME,
7
+ CURSOR_CONFIG_FILENAME,
8
+ CURSOR_PROJECT_DIRNAME,
9
+ OPENCODE_CONFIG_FILENAME,
10
+ OPENCODE_PROJECT_DIRNAME,
11
+ )
12
+
4
13
 
5
14
  class AppId(str, Enum):
6
15
  CORE = "core"
@@ -19,6 +28,7 @@ class AppMetadata:
19
28
  supports_import_agents: bool
20
29
  supports_workspace_propagation: bool
21
30
  project_dir_name: str | None = None
31
+ config_filename: str | None = None
22
32
 
23
33
 
24
34
  APP_CATALOG: dict[AppId, AppMetadata] = {
@@ -40,7 +50,8 @@ APP_CATALOG: dict[AppId, AppMetadata] = {
40
50
  importable=True,
41
51
  supports_import_agents=True,
42
52
  supports_workspace_propagation=True,
43
- project_dir_name=".opencode",
53
+ project_dir_name=OPENCODE_PROJECT_DIRNAME,
54
+ config_filename=OPENCODE_CONFIG_FILENAME,
44
55
  ),
45
56
  AppId.CURSOR: AppMetadata(
46
57
  app_id=AppId.CURSOR,
@@ -49,8 +60,9 @@ APP_CATALOG: dict[AppId, AppMetadata] = {
49
60
  toggleable=True,
50
61
  importable=True,
51
62
  supports_import_agents=True,
52
- supports_workspace_propagation=False,
53
- project_dir_name=".cursor",
63
+ supports_workspace_propagation=True,
64
+ project_dir_name=CURSOR_PROJECT_DIRNAME,
65
+ config_filename=CURSOR_CONFIG_FILENAME,
54
66
  ),
55
67
  AppId.CODEX: AppMetadata(
56
68
  app_id=AppId.CODEX,
@@ -60,7 +72,8 @@ APP_CATALOG: dict[AppId, AppMetadata] = {
60
72
  importable=True,
61
73
  supports_import_agents=True,
62
74
  supports_workspace_propagation=True,
63
- project_dir_name=".codex",
75
+ project_dir_name=CODEX_PROJECT_DIRNAME,
76
+ config_filename=CODEX_CONFIG_FILENAME,
64
77
  ),
65
78
  }
66
79
 
@@ -74,6 +87,11 @@ def app_label(app: AppId | str) -> str:
74
87
  return app_metadata(app).label
75
88
 
76
89
 
90
+ def app_scope(app: AppId | str, resource: str) -> str:
91
+ app_id = app if isinstance(app, AppId) else AppId(app)
92
+ return f"app:{app_id.value}:{resource}"
93
+
94
+
77
95
  def app_ids_by_capability(
78
96
  *,
79
97
  targetable: bool | None = None,
@@ -9,12 +9,18 @@ except ModuleNotFoundError: # pragma: no cover
9
9
  import tomlkit
10
10
 
11
11
  from code_agnostic.apps.common.interfaces.repositories import IAppConfigRepository
12
+ from code_agnostic.constants import (
13
+ AGENTS_DIRNAME,
14
+ CODEX_CONFIG_FILENAME,
15
+ CODEX_PROJECT_DIRNAME,
16
+ SKILLS_DIRNAME,
17
+ )
12
18
  from code_agnostic.errors import InvalidConfigSchemaError, InvalidJsonFormatError
13
19
 
14
20
 
15
21
  class CodexConfigRepository(IAppConfigRepository):
16
22
  def __init__(self, root: Path | None = None) -> None:
17
- self._root = root or (Path.home() / ".codex")
23
+ self._root = root or (Path.home() / CODEX_PROJECT_DIRNAME)
18
24
 
19
25
  @property
20
26
  def root(self) -> Path:
@@ -22,15 +28,15 @@ class CodexConfigRepository(IAppConfigRepository):
22
28
 
23
29
  @property
24
30
  def config_path(self) -> Path:
25
- return self.root / "config.toml"
31
+ return self.root / CODEX_CONFIG_FILENAME
26
32
 
27
33
  @property
28
34
  def skills_dir(self) -> Path:
29
- return self.root / "skills"
35
+ return self.root / SKILLS_DIRNAME
30
36
 
31
37
  @property
32
38
  def agents_dir(self) -> Path:
33
- return self.root / "agents"
39
+ return self.root / AGENTS_DIRNAME
34
40
 
35
41
  def load_config(self) -> dict[str, Any]:
36
42
  if not self.config_path.exists() or self.config_path.stat().st_size == 0:
@@ -18,6 +18,10 @@
18
18
  "AbsolutePathBuf": {
19
19
  "type": "string"
20
20
  },
21
+ "AppToolApproval": {
22
+ "enum": ["auto", "prompt", "approve"],
23
+ "type": "string"
24
+ },
21
25
  "AgentRoleToml": {
22
26
  "type": "object",
23
27
  "additionalProperties": false,
@@ -41,28 +45,68 @@
41
45
  "$ref": "#/definitions/AgentRoleToml"
42
46
  }
43
47
  },
48
+ "McpServerToolConfig": {
49
+ "type": "object",
50
+ "additionalProperties": false,
51
+ "properties": {
52
+ "approval_mode": {
53
+ "allOf": [{ "$ref": "#/definitions/AppToolApproval" }],
54
+ "description": "Approval mode for this tool."
55
+ }
56
+ },
57
+ "description": "Per-tool approval settings for a single MCP server tool."
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
+ },
44
67
  "RawMcpServerConfig": {
45
68
  "type": "object",
46
69
  "additionalProperties": false,
47
70
  "properties": {
48
71
  "args": { "type": "array", "items": { "type": "string" } },
49
- "bearer_token": { "type": "string" },
50
72
  "bearer_token_env_var": { "type": "string" },
51
73
  "command": { "type": "string" },
52
74
  "cwd": { "type": "string" },
75
+ "default_tools_approval_mode": {
76
+ "allOf": [{ "$ref": "#/definitions/AppToolApproval" }],
77
+ "default": null
78
+ },
53
79
  "disabled_tools": { "type": "array", "items": { "type": "string" } },
54
80
  "enabled": { "type": "boolean" },
55
81
  "enabled_tools": { "type": "array", "items": { "type": "string" } },
56
82
  "env": { "type": "object", "additionalProperties": { "type": "string" } },
57
83
  "env_http_headers": { "type": "object", "additionalProperties": { "type": "string" } },
58
84
  "env_vars": { "type": "array", "items": { "type": "string" } },
85
+ "experimental_environment": { "type": "string" },
59
86
  "http_headers": { "type": "object", "additionalProperties": { "type": "string" } },
87
+ "name": {
88
+ "type": "string",
89
+ "default": null,
90
+ "description": "Legacy display-name field accepted for backward compatibility."
91
+ },
92
+ "oauth": {
93
+ "allOf": [{ "$ref": "#/definitions/McpServerOAuthConfig" }],
94
+ "default": null
95
+ },
60
96
  "oauth_resource": { "type": "string", "default": null },
61
97
  "required": { "type": "boolean" },
62
98
  "scopes": { "type": "array", "items": { "type": "string" } },
63
99
  "startup_timeout_ms": { "type": "integer", "minimum": 0 },
64
100
  "startup_timeout_sec": { "type": "number" },
101
+ "supports_parallel_tool_calls": { "type": "boolean" },
65
102
  "tool_timeout_sec": { "type": "number" },
103
+ "tools": {
104
+ "type": "object",
105
+ "default": null,
106
+ "additionalProperties": {
107
+ "$ref": "#/definitions/McpServerToolConfig"
108
+ }
109
+ },
66
110
  "url": { "type": "string" }
67
111
  }
68
112
  }
@@ -0,0 +1,274 @@
1
+ from copy import deepcopy
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ from jsonschema import Draft7Validator
6
+
7
+ from code_agnostic.agents.codex import normalize_codex_agent_filename
8
+ from code_agnostic.agents.compilers import CodexAgentCompiler
9
+ from code_agnostic.agents.parser import parse_agent
10
+ from code_agnostic.apps.app_id import AppId, app_label
11
+ from code_agnostic.apps.common.framework import (
12
+ RegisteredAppConfigService,
13
+ format_schema_error,
14
+ )
15
+ from code_agnostic.apps.codex.config_repository import CodexConfigRepository
16
+ from code_agnostic.apps.codex.mapper import CodexMCPMapper
17
+ from code_agnostic.apps.codex.schema_repository import CodexSchemaRepository
18
+ from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
19
+ from code_agnostic.apps.common.interfaces.repositories import (
20
+ IAppConfigRepository,
21
+ ISchemaRepository,
22
+ )
23
+ from code_agnostic.apps.common.models import MCPServerDTO
24
+ from code_agnostic.errors import (
25
+ InvalidConfigSchemaError,
26
+ InvalidJsonFormatError,
27
+ )
28
+ from code_agnostic.models import Action, ActionKind, ActionStatus
29
+ from code_agnostic.utils import merge_dict_overlay, read_json_safe
30
+ from code_agnostic.skills.compilers import CodexSkillCompiler
31
+ from code_agnostic.skills.parser import parse_skill
32
+
33
+
34
+ class CodexConfigService(RegisteredAppConfigService):
35
+ APP_ID = AppId.CODEX
36
+ APP_LABEL = app_label(APP_ID)
37
+
38
+ def __init__(
39
+ self,
40
+ repository: CodexConfigRepository,
41
+ mapper: IAppMCPMapper,
42
+ schema_repository: ISchemaRepository,
43
+ base_config_path: Path | None = None,
44
+ ) -> None:
45
+ self._repository = repository
46
+ self._codex_repo = repository
47
+ self._mapper = mapper
48
+ self._schema_repository = schema_repository
49
+ self._base_config_path = base_config_path
50
+ self._validator = Draft7Validator(self._schema_repository.load_schema())
51
+
52
+ @classmethod
53
+ def create_default(cls, root: Path | None = None) -> "CodexConfigService":
54
+ if root is not None:
55
+ return cls(
56
+ repository=CodexConfigRepository(root=root),
57
+ mapper=CodexMCPMapper(),
58
+ schema_repository=CodexSchemaRepository(),
59
+ base_config_path=None,
60
+ )
61
+ from code_agnostic.core.repository import CoreRepository
62
+
63
+ core = CoreRepository()
64
+ return cls(
65
+ repository=CodexConfigRepository(root=root),
66
+ mapper=CodexMCPMapper(),
67
+ schema_repository=CodexSchemaRepository(),
68
+ base_config_path=core.codex_base_path,
69
+ )
70
+
71
+ @property
72
+ def app_id(self) -> AppId:
73
+ return self.APP_ID
74
+
75
+ @property
76
+ def action_kind(self) -> ActionKind:
77
+ return ActionKind.WRITE_TEXT
78
+
79
+ @property
80
+ def repository(self) -> IAppConfigRepository:
81
+ return self._repository
82
+
83
+ @property
84
+ def mapper(self) -> IAppMCPMapper:
85
+ return self._mapper
86
+
87
+ def validate_config(self, payload: Any) -> None:
88
+ error = next(iter(self._validator.iter_errors(payload)), None)
89
+ if error is not None:
90
+ raise InvalidConfigSchemaError(
91
+ self.repository.config_path, format_schema_error(error)
92
+ )
93
+
94
+ def build_action_payload(self, payload: dict[str, Any]) -> Any:
95
+ return self.repository.serialize_config(payload)
96
+
97
+ def set_mcp_payload(
98
+ self, merged: dict[str, Any], desired_mcp: dict[str, Any]
99
+ ) -> None:
100
+ merged["mcp_servers"] = desired_mcp
101
+
102
+ def derive_status(
103
+ self, existing: dict[str, Any], merged: dict[str, Any]
104
+ ) -> ActionStatus:
105
+ rendered = self.repository.serialize_config(merged)
106
+ existing_text = (
107
+ self.repository.config_path.read_text(encoding="utf-8")
108
+ if self.repository.config_path.exists()
109
+ else ""
110
+ )
111
+ if not self.repository.config_path.exists():
112
+ return ActionStatus.CREATE
113
+ if existing_text == rendered:
114
+ return ActionStatus.NOOP
115
+ return ActionStatus.UPDATE
116
+
117
+ def build_action(
118
+ self,
119
+ common_servers: dict[str, MCPServerDTO],
120
+ agent_sources: list[Path] | None = None,
121
+ ) -> Action:
122
+ existing = self._codex_repo.load_config()
123
+ if existing or self._codex_repo.config_path.exists():
124
+ self.validate_config(existing)
125
+
126
+ desired_mcp = self.mapper.from_common(common_servers)
127
+ merged = dict(existing)
128
+ base = self._load_base_config()
129
+ for key, value in base.items():
130
+ if key == "mcp_servers":
131
+ continue
132
+ if key == "agents" and isinstance(value, dict):
133
+ merged["agents"] = self._merge_agents_payload(
134
+ merged.get("agents"), value
135
+ )
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
141
+ merged[key] = deepcopy(value)
142
+ self.set_mcp_payload(merged, desired_mcp)
143
+ if agent_sources:
144
+ merged["agents"] = self._merge_agents_payload(
145
+ merged.get("agents"),
146
+ self._build_agent_registry(agent_sources),
147
+ )
148
+ self.validate_config(merged)
149
+
150
+ return Action(
151
+ kind=self.action_kind,
152
+ path=self.repository.config_path,
153
+ status=self.derive_status(existing, merged),
154
+ detail=f"sync {self.app_id.value} config from common mcp base",
155
+ payload=self.build_action_payload(merged),
156
+ app=self.app_id.value,
157
+ )
158
+
159
+ def _merge_agents_payload(
160
+ self, existing: Any, overlay: dict[str, Any]
161
+ ) -> dict[str, Any]:
162
+ merged = dict(existing) if isinstance(existing, dict) else {}
163
+ for key, value in overlay.items():
164
+ merged[key] = deepcopy(value)
165
+ return merged
166
+
167
+ def _build_agent_registry(self, sources: list[Path]) -> dict[str, dict[str, Any]]:
168
+ registry: dict[str, dict[str, Any]] = {}
169
+ for source in sources:
170
+ try:
171
+ agent = parse_agent(source)
172
+ except InvalidConfigSchemaError:
173
+ raise
174
+ except Exception as exc:
175
+ raise InvalidConfigSchemaError(source, str(exc)) from exc
176
+
177
+ agent_name = agent.metadata.name or agent.name
178
+ target_name = (
179
+ normalize_codex_agent_filename(agent.metadata.name, agent.name)
180
+ + ".toml"
181
+ )
182
+ entry: dict[str, Any] = {
183
+ "description": agent.metadata.description or agent_name,
184
+ "config_file": (Path("agents") / target_name).as_posix(),
185
+ }
186
+ if agent.metadata.nickname_candidates:
187
+ entry["nickname_candidates"] = list(agent.metadata.nickname_candidates)
188
+ registry[agent_name] = entry
189
+ return registry
190
+
191
+ def plan_skill_actions(
192
+ self,
193
+ sources: list[Path],
194
+ target_dir: Path,
195
+ scope: str,
196
+ app: str,
197
+ managed_paths: list[Path],
198
+ removable_links: list[Path],
199
+ ) -> tuple[list[Action], list[Path], list[str]]:
200
+ compiler = CodexSkillCompiler()
201
+ return self._plan_compiled_text_actions(
202
+ sources=sources,
203
+ target_dir=target_dir,
204
+ scope=scope,
205
+ app=app,
206
+ managed_paths=managed_paths,
207
+ removable_links=removable_links,
208
+ compile_source=lambda source: (
209
+ target_dir / source.name / "SKILL.md",
210
+ compiler.compile(
211
+ parse_skill(
212
+ source / "SKILL.md"
213
+ if (source / "SKILL.md").exists()
214
+ else source
215
+ )
216
+ ),
217
+ ),
218
+ create_detail="create compiled codex skill",
219
+ noop_detail="compiled codex skill already up to date",
220
+ update_detail="update compiled codex skill",
221
+ conflict_message="Codex skill sync skipped (conflict): {target}",
222
+ )
223
+
224
+ def plan_agent_actions(
225
+ self,
226
+ sources: list[Path],
227
+ target_dir: Path,
228
+ scope: str,
229
+ app: str,
230
+ managed_paths: list[Path],
231
+ removable_links: list[Path],
232
+ ) -> tuple[list[Action], list[Path], list[str]]:
233
+ compiler = CodexAgentCompiler()
234
+
235
+ def compile_source(source: Path) -> tuple[Path, str]:
236
+ try:
237
+ agent = parse_agent(source)
238
+ payload = compiler.compile(agent)
239
+ except InvalidConfigSchemaError:
240
+ raise
241
+ except Exception as exc:
242
+ raise InvalidConfigSchemaError(source, str(exc)) from exc
243
+
244
+ target_name = (
245
+ normalize_codex_agent_filename(agent.metadata.name, agent.name)
246
+ + ".toml"
247
+ )
248
+ return target_dir / target_name, payload
249
+
250
+ return self._plan_compiled_text_actions(
251
+ sources=sources,
252
+ target_dir=target_dir,
253
+ scope=scope,
254
+ app=app,
255
+ managed_paths=managed_paths,
256
+ removable_links=removable_links,
257
+ compile_source=compile_source,
258
+ create_detail="create compiled codex agent",
259
+ noop_detail="compiled codex agent already up to date",
260
+ update_detail="update compiled codex agent",
261
+ conflict_message="Codex agent sync skipped (conflict): {target}",
262
+ )
263
+
264
+ def _load_base_config(self) -> dict[str, Any]:
265
+ if self._base_config_path is None or not self._base_config_path.exists():
266
+ return {}
267
+ payload, error = read_json_safe(self._base_config_path)
268
+ if error is not None:
269
+ raise InvalidJsonFormatError(self._base_config_path, error)
270
+ if not isinstance(payload, dict):
271
+ raise InvalidConfigSchemaError(
272
+ self._base_config_path, "must be a JSON object"
273
+ )
274
+ return payload
@@ -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]: