code-agnostic 0.3.15__tar.gz → 0.3.16__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 (165) hide show
  1. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/PKG-INFO +10 -7
  2. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/README.md +9 -6
  3. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/__init__.py +1 -1
  4. code_agnostic-0.3.16/code_agnostic/agents/compilers.py +87 -0
  5. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/codex/mapper.py +28 -2
  6. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/codex/schema.json +72 -0
  7. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/commands/agents.py +6 -1
  8. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/commands/apply.py +9 -6
  9. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/commands/rules.py +6 -1
  10. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/commands/skills.py +63 -13
  11. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/commands/status.py +1 -1
  12. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/helpers.py +7 -0
  13. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/core/repository.py +24 -8
  14. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/lossiness.py +12 -2
  15. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/status.py +2 -1
  16. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/tui/renderers.py +57 -13
  17. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/tui/tables.py +3 -1
  18. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic.egg-info/PKG-INFO +10 -7
  19. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/pyproject.toml +1 -1
  20. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_agents.py +20 -0
  21. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_apps.py +4 -3
  22. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_explain_lossiness.py +33 -0
  23. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_import.py +24 -0
  24. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_rules.py +16 -0
  25. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_skills.py +123 -0
  26. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_status.py +26 -0
  27. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_workspaces.py +41 -0
  28. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_common_repository.py +3 -4
  29. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_project_config_sync.py +31 -0
  30. code_agnostic-0.3.15/code_agnostic/agents/compilers.py +0 -45
  31. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/LICENSE +0 -0
  32. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/__main__.py +0 -0
  33. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/agents/__init__.py +0 -0
  34. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/agents/claude.py +0 -0
  35. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/agents/codex.py +0 -0
  36. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/agents/models.py +0 -0
  37. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/agents/opencode.py +0 -0
  38. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/agents/parser.py +0 -0
  39. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/__init__.py +0 -0
  40. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/app_id.py +0 -0
  41. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/apps_service.py +0 -0
  42. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/claude/__init__.py +0 -0
  43. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/claude/config_repository.py +0 -0
  44. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/claude/mapper.py +0 -0
  45. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/claude/service.py +0 -0
  46. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/codex/__init__.py +0 -0
  47. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/codex/config_repository.py +0 -0
  48. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/codex/schema_repository.py +0 -0
  49. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/codex/service.py +0 -0
  50. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/common/__init__.py +0 -0
  51. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/common/compiled_planning.py +0 -0
  52. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/common/framework.py +0 -0
  53. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  54. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  55. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
  56. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/common/interfaces/service.py +0 -0
  57. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/common/loader.py +0 -0
  58. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/common/models.py +0 -0
  59. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/common/schema.py +0 -0
  60. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/common/symlink_planning.py +0 -0
  61. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/common/utils.py +0 -0
  62. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/cursor/__init__.py +0 -0
  63. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/cursor/config_repository.py +0 -0
  64. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/cursor/mapper.py +0 -0
  65. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/cursor/schema.json +0 -0
  66. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  67. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/cursor/service.py +0 -0
  68. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/opencode/__init__.py +0 -0
  69. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/opencode/config_repository.py +0 -0
  70. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/opencode/mapper.py +0 -0
  71. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/opencode/schema.json +0 -0
  72. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  73. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/apps/opencode/service.py +0 -0
  74. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/__init__.py +0 -0
  75. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/aliases.py +0 -0
  76. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/commands/__init__.py +0 -0
  77. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/commands/apps.py +0 -0
  78. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
  79. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/commands/import_.py +0 -0
  80. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/commands/mcp.py +0 -0
  81. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/commands/plan.py +0 -0
  82. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/commands/projects.py +0 -0
  83. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/commands/restore.py +0 -0
  84. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/commands/validate.py +0 -0
  85. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/commands/workspaces.py +0 -0
  86. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/cli/options.py +0 -0
  87. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/constants.py +0 -0
  88. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/core/__init__.py +0 -0
  89. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/core/project_repository.py +0 -0
  90. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/core/workspace_repository.py +0 -0
  91. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/errors.py +0 -0
  92. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/executor.py +0 -0
  93. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/generated_artifacts.py +0 -0
  94. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/git_exclude_service.py +0 -0
  95. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/imports/__init__.py +0 -0
  96. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/imports/adapters.py +0 -0
  97. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/imports/filesystem.py +0 -0
  98. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/imports/models.py +0 -0
  99. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/imports/service.py +0 -0
  100. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/mcp_service.py +0 -0
  101. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/models.py +0 -0
  102. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/planner.py +0 -0
  103. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/project_artifacts.py +0 -0
  104. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/rules/__init__.py +0 -0
  105. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/rules/compilers.py +0 -0
  106. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/rules/models.py +0 -0
  107. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/rules/parser.py +0 -0
  108. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/rules/repository.py +0 -0
  109. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/skills/__init__.py +0 -0
  110. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/skills/compilers.py +0 -0
  111. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/skills/install_sources.py +0 -0
  112. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/skills/models.py +0 -0
  113. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/skills/parser.py +0 -0
  114. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/spec/__init__.py +0 -0
  115. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/spec/loaders.py +0 -0
  116. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  117. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
  118. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
  119. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  120. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  121. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/tui/__init__.py +0 -0
  122. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/tui/enums.py +0 -0
  123. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/tui/import_selector.py +0 -0
  124. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/tui/sections.py +0 -0
  125. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/utils.py +0 -0
  126. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/validation.py +0 -0
  127. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/workspace_artifacts.py +0 -0
  128. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic/workspaces.py +0 -0
  129. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic.egg-info/SOURCES.txt +0 -0
  130. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic.egg-info/dependency_links.txt +0 -0
  131. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic.egg-info/entry_points.txt +0 -0
  132. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic.egg-info/requires.txt +0 -0
  133. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/code_agnostic.egg-info/top_level.txt +0 -0
  134. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/setup.cfg +0 -0
  135. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_aliases.py +0 -0
  136. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_apply_apps.py +0 -0
  137. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_apply_codex.py +0 -0
  138. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_apply_cursor.py +0 -0
  139. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_apply_target.py +0 -0
  140. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_flags.py +0 -0
  141. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_git_exclude.py +0 -0
  142. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_import_interactive.py +0 -0
  143. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_mcp.py +0 -0
  144. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_module_organization.py +0 -0
  145. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_plan.py +0 -0
  146. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_projects.py +0 -0
  147. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_restore.py +0 -0
  148. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_validate.py +0 -0
  149. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_cli_workspace_resolution.py +0 -0
  150. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_common_mcp_to_dto.py +0 -0
  151. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_compiled_planning.py +0 -0
  152. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_dto_to_common_mcp.py +0 -0
  153. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_git_exclude_service.py +0 -0
  154. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_mcp_service.py +0 -0
  155. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_planner_executor.py +0 -0
  156. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_planner_rules.py +0 -0
  157. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_skill_install_sources.py +0 -0
  158. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_symlink_planning.py +0 -0
  159. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_sync_plan_model.py +0 -0
  160. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_transactional_executor.py +0 -0
  161. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_utils.py +0 -0
  162. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_version.py +0 -0
  163. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_workspace_config_sync.py +0 -0
  164. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/tests/test_workspace_repo_status.py +0 -0
  165. {code_agnostic-0.3.15 → code_agnostic-0.3.16}/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.15
