code-agnostic 0.7.0__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/PKG-INFO +3 -3
  2. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/README.md +2 -2
  3. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/__init__.py +1 -1
  4. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/apps_service.py +9 -17
  5. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/claude/mapper.py +12 -25
  6. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/claude/service.py +0 -3
  7. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/codex/service.py +2 -6
  8. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/common/interfaces/repositories.py +4 -2
  9. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/common/interfaces/service.py +9 -9
  10. code_agnostic-0.9.0/code_agnostic/apps/common/mapper_utils.py +30 -0
  11. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/copilot/mapper.py +5 -10
  12. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/copilot/service.py +0 -3
  13. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/cursor/mapper.py +3 -12
  14. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/cursor/service.py +0 -6
  15. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/opencode/mapper.py +7 -21
  16. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/opencode/service.py +0 -3
  17. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/commands/projects.py +2 -2
  18. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/commands/skills.py +8 -7
  19. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/commands/workspaces.py +7 -9
  20. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/helpers.py +7 -6
  21. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/constants.py +1 -0
  22. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/core/repository.py +26 -48
  23. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/executor.py +19 -8
  24. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/git_exclude_service.py +1 -1
  25. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/mcp_service.py +1 -1
  26. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/models.py +58 -1
  27. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/planner.py +154 -282
  28. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/project_artifacts.py +2 -1
  29. code_agnostic-0.9.0/code_agnostic/rules/compilers.py +29 -0
  30. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/status.py +6 -6
  31. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/utils.py +25 -0
  32. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic.egg-info/PKG-INFO +3 -3
  33. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic.egg-info/SOURCES.txt +2 -0
  34. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/pyproject.toml +1 -1
  35. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_apply_codex.py +1 -1
  36. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_validate.py +1 -4
  37. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_common_repository.py +8 -22
  38. code_agnostic-0.9.0/tests/test_file_lock.py +53 -0
  39. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_planner_executor.py +2 -4
  40. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_sync_plan_model.py +40 -0
  41. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_workspace_config_sync.py +9 -9
  42. code_agnostic-0.7.0/code_agnostic/rules/compilers.py +0 -57
  43. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/LICENSE +0 -0
  44. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/__main__.py +0 -0
  45. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/agents/__init__.py +0 -0
  46. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/agents/claude.py +0 -0
  47. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/agents/codex.py +0 -0
  48. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/agents/compilers.py +0 -0
  49. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/agents/copilot.py +0 -0
  50. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/agents/models.py +0 -0
  51. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/agents/opencode.py +0 -0
  52. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/agents/parser.py +0 -0
  53. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/__init__.py +0 -0
  54. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/app_id.py +0 -0
  55. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/claude/__init__.py +0 -0
  56. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/claude/config_repository.py +0 -0
  57. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/codex/__init__.py +0 -0
  58. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/codex/config_repository.py +0 -0
  59. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/codex/mapper.py +0 -0
  60. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/codex/schema.json +0 -0
  61. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/codex/schema_repository.py +0 -0
  62. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/common/__init__.py +0 -0
  63. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/common/compiled_planning.py +0 -0
  64. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/common/framework.py +0 -0
  65. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  66. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  67. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/common/loader.py +0 -0
  68. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/common/models.py +0 -0
  69. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/common/schema.py +0 -0
  70. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/common/symlink_planning.py +0 -0
  71. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/common/utils.py +0 -0
  72. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/copilot/__init__.py +0 -0
  73. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/copilot/config_repository.py +0 -0
  74. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/cursor/__init__.py +0 -0
  75. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/cursor/config_repository.py +0 -0
  76. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/cursor/schema.json +0 -0
  77. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  78. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/opencode/__init__.py +0 -0
  79. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/opencode/config_repository.py +0 -0
  80. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/opencode/schema.json +0 -0
  81. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  82. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/__init__.py +0 -0
  83. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/aliases.py +0 -0
  84. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/commands/__init__.py +0 -0
  85. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/commands/agents.py +0 -0
  86. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/commands/apply.py +0 -0
  87. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/commands/apps.py +0 -0
  88. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
  89. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/commands/import_.py +0 -0
  90. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/commands/mcp.py +0 -0
  91. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/commands/plan.py +0 -0
  92. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/commands/restore.py +0 -0
  93. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/commands/rules.py +0 -0
  94. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/commands/status.py +0 -0
  95. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/commands/validate.py +0 -0
  96. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/cli/options.py +0 -0
  97. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/core/__init__.py +0 -0
  98. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/core/project_repository.py +0 -0
  99. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/core/workspace_repository.py +0 -0
  100. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/errors.py +0 -0
  101. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/generated_artifacts.py +0 -0
  102. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/imports/__init__.py +0 -0
  103. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/imports/adapters.py +0 -0
  104. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/imports/filesystem.py +0 -0
  105. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/imports/models.py +0 -0
  106. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/imports/service.py +0 -0
  107. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/lossiness.py +0 -0
  108. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/rules/__init__.py +0 -0
  109. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/rules/models.py +0 -0
  110. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/rules/parser.py +0 -0
  111. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/rules/repository.py +0 -0
  112. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/skills/__init__.py +0 -0
  113. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/skills/compilers.py +0 -0
  114. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/skills/install_sources.py +0 -0
  115. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/skills/models.py +0 -0
  116. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/skills/parser.py +0 -0
  117. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/spec/__init__.py +0 -0
  118. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/spec/loaders.py +0 -0
  119. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  120. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
  121. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
  122. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  123. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  124. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/tui/__init__.py +0 -0
  125. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/tui/enums.py +0 -0
  126. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/tui/import_selector.py +0 -0
  127. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/tui/renderers.py +0 -0
  128. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/tui/sections.py +0 -0
  129. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/tui/tables.py +0 -0
  130. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/validation.py +0 -0
  131. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/workspace_artifacts.py +0 -0
  132. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic/workspaces.py +0 -0
  133. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic.egg-info/dependency_links.txt +0 -0
  134. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic.egg-info/entry_points.txt +0 -0
  135. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic.egg-info/requires.txt +0 -0
  136. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/code_agnostic.egg-info/top_level.txt +0 -0
  137. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/setup.cfg +0 -0
  138. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_agents.py +0 -0
  139. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_aliases.py +0 -0
  140. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_apply_apps.py +0 -0
  141. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_apply_codex_agent_cleanup.py +0 -0
  142. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_apply_cursor.py +0 -0
  143. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_apply_mcp_cleanup.py +0 -0
  144. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_apply_target.py +0 -0
  145. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_apps.py +0 -0
  146. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_disable_cleanup.py +0 -0
  147. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_explain_lossiness.py +0 -0
  148. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_flags.py +0 -0
  149. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_git_exclude.py +0 -0
  150. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_import.py +0 -0
  151. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_import_interactive.py +0 -0
  152. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_mcp.py +0 -0
  153. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_module_organization.py +0 -0
  154. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_plan.py +0 -0
  155. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_projects.py +0 -0
  156. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_restore.py +0 -0
  157. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_rules.py +0 -0
  158. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_skills.py +0 -0
  159. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_status.py +0 -0
  160. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_workspace_resolution.py +0 -0
  161. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_cli_workspaces.py +0 -0
  162. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_common_mcp_to_dto.py +0 -0
  163. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_compiled_planning.py +0 -0
  164. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_dto_to_common_mcp.py +0 -0
  165. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_git_exclude_service.py +0 -0
  166. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_mcp_service.py +0 -0
  167. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_planner_rules.py +0 -0
  168. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_project_config_sync.py +0 -0
  169. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_skill_install_sources.py +0 -0
  170. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_symlink_planning.py +0 -0
  171. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_transactional_executor.py +0 -0
  172. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_utils.py +0 -0
  173. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_version.py +0 -0
  174. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_workspace_repo_status.py +0 -0
  175. {code_agnostic-0.7.0 → code_agnostic-0.9.0}/tests/test_workspaces.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-agnostic
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: Centralized hub for LLM coding config: MCP, skills, rules, and agents.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -59,7 +59,7 @@ AI coding tools each want config in a different place and format. When you use m
59
59
  ~/.copilot/ and repo .github/ Compiled & synced for GitHub Copilot
60
60
  ```
61
61
 
62
- Each resource is cross-compiled to the target editor's native format. Rules become `.mdc` files for Cursor, `AGENTS.md` sections for OpenCode/Codex/Copilot, and `CLAUDE.local.md` memory for Claude Code.
62
+ Each resource is cross-compiled to the target editor's native format. Rules become `AGENTS.md` sections consumed by Cursor/OpenCode/Codex/Copilot, mirrored into `CLAUDE.local.md` memory for Claude Code.
63
63
 
64
64
  Legacy single-file rules, `skills/<name>/SKILL.md`, and markdown agents are still supported for migration, but bundle directories are the preferred source format for new config.
65
65
 
@@ -216,7 +216,7 @@ always_apply: false
216
216
  Always use type hints. Prefer dataclasses over dicts.
217
217
  ```
218
218
 
219
- Cross-compiled per editor: Cursor gets `.mdc` files with native frontmatter, OpenCode/Codex get `AGENTS.md` sections.
219
+ Compiled to `AGENTS.md` sections, consumed natively by Cursor/OpenCode/Codex/Copilot and mirrored into `CLAUDE.local.md` for Claude Code.
220
220
  Legacy single-file rule markdown with YAML frontmatter remains supported for migration.
221
221
 
222
222
  ```bash
@@ -34,7 +34,7 @@ AI coding tools each want config in a different place and format. When you use m
34
34
  ~/.copilot/ and repo .github/ Compiled & synced for GitHub Copilot
35
35
  ```
