code-agnostic 0.3.7__tar.gz → 0.3.8__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 (158) hide show
  1. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/PKG-INFO +8 -6
  2. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/README.md +5 -5
  3. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/__init__.py +1 -1
  4. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/agents/codex.py +6 -3
  5. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/app_id.py +1 -1
  6. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/claude/service.py +2 -66
  7. code_agnostic-0.3.8/code_agnostic/apps/common/compiled_planning.py +115 -0
  8. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/common/interfaces/repositories.py +10 -0
  9. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/common/interfaces/service.py +30 -14
  10. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/opencode/service.py +2 -0
  11. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/commands/workspaces.py +6 -1
  12. code_agnostic-0.3.8/code_agnostic/generated_artifacts.py +134 -0
  13. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/git_exclude_service.py +23 -41
  14. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/imports/adapters.py +10 -10
  15. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/planner.py +15 -12
  16. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/status.py +11 -60
  17. code_agnostic-0.3.8/code_agnostic/workspace_artifacts.py +238 -0
  18. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic.egg-info/PKG-INFO +8 -6
  19. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic.egg-info/SOURCES.txt +2 -0
  20. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic.egg-info/requires.txt +2 -0
  21. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/pyproject.toml +4 -2
  22. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_apply_apps.py +38 -12
  23. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_plan.py +14 -4
  24. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_workspaces.py +53 -15
  25. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_git_exclude_service.py +31 -9
  26. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_workspace_config_sync.py +120 -16
  27. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_workspace_repo_status.py +57 -11
  28. code_agnostic-0.3.7/code_agnostic/apps/common/compiled_planning.py +0 -197
  29. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/LICENSE +0 -0
  30. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/__main__.py +0 -0
  31. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/agents/__init__.py +0 -0
  32. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/agents/claude.py +0 -0
  33. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/agents/compilers.py +0 -0
  34. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/agents/models.py +0 -0
  35. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/agents/opencode.py +0 -0
  36. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/agents/parser.py +0 -0
  37. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/__init__.py +0 -0
  38. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/apps_service.py +0 -0
  39. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/claude/__init__.py +0 -0
  40. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/claude/config_repository.py +0 -0
  41. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/claude/mapper.py +0 -0
  42. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/codex/__init__.py +0 -0
  43. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/codex/config_repository.py +0 -0
  44. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/codex/mapper.py +0 -0
  45. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/codex/schema.json +0 -0
  46. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/codex/schema_repository.py +0 -0
  47. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/codex/service.py +0 -0
  48. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/common/__init__.py +0 -0
  49. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/common/framework.py +0 -0
  50. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  51. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  52. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/common/loader.py +0 -0
  53. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/common/models.py +0 -0
  54. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/common/schema.py +0 -0
  55. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/common/symlink_planning.py +0 -0
  56. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/common/utils.py +0 -0
  57. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/cursor/__init__.py +0 -0
  58. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/cursor/config_repository.py +0 -0
  59. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/cursor/mapper.py +0 -0
  60. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/cursor/schema.json +0 -0
  61. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  62. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/cursor/service.py +0 -0
  63. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/opencode/__init__.py +0 -0
  64. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/opencode/config_repository.py +0 -0
  65. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/opencode/mapper.py +0 -0
  66. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/opencode/schema.json +0 -0
  67. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  68. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/__init__.py +0 -0
  69. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/aliases.py +0 -0
  70. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/commands/__init__.py +0 -0
  71. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/commands/agents.py +0 -0
  72. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/commands/apply.py +0 -0
  73. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/commands/apps.py +0 -0
  74. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
  75. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/commands/import_.py +0 -0
  76. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/commands/mcp.py +0 -0
  77. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/commands/plan.py +0 -0
  78. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/commands/restore.py +0 -0
  79. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/commands/rules.py +0 -0
  80. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/commands/skills.py +0 -0
  81. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/commands/status.py +0 -0
  82. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/commands/validate.py +0 -0
  83. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/helpers.py +0 -0
  84. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/cli/options.py +0 -0
  85. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/constants.py +0 -0
  86. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/core/__init__.py +0 -0
  87. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/core/repository.py +0 -0
  88. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/core/workspace_repository.py +0 -0
  89. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/errors.py +0 -0
  90. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/executor.py +0 -0
  91. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/imports/__init__.py +0 -0
  92. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/imports/filesystem.py +0 -0
  93. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/imports/models.py +0 -0
  94. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/imports/service.py +0 -0
  95. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/lossiness.py +0 -0
  96. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/mcp_service.py +0 -0
  97. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/models.py +0 -0
  98. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/rules/__init__.py +0 -0
  99. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/rules/compilers.py +0 -0
  100. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/rules/models.py +0 -0
  101. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/rules/parser.py +0 -0
  102. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/rules/repository.py +0 -0
  103. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/skills/__init__.py +0 -0
  104. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/skills/compilers.py +0 -0
  105. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/skills/models.py +0 -0
  106. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/skills/parser.py +0 -0
  107. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/spec/__init__.py +0 -0
  108. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/spec/loaders.py +0 -0
  109. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  110. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
  111. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
  112. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  113. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  114. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/tui/__init__.py +0 -0
  115. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/tui/enums.py +0 -0
  116. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/tui/import_selector.py +0 -0
  117. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/tui/renderers.py +0 -0
  118. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/tui/sections.py +0 -0
  119. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/tui/tables.py +0 -0
  120. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/utils.py +0 -0
  121. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/validation.py +0 -0
  122. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic/workspaces.py +0 -0
  123. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic.egg-info/dependency_links.txt +0 -0
  124. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic.egg-info/entry_points.txt +0 -0
  125. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/code_agnostic.egg-info/top_level.txt +0 -0
  126. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/setup.cfg +0 -0
  127. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_agents.py +0 -0
  128. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_aliases.py +0 -0
  129. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_apply_codex.py +0 -0
  130. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_apply_cursor.py +0 -0
  131. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_apply_target.py +0 -0
  132. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_apps.py +0 -0
  133. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_explain_lossiness.py +0 -0
  134. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_flags.py +0 -0
  135. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_git_exclude.py +0 -0
  136. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_import.py +0 -0
  137. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_import_interactive.py +0 -0
  138. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_mcp.py +0 -0
  139. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_module_organization.py +0 -0
  140. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_restore.py +0 -0
  141. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_rules.py +0 -0
  142. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_skills.py +0 -0
  143. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_status.py +0 -0
  144. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_validate.py +0 -0
  145. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_cli_workspace_resolution.py +0 -0
  146. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_common_mcp_to_dto.py +0 -0
  147. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_common_repository.py +0 -0
  148. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_compiled_planning.py +0 -0
  149. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_dto_to_common_mcp.py +0 -0
  150. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_mcp_service.py +0 -0
  151. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_planner_executor.py +0 -0
  152. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_planner_rules.py +0 -0
  153. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_symlink_planning.py +0 -0
  154. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_sync_plan_model.py +0 -0
  155. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_transactional_executor.py +0 -0
  156. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_utils.py +0 -0
  157. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_version.py +0 -0
  158. {code_agnostic-0.3.7 → code_agnostic-0.3.8}/tests/test_workspaces.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-agnostic
