code-agnostic 0.2.0__tar.gz → 0.2.2__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 (111) hide show
  1. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/PKG-INFO +6 -4
  2. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/README.md +5 -3
  3. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/app_id.py +11 -0
  4. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/git_exclude_service.py +11 -1
  5. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/planner.py +1 -1
  6. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic.egg-info/PKG-INFO +6 -4
  7. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/pyproject.toml +1 -1
  8. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_apply_apps.py +2 -5
  9. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_plan.py +2 -2
  10. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_workspaces.py +2 -2
  11. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_git_exclude_service.py +2 -2
  12. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_planner_rules.py +7 -5
  13. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_workspace_config_sync.py +34 -142
  14. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/LICENSE +0 -0
  15. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/__init__.py +0 -0
  16. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/__main__.py +0 -0
  17. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/agents/__init__.py +0 -0
  18. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/agents/compilers.py +0 -0
  19. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/agents/models.py +0 -0
  20. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/agents/parser.py +0 -0
  21. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/__init__.py +0 -0
  22. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/apps_service.py +0 -0
  23. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/codex/__init__.py +0 -0
  24. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/codex/config_repository.py +0 -0
  25. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/codex/mapper.py +0 -0
  26. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/codex/schema.json +0 -0
  27. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/codex/schema_repository.py +0 -0
  28. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/codex/service.py +0 -0
  29. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/common/__init__.py +0 -0
  30. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/common/framework.py +0 -0
  31. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  32. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  33. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
  34. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/common/interfaces/service.py +0 -0
  35. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/common/loader.py +0 -0
  36. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/common/models.py +0 -0
  37. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/common/schema.py +0 -0
  38. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/common/symlink_planning.py +0 -0
  39. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/common/utils.py +0 -0
  40. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/cursor/__init__.py +0 -0
  41. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/cursor/config_repository.py +0 -0
  42. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/cursor/mapper.py +0 -0
  43. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/cursor/schema.json +0 -0
  44. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  45. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/cursor/service.py +0 -0
  46. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/opencode/__init__.py +0 -0
  47. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/opencode/config_repository.py +0 -0
  48. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/opencode/mapper.py +0 -0
  49. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/opencode/schema.json +0 -0
  50. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  51. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/apps/opencode/service.py +0 -0
  52. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/constants.py +0 -0
  53. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/core/__init__.py +0 -0
  54. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/core/repository.py +0 -0
  55. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/core/workspace_repository.py +0 -0
  56. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/errors.py +0 -0
  57. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/executor.py +0 -0
  58. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/imports/__init__.py +0 -0
  59. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/imports/adapters.py +0 -0
  60. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/imports/filesystem.py +0 -0
  61. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/imports/models.py +0 -0
  62. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/imports/service.py +0 -0
  63. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/mcp_service.py +0 -0
  64. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/models.py +0 -0
  65. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/rules/__init__.py +0 -0
  66. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/rules/compilers.py +0 -0
  67. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/rules/models.py +0 -0
  68. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/rules/parser.py +0 -0
  69. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/rules/repository.py +0 -0
  70. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/skills/__init__.py +0 -0
  71. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/skills/compilers.py +0 -0
  72. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/skills/models.py +0 -0
  73. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/skills/parser.py +0 -0
  74. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/status.py +0 -0
  75. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/tui/__init__.py +0 -0
  76. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/tui/enums.py +0 -0
  77. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/tui/import_selector.py +0 -0
  78. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/tui/renderers.py +0 -0
  79. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/tui/sections.py +0 -0
  80. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/tui/tables.py +0 -0
  81. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/utils.py +0 -0
  82. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic/workspaces.py +0 -0
  83. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic.egg-info/SOURCES.txt +0 -0
  84. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic.egg-info/dependency_links.txt +0 -0
  85. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic.egg-info/entry_points.txt +0 -0
  86. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic.egg-info/requires.txt +0 -0
  87. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/code_agnostic.egg-info/top_level.txt +0 -0
  88. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/setup.cfg +0 -0
  89. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_agents.py +0 -0
  90. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_aliases.py +0 -0
  91. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_apply_codex.py +0 -0
  92. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_apply_cursor.py +0 -0
  93. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_apply_target.py +0 -0
  94. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_apps.py +0 -0
  95. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_flags.py +0 -0
  96. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_git_exclude.py +0 -0
  97. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_import.py +0 -0
  98. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_import_interactive.py +0 -0
  99. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_mcp.py +0 -0
  100. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_rules.py +0 -0
  101. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_skills.py +0 -0
  102. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_cli_status.py +0 -0
  103. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_common_mcp_to_dto.py +0 -0
  104. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_common_repository.py +0 -0
  105. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_dto_to_common_mcp.py +0 -0
  106. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_mcp_service.py +0 -0
  107. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_planner_executor.py +0 -0
  108. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_symlink_planning.py +0 -0
  109. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_sync_plan_model.py +0 -0
  110. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_utils.py +0 -0
  111. {code_agnostic-0.2.0 → code_agnostic-0.2.2}/tests/test_workspaces.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-agnostic
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
@@ -97,11 +97,11 @@ code-agnostic apply
97
97
  | Rules sync (cross-compiled) | yes | yes | yes |
98
98
  | Skills sync | yes | yes | yes |
99
99
  | Agents sync | yes | yes | -- |
100
- | Workspace propagation | yes | yes | yes |
100
+ | Workspace propagation | yes | -- | yes |
101
101
  | Import from | yes | yes | yes |
102
102
  | Interactive import (TUI) | yes | yes | yes |
103
103
 
104
- Codex does not support agents natively.
104
+ Codex does not support agents natively. Workspace propagation is intentionally disabled for Cursor 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
105
105
 
106
106
  ## Features
107
107
 
@@ -160,7 +160,9 @@ code-agnostic agents list
160
160
 
161
161
  ### Workspaces
162
162
 
163
- Register workspace directories. Repos inside them get rules, skills, and agents propagated as symlinks.
163
+ Register workspace directories. Repos inside them get rules, skills, and agents propagated as symlinks for OpenCode and Codex.
164
+
165
+ `.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).
164
166
 
165
167
  ```bash
166
168
  code-agnostic workspaces add --name myproject --path ~/code/myproject
@@ -75,11 +75,11 @@ code-agnostic apply
75
75
  | Rules sync (cross-compiled) | yes | yes | yes |
76
76
  | Skills sync | yes | yes | yes |
77
77
  | Agents sync | yes | yes | -- |
78
- | Workspace propagation | yes | yes | yes |
78
+ | Workspace propagation | yes | -- | yes |
79
79
  | Import from | yes | yes | yes |
80
80
  | Interactive import (TUI) | yes | yes | yes |
81
81
 
82
- Codex does not support agents natively.
82
+ Codex does not support agents natively. Workspace propagation is intentionally disabled for Cursor 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
83
83
 
84
84
  ## Features
85
85
 
@@ -138,7 +138,9 @@ code-agnostic agents list
138
138
 
139
139
  ### Workspaces
140
140
 
141
- Register workspace directories. Repos inside them get rules, skills, and agents propagated as symlinks.
141
+ Register workspace directories. Repos inside them get rules, skills, and agents propagated as symlinks for OpenCode and Codex.
142
+
143
+ `.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).
142
144
 
143
145
  ```bash
144
146
  code-agnostic workspaces add --name myproject --path ~/code/myproject
@@ -17,6 +17,7 @@ class AppMetadata:
17
17
  toggleable: bool
18
18
  importable: bool
19
19
  supports_import_agents: bool
20
+ supports_workspace_propagation: bool
20
21
  project_dir_name: str | None = None
21
22
 
22
23
 
@@ -28,6 +29,7 @@ APP_CATALOG: dict[AppId, AppMetadata] = {
28
29
  toggleable=False,
29
30
  importable=False,
30
31
  supports_import_agents=True,
32
+ supports_workspace_propagation=False,
31
33
  project_dir_name=None,
32
34
  ),
33
35
  AppId.OPENCODE: AppMetadata(
@@ -37,6 +39,7 @@ APP_CATALOG: dict[AppId, AppMetadata] = {
37
39
  toggleable=True,
38
40
  importable=True,
39
41
  supports_import_agents=True,
42
+ supports_workspace_propagation=True,
40
43
  project_dir_name=".opencode",
41
44
  ),
42
45
  AppId.CURSOR: AppMetadata(
@@ -46,6 +49,7 @@ APP_CATALOG: dict[AppId, AppMetadata] = {
46
49
  toggleable=True,
47
50
  importable=True,
48
51
  supports_import_agents=True,
52
+ supports_workspace_propagation=False,
49
53
  project_dir_name=".cursor",
50
54
  ),
51
55
  AppId.CODEX: AppMetadata(
@@ -55,6 +59,7 @@ APP_CATALOG: dict[AppId, AppMetadata] = {
55
59
  toggleable=True,
56
60
  importable=True,
57
61
  supports_import_agents=False,
62
+ supports_workspace_propagation=True,
58
63
  project_dir_name=".codex",
59
64
  ),
60
65
  }
@@ -74,6 +79,7 @@ def app_ids_by_capability(
74
79
  targetable: bool | None = None,
75
80
  toggleable: bool | None = None,
76
81
  importable: bool | None = None,
82
+ workspace_propagation: bool | None = None,
77
83
  ) -> list[AppId]:
78
84
  ids: list[AppId] = []
79
85
  for app_id, metadata in APP_CATALOG.items():
@@ -83,5 +89,10 @@ def app_ids_by_capability(
83
89
  continue
84
90
  if importable is not None and metadata.importable != importable:
85
91
  continue
92
+ if (
93
+ workspace_propagation is not None
94
+ and metadata.supports_workspace_propagation != workspace_propagation
95
+ ):
96
+ continue
86
97
  ids.append(app_id)
87
98
  return sorted(ids, key=lambda item: item.value)
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from pathlib import Path
6
6
  from typing import Any
7
7
 
8
+ from code_agnostic.apps.app_id import app_metadata
8
9
  from code_agnostic.constants import AGENTS_FILENAME, CLAUDE_FILENAME
9
10
  from code_agnostic.core.repository import CoreRepository
10
11
  from code_agnostic.utils import read_json_safe, write_json
@@ -46,7 +47,16 @@ class GitExcludeService:
46
47
  if not config.get("include_defaults", True):
47
48
  return extras
48
49
 
49
- defaults = [f".{app_name}" for app_name in enabled_apps] + [
50
+ workspace_apps: list[str] = []
51
+ for app_name in enabled_apps:
52
+ try:
53
+ metadata = app_metadata(app_name)
54
+ except ValueError:
55
+ continue
56
+ if metadata.supports_workspace_propagation:
57
+ workspace_apps.append(app_name)
58
+
59
+ defaults = [f".{app_name}" for app_name in workspace_apps] + [
50
60
  AGENTS_FILENAME,
51
61
  CLAUDE_FILENAME,
52
62
  ]
@@ -147,7 +147,7 @@ class SyncPlanner:
147
147
 
148
148
  for svc in self.app_services:
149
149
  meta = app_metadata(svc.app_id)
150
- if meta.project_dir_name is None:
150
+ if meta.project_dir_name is None or not meta.supports_workspace_propagation:
151
151
  continue
152
152
 
153
153
  ws_project_root = ws_source.root / meta.project_dir_name
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-agnostic
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
@@ -97,11 +97,11 @@ code-agnostic apply
97
97
  | Rules sync (cross-compiled) | yes | yes | yes |
98
98
  | Skills sync | yes | yes | yes |
99
99
  | Agents sync | yes | yes | -- |
100
- | Workspace propagation | yes | yes | yes |
100
+ | Workspace propagation | yes | -- | yes |
101
101
  | Import from | yes | yes | yes |
102
102
  | Interactive import (TUI) | yes | yes | yes |
103
103
 
104
- Codex does not support agents natively.
104
+ Codex does not support agents natively. Workspace propagation is intentionally disabled for Cursor 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
105
105
 
106
106
  ## Features
107
107
 
@@ -160,7 +160,9 @@ code-agnostic agents list
160
160
 
161
161
  ### Workspaces
162
162
 
163
- Register workspace directories. Repos inside them get rules, skills, and agents propagated as symlinks.
163
+ Register workspace directories. Repos inside them get rules, skills, and agents propagated as symlinks for OpenCode and Codex.
164
+
165
+ `.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).
164
166
 
165
167
  ```bash
166
168
  code-agnostic workspaces add --name myproject --path ~/code/myproject
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code-agnostic"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Centralized hub for LLM coding config: MCP, skills, rules, and agents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -45,7 +45,7 @@ def test_apply_all_with_cursor_and_codex_writes_both(
45
45
  assert (tmp_path / ".codex" / "config.toml").exists()
46
46
 
47
47
 
48
- def test_apply_cursor_target_also_applies_workspace_links(
48
+ def test_apply_cursor_target_does_not_apply_workspace_links(
49
49
  minimal_shared_config: Path,
50
50
  tmp_path: Path,
51
51
  core_root: Path,
@@ -79,11 +79,8 @@ def test_apply_cursor_target_also_applies_workspace_links(
79
79
  apply_result = cli_runner.invoke(cli, ["apply", "-a", "cursor"])
80
80
  assert apply_result.exit_code == 0
81
81
 
82
- # Cursor compiles rules to .mdc files in .cursor/rules/, then symlinks the dir
83
- compiled_rules_dir = ws_config_dir / ".cursor" / "rules"
84
82
  repo_rules_link = workspace_root / "service-a" / ".cursor" / "rules"
85
- assert repo_rules_link.is_symlink()
86
- assert repo_rules_link.resolve() == compiled_rules_dir.resolve()
83
+ assert not repo_rules_link.exists()
87
84
 
88
85
 
89
86
  def test_apply_cursor_aborts_on_invalid_cursor_json(
@@ -15,7 +15,7 @@ def test_plan_shows_invalid_json_error_for_mcp_base(
15
15
  assert "Invalid JSON format" in result.output
16
16
 
17
17
 
18
- def test_plan_target_cursor_includes_workspace_actions(
18
+ def test_plan_target_cursor_excludes_workspace_actions(
19
19
  minimal_shared_config: Path,
20
20
  tmp_path: Path,
21
21
  core_root: Path,
@@ -40,7 +40,7 @@ def test_plan_target_cursor_includes_workspace_actions(
40
40
  plan_result = cli_runner.invoke(cli, ["plan", "-a", "cursor"])
41
41
  assert plan_result.exit_code == 0
42
42
  assert "cursor" in plan_result.output
43
- assert "workspace config sync" in plan_result.output
43
+ assert "workspace config sync" not in plan_result.output
44
44
 
45
45
 
46
46
  def test_plan_with_no_apps_enabled(minimal_shared_config: Path, cli_runner) -> None:
@@ -151,7 +151,7 @@ def test_workspaces_git_exclude_writes_enabled_apps_and_default_rules(
151
151
  assert result.exit_code == 0
152
152
  assert "Updated git excludes" in result.output
153
153
 
154
- expected_entries = [".cursor", "AGENTS.md", "CLAUDE.md"]
154
+ expected_entries = ["AGENTS.md", "CLAUDE.md"]
155
155
  unexpected_entries = [".opencode", ".codex"]
156
156
 
157
157
  for repo_name in ["repo-a", "repo-b"]:
@@ -194,5 +194,5 @@ def test_workspaces_git_exclude_can_target_single_workspace(
194
194
  exclude_a = ws_a / "repo-a" / ".git" / "info" / "exclude"
195
195
  exclude_b = ws_b / "repo-b" / ".git" / "info" / "exclude"
196
196
 
197
- assert ".cursor" in exclude_a.read_text(encoding="utf-8")
197
+ assert ".cursor" not in exclude_a.read_text(encoding="utf-8")
198
198
  assert not exclude_b.exists()
@@ -21,7 +21,7 @@ def service_with_workspace(minimal_shared_config: Path, tmp_path: Path):
21
21
 
22
22
  def test_defaults_only(service_with_workspace) -> None:
23
23
  entries = service_with_workspace.compute_entries("myws", ["cursor", "codex"])
24
- assert ".cursor" in entries
24
+ assert ".cursor" not in entries
25
25
  assert ".codex" in entries
26
26
  assert AGENTS_FILENAME in entries
27
27
  assert CLAUDE_FILENAME in entries
@@ -30,7 +30,7 @@ def test_defaults_only(service_with_workspace) -> None:
30
30
  def test_custom_patterns_merged(service_with_workspace) -> None:
31
31
  service_with_workspace.add_pattern("myws", "*.generated")
32
32
  entries = service_with_workspace.compute_entries("myws", ["cursor"])
33
- assert ".cursor" in entries
33
+ assert ".cursor" not in entries
34
34
  assert AGENTS_FILENAME in entries
35
35
  assert "*.generated" in entries
36
36
 
@@ -63,7 +63,9 @@ def test_plan_workspace_no_rules(setup_workspace, enable_app) -> None:
63
63
  assert len(rule_actions) == 0
64
64
 
65
65
 
66
- def test_plan_rules_compiled_per_app(setup_workspace, enable_app) -> None:
66
+ def test_plan_rules_compiled_only_for_workspace_propagation_apps(
67
+ setup_workspace, enable_app
68
+ ) -> None:
67
69
  enable_app("opencode")
68
70
  enable_app("cursor")
69
71
  ws = setup_workspace
@@ -79,14 +81,14 @@ def test_plan_rules_compiled_per_app(setup_workspace, enable_app) -> None:
79
81
  plan = apps.plan_for_target("all")
80
82
 
81
83
  rule_actions = [a for a in plan.actions if a.kind == ActionKind.WRITE_RULE]
82
- # Should have rules compiled for both opencode and cursor
83
- assert len(rule_actions) >= 2
84
+ # Cursor workspace propagation is intentionally disabled.
85
+ assert len(rule_actions) == 1
84
86
 
85
87
  # OpenCode should get AGENTS.md
86
88
  opencode_rules = [a for a in rule_actions if "opencode" in a.detail]
87
89
  assert len(opencode_rules) >= 1
88
90
  assert any("AGENTS.md" in a.detail for a in opencode_rules)
89
91
 
90
- # Cursor should get .mdc file
92
+ # Cursor should not compile workspace rules.
91
93
  cursor_rules = [a for a in rule_actions if "cursor" in a.detail]
92
- assert len(cursor_rules) >= 1
94
+ assert cursor_rules == []
@@ -1,6 +1,5 @@
1
1
  """Tests for workspace-level config sync (MCP, skills, agents, rules)."""
2
2
 
3
- import json
4
3
  from pathlib import Path
5
4
 
6
5
  from code_agnostic.apps.codex.config_repository import CodexConfigRepository
@@ -158,7 +157,7 @@ def test_workspace_rules_symlinks_planned_for_each_repo(
158
157
  # --- Workspace MCP config sync ---
159
158
 
160
159
 
161
- def test_workspace_mcp_sync_to_cursor_project_dirs(
160
+ def test_workspace_mcp_sync_skips_cursor_workspace_propagation(
162
161
  minimal_shared_config: Path,
163
162
  core_root: Path,
164
163
  tmp_path: Path,
@@ -180,28 +179,9 @@ def test_workspace_mcp_sync_to_cursor_project_dirs(
180
179
  cursor_root = tmp_path / ".cursor"
181
180
  plan = SyncPlanner(core=core, app_services=[_cursor_service(cursor_root)]).build()
182
181
 
183
- mcp_actions = [
184
- a
185
- for a in plan.actions
186
- if a.app == "workspace"
187
- and a.kind in (ActionKind.WRITE_JSON, ActionKind.WRITE_TEXT)
188
- ]
189
- assert len(mcp_actions) == 1
190
- assert mcp_actions[0].workspace == "myws"
191
-
192
- # The MCP config should be rendered once into workspace config dir
193
- expected_path = ws_config / ".cursor" / "mcp.json"
194
- assert mcp_actions[0].path == expected_path
195
-
196
- # And then symlinked into each repo
197
- link_actions = [
198
- a
199
- for a in plan.actions
200
- if a.kind == ActionKind.SYMLINK and a.scope == "ws:cursor:repo_mcp"
201
- ]
202
- assert len(link_actions) == 1
203
- assert link_actions[0].path == workspace_root / "repo-a" / ".cursor" / "mcp.json"
204
- assert link_actions[0].source == expected_path
182
+ workspace_actions = [a for a in plan.actions if a.app == "workspace"]
183
+ assert workspace_actions == []
184
+ assert not (ws_config / ".cursor").exists()
205
185
 
206
186
 
207
187
  def test_workspace_mcp_sync_to_codex_project_dirs(
@@ -248,7 +228,7 @@ def test_workspace_mcp_sync_to_codex_project_dirs(
248
228
  # --- Workspace skill symlinks ---
249
229
 
250
230
 
251
- def test_workspace_skills_symlinked_to_repo_project_dirs(
231
+ def test_workspace_skills_skip_cursor_workspace_propagation(
252
232
  minimal_shared_config: Path,
253
233
  core_root: Path,
254
234
  tmp_path: Path,
@@ -267,33 +247,19 @@ def test_workspace_skills_symlinked_to_repo_project_dirs(
267
247
  cursor_root = tmp_path / ".cursor"
268
248
  plan = SyncPlanner(core=core, app_services=[_cursor_service(cursor_root)]).build()
269
249
 
270
- # Workspace skill entry symlinked into workspace project dir
271
- ws_entry_actions = [
272
- a
273
- for a in plan.actions
274
- if a.kind == ActionKind.SYMLINK and a.scope == "ws:cursor:skills_entries"
275
- ]
276
- assert len(ws_entry_actions) == 1
277
- assert ws_entry_actions[0].workspace == "myws"
278
- assert ws_entry_actions[0].path == ws_config / ".cursor" / "skills" / "my-skill"
279
- assert ws_entry_actions[0].source == ws_config / "skills" / "my-skill"
280
-
281
- # Repo links its skills dir to workspace skills dir
282
- repo_dir_actions = [
250
+ cursor_workspace_actions = [
283
251
  a
284
252
  for a in plan.actions
285
- if a.kind == ActionKind.SYMLINK and a.scope == "ws:cursor:repo_skills_dir"
253
+ if a.scope is not None and a.scope.startswith("ws:cursor:")
286
254
  ]
287
- assert len(repo_dir_actions) == 1
288
- assert repo_dir_actions[0].workspace == "myws"
289
- assert repo_dir_actions[0].path == workspace_root / "repo-a" / ".cursor" / "skills"
290
- assert repo_dir_actions[0].source == ws_config / ".cursor" / "skills"
255
+ assert cursor_workspace_actions == []
256
+ assert not (workspace_root / "repo-a" / ".cursor").exists()
291
257
 
292
258
 
293
259
  # --- Workspace agent symlinks ---
294
260
 
295
261
 
296
- def test_workspace_agents_symlinked_to_repo_project_dirs(
262
+ def test_workspace_agents_skip_cursor_workspace_propagation(
297
263
  minimal_shared_config: Path,
298
264
  core_root: Path,
299
265
  tmp_path: Path,
@@ -312,25 +278,13 @@ def test_workspace_agents_symlinked_to_repo_project_dirs(
312
278
  cursor_root = tmp_path / ".cursor"
313
279
  plan = SyncPlanner(core=core, app_services=[_cursor_service(cursor_root)]).build()
314
280
 
315
- ws_entry_actions = [
316
- a
317
- for a in plan.actions
318
- if a.kind == ActionKind.SYMLINK and a.scope == "ws:cursor:agents_entries"
319
- ]
320
- assert len(ws_entry_actions) == 1
321
- assert ws_entry_actions[0].workspace == "myws"
322
- assert ws_entry_actions[0].path == ws_config / ".cursor" / "agents" / "planner.md"
323
- assert ws_entry_actions[0].source == ws_config / "agents" / "planner.md"
324
-
325
- repo_dir_actions = [
281
+ cursor_workspace_actions = [
326
282
  a
327
283
  for a in plan.actions
328
- if a.kind == ActionKind.SYMLINK and a.scope == "ws:cursor:repo_agents_dir"
284
+ if a.scope is not None and a.scope.startswith("ws:cursor:")
329
285
  ]
330
- assert len(repo_dir_actions) == 1
331
- assert repo_dir_actions[0].workspace == "myws"
332
- assert repo_dir_actions[0].path == workspace_root / "repo-a" / ".cursor" / "agents"
333
- assert repo_dir_actions[0].source == ws_config / ".cursor" / "agents"
286
+ assert cursor_workspace_actions == []
287
+ assert not (workspace_root / "repo-a" / ".cursor").exists()
334
288
 
335
289
 
336
290
  def test_workspace_agents_not_synced_to_codex(
@@ -379,23 +333,22 @@ def test_executor_persists_workspace_state_separately(
379
333
  (ws_config / "rules").mkdir(parents=True, exist_ok=True)
380
334
  (ws_config / "rules" / "shared.md").write_text("rules", encoding="utf-8")
381
335
 
382
- cursor_root = tmp_path / ".cursor"
383
- plan = SyncPlanner(core=core, app_services=[_cursor_service(cursor_root)]).build()
336
+ codex_root = tmp_path / ".codex"
337
+ plan = SyncPlanner(core=core, app_services=[_codex_service(codex_root)]).build()
384
338
 
385
339
  applied, failed, failures = SyncExecutor(core=core).execute(plan)
386
340
  assert failed == 0
387
341
 
388
- # Workspace state persisted to workspace state file
389
- # Cursor compiles rules to .mdc in .cursor/rules/ and symlinks the dir
342
+ # Workspace state persisted to workspace state file.
390
343
  ws_repo = WorkspaceConfigRepository(root=ws_config)
391
344
  ws_state = ws_repo.load_state()
392
345
  managed = ws_state["managed_links"]
393
- assert "ws:cursor:repo_rules_dir" in managed
394
- assert len(managed["ws:cursor:repo_rules_dir"]) == 1
346
+ assert "rules" in managed
347
+ assert len(managed["rules"]) == 1
395
348
 
396
349
  # Global state should not contain workspace links
397
350
  global_state = core.load_state()
398
- assert "ws:cursor:repo_rules_dir" not in global_state.get("managed_links", {})
351
+ assert "rules" not in global_state.get("managed_links", {})
399
352
 
400
353
 
401
354
  # --- Full roundtrip with apply ---
@@ -428,12 +381,10 @@ def test_full_workspace_config_roundtrip(
428
381
  (ws_config / "agents").mkdir(parents=True)
429
382
  (ws_config / "agents" / "ws-agent.md").write_text("a", encoding="utf-8")
430
383
 
431
- cursor_root = tmp_path / ".cursor"
432
384
  plan = SyncPlanner(
433
385
  core=core,
434
386
  app_services=[
435
387
  _opencode_service(core, opencode_root),
436
- _cursor_service(cursor_root),
437
388
  ],
438
389
  ).build()
439
390
 
@@ -451,70 +402,21 @@ def test_full_workspace_config_roundtrip(
451
402
  assert link.is_symlink()
452
403
  assert link.resolve() == compiled_agents.resolve()
453
404
 
454
- # Check Cursor compiled rules dir (.cursor/rules/ with .mdc files)
455
- compiled_cursor_rules = ws_config / ".cursor" / "rules"
456
- ws_root_cursor_rules = workspace_root / ".cursor" / "rules"
457
- assert ws_root_cursor_rules.is_symlink()
458
- assert ws_root_cursor_rules.resolve() == compiled_cursor_rules.resolve()
459
-
460
- for repo_name in ["repo-a", "repo-b"]:
461
- repo_rules = workspace_root / repo_name / ".cursor" / "rules"
462
- assert repo_rules.is_symlink()
463
- assert repo_rules.resolve() == compiled_cursor_rules.resolve()
464
-
465
- # Check MCP config rendered into workspace, then linked into repos
466
- ws_cursor_mcp = ws_config / ".cursor" / "mcp.json"
467
- assert ws_cursor_mcp.exists()
468
- payload = json.loads(ws_cursor_mcp.read_text(encoding="utf-8"))
469
- assert "ws-server" in payload.get("mcpServers", {})
470
-
471
- # Workspace root also gets the shared links (for multi-root workspace sessions)
472
- ws_root_cursor_mcp = workspace_root / ".cursor" / "mcp.json"
473
- assert ws_root_cursor_mcp.is_symlink()
474
- assert ws_root_cursor_mcp.resolve() == ws_cursor_mcp.resolve()
475
-
405
+ # Cursor workspace propagation is intentionally disabled.
406
+ assert not (ws_config / ".cursor").exists()
407
+ assert not (workspace_root / ".cursor").exists()
476
408
  for repo_name in ["repo-a", "repo-b"]:
477
- cursor_mcp = workspace_root / repo_name / ".cursor" / "mcp.json"
478
- assert cursor_mcp.is_symlink()
479
- assert cursor_mcp.resolve() == ws_cursor_mcp.resolve()
480
-
481
- # Check skill links in workspace project dir and repo linkage
482
- ws_skill_link = ws_config / ".cursor" / "skills" / "ws-skill"
483
- assert ws_skill_link.is_symlink()
484
-
485
- ws_root_skill_dir = workspace_root / ".cursor" / "skills"
486
- assert ws_root_skill_dir.is_symlink()
487
-
488
- for repo_name in ["repo-a", "repo-b"]:
489
- skill_dir = workspace_root / repo_name / ".cursor" / "skills"
490
- assert skill_dir.is_symlink()
491
- skill_link = skill_dir / "ws-skill"
492
- assert skill_link.is_symlink()
493
-
494
- # Check agent links in workspace project dir and repo linkage
495
- ws_agent_link = ws_config / ".cursor" / "agents" / "ws-agent.md"
496
- assert ws_agent_link.is_symlink()
497
-
498
- ws_root_agent_dir = workspace_root / ".cursor" / "agents"
499
- assert ws_root_agent_dir.is_symlink()
500
-
501
- for repo_name in ["repo-a", "repo-b"]:
502
- agent_dir = workspace_root / repo_name / ".cursor" / "agents"
503
- assert agent_dir.is_symlink()
504
- agent_link = agent_dir / "ws-agent.md"
505
- assert agent_link.is_symlink()
409
+ assert not (workspace_root / repo_name / ".cursor").exists()
506
410
 
507
411
 
508
412
  # --- Stale workspace link cleanup ---
509
413
 
510
414
 
511
- def test_workspace_stale_rules_cleanup_on_config_removal(
415
+ def test_workspace_rules_not_linked_for_cursor(
512
416
  minimal_shared_config: Path,
513
417
  core_root: Path,
514
418
  tmp_path: Path,
515
419
  ) -> None:
516
- import shutil
517
-
518
420
  workspace_root = tmp_path / "workspace"
519
421
  workspace_root.mkdir()
520
422
  (workspace_root / "repo-a" / ".git").mkdir(parents=True)
@@ -530,20 +432,10 @@ def test_workspace_stale_rules_cleanup_on_config_removal(
530
432
  plan = SyncPlanner(core=core, app_services=[_cursor_service(cursor_root)]).build()
531
433
 
532
434
  SyncExecutor(core=core).execute(plan)
533
- # Cursor compiles rules to .cursor/rules/ dir and symlinks it into repos
534
- link = workspace_root / "repo-a" / ".cursor" / "rules"
535
- assert link.is_symlink()
536
-
537
- # Remove rules directory
538
- shutil.rmtree(ws_config / "rules")
539
-
540
- plan2 = SyncPlanner(core=core, app_services=[_cursor_service(cursor_root)]).build()
541
-
542
- SyncExecutor(core=core).execute(plan2)
543
- assert not link.is_symlink()
435
+ assert not (workspace_root / "repo-a" / ".cursor" / "rules").exists()
544
436
 
545
437
 
546
- def test_workspace_stale_skills_cleanup_when_skills_removed(
438
+ def test_workspace_stale_skills_cleanup_when_skills_removed_for_codex(
547
439
  minimal_shared_config: Path,
548
440
  core_root: Path,
549
441
  tmp_path: Path,
@@ -560,32 +452,32 @@ def test_workspace_stale_skills_cleanup_when_skills_removed(
560
452
  (ws_config / "skills" / "my-skill").mkdir(parents=True)
561
453
  (ws_config / "skills" / "my-skill" / "SKILL.md").write_text("s", encoding="utf-8")
562
454
 
563
- cursor_root = tmp_path / ".cursor"
564
- plan = SyncPlanner(core=core, app_services=[_cursor_service(cursor_root)]).build()
455
+ codex_root = tmp_path / ".codex"
456
+ plan = SyncPlanner(core=core, app_services=[_codex_service(codex_root)]).build()
565
457
 
566
458
  SyncExecutor(core=core).execute(plan)
567
- skill_dir_link = workspace_root / "repo-a" / ".cursor" / "skills"
459
+ skill_dir_link = workspace_root / "repo-a" / ".codex" / "skills"
568
460
  assert skill_dir_link.is_symlink()
569
461
 
570
462
  # Verify state was persisted with workspace scopes
571
463
  ws_repo = WorkspaceConfigRepository(root=ws_config)
572
464
  state = ws_repo.load_state()
573
- assert "ws:cursor:skills_entries" in state["managed_links"]
574
- assert "ws:cursor:repo_skills_dir" in state["managed_links"]
465
+ assert "ws:codex:skills_entries" in state["managed_links"]
466
+ assert "ws:codex:repo_skills_dir" in state["managed_links"]
575
467
 
576
468
  # Remove all skills from workspace config
577
469
  import shutil
578
470
 
579
471
  shutil.rmtree(ws_config / "skills")
580
472
 
581
- plan2 = SyncPlanner(core=core, app_services=[_cursor_service(cursor_root)]).build()
473
+ plan2 = SyncPlanner(core=core, app_services=[_codex_service(codex_root)]).build()
582
474
 
583
475
  # Should have remove actions for stale workspace entry and repo dir link
584
476
  remove_actions = [
585
477
  a
586
478
  for a in plan2.actions
587
479
  if a.kind == ActionKind.REMOVE_SYMLINK
588
- and a.scope in ("ws:cursor:skills_entries", "ws:cursor:repo_skills_dir")
480
+ and a.scope in ("ws:codex:skills_entries", "ws:codex:repo_skills_dir")
589
481
  ]
590
482
  assert len(remove_actions) == 2
591
483
 
File without changes
File without changes