codexmgr 0.4.0__tar.gz → 0.5.0__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 (143) hide show
  1. {codexmgr-0.4.0 → codexmgr-0.5.0}/PKG-INFO +35 -17
  2. {codexmgr-0.4.0 → codexmgr-0.5.0}/README.md +33 -15
  3. {codexmgr-0.4.0 → codexmgr-0.5.0}/pyproject.toml +2 -2
  4. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/__init__.py +1 -1
  5. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/commands/codex_launch.py +12 -8
  6. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/core/paths.py +12 -0
  7. codexmgr-0.5.0/src/codexmgr/skills/candidates.py +138 -0
  8. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/skills/copies.py +53 -12
  9. codexmgr-0.5.0/src/codexmgr/skills/metadata.py +100 -0
  10. codexmgr-0.5.0/src/codexmgr/skills/resolution.py +119 -0
  11. codexmgr-0.5.0/src/codexmgr/skills/selector.py +240 -0
  12. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/skills/sources.py +13 -9
  13. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_apply_check_cli.py +4 -1
  14. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_codex_cli.py +32 -15
  15. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_packages_cli.py +17 -3
  16. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_paths.py +12 -1
  17. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_project_gitignore.py +3 -2
  18. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_skill_copies_cli.py +55 -17
  19. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_skill_home_sources_cli.py +4 -1
  20. codexmgr-0.5.0/tests/test_skill_metadata.py +35 -0
  21. codexmgr-0.5.0/tests/test_skill_portability_cli.py +281 -0
  22. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_skills_cli.py +17 -11
  23. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_status_doctor_cli.py +4 -1
  24. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_tui_app.py +4 -1
  25. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_tui_selection_focus.py +4 -1
  26. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_tui_state.py +4 -1
  27. {codexmgr-0.4.0 → codexmgr-0.5.0}/uv.lock +3 -3
  28. codexmgr-0.4.0/src/codexmgr/skills/resolution.py +0 -147
  29. {codexmgr-0.4.0 → codexmgr-0.5.0}/.github/actions/install_package/action.yml +0 -0
  30. {codexmgr-0.4.0 → codexmgr-0.5.0}/.github/workflows/py_test.yml +0 -0
  31. {codexmgr-0.4.0 → codexmgr-0.5.0}/.github/workflows/version_publish_main.yml +0 -0
  32. {codexmgr-0.4.0 → codexmgr-0.5.0}/.gitignore +0 -0
  33. {codexmgr-0.4.0 → codexmgr-0.5.0}/pytest.toml +0 -0
  34. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/agents/__init__.py +0 -0
  35. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/agents/file.py +0 -0
  36. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/agents/manager.py +0 -0
  37. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/agents/renderer.py +0 -0
  38. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/commands/__init__.py +0 -0
  39. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/commands/codex.py +0 -0
  40. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/commands/codex_jit.py +0 -0
  41. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/commands/config_mutations.py +0 -0
  42. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/commands/health.py +0 -0
  43. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/commands/navigation.py +0 -0
  44. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/commands/rules.py +0 -0
  45. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/core/__init__.py +0 -0
  46. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/core/errors.py +0 -0
  47. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/core/managed_text.py +0 -0
  48. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/core/options.py +0 -0
  49. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/core/toml_io.py +0 -0
  50. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/custom_agents/__init__.py +0 -0
  51. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/custom_agents/cli.py +0 -0
  52. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/custom_agents/config.py +0 -0
  53. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/custom_agents/copies.py +0 -0
  54. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/custom_agents/listing.py +0 -0
  55. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/custom_agents/resolution.py +0 -0
  56. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/custom_agents/sources.py +0 -0
  57. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/hooks/__init__.py +0 -0
  58. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/hooks/config.py +0 -0
  59. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/hooks/copies.py +0 -0
  60. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/hooks/listing.py +0 -0
  61. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/hooks/merge.py +0 -0
  62. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/hooks/resolution.py +0 -0
  63. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/hooks/sources.py +0 -0
  64. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/interface/__init__.py +0 -0
  65. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/interface/cli.py +0 -0
  66. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/interface/parser.py +0 -0
  67. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/interface/parsers/__init__.py +0 -0
  68. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/interface/parsers/agents.py +0 -0
  69. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/interface/parsers/mcp.py +0 -0
  70. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/mcp/__init__.py +0 -0
  71. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/mcp/apply.py +0 -0
  72. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/mcp/cli.py +0 -0
  73. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/mcp/config.py +0 -0
  74. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/mcp/discovery.py +0 -0
  75. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/mcp/fields.py +0 -0
  76. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/mcp/project.py +0 -0
  77. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/mcp/resolution.py +0 -0
  78. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/mcp/sources.py +0 -0
  79. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/mcp/tables.py +0 -0
  80. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/packages/__init__.py +0 -0
  81. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/packages/cli.py +0 -0
  82. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/packages/config.py +0 -0
  83. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/packages/listing.py +0 -0
  84. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/packages/mutation.py +0 -0
  85. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/packages/sources.py +0 -0
  86. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/project/__init__.py +0 -0
  87. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/project/apply.py +0 -0
  88. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/project/config.py +0 -0
  89. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/project/generated.py +0 -0
  90. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/project/gitignore.py +0 -0
  91. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/project/resolution.py +0 -0
  92. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/project/snapshot.py +0 -0
  93. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/project/state.py +0 -0
  94. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/project/sync.py +0 -0
  95. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/py.typed +0 -0
  96. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/rules/__init__.py +0 -0
  97. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/rules/config.py +0 -0
  98. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/rules/copies.py +0 -0
  99. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/rules/listing.py +0 -0
  100. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/rules/resolution.py +0 -0
  101. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/rules/sources.py +0 -0
  102. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/rules/tree.py +0 -0
  103. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/skills/__init__.py +0 -0
  104. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/skills/config.py +0 -0
  105. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/skills/listing.py +0 -0
  106. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/__init__.py +0 -0
  107. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/app.py +0 -0
  108. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/cli.py +0 -0
  109. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/diff.py +0 -0
  110. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/focus.py +0 -0
  111. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/items.py +0 -0
  112. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/mcp_items.py +0 -0
  113. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/models.py +0 -0
  114. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/mutations.py +0 -0
  115. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/package_refs.py +0 -0
  116. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/package_selection.py +0 -0
  117. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/package_state.py +0 -0
  118. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/panels.py +0 -0
  119. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/rendering.py +0 -0
  120. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/rule_items.py +0 -0
  121. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/rule_tree.py +0 -0
  122. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/sections.py +0 -0
  123. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/staged_packages.py +0 -0
  124. {codexmgr-0.4.0 → codexmgr-0.5.0}/src/codexmgr/tui/state.py +0 -0
  125. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/conftest.py +0 -0
  126. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_agents_cli.py +0 -0
  127. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_agents_file.py +0 -0
  128. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_agentsmd_list_cli.py +0 -0
  129. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_agentsmd_tools_cli.py +0 -0
  130. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_cd_cli.py +0 -0
  131. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_cli.py +0 -0
  132. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_empty_skill_config_cli.py +0 -0
  133. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_home_resolution_cli.py +0 -0
  134. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_hooks_cli.py +0 -0
  135. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_mcp_apply_cli.py +0 -0
  136. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_mcp_cli.py +0 -0
  137. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_package_metadata.py +0 -0
  138. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_python_compatibility.py +0 -0
  139. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_renderer.py +0 -0
  140. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_rules_cli.py +0 -0
  141. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_skill_list_cli.py +0 -0
  142. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_sync_cli.py +0 -0
  143. {codexmgr-0.4.0 → codexmgr-0.5.0}/tests/test_toml_io.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codexmgr
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Manage project-local Codex configuration from reusable templates
5
5
  Project-URL: Repository, https://github.com/JulianKimmig/codexmgr/
6
6
  Keywords: agents,cli,codex,configuration,skills
@@ -14,12 +14,12 @@ Classifier: Programming Language :: Python :: 3.13
14
14
  Classifier: Topic :: Software Development
15
15
  Classifier: Typing :: Typed
16
16
  Requires-Python: >=3.11
17
+ Requires-Dist: pyyaml>=6.0.3
17
18
  Requires-Dist: textual>=8.2.7
18
19
  Requires-Dist: tomlkit>=0.15.0
19
20
  Provides-Extra: dev
20
21
  Requires-Dist: pytest-asyncio>=1.4.0; extra == 'dev'
21
22
  Requires-Dist: pytest>=9.0.3; extra == 'dev'
22
- Requires-Dist: pyyaml>=6.0.3; extra == 'dev'
23
23
  Description-Content-Type: text/markdown
24
24
 
25
25
  # codexmgr
@@ -211,17 +211,30 @@ Named `AGENTS.md` snippets resolve from
211
211
  `$CODEXMGR_HOME/agentsmd/<name>.toml`. Path-like snippet values resolve
212
212
  relative to the project unless they are absolute paths.
213
213
 
214
- Named skills resolve from `$CODEXMGR_HOME/skills/<name>/SKILL.md` or
215
- `$CODEX_HOME/skills/<name>/SKILL.md`. Duplicate names across distinct homes
216
- fail so the selected skill is not ambiguous.
214
+ Bare skill names resolve across the project `.agents/skills`,
215
+ `$CODEXMGR_HOME/skills`, and `$CODEX_HOME/skills` stores. If the same folder
216
+ name exists in more than one store, apply fails and asks for an explicit path.
217
+ The project copy recorded for an enabled `$CODEXMGR_HOME` skill is recognized
218
+ as a managed mirror and does not create a false collision on later applies.
217
219
 
218
220
  Enabled skills from `$CODEXMGR_HOME` are copied into `.agents/skills/<name>` on
219
221
  every apply. The copy overlays source files while preserving extra local files.
220
222
  Path-like skill values can point to a `SKILL.md` file or a directory containing
221
- `SKILL.md`.
222
-
223
- Missing skills are still written as name-based entries. That lets Codex resolve
224
- them later from another installed skill source.
223
+ `SKILL.md`; a path-like value that does not exist is an error.
224
+
225
+ Project-local and copied manager-home skills generate portable `name` selectors
226
+ using the `name` value in their `SKILL.md` YAML frontmatter. Selected skills in
227
+ those stores must therefore have valid frontmatter and a non-empty name. Apply
228
+ also fails if two discoverable skills declare the same generated name. Skills
229
+ resolved directly from `$CODEX_HOME` and explicit path references keep absolute
230
+ `path` selectors because those sources are machine-specific or explicitly
231
+ requested. Missing bare names remain name-based entries so Codex can resolve
232
+ them later from another installed source.
233
+
234
+ Managed skill-copy lock entries use the logical source `codexmgr_home` and a
235
+ project-relative `.agents/skills/<name>` target. Apply accepts legacy absolute
236
+ copy entries and rewrites them in this portable form, so moving or cloning a
237
+ project does not churn generated skill config or lock state.
225
238
 
226
239
  Named custom agents resolve from `$CODEXMGR_HOME/agents/<name>.toml`. Enabled
227
240
  agents are copied into `.codex/agents/<name>.toml`; disabled agents remove the
@@ -474,14 +487,19 @@ codexmgr codex --simple <args...>
474
487
  ```
475
488
 
476
489
  By default, `codexmgr codex` applies the current project config, launches the
477
- real `codex` command with `CODEX_HOME` set to `<project>/.codex`, and forwards
478
- Codex arguments unchanged. Codex reads `.codex/config.toml` directly, so
479
- user-provided `-c` and `--config` arguments retain their normal Codex behavior.
480
-
481
- The wrapper links the global authentication file into `.codex/auth.json`. It
482
- uses `CODEX_GLOBAL_AUTH` when that variable is non-empty and otherwise uses
483
- `$HOME/.codex/auth.json`. A missing auth source produces a warning but does not
484
- stop Codex, which can then authenticate normally.
490
+ real `codex` command with `CODEX_HOME` set to
491
+ `<project>/.codex/.runtime`, and forwards Codex arguments unchanged. For a
492
+ trusted project, Codex discovers the tracked `.codex/config.toml` as its
493
+ project configuration independently of `CODEX_HOME`. This keeps generated MCP
494
+ servers and other shared project settings in Git while mutable Codex state is
495
+ written below the ignored `.codex/.runtime` directory. User-provided `-c` and
496
+ `--config` arguments retain their normal Codex behavior.
497
+
498
+ The wrapper links the global authentication file into
499
+ `.codex/.runtime/auth.json`. It uses `CODEX_GLOBAL_AUTH` when that variable is
500
+ non-empty and otherwise uses `$HOME/.codex/auth.json`. A missing auth source
501
+ produces a warning but does not stop Codex, which can then authenticate within
502
+ the project runtime home.
485
503
 
486
504
  Use `--simple` immediately after `codex` to run the basic command without
487
505
  applying project state, changing the child `CODEX_HOME`, or managing an auth
@@ -187,17 +187,30 @@ Named `AGENTS.md` snippets resolve from
187
187
  `$CODEXMGR_HOME/agentsmd/<name>.toml`. Path-like snippet values resolve
188
188
  relative to the project unless they are absolute paths.
189
189
 
190
- Named skills resolve from `$CODEXMGR_HOME/skills/<name>/SKILL.md` or
191
- `$CODEX_HOME/skills/<name>/SKILL.md`. Duplicate names across distinct homes
192
- fail so the selected skill is not ambiguous.
190
+ Bare skill names resolve across the project `.agents/skills`,
191
+ `$CODEXMGR_HOME/skills`, and `$CODEX_HOME/skills` stores. If the same folder
192
+ name exists in more than one store, apply fails and asks for an explicit path.
193
+ The project copy recorded for an enabled `$CODEXMGR_HOME` skill is recognized
194
+ as a managed mirror and does not create a false collision on later applies.
193
195
 
194
196
  Enabled skills from `$CODEXMGR_HOME` are copied into `.agents/skills/<name>` on
195
197
  every apply. The copy overlays source files while preserving extra local files.
196
198
  Path-like skill values can point to a `SKILL.md` file or a directory containing
197
- `SKILL.md`.
198
-
199
- Missing skills are still written as name-based entries. That lets Codex resolve
200
- them later from another installed skill source.
199
+ `SKILL.md`; a path-like value that does not exist is an error.
200
+
201
+ Project-local and copied manager-home skills generate portable `name` selectors
202
+ using the `name` value in their `SKILL.md` YAML frontmatter. Selected skills in
203
+ those stores must therefore have valid frontmatter and a non-empty name. Apply
204
+ also fails if two discoverable skills declare the same generated name. Skills
205
+ resolved directly from `$CODEX_HOME` and explicit path references keep absolute
206
+ `path` selectors because those sources are machine-specific or explicitly
207
+ requested. Missing bare names remain name-based entries so Codex can resolve
208
+ them later from another installed source.
209
+
210
+ Managed skill-copy lock entries use the logical source `codexmgr_home` and a
211
+ project-relative `.agents/skills/<name>` target. Apply accepts legacy absolute
212
+ copy entries and rewrites them in this portable form, so moving or cloning a
213
+ project does not churn generated skill config or lock state.
201
214
 
202
215
  Named custom agents resolve from `$CODEXMGR_HOME/agents/<name>.toml`. Enabled
203
216
  agents are copied into `.codex/agents/<name>.toml`; disabled agents remove the
@@ -450,14 +463,19 @@ codexmgr codex --simple <args...>
450
463
  ```
451
464
 
452
465
  By default, `codexmgr codex` applies the current project config, launches the
453
- real `codex` command with `CODEX_HOME` set to `<project>/.codex`, and forwards
454
- Codex arguments unchanged. Codex reads `.codex/config.toml` directly, so
455
- user-provided `-c` and `--config` arguments retain their normal Codex behavior.
456
-
457
- The wrapper links the global authentication file into `.codex/auth.json`. It
458
- uses `CODEX_GLOBAL_AUTH` when that variable is non-empty and otherwise uses
459
- `$HOME/.codex/auth.json`. A missing auth source produces a warning but does not
460
- stop Codex, which can then authenticate normally.
466
+ real `codex` command with `CODEX_HOME` set to
467
+ `<project>/.codex/.runtime`, and forwards Codex arguments unchanged. For a
468
+ trusted project, Codex discovers the tracked `.codex/config.toml` as its
469
+ project configuration independently of `CODEX_HOME`. This keeps generated MCP
470
+ servers and other shared project settings in Git while mutable Codex state is
471
+ written below the ignored `.codex/.runtime` directory. User-provided `-c` and
472
+ `--config` arguments retain their normal Codex behavior.
473
+
474
+ The wrapper links the global authentication file into
475
+ `.codex/.runtime/auth.json`. It uses `CODEX_GLOBAL_AUTH` when that variable is
476
+ non-empty and otherwise uses `$HOME/.codex/auth.json`. A missing auth source
477
+ produces a warning but does not stop Codex, which can then authenticate within
478
+ the project runtime home.
461
479
 
462
480
  Use `--simple` immediately after `codex` to run the basic command without
463
481
  applying project state, changing the child `CODEX_HOME`, or managing an auth
@@ -1,10 +1,11 @@
1
1
  [project]
2
2
  name = "codexmgr"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "Manage project-local Codex configuration from reusable templates"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
7
7
  dependencies = [
8
+ "pyyaml>=6.0.3",
8
9
  "textual>=8.2.7",
9
10
  "tomlkit>=0.15.0",
10
11
  ]
@@ -30,7 +31,6 @@ codexmgr = "codexmgr.interface.cli:entrypoint"
30
31
  [project.optional-dependencies]
31
32
  dev = [
32
33
  "pytest>=9.0.3",
33
- "pyyaml>=6.0.3",
34
34
  "pytest-asyncio>=1.4.0",
35
35
  ]
36
36
 
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.4.0"
5
+ __version__ = "0.5.0"
@@ -6,6 +6,8 @@ from dataclasses import dataclass
6
6
  from pathlib import Path
7
7
  from typing import TextIO
8
8
 
9
+ from ..core.paths import project_codex_dir, project_codex_runtime_dir
10
+
9
11
 
10
12
  @dataclass(frozen=True)
11
13
  class CodexLaunchRequest:
@@ -47,19 +49,21 @@ def build_project_codex_environment(
47
49
  environ: Source process environment, or ``os.environ`` when omitted.
48
50
 
49
51
  Returns:
50
- A copied child environment. ``CODEX_HOME`` points to ``.codex`` when
51
- that directory contains ``config.toml``.
52
+ A copied child environment. ``CODEX_HOME`` points to
53
+ ``.codex/.runtime`` when the project ``.codex`` directory contains
54
+ ``config.toml``.
52
55
  """
53
56
  environment = dict(os.environ if environ is None else environ)
54
- project_codex_dir = cwd / ".codex"
55
- if not (project_codex_dir / "config.toml").is_file():
57
+ config_dir = project_codex_dir(cwd)
58
+ if not (config_dir / "config.toml").is_file():
56
59
  return environment
57
60
 
58
- project_codex_dir.mkdir(parents=True, exist_ok=True)
59
- environment["CODEX_HOME"] = str(project_codex_dir)
61
+ runtime_dir = project_codex_runtime_dir(cwd)
62
+ runtime_dir.mkdir(parents=True, exist_ok=True)
63
+ environment["CODEX_HOME"] = str(runtime_dir)
60
64
  global_auth = _global_auth_path(environment)
61
65
  if global_auth.is_file():
62
- _replace_auth_link(project_codex_dir / "auth.json", global_auth)
66
+ _replace_auth_link(runtime_dir / "auth.json", global_auth)
63
67
  else:
64
68
  stderr.write(
65
69
  "codexmgr codex: warning: no global auth found at "
@@ -87,7 +91,7 @@ def _replace_auth_link(auth_link: Path, global_auth: Path) -> None:
87
91
  """Replace the project auth entry with a link to global authentication.
88
92
 
89
93
  Args:
90
- auth_link: Project-local ``.codex/auth.json`` link path.
94
+ auth_link: Project-local ``.codex/.runtime/auth.json`` link path.
91
95
  global_auth: Existing global authentication source file.
92
96
 
93
97
  Returns:
@@ -36,6 +36,18 @@ def project_codex_dir(cwd: Path) -> Path:
36
36
  return cwd / ".codex"
37
37
 
38
38
 
39
+ def project_codex_runtime_dir(cwd: Path) -> Path:
40
+ """Return the project-local Codex runtime-home path.
41
+
42
+ Args:
43
+ cwd: Project directory.
44
+
45
+ Returns:
46
+ The ignored ``.codex/.runtime`` directory used as ``CODEX_HOME``.
47
+ """
48
+ return project_codex_dir(cwd) / ".runtime"
49
+
50
+
39
51
  def config_path(cwd: Path) -> Path:
40
52
  """Return the project codexmgr.toml path.
41
53
 
@@ -0,0 +1,138 @@
1
+ """Discover every source that may satisfy a configured skill reference."""
2
+
3
+ from pathlib import Path
4
+
5
+ from .sources import (
6
+ CODEX_HOME_SOURCE,
7
+ CODEXMGR_HOME_SOURCE,
8
+ LOCAL_SOURCE,
9
+ PATH_SOURCE,
10
+ SkillSource,
11
+ is_named_skill,
12
+ project_skill_dir,
13
+ )
14
+
15
+
16
+ def skill_reference_candidates(
17
+ skill: str,
18
+ cwd: Path,
19
+ codex_home: Path,
20
+ codexmgr_home: Path,
21
+ ) -> list[SkillSource]:
22
+ """Return every existing source matching one configured reference.
23
+
24
+ Args:
25
+ skill: Bare skill name or explicit path reference.
26
+ cwd: Project directory used for local and relative-path skills.
27
+ codex_home: Codex home directory.
28
+ codexmgr_home: codexmgr home directory.
29
+
30
+ Returns:
31
+ Matching sources in stable store-priority order.
32
+ """
33
+ if is_named_skill(skill):
34
+ return _named_skill_candidates(skill, cwd, codex_home, codexmgr_home)
35
+ source = _resolve_path_skill(skill, cwd)
36
+ return [] if source is None else [source]
37
+
38
+
39
+ def all_named_skill_sources(
40
+ cwd: Path,
41
+ codex_home: Path,
42
+ codexmgr_home: Path,
43
+ ) -> list[SkillSource]:
44
+ """Return all folder-addressable skill sources from configured stores.
45
+
46
+ Args:
47
+ cwd: Project directory whose local skills should be included.
48
+ codex_home: Codex home directory.
49
+ codexmgr_home: codexmgr home directory.
50
+
51
+ Returns:
52
+ Sources ordered by store and folder name.
53
+ """
54
+ stores = [
55
+ (CODEXMGR_HOME_SOURCE, codexmgr_home / "skills"),
56
+ (CODEX_HOME_SOURCE, codex_home / "skills"),
57
+ (LOCAL_SOURCE, cwd / ".agents" / "skills"),
58
+ ]
59
+ sources: list[SkillSource] = []
60
+ for source_type, root in stores:
61
+ for name in _skill_names(root):
62
+ skill_file = root / name / "SKILL.md"
63
+ sources.append(SkillSource(name, skill_file.resolve(), source_type))
64
+ return sources
65
+
66
+
67
+ def _named_skill_candidates(
68
+ name: str,
69
+ cwd: Path,
70
+ codex_home: Path,
71
+ codexmgr_home: Path,
72
+ ) -> list[SkillSource]:
73
+ """Return every store source whose folder matches a bare name.
74
+
75
+ Args:
76
+ name: Bare skill folder name.
77
+ cwd: Project directory used for local skills.
78
+ codex_home: Codex home directory.
79
+ codexmgr_home: codexmgr home directory.
80
+
81
+ Returns:
82
+ Existing sources in codexmgr-home, Codex-home, then project order.
83
+ """
84
+ candidates = [
85
+ SkillSource(
86
+ name,
87
+ (codexmgr_home / "skills" / name / "SKILL.md").resolve(),
88
+ CODEXMGR_HOME_SOURCE,
89
+ ),
90
+ SkillSource(
91
+ name,
92
+ (codex_home / "skills" / name / "SKILL.md").resolve(),
93
+ CODEX_HOME_SOURCE,
94
+ ),
95
+ SkillSource(
96
+ name,
97
+ (project_skill_dir(cwd, name) / "SKILL.md").resolve(),
98
+ LOCAL_SOURCE,
99
+ ),
100
+ ]
101
+ return [candidate for candidate in candidates if candidate.skill_file.is_file()]
102
+
103
+
104
+ def _resolve_path_skill(skill: str, cwd: Path) -> SkillSource | None:
105
+ """Resolve a path-like reference without applying named-store priority.
106
+
107
+ Args:
108
+ skill: Path-like skill reference.
109
+ cwd: Project directory used for relative paths.
110
+
111
+ Returns:
112
+ Resolved explicit source, or None when it does not exist.
113
+ """
114
+ path = Path(skill).expanduser()
115
+ if not path.is_absolute():
116
+ path = cwd / path
117
+ skill_file = path if path.name == "SKILL.md" else path / "SKILL.md"
118
+ if not skill_file.is_file():
119
+ return None
120
+ return SkillSource(skill, skill_file.resolve(), PATH_SOURCE)
121
+
122
+
123
+ def _skill_names(skills_dir: Path) -> list[str]:
124
+ """Return sorted child-folder names containing a skill file.
125
+
126
+ Args:
127
+ skills_dir: Store directory containing named skill folders.
128
+
129
+ Returns:
130
+ Sorted folder names with an immediate ``SKILL.md`` child.
131
+ """
132
+ if not skills_dir.is_dir():
133
+ return []
134
+ return sorted(
135
+ path.name
136
+ for path in skills_dir.iterdir()
137
+ if path.is_dir() and (path / "SKILL.md").is_file()
138
+ )
@@ -8,6 +8,7 @@ from typing import Any
8
8
 
9
9
  from ..core.errors import CommandError
10
10
  from ..core.toml_io import plain_toml_value
11
+ from .sources import CODEXMGR_HOME_SOURCE, project_skill_dir
11
12
 
12
13
 
13
14
  @dataclass(frozen=True)
@@ -38,24 +39,39 @@ class SkillCopyFile:
38
39
  content: bytes
39
40
 
40
41
 
41
- def validate_copy_targets(copies: list[SkillCopy], previous_lock: Mapping[str, Any]) -> None:
42
+ def validate_copy_targets(
43
+ copies: list[SkillCopy],
44
+ previous_lock: Mapping[str, Any],
45
+ cwd: Path,
46
+ codexmgr_home: Path,
47
+ ) -> None:
42
48
  """Reject first-time copies over unmanaged target folders.
43
49
 
44
50
  Args:
45
51
  copies: Current managed copies to create or refresh.
46
52
  previous_lock: Previous codexmgr lock data.
53
+ cwd: Current project root used to rebind portable targets.
54
+ codexmgr_home: Current codexmgr home used to rebind portable sources.
47
55
  """
48
- previous = previous_skill_copies(previous_lock)
56
+ previous = previous_skill_copies(previous_lock, cwd, codexmgr_home)
49
57
  for copy in copies:
50
58
  if copy.name not in previous and copy.target.exists():
51
- raise CommandError(f"Refusing to overwrite unmanaged skill copy: {copy.target}")
59
+ raise CommandError(
60
+ f"Refusing to overwrite unmanaged skill copy: {copy.target}"
61
+ )
52
62
 
53
63
 
54
- def previous_skill_copies(previous_lock: Mapping[str, Any]) -> dict[str, SkillCopy]:
64
+ def previous_skill_copies(
65
+ previous_lock: Mapping[str, Any],
66
+ cwd: Path,
67
+ codexmgr_home: Path,
68
+ ) -> dict[str, SkillCopy]:
55
69
  """Read managed skill-copy metadata from previous lock data.
56
70
 
57
71
  Args:
58
72
  previous_lock: Parsed .codex/codexmgr.lock data.
73
+ cwd: Current project root used to rebind portable targets.
74
+ codexmgr_home: Current codexmgr home used to rebind portable sources.
59
75
 
60
76
  Returns:
61
77
  Previous managed copies keyed by skill name.
@@ -65,7 +81,7 @@ def previous_skill_copies(previous_lock: Mapping[str, Any]) -> dict[str, SkillCo
65
81
  raise CommandError("codexmgr.lock skills.copies must be a list")
66
82
  copies: dict[str, SkillCopy] = {}
67
83
  for raw_copy in raw_copies:
68
- copy = _copy_from_lock_entry(raw_copy)
84
+ copy = _copy_from_lock_entry(raw_copy, cwd, codexmgr_home)
69
85
  copies[copy.name] = copy
70
86
  return copies
71
87
 
@@ -73,12 +89,16 @@ def previous_skill_copies(previous_lock: Mapping[str, Any]) -> dict[str, SkillCo
73
89
  def obsolete_copy_targets(
74
90
  previous_lock: Mapping[str, Any],
75
91
  current_copies: list[SkillCopy],
92
+ cwd: Path,
93
+ codexmgr_home: Path,
76
94
  ) -> list[Path]:
77
95
  """Return previous managed copy targets absent from current state.
78
96
 
79
97
  Args:
80
98
  previous_lock: Previous codexmgr lock data.
81
99
  current_copies: Current managed copies.
100
+ cwd: Current project root used to rebind portable targets.
101
+ codexmgr_home: Current codexmgr home used to rebind portable sources.
82
102
 
83
103
  Returns:
84
104
  Sorted target directories to remove.
@@ -86,7 +106,11 @@ def obsolete_copy_targets(
86
106
  current_names = {copy.name for copy in current_copies}
87
107
  return sorted(
88
108
  copy.target
89
- for name, copy in previous_skill_copies(previous_lock).items()
109
+ for name, copy in previous_skill_copies(
110
+ previous_lock,
111
+ cwd,
112
+ codexmgr_home,
113
+ ).items()
90
114
  if name not in current_names
91
115
  )
92
116
 
@@ -103,8 +127,8 @@ def copy_lock_entries(copies: list[SkillCopy]) -> list[dict[str, str]]:
103
127
  return [
104
128
  {
105
129
  "name": copy.name,
106
- "source": str(copy.source.resolve()),
107
- "target": str(copy.target.resolve()),
130
+ "source": CODEXMGR_HOME_SOURCE,
131
+ "target": f".agents/skills/{copy.name}",
108
132
  }
109
133
  for copy in copies
110
134
  ]
@@ -156,11 +180,17 @@ def remove_skill_copy_target(target: Path) -> None:
156
180
  target.unlink()
157
181
 
158
182
 
159
- def _copy_from_lock_entry(raw_copy: Any) -> SkillCopy:
183
+ def _copy_from_lock_entry(
184
+ raw_copy: Any,
185
+ cwd: Path,
186
+ codexmgr_home: Path,
187
+ ) -> SkillCopy:
160
188
  """Parse one skill-copy lock entry.
161
189
 
162
190
  Args:
163
191
  raw_copy: Plain lock entry value.
192
+ cwd: Current project root used to rebind the target.
193
+ codexmgr_home: Current codexmgr home used to rebind the source.
164
194
 
165
195
  Returns:
166
196
  Parsed managed skill copy.
@@ -170,9 +200,20 @@ def _copy_from_lock_entry(raw_copy: Any) -> SkillCopy:
170
200
  name = raw_copy.get("name")
171
201
  source = raw_copy.get("source")
172
202
  target = raw_copy.get("target")
173
- if not isinstance(name, str) or not isinstance(source, str) or not isinstance(target, str):
174
- raise CommandError("codexmgr.lock skills.copies entries must include name, source, and target")
175
- return SkillCopy(name, Path(source), Path(target))
203
+ if not all(isinstance(value, str) for value in (name, source, target)):
204
+ raise CommandError(
205
+ "codexmgr.lock skills.copies entries must include name, source, "
206
+ "and target"
207
+ )
208
+ if name in {"", ".", ".."} or Path(name).name != name:
209
+ raise CommandError(
210
+ "codexmgr.lock skills.copies entries must use a safe skill name"
211
+ )
212
+ return SkillCopy(
213
+ name,
214
+ codexmgr_home / "skills" / name,
215
+ project_skill_dir(cwd, name),
216
+ )
176
217
 
177
218
 
178
219
  def _source_dirs(source: Path) -> list[Path]:
@@ -0,0 +1,100 @@
1
+ """Read and validate metadata declared by Codex skill instruction files."""
2
+
3
+ from collections.abc import Mapping
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import yaml
8
+ from yaml import YAMLError
9
+
10
+ from ..core.errors import CommandError
11
+
12
+
13
+ def read_skill_name(skill_file: Path) -> str:
14
+ """Return the non-empty skill name declared in YAML frontmatter.
15
+
16
+ Args:
17
+ skill_file: Existing ``SKILL.md`` file to inspect.
18
+
19
+ Returns:
20
+ The trimmed frontmatter ``name`` value.
21
+
22
+ Raises:
23
+ CommandError: When frontmatter is missing, malformed, or has no valid
24
+ ``name`` value.
25
+ """
26
+ metadata = _read_frontmatter(skill_file)
27
+ name = metadata.get("name")
28
+ if not isinstance(name, str) or not name.strip():
29
+ raise _metadata_error(skill_file, "name must be a non-empty string")
30
+ return name.strip()
31
+
32
+
33
+ def try_read_skill_name(skill_file: Path) -> str | None:
34
+ """Return a declared skill name or ``None`` for unrelated invalid files.
35
+
36
+ Args:
37
+ skill_file: Existing ``SKILL.md`` file to inspect.
38
+
39
+ Returns:
40
+ The declared name when metadata is valid, otherwise ``None``.
41
+ """
42
+ try:
43
+ return read_skill_name(skill_file)
44
+ except CommandError:
45
+ return None
46
+
47
+
48
+ def _read_frontmatter(skill_file: Path) -> Mapping[str, Any]:
49
+ """Parse the YAML mapping at the beginning of one skill file.
50
+
51
+ Args:
52
+ skill_file: Existing ``SKILL.md`` file to inspect.
53
+
54
+ Returns:
55
+ Parsed YAML frontmatter mapping.
56
+
57
+ Raises:
58
+ CommandError: When the frontmatter boundaries or YAML are invalid.
59
+ """
60
+ lines = skill_file.read_text(encoding="utf-8").splitlines()
61
+ if not lines or lines[0].strip() != "---":
62
+ raise _metadata_error(skill_file, "missing YAML frontmatter")
63
+ closing_index = _closing_boundary(lines)
64
+ if closing_index is None:
65
+ raise _metadata_error(skill_file, "missing YAML frontmatter closing boundary")
66
+ try:
67
+ parsed = yaml.safe_load("\n".join(lines[1:closing_index]))
68
+ except YAMLError as exc:
69
+ raise _metadata_error(skill_file, "invalid YAML frontmatter") from exc
70
+ if not isinstance(parsed, Mapping):
71
+ raise _metadata_error(skill_file, "YAML frontmatter must be a mapping")
72
+ return parsed
73
+
74
+
75
+ def _closing_boundary(lines: list[str]) -> int | None:
76
+ """Return the closing frontmatter-boundary index when present.
77
+
78
+ Args:
79
+ lines: Complete skill file split into lines.
80
+
81
+ Returns:
82
+ Closing boundary index, or ``None`` when it is missing.
83
+ """
84
+ for index, line in enumerate(lines[1:], start=1):
85
+ if line.strip() == "---":
86
+ return index
87
+ return None
88
+
89
+
90
+ def _metadata_error(skill_file: Path, detail: str) -> CommandError:
91
+ """Build one consistently formatted metadata error.
92
+
93
+ Args:
94
+ skill_file: Invalid skill instruction file.
95
+ detail: Focused validation failure.
96
+
97
+ Returns:
98
+ Command error containing the resolved file location and detail.
99
+ """
100
+ return CommandError(f"Invalid skill metadata in {skill_file.resolve()}: {detail}")