36
36
 
37
- Each resource is cross-compiled to the target editor's native format. Rules become `.mdc` files for Cursor, `AGENTS.md` sections for OpenCode/Codex/Copilot, and `CLAUDE.local.md` memory for Claude Code.
37
+ Each resource is cross-compiled to the target editor's native format. Rules become `AGENTS.md` sections consumed by Cursor/OpenCode/Codex/Copilot, mirrored into `CLAUDE.local.md` memory for Claude Code.
38
38
 
39
39
  Legacy single-file rules, `skills/<name>/SKILL.md`, and markdown agents are still supported for migration, but bundle directories are the preferred source format for new config.
40
40
 
@@ -191,7 +191,7 @@ always_apply: false
191
191
  Always use type hints. Prefer dataclasses over dicts.
192
192
  ```
193
193
 
194
- Cross-compiled per editor: Cursor gets `.mdc` files with native frontmatter, OpenCode/Codex get `AGENTS.md` sections.
194
+ Compiled to `AGENTS.md` sections, consumed natively by Cursor/OpenCode/Codex/Copilot and mirrored into `CLAUDE.local.md` for Claude Code.
195
195
  Legacy single-file rule markdown with YAML frontmatter remains supported for migration.
196
196
 
197
197
  ```bash
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.9.0"
@@ -103,9 +103,9 @@ class AppsService:
103
103
 
104
104
  # --- Global scopes (app:<app>:*) ---
105
105
  global_state = core.load_state()
106
- global_links = self._normalize_group(global_state.get("managed_links"))
107
- global_paths = self._normalize_group(global_state.get("managed_paths"))
108
- global_mcp = self._normalize_group(global_state.get("managed_mcp"))
106
+ global_links = global_state.managed_links
107
+ global_paths = global_state.managed_paths
108
+ global_mcp = global_state.managed_mcp
109
109
  global_prefix = f"app:{app_id.value}:"
110
110
  for scope in sorted(s for s in global_links if s.startswith(global_prefix)):
111
111
  actions.extend(self._cleanup_links(global_links, scope, app_id.value))
@@ -120,11 +120,11 @@ class AppsService:
120
120
  # --- Workspace scopes (ws:<app>:*) ---
121
121
  ws_prefix = f"ws:{app_id.value}:"
122
122
  for workspace in core.load_workspaces():
123
- ws_name = workspace["name"]
123
+ ws_name = workspace.name
124
124
  ws_repo = WorkspaceConfigRepository(root=core.workspace_config_dir(ws_name))
125
125
  ws_state = ws_repo.load_state()
126
- ws_links = self._normalize_group(ws_state.get("managed_links"))
127
- ws_paths = self._normalize_group(ws_state.get("managed_paths"))
126
+ ws_links = ws_state.managed_links
127
+ ws_paths = ws_state.managed_paths
128
128
  for scope in sorted(s for s in ws_links if s.startswith(ws_prefix)):
129
129
  for action in self._cleanup_links(ws_links, scope, "workspace"):
130
130
  action.workspace = ws_name
@@ -139,13 +139,13 @@ class AppsService:
139
139
  # --- Project scopes (project:<app>:*) ---
140
140
  project_prefix = f"project:{app_id.value}:"
141
141
  for project in load_project_entries(core):
142
- project_name = project["name"]
142
+ project_name = project.name
143
143
  project_repo = ProjectConfigRepository(
144
144
  root=project_config_dir(core, project_name)
145
145
  )
146
146
  project_state = project_repo.load_state()
147
- project_links = self._normalize_group(project_state.get("managed_links"))
148
- project_paths = self._normalize_group(project_state.get("managed_paths"))
147
+ project_links = project_state.managed_links
148
+ project_paths = project_state.managed_paths
149
149
  for scope in sorted(
150
150
  s for s in project_links if s.startswith(project_prefix)
151
151
  ):
@@ -163,14 +163,6 @@ class AppsService:
163
163
 
164
164
  return SyncPlan(actions=actions, errors=[], skipped=skipped)
165
165
 
166
- @staticmethod
167
- def _normalize_group(value: Any) -> dict[str, Any]:
168
- return (
169
- {scope: paths for scope, paths in value.items() if isinstance(scope, str)}
170
- if isinstance(value, dict)
171
- else {}
172
- )
173
-
174
166
  @staticmethod
175
167
  def _cleanup_links(group: dict[str, Any], scope: str, app: str) -> list[Action]:
176
168
  return plan_stale_group(
@@ -2,27 +2,14 @@ from copy import deepcopy
2
2
  from typing import Any
3
3
 
4
4
  from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
5
+ from code_agnostic.apps.common.mapper_utils import (
6
+ as_command_list,
7
+ as_str_dict,
8
+ coerce_int_timeout_ms,
9
+ )
5
10
  from code_agnostic.apps.common.models import MCPServerDTO, MCPServerType
6
11
 
7
12
 
8
- def _as_list(value: Any) -> list[str]:
9
- if isinstance(value, list):
10
- return [str(item) for item in value]
11
- if isinstance(value, str):
12
- return [value]
13
- return []
14
-
15
-
16
- def _as_str_dict(value: Any) -> dict[str, str]:
17
- return {str(k): str(v) for k, v in value.items()} if isinstance(value, dict) else {}
18
-
19
-
20
- def _timeout(value: Any) -> int | None:
21
- if isinstance(value, int) and not isinstance(value, bool):
22
- return value
23
- return None
24
-
25
-
26
13
  class ClaudeMCPMapper(IAppMCPMapper):
27
14
  def to_common(self, payload: dict[str, Any]) -> dict[str, MCPServerDTO]:
28
15
  mapped: dict[str, MCPServerDTO] = {}
@@ -39,13 +26,13 @@ class ClaudeMCPMapper(IAppMCPMapper):
39
26
  name=name,
40
27
  type=MCPServerType.STDIO,
41
28
  command=command,
42
- args=_as_list(server.get("args")),
29
+ args=as_command_list(server.get("args")),
43
30
  cwd=server.get("cwd")
44
31
  if isinstance(server.get("cwd"), str)
45
32
  else None,
46
- env=_as_str_dict(server.get("env")),
47
- headers=_as_str_dict(server.get("headers")),
48
- timeout_ms=_timeout(server.get("timeout")),
33
+ env=as_str_dict(server.get("env")),
34
+ headers=as_str_dict(server.get("headers")),
35
+ timeout_ms=coerce_int_timeout_ms(server.get("timeout")),
49
36
  )
50
37
  continue
51
38
 
@@ -56,9 +43,9 @@ class ClaudeMCPMapper(IAppMCPMapper):
56
43
  name=name,
57
44
  type=MCPServerType.HTTP,
58
45
  url=url,
59
- env=_as_str_dict(server.get("env")),
60
- headers=_as_str_dict(server.get("headers")),
61
- timeout_ms=_timeout(server.get("timeout")),
46
+ env=as_str_dict(server.get("env")),
47
+ headers=as_str_dict(server.get("headers")),
48
+ timeout_ms=coerce_int_timeout_ms(server.get("timeout")),
62
49
  )
63
50
  return mapped
64
51
 
@@ -56,9 +56,6 @@ class ClaudeConfigService(RegisteredAppConfigService):
56
56
  def mcp_config_key(self) -> str:
57
57
  return "mcpServers"
58
58
 
59
- def build_action_payload(self, payload: dict[str, Any]) -> Any:
60
- return payload
61
-
62
59
  def derive_status(
63
60
  self, existing: dict[str, Any], merged: dict[str, Any]
64
61
  ) -> ActionStatus:
@@ -102,14 +102,10 @@ class CodexConfigService(RegisteredAppConfigService):
102
102
  def derive_status(
103
103
  self, existing: dict[str, Any], merged: dict[str, Any]
104
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
105
  if not self.repository.config_path.exists():
112
106
  return ActionStatus.CREATE
107
+ rendered = self.repository.serialize_config(merged)
108
+ existing_text = self.repository.config_path.read_text(encoding="utf-8")
113
109
  if existing_text == rendered:
114
110
  return ActionStatus.NOOP
115
111
  return ActionStatus.UPDATE
@@ -2,6 +2,8 @@ from abc import ABC, abstractmethod
2
2
  from pathlib import Path
3
3
  from typing import Any
4
4
 
5
+ from code_agnostic.models import SyncState, WorkspaceConfig
6
+
5
7
 
6
8
  class ISchemaRepository(ABC):
7
9
  @abstractmethod
@@ -59,7 +61,7 @@ class IConfigRepository(ABC):
59
61
  raise NotImplementedError
60
62
 
61
63
  @abstractmethod
62
- def load_state(self) -> dict[str, Any]:
64
+ def load_state(self) -> SyncState:
63
65
  raise NotImplementedError
64
66
 
65
67
  @abstractmethod
@@ -91,7 +93,7 @@ class ISourceRepository(IConfigRepository):
91
93
  raise NotImplementedError
92
94
 
93
95
  @abstractmethod
94
- def load_workspaces(self) -> list[dict[str, str]]:
96
+ def load_workspaces(self) -> list[WorkspaceConfig]:
95
97
  raise NotImplementedError
96
98
 
97
99
  def workspace_config_dir(self, name: str) -> Path:
@@ -79,9 +79,13 @@ class IAppConfigService(ABC):
79
79
  """Editor-specific validation hook. Default: no schema."""
80
80
  return
81
81
 
82
- @abstractmethod
83
82
  def build_action_payload(self, payload: dict[str, Any]) -> Any:
84
- raise NotImplementedError
83
+ """Transform the merged config before it becomes the action payload.
84
+
85
+ Defaults to the merged dict as-is; editors that persist a non-JSON
86
+ rendering (e.g. Codex TOML) override this.
87
+ """
88
+ return payload
85
89
 
86
90
  def set_mcp_payload(
87
91
  self,
@@ -135,10 +139,6 @@ class IAppConfigService(ABC):
135
139
  def compiled_resource_ownership_policy(self) -> OwnershipPolicy:
136
140
  return OwnershipPolicy.OWNED_ONLY
137
141
 
138
- @staticmethod
139
- def _normalize_managed_group(value: Any) -> dict[str, Any]:
140
- return value if isinstance(value, dict) else {}
141
-
142
142
  def _plan_compiled_text_actions(
143
143
  self,
144
144
  *,
@@ -308,9 +308,9 @@ class IAppConfigService(ABC):
308
308
  source_repository: ISourceRepository,
309
309
  ) -> SyncPlan:
310
310
  state = source_repository.load_state()
311
- managed_links_group = self._normalize_managed_group(state.get("managed_links"))
312
- managed_paths_group = self._normalize_managed_group(state.get("managed_paths"))
313
- managed_mcp_group = self._normalize_managed_group(state.get("managed_mcp"))
311
+ managed_links_group = state.managed_links
312
+ managed_paths_group = state.managed_paths
313
+ managed_mcp_group = state.managed_mcp
314
314
  skill_scope = app_scope(self.app_id, "skills")
315
315
  agent_scope = app_scope(self.app_id, "agents")
316
316
  previously_managed_mcp = self._load_state_names(
@@ -0,0 +1,30 @@
1
+ """Shared coercion helpers for MCP mappers.
2
+
3
+ Editors whose MCP config uses the same shapes share these instead of
4
+ re-implementing them per mapper. Editors with genuinely different semantics
5
+ (e.g. Copilot's float/negative timeout handling, Codex's seconds-based timeout)
6
+ keep their own local helpers.
7
+ """
8
+
9
+ from typing import Any
10
+
11
+
12
+ def as_command_list(value: Any) -> list[str]:
13
+ """A command/args value that may be a list or a single string."""
14
+ if isinstance(value, list):
15
+ return [str(item) for item in value]
16
+ if isinstance(value, str):
17
+ return [value]
18
+ return []
19
+
20
+
21
+ def as_str_dict(value: Any) -> dict[str, str]:
22
+ """A string-to-string mapping (env/headers), or empty if not a dict."""
23
+ return {str(k): str(v) for k, v in value.items()} if isinstance(value, dict) else {}
24
+
25
+
26
+ def coerce_int_timeout_ms(value: Any) -> int | None:
27
+ """A plain integer millisecond timeout; rejects bools and non-ints."""
28
+ if isinstance(value, int) and not isinstance(value, bool):
29
+ return int(value)
30
+ return None
@@ -3,6 +3,7 @@ from pathlib import Path
3
3
  from typing import Any
4
4
 
5
5
  from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
6
+ from code_agnostic.apps.common.mapper_utils import as_str_dict
6
7
  from code_agnostic.apps.common.models import MCPServerDTO, MCPServerType
7
8
  from code_agnostic.errors import InvalidConfigSchemaError
8
9
 
@@ -23,12 +24,6 @@ def _timeout_ms(value: Any) -> int | None:
23
24
  return None
24
25
 
25
26
 
26
- def _str_dict(value: Any) -> dict[str, str]:
27
- if not isinstance(value, dict):
28
- return {}
29
- return {str(k): str(v) for k, v in value.items()}
30
-
31
-
32
27
  class CopilotMCPMapper(IAppMCPMapper):
33
28
  def to_common(self, payload: dict[str, Any]) -> dict[str, MCPServerDTO]:
34
29
  mapped: dict[str, MCPServerDTO] = {}
@@ -47,8 +42,8 @@ class CopilotMCPMapper(IAppMCPMapper):
47
42
  command=command,
48
43
  args=_as_string_list(server.get("args")),
49
44
  timeout_ms=_timeout_ms(server.get("timeout")),
50
- env=_str_dict(server.get("env")),
51
- headers=_str_dict(server.get("headers")),
45
+ env=as_str_dict(server.get("env")),
46
+ headers=as_str_dict(server.get("headers")),
52
47
  )
53
48
  continue
54
49
 
@@ -59,8 +54,8 @@ class CopilotMCPMapper(IAppMCPMapper):
59
54
  type=MCPServerType.HTTP,
60
55
  url=url,
61
56
  timeout_ms=_timeout_ms(server.get("timeout")),
62
- env=_str_dict(server.get("env")),
63
- headers=_str_dict(server.get("headers")),
57
+ env=as_str_dict(server.get("env")),
58
+ headers=as_str_dict(server.get("headers")),
64
59
  )
65
60
  return mapped
66
61
 
@@ -62,9 +62,6 @@ class CopilotConfigService(RegisteredAppConfigService):
62
62
  self.repository.config_path, "mcpServers must be a JSON object"
63
63
  )
64
64
 
65
- def build_action_payload(self, payload: dict[str, Any]) -> Any:
66
- return payload
67
-
68
65
  def derive_status(
69
66
  self, existing: dict[str, Any], merged: dict[str, Any]
70
67
  ) -> ActionStatus:
@@ -2,6 +2,7 @@ from copy import deepcopy
2
2
  from typing import Any
3
3
 
4
4
  from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
5
+ from code_agnostic.apps.common.mapper_utils import coerce_int_timeout_ms
5
6
  from code_agnostic.apps.common.models import MCPAuthDTO, MCPServerDTO, MCPServerType
6
7
 
7
8
 
@@ -25,12 +26,7 @@ class CursorMCPMapper(IAppMCPMapper):
25
26
  env_file=server.get("envFile")
26
27
  if isinstance(server.get("envFile"), str)
27
28
  else None,
28
- timeout_ms=(
29
- int(server["timeout"])
30
- if isinstance(server.get("timeout"), int)
31
- and not isinstance(server.get("timeout"), bool)
32
- else None
33
- ),
29
+ timeout_ms=coerce_int_timeout_ms(server.get("timeout")),
34
30
  env={k: str(v) for k, v in (server.get("env") or {}).items()},
35
31
  headers={
36
32
  k: str(v) for k, v in (server.get("headers") or {}).items()
@@ -66,12 +62,7 @@ class CursorMCPMapper(IAppMCPMapper):
66
62
  name=name,
67
63
  type=server_type,
68
64
  url=url,
69
- timeout_ms=(
70
- int(server["timeout"])
71
- if isinstance(server.get("timeout"), int)
72
- and not isinstance(server.get("timeout"), bool)
73
- else None
74
- ),
65
+ timeout_ms=coerce_int_timeout_ms(server.get("timeout")),
75
66
  headers={k: str(v) for k, v in (server.get("headers") or {}).items()},
76
67
  env={k: str(v) for k, v in (server.get("env") or {}).items()},
77
68
  auth=auth,
@@ -75,9 +75,6 @@ class CursorConfigService(RegisteredAppConfigService):
75
75
  self.repository.config_path, format_schema_error(error)
76
76
  )
77
77
 
78
- def build_action_payload(self, payload: dict[str, Any]) -> Any:
79
- return payload
80
-
81
78
  def derive_status(
82
79
  self, existing: dict[str, Any], merged: dict[str, Any]
83
80
  ) -> ActionStatus:
@@ -97,9 +94,6 @@ class CursorConfigService(RegisteredAppConfigService):
97
94
  return ActionStatus.NOOP
98
95
  return ActionStatus.UPDATE
99
96
 
100
- def agent_action_removable_links(self, removable_links: list[Path]) -> list[Path]:
101
- return removable_links
102
-
103
97
  def plan_skill_actions(
104
98
  self,
105
99
  sources: list[Path],
@@ -2,17 +2,13 @@ from copy import deepcopy
2
2
  from typing import Any
3
3
 
4
4
  from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
5
+ from code_agnostic.apps.common.mapper_utils import (
6
+ as_command_list,
7
+ coerce_int_timeout_ms,
8
+ )
5
9
  from code_agnostic.apps.common.models import MCPAuthDTO, MCPServerDTO, MCPServerType
6
10
 
7
11
 
8
- def _as_list(value: Any) -> list[str]:
9
- if isinstance(value, list):
10
- return [str(item) for item in value]
11
- if isinstance(value, str):
12
- return [value]
13
- return []
14
-
15
-
16
12
  class OpenCodeMCPMapper(IAppMCPMapper):
17
13
  def to_common(self, payload: dict[str, Any]) -> dict[str, MCPServerDTO]:
18
14
  mapped: dict[str, MCPServerDTO] = {}
@@ -22,7 +18,7 @@ class OpenCodeMCPMapper(IAppMCPMapper):
22
18
 
23
19
  server_type = server.get("type")
24
20
  if server_type == "local" or "command" in server:
25
- command_parts = _as_list(server.get("command"))
21
+ command_parts = as_command_list(server.get("command"))
26
22
  command = command_parts[0] if command_parts else None
27
23
  args = command_parts[1:] if command_parts else []
28
24
  if not command:
@@ -35,12 +31,7 @@ class OpenCodeMCPMapper(IAppMCPMapper):
35
31
  cwd=server.get("cwd")
36
32
  if isinstance(server.get("cwd"), str)
37
33
  else None,
38
- timeout_ms=(
39
- int(server["timeout"])
40
- if isinstance(server.get("timeout"), int)
41
- and not isinstance(server.get("timeout"), bool)
42
- else None
43
- ),
34
+ timeout_ms=coerce_int_timeout_ms(server.get("timeout")),
44
35
  env={
45
36
  k: str(v) for k, v in (server.get("environment") or {}).items()
46
37
  },
@@ -72,12 +63,7 @@ class OpenCodeMCPMapper(IAppMCPMapper):
72
63
  name=name,
73
64
  type=normalized_type,
74
65
  url=url,
75
- timeout_ms=(
76
- int(server["timeout"])
77
- if isinstance(server.get("timeout"), int)
78
- and not isinstance(server.get("timeout"), bool)
79
- else None
80
- ),
66
+ timeout_ms=coerce_int_timeout_ms(server.get("timeout")),
81
67
  env={k: str(v) for k, v in (server.get("environment") or {}).items()},
82
68
  headers={k: str(v) for k, v in (server.get("headers") or {}).items()},
83
69
  auth=auth,
@@ -112,9 +112,6 @@ class OpenCodeConfigService(RegisteredAppConfigService):
112
112
  self.repository.config_path, format_schema_error(error)
113
113
  )
114
114
 
115
- def build_action_payload(self, payload: dict[str, Any]) -> Any:
116
- return payload
117
-
118
115
  def _load_base_config(self) -> dict[str, Any]:
119
116
  from code_agnostic.errors import InvalidJsonFormatError
120
117
  from code_agnostic.utils import read_json_safe
@@ -63,7 +63,7 @@ def projects_list(obj: dict[str, str]) -> None:
63
63
 
64
64
  for item in items:
65
65
  project_source = ProjectConfigRepository(
66
- root=core.project_config_dir(item["name"])
66
+ root=core.project_config_dir(item.name)
67
67
  )
68
68
  markers = []
69
69
  if project_source.has_mcp():
@@ -75,4 +75,4 @@ def projects_list(obj: dict[str, str]) -> None:
75
75
  if project_source.has_agents():
76
76
  markers.append("agents")
77
77
  suffix = f" [{', '.join(markers)}]" if markers else ""
78
- click.echo(f"{item['name']}: {item['path']}{suffix}")
78
+ click.echo(f"{item.name}: {item.path}{suffix}")
@@ -14,6 +14,7 @@ from code_agnostic.cli.helpers import (
14
14
  from code_agnostic.cli.options import workspace_option
15
15
  from code_agnostic.core.repository import CoreRepository
16
16
  from code_agnostic.errors import SyncAppError
17
+ from code_agnostic.models import WorkspaceConfig
17
18
  from code_agnostic.skills.install_sources import (
18
19
  SkillInstallSourceError,
19
20
  cleanup_skill_install_resolution,
@@ -25,18 +26,18 @@ from code_agnostic.utils import compact_home_path
25
26
 
26
27
 
27
28
  def _entries_containing_cwd(
28
- entries: list[dict[str, str]], cwd: Path
29
- ) -> list[dict[str, str]]:
29
+ entries: list[WorkspaceConfig], cwd: Path
30
+ ) -> list[WorkspaceConfig]:
30
31
  matches = []
31
32
  for entry in entries:
32
- root = Path(entry["path"]).expanduser().resolve()
33
+ root = entry.path.expanduser().resolve()
33
34
  if cwd == root or cwd.is_relative_to(root):
34
35
  matches.append(entry)
35
36
  return matches
36
37
 
37
38
 
38
- def _project_entries_by_name(core: CoreRepository) -> dict[str, dict[str, str]]:
39
- return {item["name"]: item for item in core.load_projects()}
39
+ def _project_entries_by_name(core: CoreRepository) -> dict[str, WorkspaceConfig]:
40
+ return {item.name: item for item in core.load_projects()}
40
41
 
41
42
 
42
43
  def _install_root(
@@ -72,12 +73,12 @@ def _install_root(
72
73
  cwd = Path.cwd().resolve()
73
74
  project_matches = _entries_containing_cwd(core.load_projects(), cwd)
74
75
  if len(project_matches) == 1:
75
- name = project_matches[0]["name"]
76
+ name = project_matches[0].name
76
77
  return core.project_config_dir(name), f"project:{name}", None
77
78
 
78
79
  workspace_matches = _entries_containing_cwd(core.load_workspaces(), cwd)
79
80
  if len(project_matches) == 0 and len(workspace_matches) == 1:
80
- name = workspace_matches[0]["name"]
81
+ name = workspace_matches[0].name
81
82
  return core.workspace_config_dir(name), f"workspace:{name}", None
82
83
  except SyncAppError as exc:
83
84
  raise click.ClickException(str(exc)) from exc
@@ -48,7 +48,7 @@ def workspaces_remove(obj: dict[str, str], name: str) -> None:
48
48
  ui = SyncConsoleUI(Console())
49
49
  core = CoreRepository()
50
50
  try:
51
- existing = {item["name"]: item["path"] for item in core.load_workspaces()}
51
+ existing = {item.name: str(item.path) for item in core.load_workspaces()}
52
52
  removed = core.remove_workspace(name)
53
53
  except SyncAppError as exc:
54
54
  raise click.ClickException(str(exc))
@@ -70,20 +70,18 @@ def workspaces_list(obj: dict[str, str]) -> None:
70
70
  except SyncAppError as exc:
71
71
  raise click.ClickException(str(exc))
72
72
  for item in workspaces:
73
- workspace_path = Path(item["path"])
73
+ workspace_path = item.path
74
74
  repos: list[str] = []
75
75
  if workspace_path.exists() and workspace_path.is_dir():
76
76
  repos = [
77
77
  str(path.relative_to(workspace_path))
78
78
  for path in workspace_service.discover_git_repos(workspace_path)
79
79
  ]
80
- ws_source = WorkspaceConfigRepository(
81
- root=core.workspace_config_dir(item["name"])
82
- )
80
+ ws_source = WorkspaceConfigRepository(root=core.workspace_config_dir(item.name))
83
81
  overview.append(
84
82
  {
85
- "name": item["name"],
86
- "path": item["path"],
83
+ "name": item.name,
84
+ "path": str(item.path),
87
85
  "repos": repos,
88
86
  "has_mcp": ws_source.has_mcp(),
89
87
  "has_rules": ws_source.has_rules(),
@@ -123,7 +121,7 @@ def workspaces_git_exclude(obj: dict[str, str], workspace: str | None) -> None:
123
121
  added_lines = 0
124
122
 
125
123
  for item in ws_list:
126
- workspace_path = Path(item["path"])
124
+ workspace_path = item.path
127
125
  if not workspace_path.exists() or not workspace_path.is_dir():
128
126
  continue
129
127
  repos = workspace_service.discover_git_repos(workspace_path)
@@ -132,7 +130,7 @@ def workspaces_git_exclude(obj: dict[str, str], workspace: str | None) -> None:
132
130
  if git_dir is None:
133
131
  continue
134
132
  entries = exclude_service.compute_entries_for_repo(
135
- item["name"],
133
+ item.name,
136
134
  enabled_apps,
137
135
  workspace_path=workspace_path,
138
136
  repo_path=repo,
@@ -12,18 +12,19 @@ from code_agnostic.models import (
12
12
  EditorStatusRow,
13
13
  EditorSyncStatus,
14
14
  SyncPlan,
15
+ WorkspaceConfig,
15
16
  )
16
17
 
17
18
 
18
- def _workspace_entries_by_name(core: CoreRepository) -> dict[str, dict[str, str]]:
19
- return {item["name"]: item for item in core.load_workspaces()}
19
+ def _workspace_entries_by_name(core: CoreRepository) -> dict[str, WorkspaceConfig]:
20
+ return {item.name: item for item in core.load_workspaces()}
20
21
 
21
22
 
22
- def _project_entries_by_name(core: CoreRepository) -> dict[str, dict[str, str]]:
23
- return {item["name"]: item for item in core.load_projects()}
23
+ def _project_entries_by_name(core: CoreRepository) -> dict[str, WorkspaceConfig]:
24
+ return {item.name: item for item in core.load_projects()}
24
25
 
25
26
 
26
- def require_workspace_entry(core: CoreRepository, workspace: str) -> dict[str, str]:
27
+ def require_workspace_entry(core: CoreRepository, workspace: str) -> WorkspaceConfig:
27
28
  try:
28
29
  entry = _workspace_entries_by_name(core).get(workspace)
29
30
  except SyncAppError as exc:
@@ -33,7 +34,7 @@ def require_workspace_entry(core: CoreRepository, workspace: str) -> dict[str, s
33
34
  return entry
34
35
 
35
36
 
36
- def require_project_entry(core: CoreRepository, project: str) -> dict[str, str]:
37
+ def require_project_entry(core: CoreRepository, project: str) -> WorkspaceConfig:
37
38
  try:
38
39
  entry = _project_entries_by_name(core).get(project)
39
40
  except SyncAppError as exc:
@@ -9,6 +9,7 @@ GIT_DIRNAME: Final[str] = ".git"
9
9
  SYNC_STATE_FILENAME: Final[str] = ".sync-state.json"
10
10
  SYNC_REVISIONS_DIRNAME: Final[str] = ".sync-revisions"
11
11
  SYNC_STAGING_DIRNAME: Final[str] = ".sync-staging"
12
+ SYNC_LOCK_FILENAME: Final[str] = ".sync.lock"
12
13
 
13
14
  RULES_DIRNAME: Final[str] = "rules"
14
15
  SKILLS_DIRNAME: Final[str] = "skills"