code-agnostic 0.8.0__tar.gz → 0.9.1__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.8.0 → code_agnostic-0.9.1}/PKG-INFO +3 -3
  2. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/README.md +2 -2
  3. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/__init__.py +1 -1
  4. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/apps_service.py +2 -2
  5. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/claude/mapper.py +12 -25
  6. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/claude/service.py +0 -3
  7. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/codex/service.py +2 -6
  8. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/common/interfaces/repositories.py +2 -2
  9. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/common/interfaces/service.py +6 -2
  10. code_agnostic-0.9.1/code_agnostic/apps/common/mapper_utils.py +30 -0
  11. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/copilot/mapper.py +5 -10
  12. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/copilot/service.py +0 -3
  13. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/cursor/mapper.py +3 -12
  14. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/cursor/service.py +0 -6
  15. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/opencode/mapper.py +7 -21
  16. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/opencode/service.py +0 -3
  17. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/commands/projects.py +2 -2
  18. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/commands/skills.py +8 -7
  19. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/commands/workspaces.py +7 -9
  20. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/helpers.py +7 -6
  21. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/constants.py +1 -0
  22. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/core/repository.py +22 -20
  23. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/executor.py +12 -1
  24. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/git_exclude_service.py +1 -1
  25. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/mcp_service.py +1 -1
  26. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/models.py +10 -15
  27. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/planner.py +148 -262
  28. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/project_artifacts.py +2 -1
  29. code_agnostic-0.9.1/code_agnostic/rules/compilers.py +29 -0
  30. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/status.py +4 -4
  31. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/utils.py +25 -0
  32. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic.egg-info/PKG-INFO +3 -3
  33. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic.egg-info/SOURCES.txt +2 -0
  34. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/pyproject.toml +1 -1
  35. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_apply_codex.py +1 -1
  36. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_validate.py +1 -4
  37. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_common_repository.py +6 -7
  38. code_agnostic-0.9.1/tests/test_file_lock.py +53 -0
  39. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_sync_plan_model.py +0 -4
  40. code_agnostic-0.8.0/code_agnostic/rules/compilers.py +0 -57
  41. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/LICENSE +0 -0
  42. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/__main__.py +0 -0
  43. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/agents/__init__.py +0 -0
  44. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/agents/claude.py +0 -0
  45. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/agents/codex.py +0 -0
  46. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/agents/compilers.py +0 -0
  47. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/agents/copilot.py +0 -0
  48. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/agents/models.py +0 -0
  49. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/agents/opencode.py +0 -0
  50. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/agents/parser.py +0 -0
  51. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/__init__.py +0 -0
  52. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/app_id.py +0 -0
  53. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/claude/__init__.py +0 -0
  54. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/claude/config_repository.py +0 -0
  55. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/codex/__init__.py +0 -0
  56. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/codex/config_repository.py +0 -0
  57. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/codex/mapper.py +0 -0
  58. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/codex/schema.json +0 -0
  59. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/codex/schema_repository.py +0 -0
  60. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/common/__init__.py +0 -0
  61. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/common/compiled_planning.py +0 -0
  62. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/common/framework.py +0 -0
  63. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  64. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  65. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/common/loader.py +0 -0
  66. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/common/models.py +0 -0
  67. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/common/schema.py +0 -0
  68. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/common/symlink_planning.py +0 -0
  69. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/common/utils.py +0 -0
  70. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/copilot/__init__.py +0 -0
  71. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/copilot/config_repository.py +0 -0
  72. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/cursor/__init__.py +0 -0
  73. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/cursor/config_repository.py +0 -0
  74. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/cursor/schema.json +0 -0
  75. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  76. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/opencode/__init__.py +0 -0
  77. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/opencode/config_repository.py +0 -0
  78. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/opencode/schema.json +0 -0
  79. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  80. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/__init__.py +0 -0
  81. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/aliases.py +0 -0
  82. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/commands/__init__.py +0 -0
  83. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/commands/agents.py +0 -0
  84. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/commands/apply.py +0 -0
  85. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/commands/apps.py +0 -0
  86. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
  87. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/commands/import_.py +0 -0
  88. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/commands/mcp.py +0 -0
  89. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/commands/plan.py +0 -0
  90. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/commands/restore.py +0 -0
  91. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/commands/rules.py +0 -0
  92. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/commands/status.py +0 -0
  93. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/commands/validate.py +0 -0
  94. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/cli/options.py +0 -0
  95. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/core/__init__.py +0 -0
  96. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/core/project_repository.py +0 -0
  97. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/core/workspace_repository.py +0 -0
  98. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/errors.py +0 -0
  99. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/generated_artifacts.py +0 -0
  100. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/imports/__init__.py +0 -0
  101. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/imports/adapters.py +0 -0
  102. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/imports/filesystem.py +0 -0
  103. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/imports/models.py +0 -0
  104. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/imports/service.py +0 -0
  105. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/lossiness.py +0 -0
  106. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/rules/__init__.py +0 -0
  107. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/rules/models.py +0 -0
  108. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/rules/parser.py +0 -0
  109. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/rules/repository.py +0 -0
  110. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/skills/__init__.py +0 -0
  111. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/skills/compilers.py +0 -0
  112. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/skills/install_sources.py +0 -0
  113. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/skills/models.py +0 -0
  114. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/skills/parser.py +0 -0
  115. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/spec/__init__.py +0 -0
  116. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/spec/loaders.py +0 -0
  117. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  118. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
  119. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
  120. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  121. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  122. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/tui/__init__.py +0 -0
  123. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/tui/enums.py +0 -0
  124. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/tui/import_selector.py +0 -0
  125. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/tui/renderers.py +0 -0
  126. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/tui/sections.py +0 -0
  127. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/tui/tables.py +0 -0
  128. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/validation.py +0 -0
  129. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/workspace_artifacts.py +0 -0
  130. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic/workspaces.py +0 -0
  131. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic.egg-info/dependency_links.txt +0 -0
  132. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic.egg-info/entry_points.txt +0 -0
  133. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic.egg-info/requires.txt +0 -0
  134. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/code_agnostic.egg-info/top_level.txt +0 -0
  135. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/setup.cfg +0 -0
  136. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_agents.py +0 -0
  137. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_aliases.py +0 -0
  138. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_apply_apps.py +0 -0
  139. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_apply_codex_agent_cleanup.py +0 -0
  140. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_apply_cursor.py +0 -0
  141. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_apply_mcp_cleanup.py +0 -0
  142. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_apply_target.py +0 -0
  143. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_apps.py +0 -0
  144. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_disable_cleanup.py +0 -0
  145. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_explain_lossiness.py +0 -0
  146. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_flags.py +0 -0
  147. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_git_exclude.py +0 -0
  148. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_import.py +0 -0
  149. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_import_interactive.py +0 -0
  150. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_mcp.py +0 -0
  151. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_module_organization.py +0 -0
  152. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_plan.py +0 -0
  153. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_projects.py +0 -0
  154. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_restore.py +0 -0
  155. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_rules.py +0 -0
  156. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_skills.py +0 -0
  157. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_status.py +0 -0
  158. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_workspace_resolution.py +0 -0
  159. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_cli_workspaces.py +0 -0
  160. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_common_mcp_to_dto.py +0 -0
  161. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_compiled_planning.py +0 -0
  162. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_dto_to_common_mcp.py +0 -0
  163. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_git_exclude_service.py +0 -0
  164. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_mcp_service.py +0 -0
  165. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_planner_executor.py +0 -0
  166. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_planner_rules.py +0 -0
  167. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_project_config_sync.py +0 -0
  168. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_skill_install_sources.py +0 -0
  169. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_symlink_planning.py +0 -0
  170. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_transactional_executor.py +0 -0
  171. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_utils.py +0 -0
  172. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_version.py +0 -0
  173. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_workspace_config_sync.py +0 -0
  174. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_workspace_repo_status.py +0 -0
  175. {code_agnostic-0.8.0 → code_agnostic-0.9.1}/tests/test_workspaces.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-agnostic
3
- Version: 0.8.0
3
+ Version: 0.9.1
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.8.0"
3
+ __version__ = "0.9.1"
@@ -120,7 +120,7 @@ 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
126
  ws_links = ws_state.managed_links
@@ -139,7 +139,7 @@ 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
  )
@@ -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,7 +2,7 @@ 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
5
+ from code_agnostic.models import SyncState, WorkspaceConfig
6
6
 
7
7
 
8
8
  class ISchemaRepository(ABC):
@@ -93,7 +93,7 @@ class ISourceRepository(IConfigRepository):
93
93
  raise NotImplementedError
94
94
 
95
95
  @abstractmethod
96
- def load_workspaces(self) -> list[dict[str, str]]:
96
+ def load_workspaces(self) -> list[WorkspaceConfig]:
97
97
  raise NotImplementedError
98
98
 
99
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,
@@ -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"