3
+ Version: 0.3.16
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
@@ -264,12 +264,14 @@ Global skills live under `~/.config/code-agnostic/skills`. Workspace-local
264
264
  skills live under `~/.config/code-agnostic/workspaces/<name>/skills` and can be
265
265
  inspected with `code-agnostic skills list -w <name>`. Project-local skills live
266
266
  under `~/.config/code-agnostic/projects/<name>/skills` and are generated into
267
- the registered project directory by `plan` / `apply`. Codex generated skill
268
- outputs are written to `~/.agents/skills`, while Codex agents and config remain
269
- under `CODEX_HOME` when set, defaulting to `~/.codex`. Claude Code generated
270
- skills and agents are written under `~/.claude/skills` and `~/.claude/agents`,
271
- with workspace/project copies under repo-local `.claude/skills` and
272
- `.claude/agents`.
267
+ the registered project directory by `plan` / `apply`. The current first-class
268
+ list/remove commands cover global and workspace scopes; project skill inventory
269
+ and removal are not supported yet, and `code-agnostic projects list` only shows
270
+ a coarse `[skills]` marker today. Codex generated skill outputs are written to
271
+ `~/.agents/skills`, while Codex agents and config remain under `CODEX_HOME` when
272
+ set, defaulting to `~/.codex`. Claude Code generated skills and agents are
273
+ written under `~/.claude/skills` and `~/.claude/agents`, with workspace/project
274
+ copies under repo-local `.claude/skills` and `.claude/agents`.
273
275
 
274
276
  If a target app discovers user-created repo-local skill folders such as `.agents/skills`, `.opencode/skills`, or `.claude/skills`, treat those as unmanaged app inputs unless they were generated from `code-agnostic` project source. Workspace and project sync write only the exact generated paths recorded in their `.sync-state.json` files.
275
277
 
@@ -355,6 +357,7 @@ The compiler migration is documented in:
355
357
  - [x] Interactive TUI for import selection
356
358
  - [x] Claude Code support
357
359
  - [x] Project-scoped skill installs and sync
360
+ - [ ] Project-scoped skill inventory and remove commands
358
361
  - [ ] `rules add` / `skills add` / `agents add` commands (open `$EDITOR` with template)
359
362
  - [ ] Shell auto-complete
360
363
  - [ ] Full TUI mode (command palette + menus)
@@ -239,12 +239,14 @@ Global skills live under `~/.config/code-agnostic/skills`. Workspace-local
239
239
  skills live under `~/.config/code-agnostic/workspaces/<name>/skills` and can be
240
240
  inspected with `code-agnostic skills list -w <name>`. Project-local skills live
241
241
  under `~/.config/code-agnostic/projects/<name>/skills` and are generated into
242
- the registered project directory by `plan` / `apply`. Codex generated skill
243
- outputs are written to `~/.agents/skills`, while Codex agents and config remain
244
- under `CODEX_HOME` when set, defaulting to `~/.codex`. Claude Code generated
245
- skills and agents are written under `~/.claude/skills` and `~/.claude/agents`,
246
- with workspace/project copies under repo-local `.claude/skills` and
247
- `.claude/agents`.
242
+ the registered project directory by `plan` / `apply`. The current first-class
243
+ list/remove commands cover global and workspace scopes; project skill inventory
244
+ and removal are not supported yet, and `code-agnostic projects list` only shows
245
+ a coarse `[skills]` marker today. Codex generated skill outputs are written to
246
+ `~/.agents/skills`, while Codex agents and config remain under `CODEX_HOME` when
247
+ set, defaulting to `~/.codex`. Claude Code generated skills and agents are
248
+ written under `~/.claude/skills` and `~/.claude/agents`, with workspace/project
249
+ copies under repo-local `.claude/skills` and `.claude/agents`.
248
250
 
249
251
  If a target app discovers user-created repo-local skill folders such as `.agents/skills`, `.opencode/skills`, or `.claude/skills`, treat those as unmanaged app inputs unless they were generated from `code-agnostic` project source. Workspace and project sync write only the exact generated paths recorded in their `.sync-state.json` files.
250
252
 
@@ -330,6 +332,7 @@ The compiler migration is documented in:
330
332
  - [x] Interactive TUI for import selection
331
333
  - [x] Claude Code support
332
334
  - [x] Project-scoped skill installs and sync
335
+ - [ ] Project-scoped skill inventory and remove commands
333
336
  - [ ] `rules add` / `skills add` / `agents add` commands (open `$EDITOR` with template)
334
337
  - [ ] Shell auto-complete
335
338
  - [ ] Full TUI mode (command palette + menus)
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.3.15"
3
+ __version__ = "0.3.16"
@@ -0,0 +1,87 @@
1
+ """Per-editor agent compilers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any
7
+
8
+ import yaml
9
+
10
+ from code_agnostic.agents.claude import serialize_claude_agent
11
+ from code_agnostic.agents.codex import serialize_codex_agent
12
+ from code_agnostic.agents.models import Agent
13
+ from code_agnostic.agents.opencode import serialize_opencode_agent
14
+ from code_agnostic.errors import InvalidConfigSchemaError
15
+
16
+ _CURSOR_AGENT_FRONTMATTER_KEYS = frozenset(
17
+ {"name", "description", "model", "readonly", "is_background"}
18
+ )
19
+
20
+
21
+ class IAgentCompiler(ABC):
22
+ @abstractmethod
23
+ def compile(self, agent: Agent) -> str:
24
+ """Return compiled agent content for target editor."""
25
+
26
+
27
+ class OpenCodeAgentCompiler(IAgentCompiler):
28
+ """Cross-compile for OpenCode agents."""
29
+
30
+ def compile(self, agent: Agent) -> str:
31
+ return serialize_opencode_agent(agent)
32
+
33
+
34
+ class CursorAgentCompiler(IAgentCompiler):
35
+ """Cross-compile for Cursor subagents."""
36
+
37
+ def compile(self, agent: Agent) -> str:
38
+ return _serialize_cursor_agent(agent)
39
+
40
+
41
+ def _serialize_cursor_agent(agent: Agent) -> str:
42
+ overrides = dict(agent.metadata.app_overrides.get("cursor", {}))
43
+ fm: dict[str, Any] = {}
44
+ if agent.metadata.name:
45
+ fm["name"] = agent.metadata.name
46
+ if agent.metadata.description:
47
+ fm["description"] = agent.metadata.description
48
+
49
+ model = overrides.pop("model", agent.metadata.model)
50
+ if model:
51
+ fm["model"] = model
52
+
53
+ if agent.metadata.tools.write is False:
54
+ fm["readonly"] = True
55
+
56
+ for key in sorted(overrides):
57
+ if key not in _CURSOR_AGENT_FRONTMATTER_KEYS:
58
+ allowed = ", ".join(sorted(_CURSOR_AGENT_FRONTMATTER_KEYS))
59
+ raise InvalidConfigSchemaError(
60
+ agent.source_path,
61
+ f"x-cursor.{key} is not supported in cursor agent "
62
+ f"frontmatter; allowed keys: {allowed}",
63
+ )
64
+ fm[key] = overrides[key]
65
+
66
+ parts: list[str] = []
67
+ if fm:
68
+ parts.append("---")
69
+ parts.append(yaml.dump(fm, default_flow_style=False, sort_keys=False).rstrip())
70
+ parts.append("---")
71
+ parts.append("")
72
+ parts.append(agent.content)
73
+ return "\n".join(parts)
74
+
75
+
76
+ class CodexAgentCompiler(IAgentCompiler):
77
+ """Cross-compile for Codex subagents."""
78
+
79
+ def compile(self, agent: Agent) -> str:
80
+ return serialize_codex_agent(agent)
81
+
82
+
83
+ class ClaudeAgentCompiler(IAgentCompiler):
84
+ """Cross-compile for Claude Code subagents."""
85
+
86
+ def compile(self, agent: Agent) -> str:
87
+ return serialize_claude_agent(agent)
@@ -1,9 +1,11 @@
1
1
  import re
2
2
  from copy import deepcopy
3
+ from pathlib import Path
3
4
  from typing import Any
4
5
 
5
6
  from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
6
7
  from code_agnostic.apps.common.models import MCPServerDTO, MCPServerType
8
+ from code_agnostic.errors import InvalidConfigSchemaError
7
9
 
8
10
  _ENV_PATTERN = re.compile(r"^\$\{(?:env:)?([A-Z_][A-Z0-9_]*)\}$")
9
11
  _BEARER_PATTERN = re.compile(r"^Bearer\s+\$\{(?:env:)?([A-Z_][A-Z0-9_]*)\}$")
@@ -34,6 +36,29 @@ def _encode_tool_timeout(timeout_ms: int) -> int | float:
34
36
  return int(seconds) if timeout_ms % 1000 == 0 else seconds
35
37
 
36
38
 
39
+ def _codex_env_var_name(server_name: str, entry: Any) -> str | None:
40
+ if isinstance(entry, str):
41
+ return entry
42
+ if not isinstance(entry, dict):
43
+ return None
44
+
45
+ name = entry.get("name")
46
+ if not isinstance(name, str):
47
+ return None
48
+
49
+ source = entry.get("source", "local")
50
+ if source in (None, "local"):
51
+ return name
52
+
53
+ detail = (
54
+ "Codex env_vars entries with source = 'remote' cannot be imported "
55
+ "into common MCP config without losing remote-executor semantics"
56
+ if source == "remote"
57
+ else f"Unsupported Codex env_vars source: {source!r}"
58
+ )
59
+ raise InvalidConfigSchemaError(Path(f"mcp_servers.{server_name}.env_vars"), detail)
60
+
61
+
37
62
  class CodexMCPMapper(IAppMCPMapper):
38
63
  def to_common(self, payload: dict[str, Any]) -> dict[str, MCPServerDTO]:
39
64
  mapped: dict[str, MCPServerDTO] = {}
@@ -49,8 +74,9 @@ class CodexMCPMapper(IAppMCPMapper):
49
74
  env: dict[str, str] = {}
50
75
  env_vars = server.get("env_vars")
51
76
  if isinstance(env_vars, list):
52
- for key in env_vars:
53
- if isinstance(key, str):
77
+ for entry in env_vars:
78
+ key = _codex_env_var_name(name, entry)
79
+ if key is not None:
54
80
  env[key] = f"${{{key}}}"
55
81
  env_table = server.get("env")
56
82
  if isinstance(env_table, dict):
@@ -218,6 +218,14 @@
218
218
  ],
219
219
  "description": "Reviewer for approval prompts unless overridden by per-app settings."
220
220
  },
221
+ "default_tools_approval_mode": {
222
+ "allOf": [
223
+ {
224
+ "$ref": "#/definitions/AppToolApproval"
225
+ }
226
+ ],
227
+ "description": "Approval mode for tools unless overridden by per-app or per-tool settings."
228
+ },
221
229
  "destructive_enabled": {
222
230
  "description": "Whether tools with `destructive_hint = true` are allowed by default.",
223
231
  "type": "boolean"
@@ -354,6 +362,13 @@
354
362
  "CodeModeConfigToml": {
355
363
  "additionalProperties": false,
356
364
  "properties": {
365
+ "direct_only_tool_namespaces": {
366
+ "description": "Exact tool namespaces to expose only as direct model tools. These tools bypass deferral, remain top-level in code-mode-only sessions, and are omitted from the nested code-mode tool surface.",
367
+ "items": {
368
+ "type": "string"
369
+ },
370
+ "type": "array"
371
+ },
357
372
  "enabled": {
358
373
  "type": "boolean"
359
374
  },
@@ -593,6 +608,12 @@
593
608
  "responses_websockets_v2": {
594
609
  "type": "boolean"
595
610
  },
611
+ "rollout_budget": {
612
+ "$ref": "#/definitions/FeatureToml_for_RolloutBudgetConfigToml"
613
+ },
614
+ "respect_system_proxy": {
615
+ "type": "boolean"
616
+ },
596
617
  "runtime_metrics": {
597
618
  "type": "boolean"
598
619
  },
@@ -617,6 +638,9 @@
617
638
  "skill_mcp_dependency_install": {
618
639
  "type": "boolean"
619
640
  },
641
+ "sleep_tool": {
642
+ "type": "boolean"
643
+ },
620
644
  "sqlite": {
621
645
  "type": "boolean"
622
646
  },
@@ -887,6 +911,16 @@
887
911
  }
888
912
  ]
889
913
  },
914
+ "FeatureToml_for_RolloutBudgetConfigToml": {
915
+ "anyOf": [
916
+ {
917
+ "type": "boolean"
918
+ },
919
+ {
920
+ "$ref": "#/definitions/RolloutBudgetConfigToml"
921
+ }
922
+ ]
923
+ },
890
924
  "FeedbackConfigToml": {
891
925
  "additionalProperties": false,
892
926
  "properties": {
@@ -2551,6 +2585,35 @@
2551
2585
  }
2552
2586
  ]
2553
2587
  },
2588
+ "RolloutBudgetConfigToml": {
2589
+ "additionalProperties": false,
2590
+ "properties": {
2591
+ "enabled": {
2592
+ "type": "boolean"
2593
+ },
2594
+ "limit_tokens": {
2595
+ "format": "int64",
2596
+ "minimum": 1.0,
2597
+ "type": "integer"
2598
+ },
2599
+ "prefill_token_weight": {
2600
+ "format": "double",
2601
+ "minimum": 0.0,
2602
+ "type": "number"
2603
+ },
2604
+ "reminder_interval_tokens": {
2605
+ "format": "int64",
2606
+ "minimum": 1.0,
2607
+ "type": "integer"
2608
+ },
2609
+ "sampling_token_weight": {
2610
+ "format": "double",
2611
+ "minimum": 0.0,
2612
+ "type": "number"
2613
+ }
2614
+ },
2615
+ "type": "object"
2616
+ },
2554
2617
  "SandboxMode": {
2555
2618
  "enum": [
2556
2619
  "read-only",
@@ -4748,6 +4811,12 @@
4748
4811
  "responses_websockets_v2": {
4749
4812
  "type": "boolean"
4750
4813
  },
4814
+ "rollout_budget": {
4815
+ "$ref": "#/definitions/FeatureToml_for_RolloutBudgetConfigToml"
4816
+ },
4817
+ "respect_system_proxy": {
4818
+ "type": "boolean"
4819
+ },
4751
4820
  "runtime_metrics": {
4752
4821
  "type": "boolean"
4753
4822
  },
@@ -4772,6 +4841,9 @@
4772
4841
  "skill_mcp_dependency_install": {
4773
4842
  "type": "boolean"
4774
4843
  },
4844
+ "sleep_tool": {
4845
+ "type": "boolean"
4846
+ },
4775
4847
  "sqlite": {
4776
4848
  "type": "boolean"
4777
4849
  },
@@ -5,7 +5,11 @@ import shutil
5
5
  import click
6
6
  from rich.console import Console
7
7
 
8
- from code_agnostic.cli.helpers import validate_resource_name, workspace_config_root
8
+ from code_agnostic.cli.helpers import (
9
+ reject_symlinked_source_dir,
10
+ validate_resource_name,
11
+ workspace_config_root,
12
+ )
9
13
  from code_agnostic.cli.options import workspace_option
10
14
  from code_agnostic.core.repository import CoreRepository
11
15
  from code_agnostic.tui import SyncConsoleUI
@@ -36,6 +40,7 @@ def agents_remove(obj: dict[str, str], name: str, workspace: str | None) -> None
36
40
  validate_resource_name(name, "agent")
37
41
  core = CoreRepository()
38
42
  root = workspace_config_root(core, workspace)
43
+ reject_symlinked_source_dir(root / "agents", "agents")
39
44
  agent_dir = root / "agents" / name
40
45
  if agent_dir.is_dir():
41
46
  shutil.rmtree(agent_dir)
@@ -35,15 +35,18 @@ def _apply_next_steps(plan: SyncPlan, target: str) -> str | None:
35
35
  if len(workspace_names) > 3:
36
36
  restore_lines.append("- code-agnostic restore -w <workspace>")
37
37
 
38
+ project_names = sorted(
39
+ {action.project for action in plan.actions if action.project}
40
+ )
41
+ for project_name in project_names[:3]:
42
+ restore_lines.append(f"- code-agnostic restore --project {project_name}")
43
+ if len(project_names) > 3:
44
+ restore_lines.append("- code-agnostic restore --project <project>")
45
+
38
46
  if restore_lines:
39
- lines.append("Repair global/workspace outputs from the active synced revision.")
47
+ lines.append("Repair managed outputs from the active synced revision.")
40
48
  lines.extend(restore_lines)
41
49
 
42
- if any(action.project is not None for action in plan.actions):
43
- lines.append(
44
- "Project outputs are checked by status; project restore is not available yet."
45
- )
46
-
47
50
  return "\n".join(lines)
48
51
 
49
52
 
@@ -3,7 +3,11 @@
3
3
  import click
4
4
  from rich.console import Console
5
5
 
6
- from code_agnostic.cli.helpers import validate_resource_name, workspace_config_root
6
+ from code_agnostic.cli.helpers import (
7
+ reject_symlinked_source_dir,
8
+ validate_resource_name,
9
+ workspace_config_root,
10
+ )
7
11
  from code_agnostic.cli.options import workspace_option
8
12
  from code_agnostic.core.repository import CoreRepository
9
13
  from code_agnostic.rules.repository import RulesRepository
@@ -40,6 +44,7 @@ def rules_remove(obj: dict[str, str], name: str, workspace: str | None) -> None:
40
44
  validate_resource_name(name, "rule")
41
45
  core = CoreRepository()
42
46
  root = workspace_config_root(core, workspace)
47
+ reject_symlinked_source_dir(root / "rules", "rules")
43
48
 
44
49
  repo = RulesRepository(root)
45
50
  if not repo.remove_rule(name):
@@ -6,7 +6,11 @@ import shutil
6
6
  import click
7
7
  from rich.console import Console
8
8
 
9
- from code_agnostic.cli.helpers import validate_resource_name, workspace_config_root
9
+ from code_agnostic.cli.helpers import (
10
+ reject_symlinked_source_dir,
11
+ validate_resource_name,
12
+ workspace_config_root,
13
+ )
10
14
  from code_agnostic.cli.options import workspace_option
11
15
  from code_agnostic.core.repository import CoreRepository
12
16
  from code_agnostic.errors import SyncAppError
@@ -15,6 +19,7 @@ from code_agnostic.skills.install_sources import (
15
19
  cleanup_skill_install_resolution,
16
20
  resolve_skill_install_source,
17
21
  )
22
+ from code_agnostic.spec.loaders import load_skill_bundle
18
23
  from code_agnostic.tui import SyncConsoleUI
19
24
  from code_agnostic.utils import compact_home_path
20
25
 
@@ -94,9 +99,18 @@ def _preflight_skill_destinations(
94
99
  if destination.exists():
95
100
  raise click.ClickException(f"Skill already exists: {scope}:{name}")
96
101
  installs.append((name, source_dir, destination))
102
+ _validate_skill_source_metadata(source_dirs)
97
103
  return installs
98
104
 
99
105
 
106
+ def _rollback_skill_destinations(destinations: list[Path]) -> None:
107
+ for destination in reversed(destinations):
108
+ if destination.is_symlink() or destination.is_file():
109
+ destination.unlink()
110
+ elif destination.is_dir():
111
+ shutil.rmtree(destination)
112
+
113
+
100
114
  def _validate_skill_source_names(source_dirs: tuple[Path, ...]) -> list[str]:
101
115
  names: list[str] = []
102
116
  seen_names: set[str] = set()
@@ -110,6 +124,12 @@ def _validate_skill_source_names(source_dirs: tuple[Path, ...]) -> list[str]:
110
124
  return names
111
125
 
112
126
 
127
+ def _validate_skill_source_metadata(source_dirs: tuple[Path, ...]) -> None:
128
+ for source_dir in source_dirs:
129
+ if (source_dir / "meta.yaml").exists() and (source_dir / "prompt.md").exists():
130
+ load_skill_bundle(source_dir)
131
+
132
+
113
133
  @click.group(
114
134
  help=(
115
135
  "Manage source skill definitions across global, workspace, and project scopes."
@@ -159,6 +179,7 @@ def skills_install(
159
179
  project=project,
160
180
  )
161
181
  try:
182
+ reject_symlinked_source_dir(root / "skills", "skills")
162
183
  if resolution is None:
163
184
  resolution = resolve_skill_install_source(
164
185
  source,
@@ -168,27 +189,44 @@ def skills_install(
168
189
  for _name, _source_dir, destination in installs:
169
190
  destination.parent.mkdir(parents=True, exist_ok=True)
170
191
  installed = False
171
- for name, source_dir, destination in installs:
172
- shutil.copytree(source_dir, destination)
173
- installed = True
174
- if scope_note is not None:
175
- click.echo(scope_note)
176
- scope_note = None
177
- click.echo(f"Installed {scope} skill: {name}")
178
- click.echo(f"Source: {compact_home_path(source_dir)}")
179
- click.echo(f"Destination: {compact_home_path(destination)}")
192
+ try:
193
+ for name, source_dir, destination in installs:
194
+ shutil.copytree(source_dir, destination)
195
+ installed = True
196
+ if scope_note is not None:
197
+ click.echo(scope_note)
198
+ scope_note = None
199
+ click.echo(f"Installed {scope} skill: {name}")
200
+ click.echo(f"Source: {compact_home_path(source_dir)}")
201
+ click.echo(f"Destination: {compact_home_path(destination)}")
202
+ except Exception as exc:
203
+ _rollback_skill_destinations(
204
+ [destination for _name, _source_dir, destination in installs]
205
+ )
206
+ raise click.ClickException(f"Skill install failed: {exc}") from exc
180
207
  if installed:
181
208
  click.echo("Next:")
182
209
  click.echo(" code-agnostic plan")
183
210
  click.echo(" code-agnostic apply")
184
- except SkillInstallSourceError as exc:
211
+ if scope.startswith("project:"):
212
+ click.echo(
213
+ "Note: project skills are separate; bare "
214
+ "`code-agnostic skills list` shows only global skills."
215
+ )
216
+ except SyncAppError as exc:
185
217
  raise click.ClickException(str(exc)) from exc
186
218
  finally:
187
219
  if resolution is not None:
188
220
  cleanup_skill_install_resolution(resolution)
189
221
 
190
222
 
191
- @skills.command("list", help="List global skills, or workspace skills with -w.")
223
+ @skills.command(
224
+ "list",
225
+ help=(
226
+ "List global skills, or workspace skills with -w. Project skills are "
227
+ "not listed here yet; use projects list for a project marker."
228
+ ),
229
+ )
192
230
  @workspace_option()
193
231
  @click.pass_obj
194
232
  def skills_list(obj: dict[str, str], workspace: str | None) -> None:
@@ -219,6 +257,11 @@ def skills_list(obj: dict[str, str], workspace: str | None) -> None:
219
257
  "- code-agnostic plan\n"
220
258
  "- code-agnostic apply"
221
259
  )
260
+ if workspace is None:
261
+ empty_message += (
262
+ "\n- Project skills are separate; run "
263
+ "code-agnostic projects list to see which projects contain skills."
264
+ )
222
265
  ui.render_list(
223
266
  "skills",
224
267
  ["Skill", "Scope", "Format", "Source"],
@@ -227,7 +270,13 @@ def skills_list(obj: dict[str, str], workspace: str | None) -> None:
227
270
  )
228
271
 
229
272
 
230
- @skills.command("remove", help="Remove a global skill, or a workspace skill with -w.")
273
+ @skills.command(
274
+ "remove",
275
+ help=(
276
+ "Remove a global skill, or a workspace skill with -w. Project skill "
277
+ "removal is not supported yet."
278
+ ),
279
+ )
231
280
  @click.option("--name", required=True, help="Skill name to remove.")
232
281
  @workspace_option()
233
282
  @click.pass_obj
@@ -235,6 +284,7 @@ def skills_remove(obj: dict[str, str], name: str, workspace: str | None) -> None
235
284
  validate_resource_name(name, "skill")
236
285
  core = CoreRepository()
237
286
  root = workspace_config_root(core, workspace)
287
+ reject_symlinked_source_dir(root / "skills", "skills")
238
288
  skill_dir = root / "skills" / name
239
289
  if not skill_dir.exists():
240
290
  raise click.ClickException(f"Skill not found: {name}")
@@ -38,7 +38,7 @@ def _status_row_for_app(app_name: str, apps: AppsService) -> EditorStatusRow:
38
38
  )
39
39
 
40
40
 
41
- @click.command(help="Show sync status for editors and workspaces.")
41
+ @click.command(help="Show sync status for editors, workspaces, and projects.")
42
42
  @app_option()
43
43
  @verbose_option()
44
44
  @click.pass_obj
@@ -57,6 +57,13 @@ def validate_resource_name(name: str, resource_type: str) -> None:
57
57
  raise click.ClickException(f"Invalid {resource_type} name: {name}")
58
58
 
59
59
 
60
+ def reject_symlinked_source_dir(path: Path, resource_type: str) -> None:
61
+ if path.is_symlink():
62
+ raise click.ClickException(
63
+ f"Refusing to modify symlinked {resource_type} source directory: {path}"
64
+ )
65
+
66
+
60
67
  def status_row_for_app(app_name: str, plan, apps: AppsService) -> EditorStatusRow:
61
68
  if not apps.is_enabled(app_name):
62
69
  return EditorStatusRow(
@@ -221,23 +221,39 @@ class CoreRepository(BaseSourceRepository):
221
221
 
222
222
  result: list[dict[str, str]] = []
223
223
  seen_names: set[str] = set()
224
+ seen_paths: set[Path] = set()
224
225
  for item in payload:
225
226
  if not isinstance(item, dict):
226
- continue
227
+ raise InvalidConfigSchemaError(
228
+ self.workspaces_path, "entries must be JSON objects"
229
+ )
227
230
  name = item.get("name")
228
231
  path = item.get("path")
229
232
  if not isinstance(name, str) or not isinstance(path, str):
230
- continue
233
+ raise InvalidConfigSchemaError(
234
+ self.workspaces_path, "entries must contain string name and path"
235
+ )
231
236
  normalized_name = name.strip()
232
237
  normalized_path = str(Path(path).expanduser().resolve())
233
- if (
234
- not normalized_name
235
- or _is_path_like_config_name(normalized_name)
236
- or normalized_name in seen_names
237
- ):
238
- continue
238
+ if not normalized_name:
239
+ raise InvalidConfigSchemaError(
240
+ self.workspaces_path, "workspace name cannot be empty"
241
+ )
242
+ if _is_path_like_config_name(normalized_name):
243
+ raise InvalidConfigSchemaError(
244
+ self.workspaces_path, f"invalid workspace name: {normalized_name}"
245
+ )
246
+ if normalized_name in seen_names:
247
+ raise InvalidConfigSchemaError(
248
+ self.workspaces_path, f"duplicate workspace name: {normalized_name}"
249
+ )
250
+ if Path(normalized_path) in seen_paths:
251
+ raise InvalidConfigSchemaError(
252
+ self.workspaces_path, f"duplicate workspace path: {normalized_path}"
253
+ )
239
254
  result.append({"name": normalized_name, "path": normalized_path})
240
255
  seen_names.add(normalized_name)
256
+ seen_paths.add(Path(normalized_path))
241
257
  return result
242
258
 
243
259
  def save_workspaces(self, workspaces: list[dict[str, str]]) -> None:
@@ -248,6 +248,16 @@ class LossinessExplainer:
248
248
  reason="target does not support agent nickname_candidates",
249
249
  )
250
250
  )
251
+ if agent.metadata.model_reasoning_effort:
252
+ findings.extend(
253
+ self._findings_for_targets(
254
+ resource_path=resource_path,
255
+ property_name="reasoning_effort",
256
+ targets=("cursor",),
257
+ app=app,
258
+ reason="target does not support agent reasoning_effort",
259
+ )
260
+ )
251
261
  if agent.metadata.sandbox_mode:
252
262
  findings.extend(
253
263
  self._findings_for_targets(
@@ -263,7 +273,7 @@ class LossinessExplainer:
263
273
  self._findings_for_targets(
264
274
  resource_path=resource_path,
265
275
  property_name="tools.read",
266
- targets=("codex",),
276
+ targets=("cursor", "codex"),
267
277
  app=app,
268
278
  reason="target does not support agent read permissions",
269
279
  )
@@ -283,7 +293,7 @@ class LossinessExplainer:
283
293
  self._findings_for_targets(
284
294
  resource_path=resource_path,
285
295
  property_name="tools.mcp",
286
- targets=("codex",),
296
+ targets=("cursor", "codex"),
287
297
  app=app,
288
298
  reason="target does not support agent MCP permissions",
289
299
  )