rolesync 1.0.0rc1__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 (61) hide show
  1. rolesync-1.0.0rc1/LICENSE +21 -0
  2. rolesync-1.0.0rc1/PKG-INFO +141 -0
  3. rolesync-1.0.0rc1/README.md +118 -0
  4. rolesync-1.0.0rc1/pyproject.toml +43 -0
  5. rolesync-1.0.0rc1/setup.cfg +4 -0
  6. rolesync-1.0.0rc1/src/rolesync/__init__.py +4 -0
  7. rolesync-1.0.0rc1/src/rolesync/__main__.py +3 -0
  8. rolesync-1.0.0rc1/src/rolesync/cli.py +211 -0
  9. rolesync-1.0.0rc1/src/rolesync/core.py +546 -0
  10. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/AGENTS.md +7 -0
  11. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/CLAUDE.md +1 -0
  12. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/catalog.json +342 -0
  13. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/common.md +1 -0
  14. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/policy.json +55 -0
  15. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/references/SOURCES.md +36 -0
  16. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/references/evaluation.md +33 -0
  17. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/references/git-pr-lifecycle.md +52 -0
  18. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/references/handoff-contract.md +70 -0
  19. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/references/routing-and-budgets.md +56 -0
  20. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/references/runtime-and-security.md +53 -0
  21. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/roles/advanced-coder.md +9 -0
  22. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/roles/architect.md +9 -0
  23. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/roles/ci-investigator.md +9 -0
  24. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/roles/deep-rescue.md +9 -0
  25. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/roles/documentation-updater.md +9 -0
  26. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/roles/general-coder.md +9 -0
  27. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/roles/issue-filer.md +9 -0
  28. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/roles/orchestrator.md +15 -0
  29. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/roles/pr-manager.md +9 -0
  30. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/roles/reader.md +7 -0
  31. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/roles/reviewer.md +11 -0
  32. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/roles/test-maintainer.md +9 -0
  33. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/skills/example-architecture/SKILL.md +12 -0
  34. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/skills/example-ci/SKILL.md +12 -0
  35. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/skills/example-context/SKILL.md +12 -0
  36. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/skills/example-document/SKILL.md +12 -0
  37. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/skills/example-escalate/SKILL.md +14 -0
  38. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/skills/example-implement/SKILL.md +14 -0
  39. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/skills/example-issue/SKILL.md +14 -0
  40. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/skills/example-orchestrate/SKILL.md +20 -0
  41. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/skills/example-pr/SKILL.md +16 -0
  42. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/skills/example-review/SKILL.md +23 -0
  43. rolesync-1.0.0rc1/src/rolesync/presets/github-workflow/skills/example-test/SKILL.md +14 -0
  44. rolesync-1.0.0rc1/src/rolesync/presets/minimal/AGENTS.md +5 -0
  45. rolesync-1.0.0rc1/src/rolesync/presets/minimal/CLAUDE.md +1 -0
  46. rolesync-1.0.0rc1/src/rolesync/presets/minimal/catalog.json +34 -0
  47. rolesync-1.0.0rc1/src/rolesync/presets/minimal/common.md +1 -0
  48. rolesync-1.0.0rc1/src/rolesync/presets/minimal/policy.json +6 -0
  49. rolesync-1.0.0rc1/src/rolesync/presets/minimal/roles/coder.md +5 -0
  50. rolesync-1.0.0rc1/src/rolesync/presets/minimal/roles/orchestrator.md +5 -0
  51. rolesync-1.0.0rc1/src/rolesync/presets/minimal/roles/reviewer.md +5 -0
  52. rolesync-1.0.0rc1/src/rolesync/presets/minimal/skills/loom-implement/SKILL.md +8 -0
  53. rolesync-1.0.0rc1/src/rolesync/presets/minimal/skills/loom-orchestrate/SKILL.md +8 -0
  54. rolesync-1.0.0rc1/src/rolesync/presets/minimal/skills/loom-review/SKILL.md +8 -0
  55. rolesync-1.0.0rc1/src/rolesync.egg-info/PKG-INFO +141 -0
  56. rolesync-1.0.0rc1/src/rolesync.egg-info/SOURCES.txt +59 -0
  57. rolesync-1.0.0rc1/src/rolesync.egg-info/dependency_links.txt +1 -0
  58. rolesync-1.0.0rc1/src/rolesync.egg-info/entry_points.txt +2 -0
  59. rolesync-1.0.0rc1/src/rolesync.egg-info/top_level.txt +1 -0
  60. rolesync-1.0.0rc1/tests/test_cli.py +81 -0
  61. rolesync-1.0.0rc1/tests/test_core.py +188 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Snarebox LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: rolesync
3
+ Version: 1.0.0rc1
4
+ Summary: Generate native Claude Code and Codex agent definitions from one canonical catalog.
5
+ Author: Brian Grubba
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ceponatia/rolesync
8
+ Project-URL: Repository, https://github.com/ceponatia/rolesync
9
+ Project-URL: Issues, https://github.com/ceponatia/rolesync/issues
10
+ Keywords: ai,agents,claude,codex,developer-tools
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Software Development :: Code Generators
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # RoleSync
25
+
26
+ RoleSync keeps one canonical set of agent roles and skills under `.agents/` and renders native definitions for both Claude Code and Codex. Generated files are tracked by checksum so accidental hand-edits are detected rather than silently overwritten.
27
+
28
+ The project is intentionally a generator, not an agent runtime: it does not run a scheduler, broker credentials, enforce GitHub permissions, or provide durable background orchestration.
29
+
30
+ ## Install
31
+
32
+ Python 3.11+ is required. Claude Code and Codex are optional runtime dependencies and are installed separately.
33
+
34
+ ```bash
35
+ # Recommended isolated CLI install after the package is published
36
+ uv tool install rolesync
37
+
38
+ # Alternative
39
+ pipx install rolesync
40
+
41
+ # One-off execution
42
+ uvx rolesync --version
43
+ ```
44
+
45
+ Until a PyPI release exists, install from a tagged/reviewed source checkout instead of assuming the registry name is live.
46
+
47
+ ## Quick start
48
+
49
+ ```bash
50
+ rolesync init . --preset minimal --platform both
51
+ rolesync check
52
+ rolesync doctor
53
+ ```
54
+
55
+ `init` refuses to replace an existing `.agents/` directory. Add `--install-root-guidance` if you want RoleSync to append a small managed block to root `AGENTS.md` and, for Claude projects, `CLAUDE.md`. Existing files are appended to rather than replaced.
56
+
57
+ Built-in presets:
58
+
59
+ - `minimal` — three generic roles (orchestrator, coder, reviewer) with no mandatory GitHub planning workflow.
60
+ - `github-workflow` — the richer example catalog shipped by this repository for issue → implementation → review → PR work. It is an opinionated starting point, not a universal policy.
61
+
62
+ Choose `--platform claude`, `--platform codex`, or `--platform both`. The selection is stored in `.agents/loom.json`.
63
+
64
+ ## Commands
65
+
66
+ ```text
67
+ rolesync init [ROOT] [--preset minimal|github-workflow] [--platform both|claude|codex]
68
+ rolesync sync [--root ROOT]
69
+ rolesync check [--root ROOT]
70
+ rolesync doctor [--root ROOT]
71
+ rolesync --version
72
+ ```
73
+
74
+ - `init` installs user-owned canonical sources from a built-in preset, then renders native output.
75
+ - `sync` validates the catalog and reconciles generated files.
76
+ - `check` performs the same validation/drift calculation without writing.
77
+ - `doctor` validates configuration and reports whether the selected Claude/Codex CLIs are currently on `PATH`; it does not launch paid/provider sessions.
78
+
79
+ The legacy source-tree command remains available:
80
+
81
+ ```bash
82
+ python scripts/sync_agents.py
83
+ python scripts/sync_agents.py --check
84
+ ```
85
+
86
+ ## Canonical project layout
87
+
88
+ ```text
89
+ .agents/
90
+ catalog.json
91
+ common.md
92
+ loom.json optional RoleSync project settings
93
+ policy.json optional workflow policy; informational to the generator
94
+ roles/<role>.md
95
+ skills/<skill>/SKILL.md
96
+ references/*.md optional supporting guidance
97
+ generated-manifest.json generated ownership/checksum record
98
+
99
+ .claude/agents/*.md generated when Claude is enabled
100
+ .claude/skills/** generated byte-for-byte mirrors of canonical skill resources
101
+ .codex/agents/*.toml generated when Codex is enabled
102
+ ```
103
+
104
+ Only paths recorded in the validated generated manifest are owned by RoleSync. Unrelated files under `.claude/` and `.codex/` remain untouched.
105
+
106
+ ## Safety and ownership
107
+
108
+ RoleSync treats repository paths and the generated manifest as untrusted input. Managed output paths must stay inside `.claude/agents/`, `.claude/skills/`, or `.codex/agents/`; traversal and escaping symlink paths are rejected. Stale entries receive the same containment validation before deletion.
109
+
110
+ Synchronization is staged and guarded by a cross-process lock. Before applying a multi-file update, RoleSync stores recovery copies and a transaction journal. A later `sync` restores an interrupted transaction before generating new output.
111
+
112
+ Canonical skill directories may contain binary resources; they are mirrored byte-for-byte for Claude. Common transient files are ignored, while likely secret files such as `.env`, private keys, and PKCS#12 bundles are rejected rather than copied. This is a guardrail, not a replacement for repository secret scanning.
113
+
114
+ ## Defining agents
115
+
116
+ Each catalog role supplies an internal ID, lowercase-kebab-case generated name, description, role source under `.agents/roles/`, a skill under `.agents/skills/`, and provider-specific native settings. RoleSync validates required types and boundaries but intentionally does not hard-code every provider model or effort value; provider availability changes and must be verified in the installed runtime. Optional roles can set `"enabled": false`; disabled roles remain in canonical sources but are omitted from native discovery output until enabled.
117
+
118
+ `policy.json`, role `activation` notes, and prose permissions do not become enforcement just because they are rendered. Filesystem sandboxing, connector credentials, repository protections, and runtime settings remain the actual security boundaries.
119
+
120
+ ## Development and release checks
121
+
122
+ ```bash
123
+ python -m unittest discover -s scripts/tests -v
124
+ python -m unittest discover -s tests -v
125
+ python scripts/sync_agents.py --check
126
+ python -m build
127
+ ```
128
+
129
+ CI runs the test suites on Linux, Windows, and macOS using the minimum supported Python and the newest explicitly supported Python. It also installs the built wheel into a clean environment and exercises the CLI outside the source tree.
130
+
131
+ Releases are built by GitHub Actions and the publish workflow is configured for PyPI Trusted Publishing through the protected `pypi` environment. Configure the matching Trusted Publisher on PyPI before publishing the first release.
132
+
133
+ ## Example workflow catalog
134
+
135
+ The repository's own `.agents/` catalog remains a worked example of a larger GitHub-centered team. Model routing is a starting hypothesis rather than a benchmark. The generator does not turn this example into a daemon or promise that a runtime will enforce prose-only limits.
136
+
137
+ ## License
138
+
139
+ MIT — see [LICENSE](LICENSE).
140
+
141
+ Built by Brian Grubba ([@ceponatia](https://github.com/ceponatia)) at Snarebox LLC.
@@ -0,0 +1,118 @@
1
+ # RoleSync
2
+
3
+ RoleSync keeps one canonical set of agent roles and skills under `.agents/` and renders native definitions for both Claude Code and Codex. Generated files are tracked by checksum so accidental hand-edits are detected rather than silently overwritten.
4
+
5
+ The project is intentionally a generator, not an agent runtime: it does not run a scheduler, broker credentials, enforce GitHub permissions, or provide durable background orchestration.
6
+
7
+ ## Install
8
+
9
+ Python 3.11+ is required. Claude Code and Codex are optional runtime dependencies and are installed separately.
10
+
11
+ ```bash
12
+ # Recommended isolated CLI install after the package is published
13
+ uv tool install rolesync
14
+
15
+ # Alternative
16
+ pipx install rolesync
17
+
18
+ # One-off execution
19
+ uvx rolesync --version
20
+ ```
21
+
22
+ Until a PyPI release exists, install from a tagged/reviewed source checkout instead of assuming the registry name is live.
23
+
24
+ ## Quick start
25
+
26
+ ```bash
27
+ rolesync init . --preset minimal --platform both
28
+ rolesync check
29
+ rolesync doctor
30
+ ```
31
+
32
+ `init` refuses to replace an existing `.agents/` directory. Add `--install-root-guidance` if you want RoleSync to append a small managed block to root `AGENTS.md` and, for Claude projects, `CLAUDE.md`. Existing files are appended to rather than replaced.
33
+
34
+ Built-in presets:
35
+
36
+ - `minimal` — three generic roles (orchestrator, coder, reviewer) with no mandatory GitHub planning workflow.
37
+ - `github-workflow` — the richer example catalog shipped by this repository for issue → implementation → review → PR work. It is an opinionated starting point, not a universal policy.
38
+
39
+ Choose `--platform claude`, `--platform codex`, or `--platform both`. The selection is stored in `.agents/loom.json`.
40
+
41
+ ## Commands
42
+
43
+ ```text
44
+ rolesync init [ROOT] [--preset minimal|github-workflow] [--platform both|claude|codex]
45
+ rolesync sync [--root ROOT]
46
+ rolesync check [--root ROOT]
47
+ rolesync doctor [--root ROOT]
48
+ rolesync --version
49
+ ```
50
+
51
+ - `init` installs user-owned canonical sources from a built-in preset, then renders native output.
52
+ - `sync` validates the catalog and reconciles generated files.
53
+ - `check` performs the same validation/drift calculation without writing.
54
+ - `doctor` validates configuration and reports whether the selected Claude/Codex CLIs are currently on `PATH`; it does not launch paid/provider sessions.
55
+
56
+ The legacy source-tree command remains available:
57
+
58
+ ```bash
59
+ python scripts/sync_agents.py
60
+ python scripts/sync_agents.py --check
61
+ ```
62
+
63
+ ## Canonical project layout
64
+
65
+ ```text
66
+ .agents/
67
+ catalog.json
68
+ common.md
69
+ loom.json optional RoleSync project settings
70
+ policy.json optional workflow policy; informational to the generator
71
+ roles/<role>.md
72
+ skills/<skill>/SKILL.md
73
+ references/*.md optional supporting guidance
74
+ generated-manifest.json generated ownership/checksum record
75
+
76
+ .claude/agents/*.md generated when Claude is enabled
77
+ .claude/skills/** generated byte-for-byte mirrors of canonical skill resources
78
+ .codex/agents/*.toml generated when Codex is enabled
79
+ ```
80
+
81
+ Only paths recorded in the validated generated manifest are owned by RoleSync. Unrelated files under `.claude/` and `.codex/` remain untouched.
82
+
83
+ ## Safety and ownership
84
+
85
+ RoleSync treats repository paths and the generated manifest as untrusted input. Managed output paths must stay inside `.claude/agents/`, `.claude/skills/`, or `.codex/agents/`; traversal and escaping symlink paths are rejected. Stale entries receive the same containment validation before deletion.
86
+
87
+ Synchronization is staged and guarded by a cross-process lock. Before applying a multi-file update, RoleSync stores recovery copies and a transaction journal. A later `sync` restores an interrupted transaction before generating new output.
88
+
89
+ Canonical skill directories may contain binary resources; they are mirrored byte-for-byte for Claude. Common transient files are ignored, while likely secret files such as `.env`, private keys, and PKCS#12 bundles are rejected rather than copied. This is a guardrail, not a replacement for repository secret scanning.
90
+
91
+ ## Defining agents
92
+
93
+ Each catalog role supplies an internal ID, lowercase-kebab-case generated name, description, role source under `.agents/roles/`, a skill under `.agents/skills/`, and provider-specific native settings. RoleSync validates required types and boundaries but intentionally does not hard-code every provider model or effort value; provider availability changes and must be verified in the installed runtime. Optional roles can set `"enabled": false`; disabled roles remain in canonical sources but are omitted from native discovery output until enabled.
94
+
95
+ `policy.json`, role `activation` notes, and prose permissions do not become enforcement just because they are rendered. Filesystem sandboxing, connector credentials, repository protections, and runtime settings remain the actual security boundaries.
96
+
97
+ ## Development and release checks
98
+
99
+ ```bash
100
+ python -m unittest discover -s scripts/tests -v
101
+ python -m unittest discover -s tests -v
102
+ python scripts/sync_agents.py --check
103
+ python -m build
104
+ ```
105
+
106
+ CI runs the test suites on Linux, Windows, and macOS using the minimum supported Python and the newest explicitly supported Python. It also installs the built wheel into a clean environment and exercises the CLI outside the source tree.
107
+
108
+ Releases are built by GitHub Actions and the publish workflow is configured for PyPI Trusted Publishing through the protected `pypi` environment. Configure the matching Trusted Publisher on PyPI before publishing the first release.
109
+
110
+ ## Example workflow catalog
111
+
112
+ The repository's own `.agents/` catalog remains a worked example of a larger GitHub-centered team. Model routing is a starting hypothesis rather than a benchmark. The generator does not turn this example into a daemon or promise that a runtime will enforce prose-only limits.
113
+
114
+ ## License
115
+
116
+ MIT — see [LICENSE](LICENSE).
117
+
118
+ Built by Brian Grubba ([@ceponatia](https://github.com/ceponatia)) at Snarebox LLC.
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "rolesync"
7
+ version = "1.0.0rc1"
8
+ description = "Generate native Claude Code and Codex agent definitions from one canonical catalog."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{name = "Brian Grubba"}]
14
+ keywords = ["ai", "agents", "claude", "codex", "developer-tools"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Topic :: Software Development :: Code Generators",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/ceponatia/rolesync"
28
+ Repository = "https://github.com/ceponatia/rolesync"
29
+ Issues = "https://github.com/ceponatia/rolesync/issues"
30
+
31
+ [project.scripts]
32
+ rolesync = "rolesync.cli:main"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+
37
+ [tool.setuptools.package-data]
38
+ rolesync = [
39
+ "presets/*/*",
40
+ "presets/*/*/*",
41
+ "presets/*/*/*/*",
42
+ "presets/*/*/*/*/*",
43
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ """RoleSync: generate native Claude Code and Codex agents from canonical sources."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "1.0.0rc1"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,211 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import shutil
6
+ import sys
7
+ from importlib import metadata, resources
8
+ from pathlib import Path
9
+
10
+ from .core import RoleSyncError, CONFIG, render, sync, validate_project
11
+
12
+
13
+ def version() -> str:
14
+ try:
15
+ return metadata.version("rolesync")
16
+ except metadata.PackageNotFoundError:
17
+ return "1.0.0rc1"
18
+
19
+
20
+ def _preset_root(name: str):
21
+ base = resources.files("rolesync").joinpath("presets", name)
22
+ if not base.is_dir():
23
+ raise RoleSyncError(f"Unknown preset: {name}")
24
+ return base
25
+
26
+
27
+ def _copy_resource_tree(source, destination: Path) -> None:
28
+ destination.mkdir(parents=True, exist_ok=True)
29
+ for child in source.iterdir():
30
+ target = destination / child.name
31
+ if child.is_dir():
32
+ _copy_resource_tree(child, target)
33
+ else:
34
+ if target.exists():
35
+ raise RoleSyncError(f"Refusing to overwrite existing file during init: {target}")
36
+ target.parent.mkdir(parents=True, exist_ok=True)
37
+ target.write_bytes(child.read_bytes())
38
+
39
+
40
+ def _append_managed_block(path: Path, body: str) -> bool:
41
+ start = "<!-- rolesync:start -->"
42
+ end = "<!-- rolesync:end -->"
43
+ if path.exists():
44
+ text = path.read_text(encoding="utf-8")
45
+ if start in text or end in text:
46
+ raise RoleSyncError(f"{path.name} already contains a rolesync managed block")
47
+ prefix = text.rstrip() + "\n\n" if text.strip() else ""
48
+ else:
49
+ prefix = ""
50
+ path.write_text(prefix + start + "\n" + body.rstrip() + "\n" + end + "\n", encoding="utf-8", newline="\n")
51
+ return True
52
+
53
+
54
+ def _prune_empty_subdirs(path: Path) -> None:
55
+ if not path.is_dir():
56
+ return
57
+ for child in sorted(path.iterdir()):
58
+ if child.is_dir():
59
+ _prune_empty_subdirs(child)
60
+ try:
61
+ child.rmdir()
62
+ except OSError:
63
+ pass
64
+
65
+
66
+ def init_project(root: Path, preset: str, platform: str, install_root_guidance: bool = False) -> None:
67
+ root = root.resolve()
68
+ root.mkdir(parents=True, exist_ok=True)
69
+ agents = root / ".agents"
70
+ if agents.exists():
71
+ raise RoleSyncError(f"Refusing to initialize over existing {agents}; adopt it manually or run sync/check instead")
72
+ source = _preset_root(preset)
73
+ managed_dirs = (root / ".codex" / "agents", root / ".claude" / "agents", root / ".claude" / "skills")
74
+ preexisting_dirs = {d for d in managed_dirs if d.exists()}
75
+ output_paths: list[Path] = []
76
+ preexisting_outputs: set[Path] = set()
77
+ guidance_writes: list[tuple[Path, bytes | None]] = []
78
+ try:
79
+ _copy_resource_tree(source, agents)
80
+ platforms = ["claude", "codex"] if platform == "both" else [platform]
81
+ (root / CONFIG).write_text(
82
+ json.dumps({"schema_version": 1, "preset": preset, "platforms": platforms}, indent=2) + "\n",
83
+ encoding="utf-8",
84
+ newline="\n",
85
+ )
86
+ output_paths = [root / rel for rel in render(root)]
87
+ preexisting_outputs = {p for p in output_paths if p.exists()}
88
+ sync(root)
89
+ if install_root_guidance:
90
+ guidance = [(
91
+ root / "AGENTS.md",
92
+ "Agent definitions are maintained under `.agents/`. Follow `.agents/AGENTS.md` for agent-system maintenance. Do not hand-edit generated files under `.claude/agents/`, `.claude/skills/`, or `.codex/agents/`.",
93
+ )]
94
+ if "claude" in platforms:
95
+ guidance.append((root / "CLAUDE.md", "@AGENTS.md"))
96
+ for path, body in guidance:
97
+ original = path.read_bytes() if path.exists() else None
98
+ _append_managed_block(path, body)
99
+ guidance_writes.append((path, original))
100
+ except BaseException:
101
+ for path, original in guidance_writes:
102
+ if original is None:
103
+ path.unlink(missing_ok=True)
104
+ else:
105
+ path.write_bytes(original)
106
+ if agents.exists():
107
+ shutil.rmtree(agents, ignore_errors=True)
108
+ for path in output_paths:
109
+ if path not in preexisting_outputs and path.exists():
110
+ path.unlink(missing_ok=True)
111
+ for managed_dir in managed_dirs:
112
+ if managed_dir.exists():
113
+ _prune_empty_subdirs(managed_dir)
114
+ if managed_dir not in preexisting_dirs:
115
+ try:
116
+ managed_dir.rmdir()
117
+ except OSError:
118
+ pass
119
+ raise
120
+
121
+
122
+ def _sync_command(root: Path, check: bool) -> int:
123
+ try:
124
+ problems = sync(root, check=check)
125
+ except (OSError, UnicodeError, RoleSyncError, KeyError, TypeError) as exc:
126
+ print(f"rolesync: {exc}", file=sys.stderr)
127
+ return 2
128
+ if check and problems:
129
+ print("Generated files differ:", file=sys.stderr)
130
+ for problem in problems:
131
+ print(problem, file=sys.stderr)
132
+ return 1
133
+ print("Generated files are current." if check else "Native agent files and mirrored skills generated.")
134
+ return 0
135
+
136
+
137
+ def _doctor(root: Path) -> int:
138
+ root = root.resolve()
139
+ issues = validate_project(root)
140
+ if issues:
141
+ print(f"Project: {root}")
142
+ for issue in issues:
143
+ print(f"ERROR: {issue}")
144
+ return 1
145
+ drift = sync(root, check=True)
146
+ config_path = root / CONFIG
147
+ platforms = ["claude", "codex"]
148
+ if config_path.is_file():
149
+ config = json.loads(config_path.read_text(encoding="utf-8"))
150
+ platforms = config.get("platforms", platforms)
151
+ print(f"Project: {root}")
152
+ print("Configuration: valid")
153
+ print("Generated output: " + ("current" if not drift else "drifted"))
154
+ for platform in platforms:
155
+ executable = "claude" if platform == "claude" else "codex"
156
+ found = shutil.which(executable)
157
+ print(f"{platform}: {'found at ' + found if found else 'CLI not found on PATH (generation still works)'}")
158
+ if drift:
159
+ print("Drift:")
160
+ for item in drift:
161
+ print(f" {item}")
162
+ return 1
163
+ return 0
164
+
165
+
166
+ def build_parser() -> argparse.ArgumentParser:
167
+ parser = argparse.ArgumentParser(prog="rolesync", description="Generate native Claude Code and Codex agent definitions from one canonical catalog.")
168
+ parser.add_argument("--version", action="version", version=f"rolesync {version()}")
169
+ sub = parser.add_subparsers(dest="command")
170
+
171
+ init = sub.add_parser("init", help="Initialize a project from a built-in preset")
172
+ init.add_argument("root", nargs="?", type=Path, default=Path.cwd())
173
+ init.add_argument("--preset", choices=["minimal", "github-workflow"], default="minimal")
174
+ init.add_argument("--platform", choices=["both", "claude", "codex"], default="both")
175
+ init.add_argument("--install-root-guidance", action="store_true", help="Append a small managed guidance block to root AGENTS.md and CLAUDE.md")
176
+
177
+ for name, help_text in (("sync", "Validate and render native agent files"), ("check", "Report drift without writing"), ("doctor", "Validate setup and local runtime availability")):
178
+ cmd = sub.add_parser(name, help=help_text)
179
+ cmd.add_argument("--root", type=Path, default=Path.cwd())
180
+ return parser
181
+
182
+
183
+ def main(argv: list[str] | None = None) -> int:
184
+ parser = build_parser()
185
+ args = parser.parse_args(argv)
186
+ if not args.command:
187
+ parser.print_help()
188
+ return 0
189
+ try:
190
+ if args.command == "init":
191
+ init_project(args.root, args.preset, args.platform, args.install_root_guidance)
192
+ print(f"Initialized rolesync in {args.root.resolve()}")
193
+ return 0
194
+ if args.command == "sync":
195
+ return _sync_command(args.root, check=False)
196
+ if args.command == "check":
197
+ return _sync_command(args.root, check=True)
198
+ if args.command == "doctor":
199
+ return _doctor(args.root)
200
+ except (OSError, UnicodeError, RoleSyncError, KeyError, TypeError, json.JSONDecodeError) as exc:
201
+ print(f"rolesync: {exc}", file=sys.stderr)
202
+ return 2
203
+ return 2
204
+
205
+
206
+ def legacy_sync_main(default_root: Path) -> int:
207
+ parser = argparse.ArgumentParser(description="Render native Codex/Claude agents and mirror portable skills.")
208
+ parser.add_argument("--root", type=Path, default=default_root)
209
+ parser.add_argument("--check", action="store_true", help="Report missing/stale/drifted outputs without writing")
210
+ args = parser.parse_args()
211
+ return _sync_command(args.root, check=args.check)