code-agnostic 0.3.9__tar.gz → 0.3.11__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.9 → code_agnostic-0.3.11}/PKG-INFO +36 -8
  2. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/README.md +33 -7
  3. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/__init__.py +1 -1
  4. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/__main__.py +4 -0
  5. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/agents/codex.py +6 -3
  6. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/app_id.py +1 -1
  7. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/claude/service.py +2 -66
  8. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/codex/schema.json +36 -17
  9. code_agnostic-0.3.11/code_agnostic/apps/common/compiled_planning.py +115 -0
  10. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/common/interfaces/repositories.py +10 -0
  11. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/common/interfaces/service.py +30 -14
  12. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/opencode/schema.json +4 -1
  13. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/opencode/service.py +2 -0
  14. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/commands/mcp.py +29 -10
  15. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/commands/skills.py +11 -4
  16. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/commands/status.py +21 -4
  17. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/commands/workspaces.py +29 -13
  18. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/helpers.py +5 -1
  19. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/core/repository.py +4 -2
  20. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/executor.py +10 -0
  21. code_agnostic-0.3.11/code_agnostic/generated_artifacts.py +134 -0
  22. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/git_exclude_service.py +23 -41
  23. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/imports/adapters.py +10 -10
  24. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/mcp_service.py +4 -2
  25. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/planner.py +15 -12
  26. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/status.py +12 -63
  27. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/tui/renderers.py +13 -0
  28. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/tui/tables.py +1 -1
  29. code_agnostic-0.3.11/code_agnostic/workspace_artifacts.py +238 -0
  30. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic.egg-info/PKG-INFO +36 -8
  31. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic.egg-info/SOURCES.txt +2 -0
  32. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic.egg-info/requires.txt +2 -0
  33. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/pyproject.toml +4 -2
  34. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_apply_apps.py +38 -12
  35. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_mcp.py +7 -4
  36. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_plan.py +14 -4
  37. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_skills.py +1 -1
  38. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_status.py +15 -0
  39. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_workspaces.py +95 -15
  40. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_common_repository.py +20 -0
  41. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_git_exclude_service.py +31 -9
  42. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_transactional_executor.py +6 -3
  43. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_workspace_config_sync.py +136 -24
  44. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_workspace_repo_status.py +57 -11
  45. code_agnostic-0.3.9/code_agnostic/apps/common/compiled_planning.py +0 -197
  46. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/LICENSE +0 -0
  47. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/agents/__init__.py +0 -0
  48. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/agents/claude.py +0 -0
  49. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/agents/compilers.py +0 -0
  50. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/agents/models.py +0 -0
  51. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/agents/opencode.py +0 -0
  52. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/agents/parser.py +0 -0
  53. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/__init__.py +0 -0
  54. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/apps_service.py +0 -0
  55. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/claude/__init__.py +0 -0
  56. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/claude/config_repository.py +0 -0
  57. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/claude/mapper.py +0 -0
  58. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/codex/__init__.py +0 -0
  59. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/codex/config_repository.py +0 -0
  60. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/codex/mapper.py +0 -0
  61. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/codex/schema_repository.py +0 -0
  62. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/codex/service.py +0 -0
  63. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/common/__init__.py +0 -0
  64. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/common/framework.py +0 -0
  65. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  66. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  67. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/common/loader.py +0 -0
  68. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/common/models.py +0 -0
  69. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/common/schema.py +0 -0
  70. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/common/symlink_planning.py +0 -0
  71. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/common/utils.py +0 -0
  72. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/cursor/__init__.py +0 -0
  73. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/cursor/config_repository.py +0 -0
  74. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/cursor/mapper.py +0 -0
  75. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/cursor/schema.json +0 -0
  76. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  77. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/cursor/service.py +0 -0
  78. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/opencode/__init__.py +0 -0
  79. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/opencode/config_repository.py +0 -0
  80. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/opencode/mapper.py +0 -0
  81. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  82. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/__init__.py +0 -0
  83. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/aliases.py +0 -0
  84. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/commands/__init__.py +0 -0
  85. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/commands/agents.py +0 -0
  86. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/commands/apply.py +0 -0
  87. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/commands/apps.py +0 -0
  88. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
  89. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/commands/import_.py +0 -0
  90. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/commands/plan.py +0 -0
  91. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/commands/restore.py +0 -0
  92. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/commands/rules.py +0 -0
  93. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/commands/validate.py +0 -0
  94. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/cli/options.py +0 -0
  95. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/constants.py +0 -0
  96. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/core/__init__.py +0 -0
  97. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/core/workspace_repository.py +0 -0
  98. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/errors.py +0 -0
  99. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/imports/__init__.py +0 -0
  100. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/imports/filesystem.py +0 -0
  101. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/imports/models.py +0 -0
  102. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/imports/service.py +0 -0
  103. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/lossiness.py +0 -0
  104. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/models.py +0 -0
  105. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/rules/__init__.py +0 -0
  106. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/rules/compilers.py +0 -0
  107. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/rules/models.py +0 -0
  108. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/rules/parser.py +0 -0
  109. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/rules/repository.py +0 -0
  110. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/skills/__init__.py +0 -0
  111. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/skills/compilers.py +0 -0
  112. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/skills/models.py +0 -0
  113. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/skills/parser.py +0 -0
  114. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/spec/__init__.py +0 -0
  115. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/spec/loaders.py +0 -0
  116. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  117. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
  118. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
  119. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  120. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  121. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/tui/__init__.py +0 -0
  122. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/tui/enums.py +0 -0
  123. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/tui/import_selector.py +0 -0
  124. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/tui/sections.py +0 -0
  125. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/utils.py +0 -0
  126. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/validation.py +0 -0
  127. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic/workspaces.py +0 -0
  128. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic.egg-info/dependency_links.txt +0 -0
  129. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic.egg-info/entry_points.txt +0 -0
  130. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/code_agnostic.egg-info/top_level.txt +0 -0
  131. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/setup.cfg +0 -0
  132. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_agents.py +0 -0
  133. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_aliases.py +0 -0
  134. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_apply_codex.py +0 -0
  135. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_apply_cursor.py +0 -0
  136. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_apply_target.py +0 -0
  137. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_apps.py +0 -0
  138. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_explain_lossiness.py +0 -0
  139. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_flags.py +0 -0
  140. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_git_exclude.py +0 -0
  141. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_import.py +0 -0
  142. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_import_interactive.py +0 -0
  143. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_module_organization.py +0 -0
  144. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_restore.py +0 -0
  145. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_rules.py +0 -0
  146. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_validate.py +0 -0
  147. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_cli_workspace_resolution.py +0 -0
  148. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_common_mcp_to_dto.py +0 -0
  149. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_compiled_planning.py +0 -0
  150. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_dto_to_common_mcp.py +0 -0
  151. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_mcp_service.py +0 -0
  152. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_planner_executor.py +0 -0
  153. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_planner_rules.py +0 -0
  154. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_symlink_planning.py +0 -0
  155. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_sync_plan_model.py +0 -0
  156. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_utils.py +0 -0
  157. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/tests/test_version.py +0 -0
  158. {code_agnostic-0.3.9 → code_agnostic-0.3.11}/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.9
3
+ Version: 0.3.11
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
@@ -62,6 +64,21 @@ Legacy single-file rules, `skills/<name>/SKILL.md`, and markdown agents are stil
62
64
 
63
65
  Today the implementation is still mixed: some assets are compiled and some are symlinked. The active migration plan is to move to generated outputs everywhere with a strict compiler contract instead of implicit per-app behavior.
64
66
 
67
+ ## Scope model
68
+
69
+ `code-agnostic` has two managed source scopes today:
70
+
71
+ - global source config under `~/.config/code-agnostic/`, synced to enabled
72
+ user-level app config;
73
+ - workspace source config under `~/.config/code-agnostic/workspaces/<name>/`,
74
+ propagated into repos inside a registered workspace.
75
+
76
+ Project-local skill folders that users create directly inside a repo, such as
77
+ `.agents/skills` or `.opencode/skills`, are app-native inputs but are not
78
+ managed as source by `code-agnostic` yet. First-class project-scoped installs
79
+ are planned so a single registered project can have managed local source config
80
+ without bypassing the hub.
81
+
65
82
  ## Install
66
83
 
67
84
  ```bash
@@ -115,7 +132,7 @@ code-agnostic apply
115
132
  | Native repo config include for workspace `AGENTS.md` | yes | -- | -- | -- |
116
133
  | Repo/subdir gets shared workspace instructions today | yes | -- | yes | yes |
117
134
  | Nested `AGENTS.md` discovery | -- | yes | yes | -- |
118
- | Workspace propagation | yes | -- | yes | yes |
135
+ | Workspace propagation | yes | yes | yes | yes |
119
136
  | Import from | yes | yes | yes | yes |
120
137
  | Interactive import (TUI) | yes | yes | yes | yes |
121
138
 
@@ -123,11 +140,11 @@ code-agnostic apply
123
140
  target-specific or lossy; run `code-agnostic explain-lossiness` to see fields
124
141
  that are omitted or rejected for a selected target.
125
142
 
126
- 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
143
+ Cursor workspace propagation writes repo-local MCP, skills, and agents when those resources exist in the workspace source config.
127
144
 
128
145
  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`.
129
146
 
130
- 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.
147
+ 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.
131
148
 
132
149
  ## Features
133
150
 
@@ -139,10 +156,15 @@ Plan-then-apply workflow. Preview every change before it touches disk.
139
156
  code-agnostic validate # check canonical source files
140
157
  code-agnostic plan -a cursor # dry-run for one editor
141
158
  code-agnostic plan # dry-run for all
142
- code-agnostic apply # apply changes
143
- code-agnostic status # check drift
159
+ code-agnostic apply # apply changes for all enabled editors
160
+ code-agnostic status # check drift and disabled app states
161
+ code-agnostic explain-lossiness # show fields omitted or rejected per editor
144
162
  ```
145
163
 
164
+ Bare `plan` and `apply` target every enabled editor; bare `status` also shows
165
+ disabled app states. Use `-a codex`, `-a cursor`, `-a opencode`, or `-a claude`
166
+ when you want one editor at a time.
167
+
146
168
  If managed outputs need repair after an apply, restore the active synced revision:
147
169
 
148
170
  ```bash
@@ -230,9 +252,9 @@ That command should copy the skill into the source of truth and then run the nor
230
252
 
231
253
  ### Workspaces
232
254
 
233
- 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.
255
+ 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"]`. Workspace source config, skills, and agents are propagated into repo-local generated paths for OpenCode, Cursor, Codex, and Claude; user-created project-local skill folders remain unmanaged until project-scoped installs are supported.
234
256
 
235
- `.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).
257
+ Cursor propagation intentionally stays to repo-local MCP, skills, and agents; it does not copy the shared workspace `AGENTS.md` into child repos.
236
258
 
237
259
  ```bash
238
260
  code-agnostic workspaces add --name myproject --path ~/code/myproject
@@ -262,6 +284,12 @@ code-agnostic import apply -a cursor --include mcp --on-conflict overwrite
262
284
  code-agnostic import plan -a codex -i # interactive TUI picker
263
285
  ```
264
286
 
287
+ `import plan` previews what will be copied into the hub; `import apply` writes
288
+ only the selected sections. Conflicts are skipped by default, so rerun with
289
+ `--on-conflict overwrite` only after reviewing the preview. Use `--include`,
290
+ `--exclude`, `--source-root`, and `--follow-symlinks` to narrow what gets
291
+ imported.
292
+
265
293
  ### CLI conventions
266
294
 
267
295
  All commands use named flags (`-a`, `-w`, `-v`). Singular aliases work too: `app` = `apps`, `workspace` = `workspaces`.
@@ -39,6 +39,21 @@ Legacy single-file rules, `skills/<name>/SKILL.md`, and markdown agents are stil
39
39
 
40
40
  Today the implementation is still mixed: some assets are compiled and some are symlinked. The active migration plan is to move to generated outputs everywhere with a strict compiler contract instead of implicit per-app behavior.
41
41
 
42
+ ## Scope model
43
+
44
+ `code-agnostic` has two managed source scopes today:
45
+
46
+ - global source config under `~/.config/code-agnostic/`, synced to enabled
47
+ user-level app config;
48
+ - workspace source config under `~/.config/code-agnostic/workspaces/<name>/`,
49
+ propagated into repos inside a registered workspace.
50
+
51
+ Project-local skill folders that users create directly inside a repo, such as
52
+ `.agents/skills` or `.opencode/skills`, are app-native inputs but are not
53
+ managed as source by `code-agnostic` yet. First-class project-scoped installs
54
+ are planned so a single registered project can have managed local source config
55
+ without bypassing the hub.
56
+
42
57
  ## Install
43
58
 
44
59
  ```bash
@@ -92,7 +107,7 @@ code-agnostic apply
92
107
  | Native repo config include for workspace `AGENTS.md` | yes | -- | -- | -- |
93
108
  | Repo/subdir gets shared workspace instructions today | yes | -- | yes | yes |
94
109
  | Nested `AGENTS.md` discovery | -- | yes | yes | -- |
95
- | Workspace propagation | yes | -- | yes | yes |
110
+ | Workspace propagation | yes | yes | yes | yes |
96
111
  | Import from | yes | yes | yes | yes |
97
112
  | Interactive import (TUI) | yes | yes | yes | yes |
98
113
 
@@ -100,11 +115,11 @@ code-agnostic apply
100
115
  target-specific or lossy; run `code-agnostic explain-lossiness` to see fields
101
116
  that are omitted or rejected for a selected target.
102
117
 
103
- 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
118
+ Cursor workspace propagation writes repo-local MCP, skills, and agents when those resources exist in the workspace source config.
104
119
 
105
120
  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`.
106
121
 
107
- 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.
122
+ 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.
108
123
 
109
124
  ## Features
110
125
 
@@ -116,10 +131,15 @@ Plan-then-apply workflow. Preview every change before it touches disk.
116
131
  code-agnostic validate # check canonical source files
117
132
  code-agnostic plan -a cursor # dry-run for one editor
118
133
  code-agnostic plan # dry-run for all
119
- code-agnostic apply # apply changes
120
- code-agnostic status # check drift
134
+ code-agnostic apply # apply changes for all enabled editors
135
+ code-agnostic status # check drift and disabled app states
136
+ code-agnostic explain-lossiness # show fields omitted or rejected per editor
121
137
  ```
122
138
 
139
+ Bare `plan` and `apply` target every enabled editor; bare `status` also shows
140
+ disabled app states. Use `-a codex`, `-a cursor`, `-a opencode`, or `-a claude`
141
+ when you want one editor at a time.
142
+
123
143
  If managed outputs need repair after an apply, restore the active synced revision:
124
144
 
125
145
  ```bash
@@ -207,9 +227,9 @@ That command should copy the skill into the source of truth and then run the nor
207
227
 
208
228
  ### Workspaces
209
229
 
210
- 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.
230
+ 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"]`. Workspace source config, skills, and agents are propagated into repo-local generated paths for OpenCode, Cursor, Codex, and Claude; user-created project-local skill folders remain unmanaged until project-scoped installs are supported.
211
231
 
212
- `.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).
232
+ Cursor propagation intentionally stays to repo-local MCP, skills, and agents; it does not copy the shared workspace `AGENTS.md` into child repos.
213
233
 
214
234
  ```bash
215
235
  code-agnostic workspaces add --name myproject --path ~/code/myproject
@@ -239,6 +259,12 @@ code-agnostic import apply -a cursor --include mcp --on-conflict overwrite
239
259
  code-agnostic import plan -a codex -i # interactive TUI picker
240
260
  ```
241
261
 
262
+ `import plan` previews what will be copied into the hub; `import apply` writes
263
+ only the selected sections. Conflicts are skipped by default, so rerun with
264
+ `--on-conflict overwrite` only after reviewing the preview. Use `--include`,
265
+ `--exclude`, `--source-root`, and `--follow-symlinks` to narrow what gets
266
+ imported.
267
+
242
268
  ### CLI conventions
243
269
 
244
270
  All commands use named flags (`-a`, `-w`, `-v`). Singular aliases work too: `app` = `apps`, `workspace` = `workspaces`.
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.3.9"
3
+ __version__ = "0.3.11"
@@ -16,6 +16,7 @@ from code_agnostic.cli.commands.skills import skills
16
16
  from code_agnostic.cli.commands.status import status
17
17
  from code_agnostic.cli.commands.validate import validate
18
18
  from code_agnostic.cli.commands.workspaces import workspaces
19
+ from code_agnostic.errors import SyncAppError
19
20
 
20
21
 
21
22
  @click.group(
@@ -52,6 +53,9 @@ def main() -> int:
52
53
  except click.exceptions.Exit as exc:
53
54
  code = exc.exit_code
54
55
  return code if isinstance(code, int) else 1
56
+ except SyncAppError as exc:
57
+ click.ClickException(str(exc)).show()
58
+ return 2
55
59
  except click.ClickException as exc:
56
60
  exc.show()
57
61
  return 2
@@ -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
@@ -355,6 +355,22 @@
355
355
  },
356
356
  "type": "object"
357
357
  },
358
+ "CodeModeConfigToml": {
359
+ "additionalProperties": false,
360
+ "properties": {
361
+ "enabled": {
362
+ "type": "boolean"
363
+ },
364
+ "excluded_tool_namespaces": {
365
+ "description": "Exact tool namespaces to omit from the code-mode nested tool surface.",
366
+ "items": {
367
+ "type": "string"
368
+ },
369
+ "type": "array"
370
+ }
371
+ },
372
+ "type": "object"
373
+ },
358
374
  "ConfigProfile": {
359
375
  "additionalProperties": false,
360
376
  "description": "Collection of common configuration options that a user can define as a unit in `config.toml`.",
@@ -410,7 +426,7 @@
410
426
  "type": "boolean"
411
427
  },
412
428
  "code_mode": {
413
- "type": "boolean"
429
+ "$ref": "#/definitions/FeatureToml_for_CodeModeConfigToml"
414
430
  },
415
431
  "code_mode_only": {
416
432
  "type": "boolean"
@@ -556,9 +572,6 @@
556
572
  "request_rule": {
557
573
  "type": "boolean"
558
574
  },
559
- "responses_websocket_response_processed": {
560
- "type": "boolean"
561
- },
562
575
  "responses_websockets": {
563
576
  "type": "boolean"
564
577
  },
@@ -601,6 +614,9 @@
601
614
  "terminal_resize_reflow": {
602
615
  "type": "boolean"
603
616
  },
617
+ "terminal_visualization_instructions": {
618
+ "type": "boolean"
619
+ },
604
620
  "tool_call_mcp_elicitation": {
605
621
  "type": "boolean"
606
622
  },
@@ -830,6 +846,16 @@
830
846
  }
831
847
  ]
832
848
  },
849
+ "FeatureToml_for_CodeModeConfigToml": {
850
+ "anyOf": [
851
+ {
852
+ "type": "boolean"
853
+ },
854
+ {
855
+ "$ref": "#/definitions/CodeModeConfigToml"
856
+ }
857
+ ]
858
+ },
833
859
  "FeatureToml_for_MultiAgentV2ConfigToml": {
834
860
  "anyOf": [
835
861
  {
@@ -2480,15 +2506,8 @@
2480
2506
  "type": "string"
2481
2507
  },
2482
2508
  "ReasoningEffort": {
2483
- "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
2484
- "enum": [
2485
- "none",
2486
- "minimal",
2487
- "low",
2488
- "medium",
2489
- "high",
2490
- "xhigh"
2491
- ],
2509
+ "description": "A non-empty reasoning effort value advertised by the model.",
2510
+ "minLength": 1,
2492
2511
  "type": "string"
2493
2512
  },
2494
2513
  "ReasoningSummary": {
@@ -4533,7 +4552,7 @@
4533
4552
  "type": "boolean"
4534
4553
  },
4535
4554
  "code_mode": {
4536
- "type": "boolean"
4555
+ "$ref": "#/definitions/FeatureToml_for_CodeModeConfigToml"
4537
4556
  },
4538
4557
  "code_mode_only": {
4539
4558
  "type": "boolean"
@@ -4679,9 +4698,6 @@
4679
4698
  "request_rule": {
4680
4699
  "type": "boolean"
4681
4700
  },
4682
- "responses_websocket_response_processed": {
4683
- "type": "boolean"
4684
- },
4685
4701
  "responses_websockets": {
4686
4702
  "type": "boolean"
4687
4703
  },
@@ -4724,6 +4740,9 @@
4724
4740
  "terminal_resize_reflow": {
4725
4741
  "type": "boolean"
4726
4742
  },
4743
+ "terminal_visualization_instructions": {
4744
+ "type": "boolean"
4745
+ },
4727
4746
  "tool_call_mcp_elicitation": {
4728
4747
  "type": "boolean"
4729
4748
  },
@@ -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: