rolesync 1.0.0__py3-none-any.whl
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.
- rolesync/__init__.py +4 -0
- rolesync/__main__.py +3 -0
- rolesync/cli.py +212 -0
- rolesync/core.py +546 -0
- rolesync/presets/github-workflow/AGENTS.md +7 -0
- rolesync/presets/github-workflow/CLAUDE.md +1 -0
- rolesync/presets/github-workflow/catalog.json +342 -0
- rolesync/presets/github-workflow/common.md +1 -0
- rolesync/presets/github-workflow/policy.json +55 -0
- rolesync/presets/github-workflow/references/SOURCES.md +36 -0
- rolesync/presets/github-workflow/references/evaluation.md +33 -0
- rolesync/presets/github-workflow/references/git-pr-lifecycle.md +52 -0
- rolesync/presets/github-workflow/references/handoff-contract.md +70 -0
- rolesync/presets/github-workflow/references/routing-and-budgets.md +56 -0
- rolesync/presets/github-workflow/references/runtime-and-security.md +53 -0
- rolesync/presets/github-workflow/roles/advanced-coder.md +9 -0
- rolesync/presets/github-workflow/roles/architect.md +9 -0
- rolesync/presets/github-workflow/roles/ci-investigator.md +9 -0
- rolesync/presets/github-workflow/roles/deep-rescue.md +9 -0
- rolesync/presets/github-workflow/roles/documentation-updater.md +9 -0
- rolesync/presets/github-workflow/roles/general-coder.md +9 -0
- rolesync/presets/github-workflow/roles/issue-filer.md +9 -0
- rolesync/presets/github-workflow/roles/orchestrator.md +15 -0
- rolesync/presets/github-workflow/roles/pr-manager.md +9 -0
- rolesync/presets/github-workflow/roles/reader.md +7 -0
- rolesync/presets/github-workflow/roles/reviewer.md +11 -0
- rolesync/presets/github-workflow/roles/test-maintainer.md +9 -0
- rolesync/presets/github-workflow/skills/example-architecture/SKILL.md +12 -0
- rolesync/presets/github-workflow/skills/example-ci/SKILL.md +12 -0
- rolesync/presets/github-workflow/skills/example-context/SKILL.md +12 -0
- rolesync/presets/github-workflow/skills/example-document/SKILL.md +12 -0
- rolesync/presets/github-workflow/skills/example-escalate/SKILL.md +14 -0
- rolesync/presets/github-workflow/skills/example-implement/SKILL.md +14 -0
- rolesync/presets/github-workflow/skills/example-issue/SKILL.md +14 -0
- rolesync/presets/github-workflow/skills/example-orchestrate/SKILL.md +20 -0
- rolesync/presets/github-workflow/skills/example-pr/SKILL.md +16 -0
- rolesync/presets/github-workflow/skills/example-review/SKILL.md +23 -0
- rolesync/presets/github-workflow/skills/example-test/SKILL.md +14 -0
- rolesync/presets/minimal/AGENTS.md +5 -0
- rolesync/presets/minimal/CLAUDE.md +1 -0
- rolesync/presets/minimal/catalog.json +34 -0
- rolesync/presets/minimal/common.md +1 -0
- rolesync/presets/minimal/policy.json +6 -0
- rolesync/presets/minimal/roles/coder.md +5 -0
- rolesync/presets/minimal/roles/orchestrator.md +5 -0
- rolesync/presets/minimal/roles/reviewer.md +5 -0
- rolesync/presets/minimal/skills/loom-implement/SKILL.md +8 -0
- rolesync/presets/minimal/skills/loom-orchestrate/SKILL.md +8 -0
- rolesync/presets/minimal/skills/loom-review/SKILL.md +8 -0
- rolesync-1.0.0.dist-info/METADATA +141 -0
- rolesync-1.0.0.dist-info/RECORD +55 -0
- rolesync-1.0.0.dist-info/WHEEL +5 -0
- rolesync-1.0.0.dist-info/entry_points.txt +2 -0
- rolesync-1.0.0.dist-info/licenses/LICENSE +21 -0
- rolesync-1.0.0.dist-info/top_level.txt +1 -0
rolesync/__init__.py
ADDED
rolesync/__main__.py
ADDED
rolesync/cli.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
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 . import __version__
|
|
11
|
+
from .core import RoleSyncError, CONFIG, render, sync, validate_project
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def version() -> str:
|
|
15
|
+
try:
|
|
16
|
+
return metadata.version("rolesync")
|
|
17
|
+
except metadata.PackageNotFoundError:
|
|
18
|
+
return __version__
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _preset_root(name: str):
|
|
22
|
+
base = resources.files("rolesync").joinpath("presets", name)
|
|
23
|
+
if not base.is_dir():
|
|
24
|
+
raise RoleSyncError(f"Unknown preset: {name}")
|
|
25
|
+
return base
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _copy_resource_tree(source, destination: Path) -> None:
|
|
29
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
for child in source.iterdir():
|
|
31
|
+
target = destination / child.name
|
|
32
|
+
if child.is_dir():
|
|
33
|
+
_copy_resource_tree(child, target)
|
|
34
|
+
else:
|
|
35
|
+
if target.exists():
|
|
36
|
+
raise RoleSyncError(f"Refusing to overwrite existing file during init: {target}")
|
|
37
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
target.write_bytes(child.read_bytes())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _append_managed_block(path: Path, body: str) -> bool:
|
|
42
|
+
start = "<!-- rolesync:start -->"
|
|
43
|
+
end = "<!-- rolesync:end -->"
|
|
44
|
+
if path.exists():
|
|
45
|
+
text = path.read_text(encoding="utf-8")
|
|
46
|
+
if start in text or end in text:
|
|
47
|
+
raise RoleSyncError(f"{path.name} already contains a rolesync managed block")
|
|
48
|
+
prefix = text.rstrip() + "\n\n" if text.strip() else ""
|
|
49
|
+
else:
|
|
50
|
+
prefix = ""
|
|
51
|
+
path.write_text(prefix + start + "\n" + body.rstrip() + "\n" + end + "\n", encoding="utf-8", newline="\n")
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _prune_empty_subdirs(path: Path) -> None:
|
|
56
|
+
if not path.is_dir():
|
|
57
|
+
return
|
|
58
|
+
for child in sorted(path.iterdir()):
|
|
59
|
+
if child.is_dir():
|
|
60
|
+
_prune_empty_subdirs(child)
|
|
61
|
+
try:
|
|
62
|
+
child.rmdir()
|
|
63
|
+
except OSError:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def init_project(root: Path, preset: str, platform: str, install_root_guidance: bool = False) -> None:
|
|
68
|
+
root = root.resolve()
|
|
69
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
agents = root / ".agents"
|
|
71
|
+
if agents.exists():
|
|
72
|
+
raise RoleSyncError(f"Refusing to initialize over existing {agents}; adopt it manually or run sync/check instead")
|
|
73
|
+
source = _preset_root(preset)
|
|
74
|
+
managed_dirs = (root / ".codex" / "agents", root / ".claude" / "agents", root / ".claude" / "skills")
|
|
75
|
+
preexisting_dirs = {d for d in managed_dirs if d.exists()}
|
|
76
|
+
output_paths: list[Path] = []
|
|
77
|
+
preexisting_outputs: set[Path] = set()
|
|
78
|
+
guidance_writes: list[tuple[Path, bytes | None]] = []
|
|
79
|
+
try:
|
|
80
|
+
_copy_resource_tree(source, agents)
|
|
81
|
+
platforms = ["claude", "codex"] if platform == "both" else [platform]
|
|
82
|
+
(root / CONFIG).write_text(
|
|
83
|
+
json.dumps({"schema_version": 1, "preset": preset, "platforms": platforms}, indent=2) + "\n",
|
|
84
|
+
encoding="utf-8",
|
|
85
|
+
newline="\n",
|
|
86
|
+
)
|
|
87
|
+
output_paths = [root / rel for rel in render(root)]
|
|
88
|
+
preexisting_outputs = {p for p in output_paths if p.exists()}
|
|
89
|
+
sync(root)
|
|
90
|
+
if install_root_guidance:
|
|
91
|
+
guidance = [(
|
|
92
|
+
root / "AGENTS.md",
|
|
93
|
+
"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/`.",
|
|
94
|
+
)]
|
|
95
|
+
if "claude" in platforms:
|
|
96
|
+
guidance.append((root / "CLAUDE.md", "@AGENTS.md"))
|
|
97
|
+
for path, body in guidance:
|
|
98
|
+
original = path.read_bytes() if path.exists() else None
|
|
99
|
+
_append_managed_block(path, body)
|
|
100
|
+
guidance_writes.append((path, original))
|
|
101
|
+
except BaseException:
|
|
102
|
+
for path, original in guidance_writes:
|
|
103
|
+
if original is None:
|
|
104
|
+
path.unlink(missing_ok=True)
|
|
105
|
+
else:
|
|
106
|
+
path.write_bytes(original)
|
|
107
|
+
if agents.exists():
|
|
108
|
+
shutil.rmtree(agents, ignore_errors=True)
|
|
109
|
+
for path in output_paths:
|
|
110
|
+
if path not in preexisting_outputs and path.exists():
|
|
111
|
+
path.unlink(missing_ok=True)
|
|
112
|
+
for managed_dir in managed_dirs:
|
|
113
|
+
if managed_dir.exists():
|
|
114
|
+
_prune_empty_subdirs(managed_dir)
|
|
115
|
+
if managed_dir not in preexisting_dirs:
|
|
116
|
+
try:
|
|
117
|
+
managed_dir.rmdir()
|
|
118
|
+
except OSError:
|
|
119
|
+
pass
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _sync_command(root: Path, check: bool) -> int:
|
|
124
|
+
try:
|
|
125
|
+
problems = sync(root, check=check)
|
|
126
|
+
except (OSError, UnicodeError, RoleSyncError, KeyError, TypeError) as exc:
|
|
127
|
+
print(f"rolesync: {exc}", file=sys.stderr)
|
|
128
|
+
return 2
|
|
129
|
+
if check and problems:
|
|
130
|
+
print("Generated files differ:", file=sys.stderr)
|
|
131
|
+
for problem in problems:
|
|
132
|
+
print(problem, file=sys.stderr)
|
|
133
|
+
return 1
|
|
134
|
+
print("Generated files are current." if check else "Native agent files and mirrored skills generated.")
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _doctor(root: Path) -> int:
|
|
139
|
+
root = root.resolve()
|
|
140
|
+
issues = validate_project(root)
|
|
141
|
+
if issues:
|
|
142
|
+
print(f"Project: {root}")
|
|
143
|
+
for issue in issues:
|
|
144
|
+
print(f"ERROR: {issue}")
|
|
145
|
+
return 1
|
|
146
|
+
drift = sync(root, check=True)
|
|
147
|
+
config_path = root / CONFIG
|
|
148
|
+
platforms = ["claude", "codex"]
|
|
149
|
+
if config_path.is_file():
|
|
150
|
+
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
151
|
+
platforms = config.get("platforms", platforms)
|
|
152
|
+
print(f"Project: {root}")
|
|
153
|
+
print("Configuration: valid")
|
|
154
|
+
print("Generated output: " + ("current" if not drift else "drifted"))
|
|
155
|
+
for platform in platforms:
|
|
156
|
+
executable = "claude" if platform == "claude" else "codex"
|
|
157
|
+
found = shutil.which(executable)
|
|
158
|
+
print(f"{platform}: {'found at ' + found if found else 'CLI not found on PATH (generation still works)'}")
|
|
159
|
+
if drift:
|
|
160
|
+
print("Drift:")
|
|
161
|
+
for item in drift:
|
|
162
|
+
print(f" {item}")
|
|
163
|
+
return 1
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
168
|
+
parser = argparse.ArgumentParser(prog="rolesync", description="Generate native Claude Code and Codex agent definitions from one canonical catalog.")
|
|
169
|
+
parser.add_argument("--version", action="version", version=f"rolesync {version()}")
|
|
170
|
+
sub = parser.add_subparsers(dest="command")
|
|
171
|
+
|
|
172
|
+
init = sub.add_parser("init", help="Initialize a project from a built-in preset")
|
|
173
|
+
init.add_argument("root", nargs="?", type=Path, default=Path.cwd())
|
|
174
|
+
init.add_argument("--preset", choices=["minimal", "github-workflow"], default="minimal")
|
|
175
|
+
init.add_argument("--platform", choices=["both", "claude", "codex"], default="both")
|
|
176
|
+
init.add_argument("--install-root-guidance", action="store_true", help="Append a small managed guidance block to root AGENTS.md and CLAUDE.md")
|
|
177
|
+
|
|
178
|
+
for name, help_text in (("sync", "Validate and render native agent files"), ("check", "Report drift without writing"), ("doctor", "Validate setup and local runtime availability")):
|
|
179
|
+
cmd = sub.add_parser(name, help=help_text)
|
|
180
|
+
cmd.add_argument("--root", type=Path, default=Path.cwd())
|
|
181
|
+
return parser
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def main(argv: list[str] | None = None) -> int:
|
|
185
|
+
parser = build_parser()
|
|
186
|
+
args = parser.parse_args(argv)
|
|
187
|
+
if not args.command:
|
|
188
|
+
parser.print_help()
|
|
189
|
+
return 0
|
|
190
|
+
try:
|
|
191
|
+
if args.command == "init":
|
|
192
|
+
init_project(args.root, args.preset, args.platform, args.install_root_guidance)
|
|
193
|
+
print(f"Initialized rolesync in {args.root.resolve()}")
|
|
194
|
+
return 0
|
|
195
|
+
if args.command == "sync":
|
|
196
|
+
return _sync_command(args.root, check=False)
|
|
197
|
+
if args.command == "check":
|
|
198
|
+
return _sync_command(args.root, check=True)
|
|
199
|
+
if args.command == "doctor":
|
|
200
|
+
return _doctor(args.root)
|
|
201
|
+
except (OSError, UnicodeError, RoleSyncError, KeyError, TypeError, json.JSONDecodeError) as exc:
|
|
202
|
+
print(f"rolesync: {exc}", file=sys.stderr)
|
|
203
|
+
return 2
|
|
204
|
+
return 2
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def legacy_sync_main(default_root: Path) -> int:
|
|
208
|
+
parser = argparse.ArgumentParser(description="Render native Codex/Claude agents and mirror portable skills.")
|
|
209
|
+
parser.add_argument("--root", type=Path, default=default_root)
|
|
210
|
+
parser.add_argument("--check", action="store_true", help="Report missing/stale/drifted outputs without writing")
|
|
211
|
+
args = parser.parse_args()
|
|
212
|
+
return _sync_command(args.root, check=args.check)
|