3
- Version: 0.3.7
3
+ Version: 0.3.8
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
@@ -19,6 +19,8 @@ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
19
19
  Requires-Dist: jsonschema>=4.0; extra == "dev"
20
20
  Requires-Dist: ruff>=0.8.0; extra == "dev"
21
21
  Requires-Dist: mypy>=1.11; extra == "dev"
22
+ Requires-Dist: types-jsonschema>=4.0; extra == "dev"
23
+ Requires-Dist: types-PyYAML>=6.0; extra == "dev"
22
24
  Dynamic: license-file
23
25
 
24
26
  # code-agnostic
@@ -115,15 +117,15 @@ code-agnostic apply
115
117
  | Native repo config include for workspace `AGENTS.md` | yes | -- | -- | -- |
116
118
  | Repo/subdir gets shared workspace instructions today | yes | -- | yes | yes |
117
119
  | Nested `AGENTS.md` discovery | -- | yes | yes | -- |
118
- | Workspace propagation | yes | -- | yes | yes |
120
+ | Workspace propagation | yes | yes | yes | yes |
119
121
  | Import from | yes | yes | yes | yes |
120
122
  | Interactive import (TUI) | yes | yes | yes | yes |
121
123
 
122
- 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
124
+ Cursor workspace propagation writes repo-local MCP, skills, and agents when those resources exist in the workspace source config.
123
125
 
124
126
  OpenCode workspace configs include the shared workspace `AGENTS.md` natively via `instructions`, so repos under the workspace get both repo-local and shared workspace instructions. Codex repos receive workspace instructions through a generated `AGENTS.override.md`, which is added to each repo's `.git/info/exclude`. Claude Code receives workspace instructions through generated `CLAUDE.local.md` files, never by editing committed `CLAUDE.md`.
125
127
 
126
- Cursor documents `AGENTS.md` support in project roots and subdirectories. `code-agnostic` still disables Cursor workspace propagation, so it does not copy or link the shared workspace `AGENTS.md` into child repos; Cursor will load repo-local or nested `AGENTS.md` files that already exist in the opened project. Codex documents nested `AGENTS.md` discovery, but not a native config include for an extra workspace file.
128
+ Cursor documents `AGENTS.md` support in project roots and subdirectories. `code-agnostic` does not copy or link the shared workspace `AGENTS.md` into child repos; Cursor will load repo-local or nested `AGENTS.md` files that already exist in the opened project. Codex documents nested `AGENTS.md` discovery, but not a native config include for an extra workspace file.
127
129
 
128
130
  ## Features
129
131
 
@@ -211,9 +213,9 @@ That command should copy the skill into the source of truth and then run the nor
211
213
 
212
214
  ### Workspaces
213
215
 
214
- 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`. Claude receives generated `CLAUDE.local.md` files and project MCP entries in `~/.claude.json["projects"][absolute_repo_path]["mcpServers"]`. Repo-local app config, skills, and agents are propagated for OpenCode, Codex, and Claude.
216
+ 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`. Claude receives generated `CLAUDE.local.md` files and project MCP entries in `~/.claude.json["projects"][absolute_repo_path]["mcpServers"]`. Repo-local app config, skills, and agents are propagated for OpenCode, Cursor, Codex, and Claude.
215
217
 
216
- `.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).
218
+ Cursor propagation intentionally stays to repo-local MCP, skills, and agents; it does not copy the shared workspace `AGENTS.md` into child repos.
217
219
 
218
220
  ```bash
219
221
  code-agnostic workspaces add --name myproject --path ~/code/myproject
@@ -92,15 +92,15 @@ code-agnostic apply
92
92
  | Native repo config include for workspace `AGENTS.md` | yes | -- | -- | -- |
93
93
  | Repo/subdir gets shared workspace instructions today | yes | -- | yes | yes |
94
94
  | Nested `AGENTS.md` discovery | -- | yes | yes | -- |
95
- | Workspace propagation | yes | -- | yes | yes |
95
+ | Workspace propagation | yes | yes | yes | yes |
96
96
  | Import from | yes | yes | yes | yes |
97
97
  | Interactive import (TUI) | yes | yes | yes | yes |
98
98
 
99
- 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
99
+ Cursor workspace propagation writes repo-local MCP, skills, and agents when those resources exist in the workspace source config.
100
100
 
101
101
  OpenCode workspace configs include the shared workspace `AGENTS.md` natively via `instructions`, so repos under the workspace get both repo-local and shared workspace instructions. Codex repos receive workspace instructions through a generated `AGENTS.override.md`, which is added to each repo's `.git/info/exclude`. Claude Code receives workspace instructions through generated `CLAUDE.local.md` files, never by editing committed `CLAUDE.md`.
102
102
 
103
- Cursor documents `AGENTS.md` support in project roots and subdirectories. `code-agnostic` still disables Cursor workspace propagation, so it does not copy or link the shared workspace `AGENTS.md` into child repos; Cursor will load repo-local or nested `AGENTS.md` files that already exist in the opened project. Codex documents nested `AGENTS.md` discovery, but not a native config include for an extra workspace file.
103
+ Cursor documents `AGENTS.md` support in project roots and subdirectories. `code-agnostic` does not copy or link the shared workspace `AGENTS.md` into child repos; Cursor will load repo-local or nested `AGENTS.md` files that already exist in the opened project. Codex documents nested `AGENTS.md` discovery, but not a native config include for an extra workspace file.
104
104
 
105
105
  ## Features
106
106
 
@@ -188,9 +188,9 @@ That command should copy the skill into the source of truth and then run the nor
188
188
 
189
189
  ### Workspaces
190
190
 
191
- 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`. Claude receives generated `CLAUDE.local.md` files and project MCP entries in `~/.claude.json["projects"][absolute_repo_path]["mcpServers"]`. Repo-local app config, skills, and agents are propagated for OpenCode, Codex, and Claude.
191
+ 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`. Claude receives generated `CLAUDE.local.md` files and project MCP entries in `~/.claude.json["projects"][absolute_repo_path]["mcpServers"]`. Repo-local app config, skills, and agents are propagated for OpenCode, Cursor, Codex, and Claude.
192
192
 
193
- `.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).
193
+ Cursor propagation intentionally stays to repo-local MCP, skills, and agents; it does not copy the shared workspace `AGENTS.md` into child repos.
194
194
 
195
195
  ```bash
196
196
  code-agnostic workspaces add --name myproject --path ~/code/myproject
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.3.7"
3
+ __version__ = "0.3.8"
@@ -69,10 +69,13 @@ def serialize_codex_agent(agent: Agent) -> str:
69
69
  description = agent.metadata.description or agent.metadata.name or agent.name
70
70
 
71
71
  doc = tomlkit.document()
72
- doc.add("name", agent.metadata.name or agent.name)
73
- doc.add("description", description)
72
+ doc.add("name", tomlkit.item(agent.metadata.name or agent.name))
73
+ doc.add("description", tomlkit.item(description))
74
74
  if agent.metadata.nickname_candidates:
75
- doc.add("nickname_candidates", list(agent.metadata.nickname_candidates))
75
+ doc.add(
76
+ "nickname_candidates",
77
+ tomlkit.item(list(agent.metadata.nickname_candidates)),
78
+ )
76
79
  model = agent.metadata.effective_value("codex", "model")
77
80
  if model:
78
81
  doc.add("model", model)
@@ -63,7 +63,7 @@ APP_CATALOG: dict[AppId, AppMetadata] = {
63
63
  toggleable=True,
64
64
  importable=True,
65
65
  supports_import_agents=True,
66
- supports_workspace_propagation=False,
66
+ supports_workspace_propagation=True,
67
67
  project_dir_name=CURSOR_PROJECT_DIRNAME,
68
68
  config_filename=CURSOR_CONFIG_FILENAME,
69
69
  ),
@@ -8,10 +8,6 @@ from code_agnostic.agents.parser import parse_agent
8
8
  from code_agnostic.apps.app_id import AppId, app_label
9
9
  from code_agnostic.apps.claude.config_repository import ClaudeConfigRepository
10
10
  from code_agnostic.apps.claude.mapper import ClaudeMCPMapper
11
- from code_agnostic.apps.common.compiled_planning import (
12
- find_replaceable_symlink_ancestor,
13
- plan_owned_compiled_text_action,
14
- )
15
11
  from code_agnostic.apps.common.framework import RegisteredAppConfigService
16
12
  from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
17
13
  from code_agnostic.apps.common.interfaces.repositories import IAppConfigRepository
@@ -135,7 +131,7 @@ class ClaudeConfigService(RegisteredAppConfigService):
135
131
  removable_links: list[Path],
136
132
  ) -> tuple[list[Action], list[Path], list[str]]:
137
133
  compiler = ClaudeSkillCompiler()
