code-agnostic 0.3.8__tar.gz → 0.3.9__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 (159) hide show
  1. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/PKG-INFO +31 -14
  2. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/README.md +30 -11
  3. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/__init__.py +1 -1
  4. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/agents/codex.py +3 -6
  5. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/app_id.py +1 -1
  6. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/claude/service.py +66 -2
  7. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/codex/schema.json +8 -0
  8. code_agnostic-0.3.9/code_agnostic/apps/common/compiled_planning.py +197 -0
  9. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/common/interfaces/repositories.py +0 -10
  10. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/common/interfaces/service.py +14 -30
  11. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/opencode/schema.json +1 -10
  12. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/opencode/service.py +0 -2
  13. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/commands/workspaces.py +4 -9
  14. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/git_exclude_service.py +41 -23
  15. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/imports/adapters.py +10 -10
  16. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/imports/service.py +5 -1
  17. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/lossiness.py +30 -0
  18. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/planner.py +12 -15
  19. code_agnostic-0.3.9/code_agnostic/status.py +253 -0
  20. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/tui/renderers.py +60 -2
  21. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/utils.py +2 -2
  22. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic.egg-info/PKG-INFO +31 -14
  23. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic.egg-info/SOURCES.txt +0 -2
  24. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic.egg-info/requires.txt +0 -2
  25. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/pyproject.toml +2 -4
  26. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_aliases.py +1 -1
  27. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_apply_apps.py +12 -38
  28. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_explain_lossiness.py +32 -0
  29. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_flags.py +1 -1
  30. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_import.py +25 -0
  31. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_plan.py +4 -14
  32. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_status.py +44 -0
  33. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_workspaces.py +17 -54
  34. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_git_exclude_service.py +9 -31
  35. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_utils.py +45 -0
  36. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_workspace_config_sync.py +16 -120
  37. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_workspace_repo_status.py +11 -57
  38. code_agnostic-0.3.8/code_agnostic/apps/common/compiled_planning.py +0 -115
  39. code_agnostic-0.3.8/code_agnostic/generated_artifacts.py +0 -134
  40. code_agnostic-0.3.8/code_agnostic/status.py +0 -143
  41. code_agnostic-0.3.8/code_agnostic/workspace_artifacts.py +0 -238
  42. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/LICENSE +0 -0
  43. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/__main__.py +0 -0
  44. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/agents/__init__.py +0 -0
  45. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/agents/claude.py +0 -0
  46. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/agents/compilers.py +0 -0
  47. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/agents/models.py +0 -0
  48. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/agents/opencode.py +0 -0
  49. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/agents/parser.py +0 -0
  50. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/__init__.py +0 -0
  51. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/apps_service.py +0 -0
  52. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/claude/__init__.py +0 -0
  53. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/claude/config_repository.py +0 -0
  54. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/claude/mapper.py +0 -0
  55. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/codex/__init__.py +0 -0
  56. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/codex/config_repository.py +0 -0
  57. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/codex/mapper.py +0 -0
  58. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/codex/schema_repository.py +0 -0
  59. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/codex/service.py +0 -0
  60. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/common/__init__.py +0 -0
  61. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/common/framework.py +0 -0
  62. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  63. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  64. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/common/loader.py +0 -0
  65. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/common/models.py +0 -0
  66. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/common/schema.py +0 -0
  67. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/common/symlink_planning.py +0 -0
  68. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/common/utils.py +0 -0
  69. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/cursor/__init__.py +0 -0
  70. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/cursor/config_repository.py +0 -0
  71. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/cursor/mapper.py +0 -0
  72. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/cursor/schema.json +0 -0
  73. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  74. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/cursor/service.py +0 -0
  75. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/opencode/__init__.py +0 -0
  76. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/opencode/config_repository.py +0 -0
  77. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/opencode/mapper.py +0 -0
  78. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  79. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/__init__.py +0 -0
  80. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/aliases.py +0 -0
  81. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/commands/__init__.py +0 -0
  82. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/commands/agents.py +0 -0
  83. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/commands/apply.py +0 -0
  84. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/commands/apps.py +0 -0
  85. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
  86. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/commands/import_.py +0 -0
  87. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/commands/mcp.py +0 -0
  88. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/commands/plan.py +0 -0
  89. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/commands/restore.py +0 -0
  90. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/commands/rules.py +0 -0
  91. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/commands/skills.py +0 -0
  92. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/commands/status.py +0 -0
  93. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/commands/validate.py +0 -0
  94. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/helpers.py +0 -0
  95. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/cli/options.py +0 -0
  96. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/constants.py +0 -0
  97. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/core/__init__.py +0 -0
  98. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/core/repository.py +0 -0
  99. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/core/workspace_repository.py +0 -0
  100. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/errors.py +0 -0
  101. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/executor.py +0 -0
  102. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/imports/__init__.py +0 -0
  103. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/imports/filesystem.py +0 -0
  104. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/imports/models.py +0 -0
  105. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/mcp_service.py +0 -0
  106. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/models.py +0 -0
  107. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/rules/__init__.py +0 -0
  108. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/rules/compilers.py +0 -0
  109. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/rules/models.py +0 -0
  110. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/rules/parser.py +0 -0
  111. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/rules/repository.py +0 -0
  112. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/skills/__init__.py +0 -0
  113. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/skills/compilers.py +0 -0
  114. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/skills/models.py +0 -0
  115. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/skills/parser.py +0 -0
  116. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/spec/__init__.py +0 -0
  117. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/spec/loaders.py +0 -0
  118. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  119. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
  120. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
  121. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  122. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  123. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/tui/__init__.py +0 -0
  124. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/tui/enums.py +0 -0
  125. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/tui/import_selector.py +0 -0
  126. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/tui/sections.py +0 -0
  127. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/tui/tables.py +0 -0
  128. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/validation.py +0 -0
  129. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic/workspaces.py +0 -0
  130. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic.egg-info/dependency_links.txt +0 -0
  131. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic.egg-info/entry_points.txt +0 -0
  132. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/code_agnostic.egg-info/top_level.txt +0 -0
  133. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/setup.cfg +0 -0
  134. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_agents.py +0 -0
  135. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_apply_codex.py +0 -0
  136. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_apply_cursor.py +0 -0
  137. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_apply_target.py +0 -0
  138. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_apps.py +0 -0
  139. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_git_exclude.py +0 -0
  140. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_import_interactive.py +0 -0
  141. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_mcp.py +0 -0
  142. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_module_organization.py +0 -0
  143. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_restore.py +0 -0
  144. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_rules.py +0 -0
  145. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_skills.py +0 -0
  146. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_validate.py +0 -0
  147. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_cli_workspace_resolution.py +0 -0
  148. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_common_mcp_to_dto.py +0 -0
  149. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_common_repository.py +0 -0
  150. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_compiled_planning.py +0 -0
  151. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_dto_to_common_mcp.py +0 -0
  152. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_mcp_service.py +0 -0
  153. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_planner_executor.py +0 -0
  154. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_planner_rules.py +0 -0
  155. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_symlink_planning.py +0 -0
  156. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_sync_plan_model.py +0 -0
  157. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_transactional_executor.py +0 -0
  158. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/tests/test_version.py +0 -0
  159. {code_agnostic-0.3.8 → code_agnostic-0.3.9}/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.8
3
+ Version: 0.3.9
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,8 +19,6 @@ 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"
24
22
  Dynamic: license-file
25
23
 
26
24
  # code-agnostic
@@ -117,15 +115,19 @@ code-agnostic apply
117
115
  | Native repo config include for workspace `AGENTS.md` | yes | -- | -- | -- |
118
116
  | Repo/subdir gets shared workspace instructions today | yes | -- | yes | yes |
119
117
  | Nested `AGENTS.md` discovery | -- | yes | yes | -- |
120
- | Workspace propagation | yes | yes | yes | yes |
118
+ | Workspace propagation | yes | -- | yes | yes |
121
119
  | Import from | yes | yes | yes | yes |
122
120
  | Interactive import (TUI) | yes | yes | yes | yes |
123
121
 
124
- Cursor workspace propagation writes repo-local MCP, skills, and agents when those resources exist in the workspace source config.
122
+ `yes` means the resource type is synced for that editor. Some metadata is still
123
+ target-specific or lossy; run `code-agnostic explain-lossiness` to see fields
124
+ that are omitted or rejected for a selected target.
125
+
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
125
127
 
126
128
  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`.
127
129
 
128
- Cursor documents `AGENTS.md` support in project roots and subdirectories. `code-agnostic` does not copy or link the shared workspace `AGENTS.md` into child repos; Cursor will load repo-local or nested `AGENTS.md` files that already exist in the opened project. Codex documents nested `AGENTS.md` discovery, but not a native config include for an extra workspace file.
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.
129
131
 
130
132
  ## Features
131
133
 
@@ -162,19 +164,31 @@ Env vars without a value (`--env GITHUB_TOKEN`) are stored as `${GITHUB_TOKEN}`
162
164
 
163
165
  ### Rules with metadata
164
166
 
165
- Rules live in `rules/` as markdown files with optional YAML frontmatter:
167
+ New rules should use bundle directories with schema-validated metadata and a
168
+ separate prompt body:
166
169
 
167
- ```markdown
168
- ---
170
+ ```text
171
+ rules/python-style/
172
+ ├── meta.yaml
173
+ └── prompt.md
174
+ ```
175
+
176
+ ```yaml
177
+ # rules/python-style/meta.yaml
178
+ spec_version: v1
179
+ kind: rule
169
180
  description: "Python coding standards"
170
181
  globs: ["*.py"]
171
182
  always_apply: false
172
- ---
183
+ ```
173
184
 
185
+ ```markdown
186
+ <!-- rules/python-style/prompt.md -->
174
187
  Always use type hints. Prefer dataclasses over dicts.
175
188
  ```
176
189
 
177
190
  Cross-compiled per editor: Cursor gets `.mdc` files with native frontmatter, OpenCode/Codex get `AGENTS.md` sections.
191
+ Legacy single-file rule markdown with YAML frontmatter remains supported for migration.
178
192
 
179
193
  ```bash
180
194
  code-agnostic rules list
@@ -183,7 +197,10 @@ code-agnostic rules remove --name python-style
183
197
 
184
198
  ### Skills and agents
185
199
 
186
- Canonical YAML frontmatter format, cross-compiled per editor. Install or edit skills in the `code-agnostic` source of truth, then run `plan` / `apply`; do not hand-copy generated skills into `.codex`, `.cursor`, or OpenCode directories.
200
+ Use bundle directories for new skills and agents, then let `code-agnostic`
201
+ cross-compile them per editor. Install or edit skills in the `code-agnostic`
202
+ source of truth, then run `plan` / `apply`; do not hand-copy generated skills
203
+ into `.codex`, `.cursor`, `.agents`, or OpenCode directories.
187
204
 
188
205
  ```bash
189
206
  code-agnostic skills list
@@ -213,9 +230,9 @@ That command should copy the skill into the source of truth and then run the nor
213
230
 
214
231
  ### Workspaces
215
232
 
216
- Register workspace directories. Workspace rules are compiled into a canonical `AGENTS.md` at the workspace root. Repos keep their own repo-specific `AGENTS.md`; Codex receives the workspace rules through generated, git-excluded `AGENTS.override.md` files, while OpenCode workspace configs reference the shared workspace file through `instructions`. Claude receives generated `CLAUDE.local.md` files and project MCP entries in `~/.claude.json["projects"][absolute_repo_path]["mcpServers"]`. Repo-local app config, skills, and agents are propagated for OpenCode, Cursor, Codex, and Claude.
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.
217
234
 
218
- Cursor propagation intentionally stays to repo-local MCP, skills, and agents; it does not copy the shared workspace `AGENTS.md` into child repos.
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).
219
236
 
220
237
  ```bash
221
238
  code-agnostic workspaces add --name myproject --path ~/code/myproject
@@ -264,7 +281,7 @@ The compiler migration is documented in:
264
281
 
265
282
  - [x] Plan/apply/status sync engine
266
283
  - [x] MCP server sync across editors
267
- - [x] Skills and agents sync (symlink-based)
284
+ - [x] Skills and agents sync across editors
268
285
  - [x] Workspace propagation into git repos
269
286
  - [x] Import from existing editor configs
270
287
  - [x] Consistent CLI with named flags and aliases
@@ -92,15 +92,19 @@ code-agnostic apply
92
92
  | Native repo config include for workspace `AGENTS.md` | yes | -- | -- | -- |
93
93
  | Repo/subdir gets shared workspace instructions today | yes | -- | yes | yes |
94
94
  | Nested `AGENTS.md` discovery | -- | yes | yes | -- |
95
- | Workspace propagation | yes | yes | yes | yes |
95
+ | Workspace propagation | yes | -- | yes | yes |
96
96
  | Import from | yes | yes | yes | yes |
97
97
  | Interactive import (TUI) | yes | yes | yes | yes |
98
98
 
99
- Cursor workspace propagation writes repo-local MCP, skills, and agents when those resources exist in the workspace source config.
99
+ `yes` means the resource type is synced for that editor. Some metadata is still
100
+ target-specific or lossy; run `code-agnostic explain-lossiness` to see fields
101
+ that are omitted or rejected for a selected target.
102
+
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
100
104
 
101
105
  OpenCode workspace configs include the shared workspace `AGENTS.md` natively via `instructions`, so repos under the workspace get both repo-local and shared workspace instructions. Codex repos receive workspace instructions through a generated `AGENTS.override.md`, which is added to each repo's `.git/info/exclude`. Claude Code receives workspace instructions through generated `CLAUDE.local.md` files, never by editing committed `CLAUDE.md`.
102
106
 
103
- Cursor documents `AGENTS.md` support in project roots and subdirectories. `code-agnostic` does not copy or link the shared workspace `AGENTS.md` into child repos; Cursor will load repo-local or nested `AGENTS.md` files that already exist in the opened project. Codex documents nested `AGENTS.md` discovery, but not a native config include for an extra workspace file.
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.
104
108
 
105
109
  ## Features
106
110
 
@@ -137,19 +141,31 @@ Env vars without a value (`--env GITHUB_TOKEN`) are stored as `${GITHUB_TOKEN}`
137
141
 
138
142
  ### Rules with metadata
139
143
 
140
- Rules live in `rules/` as markdown files with optional YAML frontmatter:
144
+ New rules should use bundle directories with schema-validated metadata and a
145
+ separate prompt body:
141
146
 
142
- ```markdown
143
- ---
147
+ ```text
148
+ rules/python-style/
149
+ ├── meta.yaml
150
+ └── prompt.md
151
+ ```
152
+
153
+ ```yaml
154
+ # rules/python-style/meta.yaml
155
+ spec_version: v1
156
+ kind: rule
144
157
  description: "Python coding standards"
145
158
  globs: ["*.py"]
146
159
  always_apply: false
147
- ---
160
+ ```
148
161
 
162
+ ```markdown
163
+ <!-- rules/python-style/prompt.md -->
149
164
  Always use type hints. Prefer dataclasses over dicts.
150
165
  ```
151
166
 
152
167
  Cross-compiled per editor: Cursor gets `.mdc` files with native frontmatter, OpenCode/Codex get `AGENTS.md` sections.
168
+ Legacy single-file rule markdown with YAML frontmatter remains supported for migration.
153
169
 
154
170
  ```bash
155
171
  code-agnostic rules list
@@ -158,7 +174,10 @@ code-agnostic rules remove --name python-style
158
174
 
159
175
  ### Skills and agents
160
176
 
161
- Canonical YAML frontmatter format, cross-compiled per editor. Install or edit skills in the `code-agnostic` source of truth, then run `plan` / `apply`; do not hand-copy generated skills into `.codex`, `.cursor`, or OpenCode directories.
177
+ Use bundle directories for new skills and agents, then let `code-agnostic`
178
+ cross-compile them per editor. Install or edit skills in the `code-agnostic`
179
+ source of truth, then run `plan` / `apply`; do not hand-copy generated skills
180
+ into `.codex`, `.cursor`, `.agents`, or OpenCode directories.
162
181
 
163
182
  ```bash
164
183
  code-agnostic skills list
@@ -188,9 +207,9 @@ That command should copy the skill into the source of truth and then run the nor
188
207
 
189
208
  ### Workspaces
190
209
 
191
- Register workspace directories. Workspace rules are compiled into a canonical `AGENTS.md` at the workspace root. Repos keep their own repo-specific `AGENTS.md`; Codex receives the workspace rules through generated, git-excluded `AGENTS.override.md` files, while OpenCode workspace configs reference the shared workspace file through `instructions`. Claude receives generated `CLAUDE.local.md` files and project MCP entries in `~/.claude.json["projects"][absolute_repo_path]["mcpServers"]`. Repo-local app config, skills, and agents are propagated for OpenCode, Cursor, Codex, and Claude.
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.
192
211
 
193
- Cursor propagation intentionally stays to repo-local MCP, skills, and agents; it does not copy the shared workspace `AGENTS.md` into child repos.
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).
194
213
 
195
214
  ```bash
196
215
  code-agnostic workspaces add --name myproject --path ~/code/myproject
@@ -239,7 +258,7 @@ The compiler migration is documented in:
239
258
 
240
259
  - [x] Plan/apply/status sync engine
241
260
  - [x] MCP server sync across editors
242
- - [x] Skills and agents sync (symlink-based)
261
+ - [x] Skills and agents sync across editors
243
262
  - [x] Workspace propagation into git repos
244
263
  - [x] Import from existing editor configs
245
264
  - [x] Consistent CLI with named flags and aliases
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.3.8"
3
+ __version__ = "0.3.9"
@@ -69,13 +69,10 @@ 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", tomlkit.item(agent.metadata.name or agent.name))
73
- doc.add("description", tomlkit.item(description))
72
+ doc.add("name", agent.metadata.name or agent.name)
73
+ doc.add("description", description)
74
74
  if agent.metadata.nickname_candidates:
75
- doc.add(
76
- "nickname_candidates",
77
- tomlkit.item(list(agent.metadata.nickname_candidates)),
78
- )
75
+ doc.add("nickname_candidates", list(agent.metadata.nickname_candidates))
79
76
  model = agent.metadata.effective_value("codex", "model")
80
77
  if model:
81
78
  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=True,
66
+ supports_workspace_propagation=False,
67
67
  project_dir_name=CURSOR_PROJECT_DIRNAME,
68
68
  config_filename=CURSOR_CONFIG_FILENAME,
69
69
  ),
@@ -8,6 +8,10 @@ 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
+ )
11
15
  from code_agnostic.apps.common.framework import RegisteredAppConfigService
12
16
  from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
13
17
  from code_agnostic.apps.common.interfaces.repositories import IAppConfigRepository
@@ -131,7 +135,7 @@ class ClaudeConfigService(RegisteredAppConfigService):
131
135
  removable_links: list[Path],
132
136
  ) -> tuple[list[Action], list[Path], list[str]]:
133
137
  compiler = ClaudeSkillCompiler()
134
- return self._plan_compiled_text_actions(
138
+ return self._plan_owned_text_actions(
135
139
  sources=sources,
136
140
  target_dir=target_dir,
137
141
  scope=scope,
@@ -169,7 +173,7 @@ class ClaudeConfigService(RegisteredAppConfigService):
169
173
  agent = parse_agent(source)
170
174
  return claude_agent_target_path(target_dir, agent), compiler.compile(agent)
171
175
 
172
- return self._plan_compiled_text_actions(
176
+ return self._plan_owned_text_actions(
173
177
  sources=sources,
174
178
  target_dir=target_dir,
175
179
  scope=scope,
@@ -182,3 +186,63 @@ class ClaudeConfigService(RegisteredAppConfigService):
182
186
  update_detail="update compiled claude agent",
183
187
  conflict_message="Claude agent sync skipped (conflict): {target}",
184
188
  )
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
@@ -102,6 +102,14 @@
102
102
  "additionalProperties": false,
103
103
  "description": "Config values for a single app/connector.",
104
104
  "properties": {
105
+ "approvals_reviewer": {
106
+ "allOf": [
107
+ {
108
+ "$ref": "#/definitions/ApprovalsReviewer"
109
+ }
110
+ ],
111
+ "description": "Reviewer for approval prompts from this app, overriding the thread default."
112
+ },
105
113
  "default_tools_approval_mode": {
106
114
  "allOf": [
107
115
  {
@@ -0,0 +1,197 @@
1
+ from pathlib import Path
2
+
3
+ from code_agnostic.models import Action, ActionKind, ActionStatus
4
+
5
+
6
+ def find_replaceable_symlink_ancestor(target: Path, managed_root: Path) -> Path | None:
7
+ current = target
8
+ while True:
9
+ if current.is_symlink() and (
10
+ current == managed_root or current.is_relative_to(managed_root)
11
+ ):
12
+ return current
13
+ if current.parent == current:
14
+ return None
15
+ current = current.parent
16
+
17
+
18
+ def _symlink_ancestor_state(
19
+ target: Path, removable_link_paths: set[Path], managed_root: Path | None
20
+ ) -> tuple[bool, bool]:
21
+ current = target
22
+ found_symlink = False
23
+ while True:
24
+ in_managed_root = managed_root is None or (
25
+ current == managed_root or current.is_relative_to(managed_root)
26
+ )
27
+ if current.is_symlink() and in_managed_root:
28
+ found_symlink = True
29
+ current_key = current.resolve(strict=False)
30
+ if current_key in removable_link_paths:
31
+ return True, True
32
+ if current.parent == current:
33
+ return found_symlink, False
34
+ current = current.parent
35
+
36
+
37
+ def plan_compiled_text_action(
38
+ *,
39
+ target: Path,
40
+ payload: str,
41
+ managed_paths: set[Path],
42
+ removable_link_paths: set[Path] | None = None,
43
+ managed_root: Path | None = None,
44
+ scope: str,
45
+ app: str,
46
+ create_detail: str,
47
+ noop_detail: str,
48
+ update_detail: str,
49
+ conflict_detail: str = "non-managed path exists",
50
+ ) -> Action:
51
+ removable = removable_link_paths or set()
52
+ has_symlink_ancestor, is_removable_ancestor = _symlink_ancestor_state(
53
+ target, removable, managed_root
54
+ )
55
+
56
+ if has_symlink_ancestor and not is_removable_ancestor:
57
+ return Action(
58
+ kind=ActionKind.WRITE_TEXT,
59
+ path=target,
60
+ status=ActionStatus.CONFLICT,
61
+ detail=conflict_detail,
62
+ payload=payload,
63
+ app=app,
64
+ scope=scope,
65
+ )
66
+
67
+ if has_symlink_ancestor and is_removable_ancestor:
68
+ if target.is_file():
69
+ existing = target.read_text(encoding="utf-8")
70
+ if existing == payload:
71
+ return Action(
72
+ kind=ActionKind.WRITE_TEXT,
73
+ path=target,
74
+ status=ActionStatus.NOOP,
75
+ detail=noop_detail,
76
+ payload=payload,
77
+ app=app,
78
+ scope=scope,
79
+ )
80
+ return Action(
81
+ kind=ActionKind.WRITE_TEXT,
82
+ path=target,
83
+ status=ActionStatus.UPDATE,
84
+ detail=update_detail,
85
+ payload=payload,
86
+ app=app,
87
+ scope=scope,
88
+ )
89
+ return Action(
90
+ kind=ActionKind.WRITE_TEXT,
91
+ path=target,
92
+ status=ActionStatus.CREATE,
93
+ detail=create_detail,
94
+ payload=payload,
95
+ app=app,
96
+ scope=scope,
97
+ )
98
+
99
+ if not target.exists() and not target.is_symlink():
100
+ return Action(
101
+ kind=ActionKind.WRITE_TEXT,
102
+ path=target,
103
+ status=ActionStatus.CREATE,
104
+ detail=create_detail,
105
+ payload=payload,
106
+ app=app,
107
+ scope=scope,
108
+ )
109
+
110
+ if target.is_file():
111
+ existing = target.read_text(encoding="utf-8")
112
+ if existing == payload:
113
+ return Action(
114
+ kind=ActionKind.WRITE_TEXT,
115
+ path=target,
116
+ status=ActionStatus.NOOP,
117
+ detail=noop_detail,
118
+ payload=payload,
119
+ app=app,
120
+ scope=scope,
121
+ )
122
+ return Action(
123
+ kind=ActionKind.WRITE_TEXT,
124
+ path=target,
125
+ status=ActionStatus.UPDATE,
126
+ detail=update_detail,
127
+ payload=payload,
128
+ app=app,
129
+ scope=scope,
130
+ )
131
+
132
+ return Action(
133
+ kind=ActionKind.WRITE_TEXT,
134
+ path=target,
135
+ status=ActionStatus.CONFLICT,
136
+ detail=conflict_detail,
137
+ payload=payload,
138
+ app=app,
139
+ scope=scope,
140
+ )
141
+
142
+
143
+ def plan_owned_compiled_text_action(
144
+ *,
145
+ target: Path,
146
+ payload: str,
147
+ managed_paths: set[Path],
148
+ removable_link_paths: set[Path] | None = None,
149
+ managed_root: Path | None = None,
150
+ scope: str,
151
+ app: str,
152
+ create_detail: str,
153
+ noop_detail: str,
154
+ update_detail: str,
155
+ conflict_detail: str = "non-managed path exists",
156
+ ) -> Action:
157
+ target_key = target.resolve(strict=False)
158
+ removable = removable_link_paths or set()
159
+ has_symlink_ancestor, is_removable_ancestor = _symlink_ancestor_state(
160
+ target, removable, managed_root
161
+ )
162
+
163
+ if target_key in managed_paths or is_removable_ancestor:
164
+ return plan_compiled_text_action(
165
+ target=target,
166
+ payload=payload,
167
+ managed_paths=managed_paths,
168
+ removable_link_paths=removable,
169
+ managed_root=managed_root,
170
+ scope=scope,
171
+ app=app,
172
+ create_detail=create_detail,
173
+ noop_detail=noop_detail,
174
+ update_detail=update_detail,
175
+ conflict_detail=conflict_detail,
176
+ )
177
+
178
+ if not target.exists() and not target.is_symlink() and not has_symlink_ancestor:
179
+ return Action(
180
+ kind=ActionKind.WRITE_TEXT,
181
+ path=target,
182
+ status=ActionStatus.CREATE,
183
+ detail=create_detail,
184
+ payload=payload,
185
+ app=app,
186
+ scope=scope,
187
+ )
188
+
189
+ return Action(
190
+ kind=ActionKind.WRITE_TEXT,
191
+ path=target,
192
+ status=ActionStatus.CONFLICT,
193
+ detail=conflict_detail,
194
+ payload=payload,
195
+ app=app,
196
+ scope=scope,
197
+ )
@@ -20,16 +20,6 @@ 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
-
33
23
  @abstractmethod
34
24
  def load_config(self) -> dict[str, Any]:
35
25
  raise NotImplementedError
@@ -6,6 +6,7 @@ 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,
9
10
  )
10
11
  from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
11
12
  from code_agnostic.apps.common.interfaces.repositories import IAppConfigRepository
@@ -17,12 +18,6 @@ from code_agnostic.apps.common.symlink_planning import (
17
18
  plan_stale_files_group,
18
19
  plan_stale_group,
19
20
  )
20
- from code_agnostic.generated_artifacts import (
21
- ArtifactKind,
22
- GeneratedArtifact,
23
- OwnershipPolicy,
24
- plan_generated_artifact,
25
- )
26
21
  from code_agnostic.models import Action, ActionKind, ActionStatus, SyncPlan
27
22
 
28
23
 
@@ -97,10 +92,7 @@ class IAppConfigService(ABC):
97
92
  raise NotImplementedError
98
93
 
99
94
  def agent_action_removable_links(self, removable_links: list[Path]) -> list[Path]:
100
- return removable_links
101
-
102
- def compiled_resource_ownership_policy(self) -> OwnershipPolicy:
103
- return OwnershipPolicy.OWNED_ONLY
95
+ return []
104
96
 
105
97
  @staticmethod
106
98
  def _normalize_managed_group(value: Any) -> dict[str, Any]:
@@ -127,18 +119,14 @@ class IAppConfigService(ABC):
127
119
  desired_paths: list[Path] = []
128
120
  skipped: list[str] = []
129
121
  scheduled_removals: set[Path] = set()
130
- ownership = self.compiled_resource_ownership_policy()
131
122
 
132
123
  for source in sources:
133
124
  target, payload = compile_source(source)
134
125
  desired_paths.append(target)
135
126
  replaceable_symlink = find_replaceable_symlink_ancestor(target, target_dir)
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
127
+ if (
128
+ replaceable_symlink is not None
129
+ and replaceable_symlink not in scheduled_removals
142
130
  ):
143
131
  scheduled_removals.add(replaceable_symlink)
144
132
  removable_link_set.add(replaceable_symlink.resolve(strict=False))
@@ -152,21 +140,17 @@ class IAppConfigService(ABC):
152
140
  scope=scope,
153
141
  )
154
142
  )
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
- ),
143
+ action = plan_compiled_text_action(
144
+ target=target,
145
+ payload=payload,
168
146
  managed_paths=managed_path_set,
169
147
  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,
170
154
  )
171
155
  actions.append(action)
172
156
  if action.status == ActionStatus.CONFLICT: