code-agnostic 0.3.10__tar.gz → 0.3.12__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 (157) hide show
  1. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/PKG-INFO +18 -12
  2. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/README.md +17 -11
  3. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/__init__.py +1 -1
  4. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/__main__.py +1 -1
  5. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/agents/opencode.py +7 -2
  6. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/codex/config_repository.py +17 -2
  7. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/codex/schema.json +34 -24
  8. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/opencode/config_repository.py +18 -5
  9. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/opencode/schema.json +65 -38
  10. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/commands/agents.py +2 -1
  11. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/commands/apps.py +3 -1
  12. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/commands/rules.py +2 -1
  13. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/commands/skills.py +12 -5
  14. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/helpers.py +13 -1
  15. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/imports/service.py +70 -0
  16. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/mcp_service.py +10 -3
  17. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/planner.py +19 -0
  18. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/skills/compilers.py +8 -1
  19. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/tui/renderers.py +44 -2
  20. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/workspace_artifacts.py +1 -1
  21. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic.egg-info/PKG-INFO +18 -12
  22. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/pyproject.toml +1 -1
  23. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_agents.py +15 -0
  24. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_apply_target.py +4 -4
  25. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_apps.py +2 -0
  26. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_mcp.py +33 -0
  27. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_module_organization.py +1 -1
  28. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_rules.py +15 -0
  29. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_skills.py +37 -0
  30. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_status.py +13 -1
  31. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_workspaces.py +3 -0
  32. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_planner_executor.py +17 -9
  33. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_workspace_config_sync.py +58 -4
  34. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/LICENSE +0 -0
  35. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/agents/__init__.py +0 -0
  36. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/agents/claude.py +0 -0
  37. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/agents/codex.py +0 -0
  38. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/agents/compilers.py +0 -0
  39. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/agents/models.py +0 -0
  40. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/agents/parser.py +0 -0
  41. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/__init__.py +0 -0
  42. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/app_id.py +0 -0
  43. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/apps_service.py +0 -0
  44. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/claude/__init__.py +0 -0
  45. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/claude/config_repository.py +0 -0
  46. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/claude/mapper.py +0 -0
  47. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/claude/service.py +0 -0
  48. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/codex/__init__.py +0 -0
  49. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/codex/mapper.py +0 -0
  50. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/codex/schema_repository.py +0 -0
  51. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/codex/service.py +0 -0
  52. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/common/__init__.py +0 -0
  53. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/common/compiled_planning.py +0 -0
  54. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/common/framework.py +0 -0
  55. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
  56. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
  57. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
  58. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/common/interfaces/service.py +0 -0
  59. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/common/loader.py +0 -0
  60. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/common/models.py +0 -0
  61. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/common/schema.py +0 -0
  62. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/common/symlink_planning.py +0 -0
  63. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/common/utils.py +0 -0
  64. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/cursor/__init__.py +0 -0
  65. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/cursor/config_repository.py +0 -0
  66. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/cursor/mapper.py +0 -0
  67. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/cursor/schema.json +0 -0
  68. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/cursor/schema_repository.py +0 -0
  69. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/cursor/service.py +0 -0
  70. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/opencode/__init__.py +0 -0
  71. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/opencode/mapper.py +0 -0
  72. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/opencode/schema_repository.py +0 -0
  73. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/apps/opencode/service.py +0 -0
  74. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/__init__.py +0 -0
  75. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/aliases.py +0 -0
  76. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/commands/__init__.py +0 -0
  77. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/commands/apply.py +0 -0
  78. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
  79. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/commands/import_.py +0 -0
  80. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/commands/mcp.py +0 -0
  81. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/commands/plan.py +0 -0
  82. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/commands/restore.py +0 -0
  83. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/commands/status.py +0 -0
  84. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/commands/validate.py +0 -0
  85. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/commands/workspaces.py +0 -0
  86. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/cli/options.py +0 -0
  87. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/constants.py +0 -0
  88. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/core/__init__.py +0 -0
  89. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/core/repository.py +0 -0
  90. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/core/workspace_repository.py +0 -0
  91. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/errors.py +0 -0
  92. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/executor.py +0 -0
  93. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/generated_artifacts.py +0 -0
  94. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/git_exclude_service.py +0 -0
  95. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/imports/__init__.py +0 -0
  96. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/imports/adapters.py +0 -0
  97. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/imports/filesystem.py +0 -0
  98. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/imports/models.py +0 -0
  99. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/lossiness.py +0 -0
  100. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/models.py +0 -0
  101. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/rules/__init__.py +0 -0
  102. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/rules/compilers.py +0 -0
  103. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/rules/models.py +0 -0
  104. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/rules/parser.py +0 -0
  105. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/rules/repository.py +0 -0
  106. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/skills/__init__.py +0 -0
  107. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/skills/models.py +0 -0
  108. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/skills/parser.py +0 -0
  109. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/spec/__init__.py +0 -0
  110. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/spec/loaders.py +0 -0
  111. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
  112. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
  113. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
  114. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
  115. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
  116. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/status.py +0 -0
  117. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/tui/__init__.py +0 -0
  118. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/tui/enums.py +0 -0
  119. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/tui/import_selector.py +0 -0
  120. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/tui/sections.py +0 -0
  121. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/tui/tables.py +0 -0
  122. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/utils.py +0 -0
  123. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/validation.py +0 -0
  124. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic/workspaces.py +0 -0
  125. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic.egg-info/SOURCES.txt +0 -0
  126. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic.egg-info/dependency_links.txt +0 -0
  127. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic.egg-info/entry_points.txt +0 -0
  128. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic.egg-info/requires.txt +0 -0
  129. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/code_agnostic.egg-info/top_level.txt +0 -0
  130. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/setup.cfg +0 -0
  131. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_aliases.py +0 -0
  132. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_apply_apps.py +0 -0
  133. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_apply_codex.py +0 -0
  134. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_apply_cursor.py +0 -0
  135. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_explain_lossiness.py +0 -0
  136. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_flags.py +0 -0
  137. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_git_exclude.py +0 -0
  138. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_import.py +0 -0
  139. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_import_interactive.py +0 -0
  140. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_plan.py +0 -0
  141. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_restore.py +0 -0
  142. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_validate.py +0 -0
  143. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_cli_workspace_resolution.py +0 -0
  144. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_common_mcp_to_dto.py +0 -0
  145. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_common_repository.py +0 -0
  146. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_compiled_planning.py +0 -0
  147. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_dto_to_common_mcp.py +0 -0
  148. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_git_exclude_service.py +0 -0
  149. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_mcp_service.py +0 -0
  150. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_planner_rules.py +0 -0
  151. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_symlink_planning.py +0 -0
  152. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_sync_plan_model.py +0 -0
  153. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_transactional_executor.py +0 -0
  154. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_utils.py +0 -0
  155. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_version.py +0 -0
  156. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/tests/test_workspace_repo_status.py +0 -0
  157. {code_agnostic-0.3.10 → code_agnostic-0.3.12}/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.10