138
- return self._plan_owned_text_actions(
134
+ return self._plan_compiled_text_actions(
139
135
  sources=sources,
140
136
  target_dir=target_dir,
141
137
  scope=scope,
@@ -173,7 +169,7 @@ class ClaudeConfigService(RegisteredAppConfigService):
173
169
  agent = parse_agent(source)
174
170
  return claude_agent_target_path(target_dir, agent), compiler.compile(agent)
175
171
 
176
- return self._plan_owned_text_actions(
172
+ return self._plan_compiled_text_actions(
177
173
  sources=sources,
178
174
  target_dir=target_dir,
179
175
  scope=scope,
@@ -186,63 +182,3 @@ class ClaudeConfigService(RegisteredAppConfigService):
186
182
  update_detail="update compiled claude agent",
187
183
  conflict_message="Claude agent sync skipped (conflict): {target}",
188
184
  )
189
-
190
- def _plan_owned_text_actions(
191
- self,
192
- *,
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
- compile_source,
200
- create_detail: str,
201
- noop_detail: str,
202
- update_detail: str,
203
- conflict_message: str,
204
- ) -> tuple[list[Action], list[Path], list[str]]:
205
- managed_path_set = {path.resolve(strict=False) for path in managed_paths}
206
- removable_link_set = {path.resolve(strict=False) for path in removable_links}
207
- actions: list[Action] = []
208
- desired_paths: list[Path] = []
209
- skipped: list[str] = []
210
- scheduled_removals: set[Path] = set()
211
-
212
- for source in sources:
213
- target, payload = compile_source(source)
214
- desired_paths.append(target)
215
- replaceable_symlink = find_replaceable_symlink_ancestor(target, target_dir)
216
- if (
217
- replaceable_symlink is not None
218
- and replaceable_symlink not in scheduled_removals
219
- ):
220
- scheduled_removals.add(replaceable_symlink)
221
- removable_link_set.add(replaceable_symlink.resolve(strict=False))
222
- actions.append(
223
- Action(
224
- kind=ActionKind.REMOVE_SYMLINK,
225
- path=replaceable_symlink,
226
- status=ActionStatus.REMOVE,
227
- detail=f"replace compiled {scope} symlink",
228
- app=app,
229
- scope=scope,
230
- )
231
- )
232
- action = plan_owned_compiled_text_action(
233
- target=target,
234
- payload=payload,
235
- managed_paths=managed_path_set,
236
- removable_link_paths=removable_link_set,
237
- managed_root=target_dir,
238
- scope=scope,
239
- app=app,
240
- create_detail=create_detail,
241
- noop_detail=noop_detail,
242
- update_detail=update_detail,
243
- )
244
- actions.append(action)
245
- if action.status == ActionStatus.CONFLICT:
246
- skipped.append(conflict_message.format(target=target))
247
-
248
- return actions, desired_paths, skipped
@@ -0,0 +1,115 @@
1
+ from pathlib import Path
2
+
3
+ from code_agnostic.generated_artifacts import (
4
+ ArtifactKind,
5
+ GeneratedArtifact,
6
+ OwnershipPolicy,
7
+ plan_generated_artifact,
8
+ )
9
+ from code_agnostic.models import Action
10
+
11
+
12
+ def find_replaceable_symlink_ancestor(target: Path, managed_root: Path) -> Path | None:
13
+ current = target
14
+ while True:
15
+ if current.is_symlink() and (
16
+ current == managed_root or current.is_relative_to(managed_root)
17
+ ):
18
+ return current
19
+ if current.parent == current:
20
+ return None
21
+ current = current.parent
22
+
23
+
24
+ def plan_compiled_text_action(
25
+ *,
26
+ target: Path,
27
+ payload: str,
28
+ managed_paths: set[Path],
29
+ removable_link_paths: set[Path] | None = None,
30
+ managed_root: Path | None = None,
31
+ scope: str,
32
+ app: str,
33
+ create_detail: str,
34
+ noop_detail: str,
35
+ update_detail: str,
36
+ conflict_detail: str = "non-managed path exists",
37
+ ) -> Action:
38
+ return _plan_text_action(
39
+ ownership=OwnershipPolicy.MANAGED_REPLACE,
40
+ target=target,
41
+ payload=payload,
42
+ managed_paths=managed_paths,
43
+ removable_link_paths=removable_link_paths,
44
+ managed_root=managed_root,
45
+ scope=scope,
46
+ app=app,
47
+ create_detail=create_detail,
48
+ noop_detail=noop_detail,
49
+ update_detail=update_detail,
50
+ conflict_detail=conflict_detail,
51
+ )
52
+
53
+
54
+ def plan_owned_compiled_text_action(
55
+ *,
56
+ target: Path,
57
+ payload: str,
58
+ managed_paths: set[Path],
59
+ removable_link_paths: set[Path] | None = None,
60
+ managed_root: Path | None = None,
61
+ scope: str,
62
+ app: str,
63
+ create_detail: str,
64
+ noop_detail: str,
65
+ update_detail: str,
66
+ conflict_detail: str = "non-managed path exists",
67
+ ) -> Action:
68
+ return _plan_text_action(
69
+ ownership=OwnershipPolicy.OWNED_ONLY,
70
+ target=target,
71
+ payload=payload,
72
+ managed_paths=managed_paths,
73
+ removable_link_paths=removable_link_paths,
74
+ managed_root=managed_root,
75
+ scope=scope,
76
+ app=app,
77
+ create_detail=create_detail,
78
+ noop_detail=noop_detail,
79
+ update_detail=update_detail,
80
+ conflict_detail=conflict_detail,
81
+ )
82
+
83
+
84
+ def _plan_text_action(
85
+ *,
86
+ ownership: OwnershipPolicy,
87
+ target: Path,
88
+ payload: str,
89
+ managed_paths: set[Path],
90
+ removable_link_paths: set[Path] | None,
91
+ managed_root: Path | None,
92
+ scope: str,
93
+ app: str,
94
+ create_detail: str,
95
+ noop_detail: str,
96
+ update_detail: str,
97
+ conflict_detail: str,
98
+ ) -> Action:
99
+ return plan_generated_artifact(
100
+ GeneratedArtifact(
101
+ path=target,
102
+ kind=ArtifactKind.TEXT,
103
+ payload=payload,
104
+ ownership=ownership,
105
+ managed_root=managed_root,
106
+ scope=scope,
107
+ app=app,
108
+ create_detail=create_detail,
109
+ noop_detail=noop_detail,
110
+ update_detail=update_detail,
111
+ conflict_detail=conflict_detail,
112
+ ),
113
+ managed_paths=managed_paths,
114
+ removable_link_paths=removable_link_paths,
115
+ )
@@ -20,6 +20,16 @@ class IAppConfigRepository(ABC):
20
20
  def config_path(self) -> Path:
21
21
  raise NotImplementedError
22
22
 
23
+ @property
24
+ @abstractmethod
25
+ def skills_dir(self) -> Path:
26
+ raise NotImplementedError
27
+
28
+ @property
29
+ @abstractmethod
30
+ def agents_dir(self) -> Path:
31
+ raise NotImplementedError
32
+
23
33
  @abstractmethod
24
34
  def load_config(self) -> dict[str, Any]:
25
35
  raise NotImplementedError
@@ -6,7 +6,6 @@ from typing import Any
6
6
  from code_agnostic.apps.app_id import AppId, app_scope
7
7
  from code_agnostic.apps.common.compiled_planning import (
8
8
  find_replaceable_symlink_ancestor,
9
- plan_compiled_text_action,
10
9
  )
11
10
  from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
12
11
  from code_agnostic.apps.common.interfaces.repositories import IAppConfigRepository
@@ -18,6 +17,12 @@ from code_agnostic.apps.common.symlink_planning import (
18
17
  plan_stale_files_group,
19
18
  plan_stale_group,
20
19
  )
20
+ from code_agnostic.generated_artifacts import (
21
+ ArtifactKind,
22
+ GeneratedArtifact,
23
+ OwnershipPolicy,
24
+ plan_generated_artifact,
25
+ )
21
26
  from code_agnostic.models import Action, ActionKind, ActionStatus, SyncPlan
22
27
 
23
28
 
@@ -92,7 +97,10 @@ class IAppConfigService(ABC):
92
97
  raise NotImplementedError
93
98
 
94
99
  def agent_action_removable_links(self, removable_links: list[Path]) -> list[Path]:
95
- return []
100
+ return removable_links
101
+
102
+ def compiled_resource_ownership_policy(self) -> OwnershipPolicy:
103
+ return OwnershipPolicy.OWNED_ONLY
96
104
 
97
105
  @staticmethod
98
106
  def _normalize_managed_group(value: Any) -> dict[str, Any]:
@@ -119,14 +127,18 @@ class IAppConfigService(ABC):
119
127
  desired_paths: list[Path] = []
120
128
  skipped: list[str] = []
121
129
  scheduled_removals: set[Path] = set()
130
+ ownership = self.compiled_resource_ownership_policy()
122
131
 
123
132
  for source in sources:
124
133
  target, payload = compile_source(source)
125
134
  desired_paths.append(target)
126
135
  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
136
+ can_replace_symlink = replaceable_symlink is not None and (
137
+ ownership != OwnershipPolicy.OWNED_ONLY
138
+ or replaceable_symlink.resolve(strict=False) in removable_link_set
139
+ )
140
+ if replaceable_symlink is not None and (
141
+ can_replace_symlink and replaceable_symlink not in scheduled_removals
130
142
  ):
131
143
  scheduled_removals.add(replaceable_symlink)
132
144
  removable_link_set.add(replaceable_symlink.resolve(strict=False))
@@ -140,17 +152,21 @@ class IAppConfigService(ABC):
140
152
  scope=scope,
141
153
  )
142
154
  )
143
- action = plan_compiled_text_action(
144
- target=target,
145
- payload=payload,
155
+ action = plan_generated_artifact(
156
+ GeneratedArtifact(
157
+ path=target,
158
+ kind=ArtifactKind.TEXT,
159
+ payload=payload,
160
+ ownership=ownership,
161
+ managed_root=target_dir,
162
+ scope=scope,
163
+ app=app,
164
+ create_detail=create_detail,
165
+ noop_detail=noop_detail,
166
+ update_detail=update_detail,
167
+ ),
146
168
  managed_paths=managed_path_set,
147
169
  removable_link_paths=removable_link_set,
148
- managed_root=target_dir,
149
- scope=scope,
150
- app=app,
151
- create_detail=create_detail,
152
- noop_detail=noop_detail,
153
- update_detail=update_detail,
154
170
  )
155
171
  actions.append(action)
156
172
  if action.status == ActionStatus.CONFLICT:
@@ -119,6 +119,8 @@ class OpenCodeConfigService(RegisteredAppConfigService):
119
119
  from code_agnostic.errors import InvalidJsonFormatError
120
120
  from code_agnostic.utils import read_json_safe
121
121
 
122
+ if self._base_config_path is None:
123
+ return {}
122
124
  if not self._base_config_path.exists():
123
125
  return {}
124
126
  payload, error = read_json_safe(self._base_config_path)
@@ -114,12 +114,17 @@ def workspaces_git_exclude(obj: dict[str, str], workspace: str | None) -> None:
114
114
  workspace_path = Path(item["path"])
115
115
  if not workspace_path.exists() or not workspace_path.is_dir():
116
116
  continue
117
- entries = exclude_service.compute_entries(item["name"], enabled_apps)
118
117
  repos = workspace_service.discover_git_repos(workspace_path)
119
118
  for repo in repos:
120
119
  git_dir = workspace_service.resolve_git_dir(repo)
121
120
  if git_dir is None:
122
121
  continue
122
+ entries = exclude_service.compute_entries_for_repo(
123
+ item["name"],
124
+ enabled_apps,
125
+ workspace_path=workspace_path,
126
+ repo_path=repo,
127
+ )
123
128
  exclude_path = git_dir / "info" / "exclude"
124
129
  added, changed = ensure_exclude_entries(exclude_path, entries)
125
130
  processed += 1
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from code_agnostic.models import Action, ActionKind, ActionStatus
9
+
10
+
11
+ class ArtifactKind(str, Enum):
12
+ TEXT = "text"
13
+
14
+
15
+ class OwnershipPolicy(str, Enum):
16
+ OWNED_ONLY = "owned_only"
17
+ MANAGED_REPLACE = "managed_replace"
18
+ MERGE_NATIVE_CONFIG = "merge_native_config"
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class GeneratedArtifact:
23
+ path: Path
24
+ kind: ArtifactKind
25
+ payload: Any
26
+ ownership: OwnershipPolicy
27
+ scope: str
28
+ app: str
29
+ create_detail: str
30
+ noop_detail: str
31
+ update_detail: str
32
+ conflict_detail: str = "non-managed path exists"
33
+ managed_root: Path | None = None
34
+
35
+
36
+ def plan_generated_artifact(
37
+ artifact: GeneratedArtifact,
38
+ *,
39
+ managed_paths: set[Path],
40
+ removable_link_paths: set[Path] | None = None,
41
+ ) -> Action:
42
+ if artifact.kind != ArtifactKind.TEXT:
43
+ raise NotImplementedError(
44
+ f"Unsupported generated artifact kind: {artifact.kind}"
45
+ )
46
+ if not isinstance(artifact.payload, str):
47
+ raise TypeError("Text generated artifacts require a string payload")
48
+
49
+ managed_path_set = {path.resolve(strict=False) for path in managed_paths}
50
+ removable = {path.resolve(strict=False) for path in (removable_link_paths or set())}
51
+ has_symlink_ancestor, is_removable_ancestor = _symlink_ancestor_state(
52
+ artifact.path, removable, artifact.managed_root
53
+ )
54
+
55
+ if artifact.ownership == OwnershipPolicy.OWNED_ONLY:
56
+ target_key = artifact.path.resolve(strict=False)
57
+ is_managed_target = target_key in managed_path_set or _is_under_any(
58
+ target_key, removable
59
+ )
60
+ if not is_managed_target and not is_removable_ancestor:
61
+ if (
62
+ not artifact.path.exists()
63
+ and not artifact.path.is_symlink()
64
+ and not has_symlink_ancestor
65
+ ):
66
+ return _action(artifact, ActionStatus.CREATE, artifact.create_detail)
67
+ return _action(artifact, ActionStatus.CONFLICT, artifact.conflict_detail)
68
+
69
+ return _plan_replace_text_artifact(
70
+ artifact,
71
+ has_symlink_ancestor=has_symlink_ancestor,
72
+ is_removable_ancestor=is_removable_ancestor,
73
+ )
74
+
75
+
76
+ def _is_under_any(path: Path, ancestors: set[Path]) -> bool:
77
+ return any(
78
+ path == ancestor or path.is_relative_to(ancestor) for ancestor in ancestors
79
+ )
80
+
81
+
82
+ def _symlink_ancestor_state(
83
+ target: Path, removable_link_paths: set[Path], managed_root: Path | None
84
+ ) -> tuple[bool, bool]:
85
+ current = target
86
+ found_symlink = False
87
+ while True:
88
+ in_managed_root = managed_root is None or (
89
+ current == managed_root or current.is_relative_to(managed_root)
90
+ )
91
+ if current.is_symlink() and in_managed_root:
92
+ found_symlink = True
93
+ if current.resolve(strict=False) in removable_link_paths:
94
+ return True, True
95
+ if current.parent == current:
96
+ return found_symlink, False
97
+ current = current.parent
98
+
99
+
100
+ def _plan_replace_text_artifact(
101
+ artifact: GeneratedArtifact,
102
+ *,
103
+ has_symlink_ancestor: bool,
104
+ is_removable_ancestor: bool,
105
+ ) -> Action:
106
+ if has_symlink_ancestor and not is_removable_ancestor:
107
+ return _action(artifact, ActionStatus.CONFLICT, artifact.conflict_detail)
108
+
109
+ if not artifact.path.exists() and not artifact.path.is_symlink():
110
+ return _action(artifact, ActionStatus.CREATE, artifact.create_detail)
111
+
112
+ if artifact.path.is_file():
113
+ existing = artifact.path.read_text(encoding="utf-8")
114
+ if existing == artifact.payload:
115
+ return _action(artifact, ActionStatus.NOOP, artifact.noop_detail)
116
+ return _action(artifact, ActionStatus.UPDATE, artifact.update_detail)
117
+
118
+ return _action(artifact, ActionStatus.CONFLICT, artifact.conflict_detail)
119
+
120
+
121
+ def _action(
122
+ artifact: GeneratedArtifact,
123
+ status: ActionStatus,
124
+ detail: str,
125
+ ) -> Action:
126
+ return Action(
127
+ kind=ActionKind.WRITE_TEXT,
128
+ path=artifact.path,
129
+ status=status,
130
+ detail=detail,
131
+ payload=artifact.payload,
132
+ app=artifact.app,
133
+ scope=artifact.scope,
134
+ )