3
+ Version: 0.3.12
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
@@ -54,7 +54,7 @@ AI coding tools each want config in a different place and format. When you use m
54
54
 
55
55
  ~/.config/opencode/ Compiled & synced for OpenCode
56
56
  ~/.cursor/ Compiled & synced for Cursor
57
- ~/.codex/ Compiled & synced for Codex
57
+ ~/.codex/ Compiled & synced for Codex (or CODEX_HOME)
58
58
  ~/.claude.json and ~/.claude/ Compiled & synced for Claude Code
59
59
  ```
60
60
 
@@ -73,11 +73,12 @@ Today the implementation is still mixed: some assets are compiled and some are s
73
73
  - workspace source config under `~/.config/code-agnostic/workspaces/<name>/`,
74
74
  propagated into repos inside a registered workspace.
75
75
 
76
- Project-local skill folders that users create directly inside a repo, such as
77
- `.agents/skills` or `.opencode/skills`, are app-native inputs but are not
78
- managed as source by `code-agnostic` yet. First-class project-scoped installs
79
- are planned so a single registered project can have managed local source config
80
- without bypassing the hub.
76
+ Workspace sync may generate repo-local outputs, but those outputs are not
77
+ source. Project-local skill folders that users create directly inside a repo,
78
+ such as `.agents/skills` or `.opencode/skills`, are app-native inputs but are
79
+ not managed as source by `code-agnostic` yet. First-class project-scoped
80
+ installs are planned so a single registered project can have managed local
81
+ source config without bypassing the hub.
81
82
 
82
83
  ## Install
83
84
 
@@ -142,9 +143,9 @@ that are omitted or rejected for a selected target.
142
143
 
143
144
  Cursor workspace propagation writes repo-local MCP, skills, and agents when those resources exist in the workspace source config.
144
145
 
145
- OpenCode workspace configs include the shared workspace `AGENTS.md` natively via `instructions`, so repos under the workspace get both repo-local and shared workspace instructions. Codex repos receive workspace instructions through a generated `AGENTS.override.md`, which is added to each repo's `.git/info/exclude`. Claude Code receives workspace instructions through generated `CLAUDE.local.md` files, never by editing committed `CLAUDE.md`.
146
+ OpenCode workspace configs write project-root `opencode.json` files that 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`.
146
147
 
147
- Cursor documents `AGENTS.md` support in project roots and subdirectories. `code-agnostic` does not copy or link the shared workspace `AGENTS.md` into child repos; Cursor will load repo-local or nested `AGENTS.md` files that already exist in the opened project. Codex documents nested `AGENTS.md` discovery, but not a native config include for an extra workspace file.
148
+ 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 `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.
148
149
 
149
150
  ## Features
150
151
 
@@ -238,7 +239,10 @@ code-agnostic plan
238
239
  code-agnostic apply
239
240
  ```
240
241
 
241
- Global skills live under `~/.config/code-agnostic/skills`. Workspace-local skills live under `~/.config/code-agnostic/workspaces/<name>/skills` and can be inspected with `code-agnostic skills list -w <name>`. Codex generated skill outputs are written to `~/.agents/skills`, while Codex agents and config remain under `~/.codex`. Claude Code generated skills and agents are written under `~/.claude/skills` and `~/.claude/agents`, with workspace copies under repo-local `.claude/skills` and `.claude/agents`.
242
+ There is no `skills install` command yet; copy skills into managed source first,
243
+ then use the normal `plan` / `apply` workflow.
244
+
245
+ Global skills live under `~/.config/code-agnostic/skills`. Workspace-local skills live under `~/.config/code-agnostic/workspaces/<name>/skills` and can be inspected with `code-agnostic skills list -w <name>`. Codex generated skill outputs are written to `~/.agents/skills`, while Codex agents and config remain under `CODEX_HOME` when set, defaulting to `~/.codex`. Claude Code generated skills and agents are written under `~/.claude/skills` and `~/.claude/agents`, with workspace copies under repo-local `.claude/skills` and `.claude/agents`.
242
246
 
243
247
  Project-local skills are not first-class source inputs in `code-agnostic` yet. If a target app discovers repo-local skill folders such as `.agents/skills`, `.opencode/skills`, or user-created `.claude/skills`, treat those as unmanaged app inputs. Workspace sync writes only the exact generated paths recorded in `.sync-state.json`.
244
248
 
@@ -249,10 +253,12 @@ code-agnostic skills install ./my-skill --apply
249
253
  ```
250
254
 
251
255
  That command should copy the skill into the source of truth and then run the normal compiler/apply flow.
256
+ See [docs/project-scoped-skills.md](docs/project-scoped-skills.md) for the
257
+ first implementation slice.
252
258
 
253
259
  ### Workspaces
254
260
 
255
- Register workspace directories. Workspace rules are compiled into a canonical `AGENTS.md` at the workspace root. Repos keep their own repo-specific `AGENTS.md`; Codex receives the workspace rules through generated, git-excluded `AGENTS.override.md` files, while OpenCode workspace configs reference the shared workspace file through `instructions`. Claude receives generated `CLAUDE.local.md` files and project MCP entries in `~/.claude.json["projects"][absolute_repo_path]["mcpServers"]`. Workspace source config, skills, and agents are propagated into repo-local generated paths for OpenCode, Cursor, Codex, and Claude; user-created project-local skill folders remain unmanaged until project-scoped installs are supported.
261
+ 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 write project-root `opencode.json` files that reference the shared workspace file through `instructions`. Claude receives generated `CLAUDE.local.md` files and project MCP entries in `~/.claude.json["projects"][absolute_repo_path]["mcpServers"]`. Workspace source config, skills, and agents are propagated into repo-local generated paths for OpenCode, Cursor, Codex, and Claude; user-created project-local skill folders remain unmanaged until project-scoped installs are supported.
256
262
 
257
263
  Cursor propagation intentionally stays to repo-local MCP, skills, and agents; it does not copy the shared workspace `AGENTS.md` into child repos.
258
264
 
@@ -316,12 +322,12 @@ The compiler migration is documented in:
316
322
  - [x] MCP add/remove/list commands
317
323
  - [x] Rules system with YAML frontmatter and per-editor compilation
318
324
  - [x] Cross-compilation for skills and agents
325
+ - [x] Planner integration for cross-compiled skills and agents
319
326
  - [x] Per-workspace git-exclude customization
320
327
  - [x] Interactive TUI for import selection
321
328
  - [x] Claude Code support
322
329
  - [ ] Project-scoped skill installs and sync
323
330
  - [ ] `rules add` / `skills add` / `agents add` commands (open `$EDITOR` with template)
324
- - [ ] Planner integration for cross-compiled skills and agents
325
331
  - [ ] Shell auto-complete
326
332
  - [ ] Full TUI mode (command palette + menus)
327
333
 
@@ -29,7 +29,7 @@ AI coding tools each want config in a different place and format. When you use m
29
29
 
30
30
  ~/.config/opencode/ Compiled & synced for OpenCode
31
31
  ~/.cursor/ Compiled & synced for Cursor
32
- ~/.codex/ Compiled & synced for Codex
32
+ ~/.codex/ Compiled & synced for Codex (or CODEX_HOME)
33
33
  ~/.claude.json and ~/.claude/ Compiled & synced for Claude Code
34
34
  ```
35
35
 
@@ -48,11 +48,12 @@ Today the implementation is still mixed: some assets are compiled and some are s
48
48
  - workspace source config under `~/.config/code-agnostic/workspaces/<name>/`,
49
49
  propagated into repos inside a registered workspace.
50
50
 
51
- Project-local skill folders that users create directly inside a repo, such as
52
- `.agents/skills` or `.opencode/skills`, are app-native inputs but are not
53
- managed as source by `code-agnostic` yet. First-class project-scoped installs
54
- are planned so a single registered project can have managed local source config
55
- without bypassing the hub.
51
+ Workspace sync may generate repo-local outputs, but those outputs are not
52
+ source. Project-local skill folders that users create directly inside a repo,
53
+ such as `.agents/skills` or `.opencode/skills`, are app-native inputs but are
54
+ not managed as source by `code-agnostic` yet. First-class project-scoped
55
+ installs are planned so a single registered project can have managed local
56
+ source config without bypassing the hub.
56
57
 
57
58
  ## Install
58
59
 
@@ -117,9 +118,9 @@ that are omitted or rejected for a selected target.
117
118
 
118
119
  Cursor workspace propagation writes repo-local MCP, skills, and agents when those resources exist in the workspace source config.
119
120
 
120
- OpenCode workspace configs include the shared workspace `AGENTS.md` natively via `instructions`, so repos under the workspace get both repo-local and shared workspace instructions. Codex repos receive workspace instructions through a generated `AGENTS.override.md`, which is added to each repo's `.git/info/exclude`. Claude Code receives workspace instructions through generated `CLAUDE.local.md` files, never by editing committed `CLAUDE.md`.
121
+ OpenCode workspace configs write project-root `opencode.json` files that 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`.
121
122
 
122
- Cursor documents `AGENTS.md` support in project roots and subdirectories. `code-agnostic` does not copy or link the shared workspace `AGENTS.md` into child repos; Cursor will load repo-local or nested `AGENTS.md` files that already exist in the opened project. Codex documents nested `AGENTS.md` discovery, but not a native config include for an extra workspace file.
123
+ 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 `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.
123
124
 
124
125
  ## Features
125
126
 
@@ -213,7 +214,10 @@ code-agnostic plan
213
214
  code-agnostic apply
214
215
  ```
215
216
 
216
- Global skills live under `~/.config/code-agnostic/skills`. Workspace-local skills live under `~/.config/code-agnostic/workspaces/<name>/skills` and can be inspected with `code-agnostic skills list -w <name>`. Codex generated skill outputs are written to `~/.agents/skills`, while Codex agents and config remain under `~/.codex`. Claude Code generated skills and agents are written under `~/.claude/skills` and `~/.claude/agents`, with workspace copies under repo-local `.claude/skills` and `.claude/agents`.
217
+ There is no `skills install` command yet; copy skills into managed source first,
218
+ then use the normal `plan` / `apply` workflow.
219
+
220
+ Global skills live under `~/.config/code-agnostic/skills`. Workspace-local skills live under `~/.config/code-agnostic/workspaces/<name>/skills` and can be inspected with `code-agnostic skills list -w <name>`. Codex generated skill outputs are written to `~/.agents/skills`, while Codex agents and config remain under `CODEX_HOME` when set, defaulting to `~/.codex`. Claude Code generated skills and agents are written under `~/.claude/skills` and `~/.claude/agents`, with workspace copies under repo-local `.claude/skills` and `.claude/agents`.
217
221
 
218
222
  Project-local skills are not first-class source inputs in `code-agnostic` yet. If a target app discovers repo-local skill folders such as `.agents/skills`, `.opencode/skills`, or user-created `.claude/skills`, treat those as unmanaged app inputs. Workspace sync writes only the exact generated paths recorded in `.sync-state.json`.
219
223
 
@@ -224,10 +228,12 @@ code-agnostic skills install ./my-skill --apply
224
228
  ```
225
229
 
226
230
  That command should copy the skill into the source of truth and then run the normal compiler/apply flow.
231
+ See [docs/project-scoped-skills.md](docs/project-scoped-skills.md) for the
232
+ first implementation slice.
227
233
 
228
234
  ### Workspaces
229
235
 
230
- Register workspace directories. Workspace rules are compiled into a canonical `AGENTS.md` at the workspace root. Repos keep their own repo-specific `AGENTS.md`; Codex receives the workspace rules through generated, git-excluded `AGENTS.override.md` files, while OpenCode workspace configs reference the shared workspace file through `instructions`. Claude receives generated `CLAUDE.local.md` files and project MCP entries in `~/.claude.json["projects"][absolute_repo_path]["mcpServers"]`. Workspace source config, skills, and agents are propagated into repo-local generated paths for OpenCode, Cursor, Codex, and Claude; user-created project-local skill folders remain unmanaged until project-scoped installs are supported.
236
+ 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 write project-root `opencode.json` files that reference the shared workspace file through `instructions`. Claude receives generated `CLAUDE.local.md` files and project MCP entries in `~/.claude.json["projects"][absolute_repo_path]["mcpServers"]`. Workspace source config, skills, and agents are propagated into repo-local generated paths for OpenCode, Cursor, Codex, and Claude; user-created project-local skill folders remain unmanaged until project-scoped installs are supported.
231
237
 
232
238
  Cursor propagation intentionally stays to repo-local MCP, skills, and agents; it does not copy the shared workspace `AGENTS.md` into child repos.
233
239
 
@@ -291,12 +297,12 @@ The compiler migration is documented in:
291
297
  - [x] MCP add/remove/list commands
292
298
  - [x] Rules system with YAML frontmatter and per-editor compilation
293
299
  - [x] Cross-compilation for skills and agents
300
+ - [x] Planner integration for cross-compiled skills and agents
294
301
  - [x] Per-workspace git-exclude customization
295
302
  - [x] Interactive TUI for import selection
296
303
  - [x] Claude Code support
297
304
  - [ ] Project-scoped skill installs and sync
298
305
  - [ ] `rules add` / `skills add` / `agents add` commands (open `$EDITOR` with template)
299
- - [ ] Planner integration for cross-compiled skills and agents
300
306
  - [ ] Shell auto-complete
301
307
  - [ ] Full TUI mode (command palette + menus)
302
308
 
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.3.10"
3
+ __version__ = "0.3.12"
@@ -25,7 +25,7 @@ from code_agnostic.errors import SyncAppError
25
25
  )
26
26
  @click.pass_context
27
27
  def cli(ctx: click.Context) -> None:
28
- """App-based config sync."""
28
+ """Manage one source of truth for AI coding-tool config."""
29
29
  ctx.obj = {}
30
30
 
31
31
 
@@ -7,14 +7,19 @@ from typing import Any
7
7
  import yaml
8
8
 
9
9
  from code_agnostic.agents.models import Agent
10
+ from code_agnostic.errors import InvalidConfigSchemaError
10
11
 
11
12
 
12
13
  def serialize_opencode_agent(agent: Agent) -> str:
13
14
  fm: dict[str, Any] = {}
14
15
  if agent.metadata.name:
15
16
  fm["name"] = agent.metadata.name
16
- if agent.metadata.description:
17
- fm["description"] = agent.metadata.description
17
+ if not agent.metadata.description:
18
+ raise InvalidConfigSchemaError(
19
+ agent.source_path,
20
+ "OpenCode agents require a description",
21
+ )
22
+ fm["description"] = agent.metadata.description
18
23
  model = agent.metadata.effective_value("opencode", "model")
19
24
  if model:
20
25
  fm["model"] = model
@@ -1,3 +1,4 @@
1
+ import os
1
2
  from pathlib import Path
2
3
  from typing import Any
3
4
 
@@ -21,7 +22,8 @@ from code_agnostic.errors import InvalidConfigSchemaError, InvalidJsonFormatErro
21
22
 
22
23
  class CodexConfigRepository(IAppConfigRepository):
23
24
  def __init__(self, root: Path | None = None) -> None:
24
- self._root = root or (Path.home() / CODEX_PROJECT_DIRNAME)
25
+ self._root = root or _default_codex_root()
26
+ self._skills_dir = _default_codex_skills_dir(root)
25
27
 
26
28
  @property
27
29
  def root(self) -> Path:
@@ -33,7 +35,7 @@ class CodexConfigRepository(IAppConfigRepository):
33
35
 
34
36
  @property
35
37
  def skills_dir(self) -> Path:
36
- return self.root.parent / AGENTS_PROJECT_DIRNAME / SKILLS_DIRNAME
38
+ return self._skills_dir
37
39
 
38
40
  @property
39
41
  def agents_dir(self) -> Path:
@@ -81,3 +83,16 @@ class CodexConfigRepository(IAppConfigRepository):
81
83
  config = self.load_config()
82
84
  config["agents"] = payload
83
85
  self.save_config(config)
86
+
87
+
88
+ def _default_codex_root() -> Path:
89
+ codex_home = os.environ.get("CODEX_HOME")
90
+ if codex_home:
91
+ return Path(codex_home).expanduser()
92
+ return Path.home() / CODEX_PROJECT_DIRNAME
93
+
94
+
95
+ def _default_codex_skills_dir(root: Path | None) -> Path:
96
+ if root is not None:
97
+ return root.parent / AGENTS_PROJECT_DIRNAME / SKILLS_DIRNAME
98
+ return Path.home() / AGENTS_PROJECT_DIRNAME / SKILLS_DIRNAME
@@ -226,18 +226,6 @@
226
226
  },
227
227
  "type": "object"
228
228
  },
229
- "AppsMcpPathOverrideConfigToml": {
230
- "additionalProperties": false,
231
- "properties": {
232
- "enabled": {
233
- "type": "boolean"
234
- },
235
- "path": {
236
- "type": "string"
237
- }
238
- },
239
- "type": "object"
240
- },
241
229
  "AskForApproval": {
242
230
  "description": "Determines the conditions under which the user is consulted to approve running the command proposed by Codex.",
243
231
  "oneOf": [
@@ -408,7 +396,23 @@
408
396
  "type": "boolean"
409
397
  },
410
398
  "apps_mcp_path_override": {
411
- "$ref": "#/definitions/FeatureToml_for_AppsMcpPathOverrideConfigToml"
399
+ "anyOf": [
400
+ {
401
+ "type": "boolean"
402
+ },
403
+ {
404
+ "additionalProperties": false,
405
+ "properties": {
406
+ "enabled": {
407
+ "type": "boolean"
408
+ },
409
+ "path": {
410
+ "type": "string"
411
+ }
412
+ },
413
+ "type": "object"
414
+ }
415
+ ]
412
416
  },
413
417
  "auth_elicitation": {
414
418
  "type": "boolean"
@@ -836,16 +840,6 @@
836
840
  },
837
841
  "type": "object"
838
842
  },
839
- "FeatureToml_for_AppsMcpPathOverrideConfigToml": {
840
- "anyOf": [
841
- {
842
- "type": "boolean"
843
- },
844
- {
845
- "$ref": "#/definitions/AppsMcpPathOverrideConfigToml"
846
- }
847
- ]
848
- },
849
843
  "FeatureToml_for_CodeModeConfigToml": {
850
844
  "anyOf": [
851
845
  {
@@ -4534,7 +4528,23 @@
4534
4528
  "type": "boolean"
4535
4529
  },
4536
4530
  "apps_mcp_path_override": {
4537
- "$ref": "#/definitions/FeatureToml_for_AppsMcpPathOverrideConfigToml"
4531
+ "anyOf": [
4532
+ {
4533
+ "type": "boolean"
4534
+ },
4535
+ {
4536
+ "additionalProperties": false,
4537
+ "properties": {
4538
+ "enabled": {
4539
+ "type": "boolean"
4540
+ },
4541
+ "path": {
4542
+ "type": "string"
4543
+ }
4544
+ },
4545
+ "type": "object"
4546
+ }
4547
+ ]
4538
4548
  },
4539
4549
  "auth_elicitation": {
4540
4550
  "type": "boolean"
@@ -13,8 +13,15 @@ from code_agnostic.utils import merge_dict_overlay, read_json_safe, write_json
13
13
 
14
14
 
15
15
  class OpenCodeConfigRepository(IAppConfigRepository):
16
- def __init__(self, root: Path | None = None) -> None:
16
+ def __init__(
17
+ self,
18
+ root: Path | None = None,
19
+ config_path: Path | None = None,
20
+ legacy_config_path: Path | None = None,
21
+ ) -> None:
17
22
  self._root = root or (Path.home() / ".config" / "opencode")
23
+ self._config_path = config_path
24
+ self._legacy_config_path = legacy_config_path
18
25
 
19
26
  @property
20
27
  def root(self) -> Path:
@@ -22,7 +29,7 @@ class OpenCodeConfigRepository(IAppConfigRepository):
22
29
 
23
30
  @property
24
31
  def config_path(self) -> Path:
25
- return self.root / OPENCODE_CONFIG_FILENAME
32
+ return self._config_path or self.root / OPENCODE_CONFIG_FILENAME
26
33
 
27
34
  @property
28
35
  def skills_dir(self) -> Path:
@@ -39,13 +46,14 @@ class OpenCodeConfigRepository(IAppConfigRepository):
39
46
  return plural
40
47
 
41
48
  def load_config(self) -> dict[str, Any]:
42
- payload, error = read_json_safe(self.config_path)
49
+ config_path = self._read_config_path()
50
+ payload, error = read_json_safe(config_path)
43
51
  if error is not None:
44
- raise InvalidJsonFormatError(self.config_path, error)
52
+ raise InvalidJsonFormatError(config_path, error)
45
53
  if payload is None:
46
54
  return {}
47
55
  if not isinstance(payload, dict):
48
- raise InvalidConfigSchemaError(self.config_path, "must be a JSON object")
56
+ raise InvalidConfigSchemaError(config_path, "must be a JSON object")
49
57
  return payload
50
58
 
51
59
  def save_config(self, payload: dict[str, Any]) -> None:
@@ -105,3 +113,8 @@ class OpenCodeConfigRepository(IAppConfigRepository):
105
113
 
106
114
  if tools:
107
115
  merged["tools"] = tools
116
+
117
+ def _read_config_path(self) -> Path:
118
+ if self.config_path.exists() or self._legacy_config_path is None:
119
+ return self.config_path
120
+ return self._legacy_config_path
@@ -43,48 +43,44 @@
43
43
  },
44
44
  "additionalProperties": false
45
45
  },
46
- "ReferenceConfigEntry": {
47
- "anyOf": [
48
- {
46
+ "ConfigV2.Reference.Git": {
47
+ "type": "object",
48
+ "properties": {
49
+ "repository": {
49
50
  "type": "string"
50
51
  },
51
- {
52
- "type": "object",
53
- "properties": {
54
- "repository": {
55
- "type": "string",
56
- "description": "Git repository URL, host/path reference, or GitHub owner/repo shorthand"
57
- },
58
- "branch": {
59
- "type": "string",
60
- "description": "Branch or ref to clone and inspect"
61
- }
62
- },
63
- "required": [
64
- "repository"
65
- ],
66
- "additionalProperties": false
52
+ "branch": {
53
+ "type": "string"
67
54
  },
68
- {
69
- "type": "object",
70
- "properties": {
71
- "path": {
72
- "type": "string",
73
- "description": "Absolute path, ~/ path, or workspace-relative path to a local reference directory"
74
- }
75
- },
76
- "required": [
77
- "path"
78
- ],
79
- "additionalProperties": false
55
+ "description": {
56
+ "type": "string"
57
+ },
58
+ "hidden": {
59
+ "type": "boolean"
80
60
  }
81
- ]
61
+ },
62
+ "required": [
63
+ "repository"
64
+ ],
65
+ "additionalProperties": false
82
66
  },
83
- "ReferenceConfig": {
67
+ "ConfigV2.Reference.Local": {
84
68
  "type": "object",
85
- "additionalProperties": {
86
- "$ref": "#/$defs/ReferenceConfigEntry"
87
- }
69
+ "properties": {
70
+ "path": {
71
+ "type": "string"
72
+ },
73
+ "description": {
74
+ "type": "string"
75
+ },
76
+ "hidden": {
77
+ "type": "boolean"
78
+ }
79
+ },
80
+ "required": [
81
+ "path"
82
+ ],
83
+ "additionalProperties": false
88
84
  },
89
85
  "PermissionActionConfig": {
90
86
  "type": "string",
@@ -390,6 +386,7 @@
390
386
  "field": {
391
387
  "type": "string",
392
388
  "enum": [
389
+ "reasoning",
393
390
  "reasoning_content",
394
391
  "reasoning_details"
395
392
  ]
@@ -826,9 +823,39 @@
826
823
  "additionalProperties": false,
827
824
  "description": "Additional skill folder paths"
828
825
  },
826
+ "references": {
827
+ "type": "object",
828
+ "additionalProperties": {
829
+ "anyOf": [
830
+ {
831
+ "type": "string"
832
+ },
833
+ {
834
+ "$ref": "#/$defs/ConfigV2.Reference.Git"
835
+ },
836
+ {
837
+ "$ref": "#/$defs/ConfigV2.Reference.Local"
838
+ }
839
+ ]
840
+ },
841
+ "description": "Named git or local directory references"
842
+ },
829
843
  "reference": {
830
- "$ref": "#/$defs/ReferenceConfig",
831
- "description": "Named git or local directory references that can be mentioned as @alias or @alias/path"
844
+ "type": "object",
845
+ "additionalProperties": {
846
+ "anyOf": [
847
+ {
848
+ "type": "string"
849
+ },
850
+ {
851
+ "$ref": "#/$defs/ConfigV2.Reference.Git"
852
+ },
853
+ {
854
+ "$ref": "#/$defs/ConfigV2.Reference.Local"
855
+ }
856
+ ]
857
+ },
858
+ "description": "@deprecated Use 'references' field instead. Named git or local directory references"
832
859
  },
833
860
  "watcher": {
834
861
  "type": "object",
@@ -5,7 +5,7 @@ import shutil
5
5
  import click
6
6
  from rich.console import Console
7
7
 
8
- from code_agnostic.cli.helpers import workspace_config_root
8
+ from code_agnostic.cli.helpers import validate_resource_name, workspace_config_root
9
9
  from code_agnostic.cli.options import workspace_option
10
10
  from code_agnostic.core.repository import CoreRepository
11
11
  from code_agnostic.tui import SyncConsoleUI
@@ -33,6 +33,7 @@ def agents_list(obj: dict[str, str], workspace: str | None) -> None:
33
33
  @workspace_option()
34
34
  @click.pass_obj
35
35
  def agents_remove(obj: dict[str, str], name: str, workspace: str | None) -> None:
36
+ validate_resource_name(name, "agent")
36
37
  core = CoreRepository()
37
38
  root = workspace_config_root(core, workspace)
38
39
  agent_dir = root / "agents" / name
@@ -30,8 +30,10 @@ def apps_enable(obj: dict[str, str], app: str) -> None:
30
30
  ui = SyncConsoleUI(Console())
31
31
  core = CoreRepository()
32
32
  service = AppsService(core)
33
- service.enable(app.lower())
33
+ app = app.lower()
34
+ service.enable(app)
34
35
  ui.render_apps(service.list_status_rows())
36
+ ui.render_app_enabled_next_steps(app)
35
37
 
36
38
 
37
39
  @apps.command("disable", help="Disable app sync target.")
@@ -3,7 +3,7 @@
3
3
  import click
4
4
  from rich.console import Console
5
5
 
6
- from code_agnostic.cli.helpers import workspace_config_root
6
+ from code_agnostic.cli.helpers import validate_resource_name, workspace_config_root
7
7
  from code_agnostic.cli.options import workspace_option
8
8
  from code_agnostic.core.repository import CoreRepository
9
9
  from code_agnostic.rules.repository import RulesRepository
@@ -37,6 +37,7 @@ def rules_list(obj: dict[str, str], workspace: str | None) -> None:
37
37
  @workspace_option()
38
38
  @click.pass_obj
39
39
  def rules_remove(obj: dict[str, str], name: str, workspace: str | None) -> None:
40
+ validate_resource_name(name, "rule")
40
41
  core = CoreRepository()
41
42
  root = workspace_config_root(core, workspace)
42
43
 
@@ -5,7 +5,7 @@ import shutil
5
5
  import click
6
6
  from rich.console import Console
7
7
 
8
- from code_agnostic.cli.helpers import workspace_config_root
8
+ from code_agnostic.cli.helpers import validate_resource_name, workspace_config_root
9
9
  from code_agnostic.cli.options import workspace_option
10
10
  from code_agnostic.core.repository import CoreRepository
11
11
  from code_agnostic.tui import SyncConsoleUI
@@ -41,11 +41,17 @@ def skills_list(obj: dict[str, str], workspace: str | None) -> None:
41
41
  ]
42
42
  for source in skill_sources
43
43
  ]
44
- empty_message = (
45
- f"No workspace skills configured for {workspace} in "
46
- f"{compact_home_path(root / 'skills')}."
44
+ skill_dir = compact_home_path(root / "skills")
45
+ scope_message = (
46
+ f"No workspace skills configured for {workspace}"
47
47
  if workspace
48
- else f"No global skills configured in {compact_home_path(root / 'skills')}."
48
+ else "No global skills configured"
49
+ )
50
+ empty_message = (
51
+ f"{scope_message} in {skill_dir}.\n"
52
+ f"- Copy a skill into {skill_dir}/<name>\n"
53
+ "- code-agnostic plan\n"
54
+ "- code-agnostic apply"
49
55
  )
50
56
  ui.render_list(
51
57
  "skills",
@@ -60,6 +66,7 @@ def skills_list(obj: dict[str, str], workspace: str | None) -> None:
60
66
  @workspace_option()
61
67
  @click.pass_obj
62
68
  def skills_remove(obj: dict[str, str], name: str, workspace: str | None) -> None:
69
+ validate_resource_name(name, "skill")
63
70
  core = CoreRepository()
64
71
  root = workspace_config_root(core, workspace)
65
72
  skill_dir = root / "skills" / name
@@ -1,6 +1,6 @@
1
1
  """Shared helper functions for CLI commands."""
2
2
 
3
- from pathlib import Path
3
+ from pathlib import Path, PureWindowsPath
4
4
 
5
5
  import click
6
6
 
@@ -31,6 +31,18 @@ def workspace_config_root(core: CoreRepository, workspace: str | None) -> Path:
31
31
  return core.workspace_config_dir(workspace)
32
32
 
33
33
 
34
+ def validate_resource_name(name: str, resource_type: str) -> None:
35
+ if (
36
+ not name.strip()
37
+ or name in {".", ".."}
38
+ or "/" in name
39
+ or "\\" in name
40
+ or Path(name).is_absolute()
41
+ or PureWindowsPath(name).drive
42
+ ):
43
+ raise click.ClickException(f"Invalid {resource_type} name: {name}")
44
+
45
+
34
46
  def status_row_for_app(app_name: str, plan, apps: AppsService) -> EditorStatusRow:
35
47
  if not apps.is_enabled(app_name):
36
48
  return EditorStatusRow(