claudekit-codex-sync 0.1.0
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.
- package/AGENTS.md +45 -0
- package/README.md +131 -0
- package/bin/ck-codex-sync +12 -0
- package/bin/ck-codex-sync.js +9 -0
- package/docs/code-standards.md +62 -0
- package/docs/codebase-summary.md +83 -0
- package/docs/codex-vs-claude-agents.md +74 -0
- package/docs/installation-guide.md +64 -0
- package/docs/project-overview-pdr.md +44 -0
- package/docs/project-roadmap.md +51 -0
- package/docs/system-architecture.md +106 -0
- package/package.json +16 -0
- package/plans/260222-2051-claudekit-codex-community-sync/phase-01-productization.md +36 -0
- package/plans/260222-2051-claudekit-codex-community-sync/phase-02-core-refactor.md +32 -0
- package/plans/260222-2051-claudekit-codex-community-sync/phase-03-agent-transpiler.md +33 -0
- package/plans/260222-2051-claudekit-codex-community-sync/phase-04-parity-harness.md +43 -0
- package/plans/260222-2051-claudekit-codex-community-sync/phase-05-distribution-npm.md +35 -0
- package/plans/260222-2051-claudekit-codex-community-sync/phase-06-git-clone-docs.md +28 -0
- package/plans/260222-2051-claudekit-codex-community-sync/phase-07-qa-release.md +35 -0
- package/plans/260222-2051-claudekit-codex-community-sync/plan.md +99 -0
- package/plans/260223-0951-refactor-and-upgrade/phase-01-project-structure.md +79 -0
- package/plans/260223-0951-refactor-and-upgrade/phase-02-extract-templates.md +36 -0
- package/plans/260223-0951-refactor-and-upgrade/phase-03-modularize-python.md +107 -0
- package/plans/260223-0951-refactor-and-upgrade/phase-04-live-source-detection.md +76 -0
- package/plans/260223-0951-refactor-and-upgrade/phase-05-agent-toml-config.md +88 -0
- package/plans/260223-0951-refactor-and-upgrade/phase-06-backup-registry.md +58 -0
- package/plans/260223-0951-refactor-and-upgrade/phase-07-tests-docs-push.md +54 -0
- package/plans/260223-0951-refactor-and-upgrade/plan.md +72 -0
- package/reports/brainstorm-260222-2051-claudekit-codex-community-sync.md +113 -0
- package/scripts/bootstrap-claudekit-skill-scripts.sh +150 -0
- package/scripts/claudekit-sync-all.py +1150 -0
- package/scripts/export-claudekit-prompts.sh +221 -0
- package/scripts/normalize-claudekit-for-codex.sh +261 -0
- package/src/claudekit_codex_sync/__init__.py +0 -0
- package/src/claudekit_codex_sync/asset_sync_dir.py +125 -0
- package/src/claudekit_codex_sync/asset_sync_zip.py +140 -0
- package/src/claudekit_codex_sync/bridge_generator.py +33 -0
- package/src/claudekit_codex_sync/cli.py +199 -0
- package/src/claudekit_codex_sync/config_enforcer.py +140 -0
- package/src/claudekit_codex_sync/constants.py +104 -0
- package/src/claudekit_codex_sync/dep_bootstrapper.py +73 -0
- package/src/claudekit_codex_sync/path_normalizer.py +248 -0
- package/src/claudekit_codex_sync/prompt_exporter.py +89 -0
- package/src/claudekit_codex_sync/runtime_verifier.py +32 -0
- package/src/claudekit_codex_sync/source_resolver.py +78 -0
- package/src/claudekit_codex_sync/sync_registry.py +77 -0
- package/src/claudekit_codex_sync/utils.py +130 -0
- package/templates/agents-md.md +45 -0
- package/templates/bridge-docs-init.sh +25 -0
- package/templates/bridge-project-status.sh +49 -0
- package/templates/bridge-resolve-command.py +52 -0
- package/templates/bridge-skill.md +63 -0
- package/templates/command-map.md +44 -0
- package/tests/__init__.py +1 -0
- package/tests/test_config_enforcer.py +44 -0
- package/tests/test_path_normalizer.py +61 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Asset and skill synchronization from zip files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import zipfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Tuple
|
|
9
|
+
|
|
10
|
+
from .constants import (
|
|
11
|
+
ASSET_DIRS,
|
|
12
|
+
ASSET_FILES,
|
|
13
|
+
ASSET_MANIFEST,
|
|
14
|
+
CONFLICT_SKILLS,
|
|
15
|
+
EXCLUDED_SKILLS_ALWAYS,
|
|
16
|
+
MCP_SKILLS,
|
|
17
|
+
)
|
|
18
|
+
from .source_resolver import collect_skill_entries, zip_mode
|
|
19
|
+
from .utils import load_manifest, save_manifest, write_bytes_if_changed
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def sync_assets(
|
|
23
|
+
zf: zipfile.ZipFile,
|
|
24
|
+
*,
|
|
25
|
+
codex_home: Path,
|
|
26
|
+
include_hooks: bool,
|
|
27
|
+
dry_run: bool,
|
|
28
|
+
) -> Dict[str, int]:
|
|
29
|
+
"""Sync non-skill assets from zip to codex_home/claudekit."""
|
|
30
|
+
claudekit_dir = codex_home / "claudekit"
|
|
31
|
+
manifest_path = claudekit_dir / ASSET_MANIFEST
|
|
32
|
+
old_manifest = load_manifest(manifest_path)
|
|
33
|
+
|
|
34
|
+
selected: List[Tuple[str, str]] = []
|
|
35
|
+
for name in zf.namelist():
|
|
36
|
+
if name.endswith("/") or not name.startswith(".claude/"):
|
|
37
|
+
continue
|
|
38
|
+
rel = name[len(".claude/") :]
|
|
39
|
+
first = rel.split("/", 1)[0]
|
|
40
|
+
if first == "hooks" and include_hooks:
|
|
41
|
+
selected.append((name, rel))
|
|
42
|
+
continue
|
|
43
|
+
if first in ASSET_DIRS or rel in ASSET_FILES:
|
|
44
|
+
selected.append((name, rel))
|
|
45
|
+
|
|
46
|
+
new_manifest = {rel for _, rel in selected}
|
|
47
|
+
added = updated = removed = 0
|
|
48
|
+
|
|
49
|
+
for rel in sorted(old_manifest - new_manifest):
|
|
50
|
+
target = claudekit_dir / rel
|
|
51
|
+
if target.exists():
|
|
52
|
+
removed += 1
|
|
53
|
+
print(f"remove: {rel}")
|
|
54
|
+
if not dry_run:
|
|
55
|
+
target.unlink()
|
|
56
|
+
|
|
57
|
+
for zip_name, rel in sorted(selected, key=lambda x: x[1]):
|
|
58
|
+
info = zf.getinfo(zip_name)
|
|
59
|
+
data = zf.read(zip_name)
|
|
60
|
+
dst = claudekit_dir / rel
|
|
61
|
+
changed, is_added = write_bytes_if_changed(dst, data, mode=zip_mode(info), dry_run=dry_run)
|
|
62
|
+
if changed:
|
|
63
|
+
if is_added:
|
|
64
|
+
added += 1
|
|
65
|
+
print(f"add: {rel}")
|
|
66
|
+
else:
|
|
67
|
+
updated += 1
|
|
68
|
+
print(f"update: {rel}")
|
|
69
|
+
|
|
70
|
+
if not dry_run:
|
|
71
|
+
claudekit_dir.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
save_manifest(manifest_path, new_manifest, dry_run=dry_run)
|
|
73
|
+
|
|
74
|
+
if not dry_run:
|
|
75
|
+
for d in sorted(claudekit_dir.rglob("*"), reverse=True):
|
|
76
|
+
if d.is_dir():
|
|
77
|
+
try:
|
|
78
|
+
d.rmdir()
|
|
79
|
+
except OSError:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
return {"added": added, "updated": updated, "removed": removed, "managed_files": len(new_manifest)}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def sync_skills(
|
|
86
|
+
zf: zipfile.ZipFile,
|
|
87
|
+
*,
|
|
88
|
+
codex_home: Path,
|
|
89
|
+
include_mcp: bool,
|
|
90
|
+
include_conflicts: bool,
|
|
91
|
+
dry_run: bool,
|
|
92
|
+
) -> Dict[str, int]:
|
|
93
|
+
"""Sync skills from zip to codex_home/skills."""
|
|
94
|
+
skills_dir = codex_home / "skills"
|
|
95
|
+
skill_entries = collect_skill_entries(zf)
|
|
96
|
+
added = updated = skipped = 0
|
|
97
|
+
|
|
98
|
+
for skill in sorted(skill_entries):
|
|
99
|
+
if skill in EXCLUDED_SKILLS_ALWAYS:
|
|
100
|
+
skipped += 1
|
|
101
|
+
print(f"skip: {skill}")
|
|
102
|
+
continue
|
|
103
|
+
if not include_mcp and skill in MCP_SKILLS:
|
|
104
|
+
skipped += 1
|
|
105
|
+
print(f"skip: {skill}")
|
|
106
|
+
continue
|
|
107
|
+
if skill in CONFLICT_SKILLS:
|
|
108
|
+
skipped += 1
|
|
109
|
+
print(f"skip: {skill}")
|
|
110
|
+
continue
|
|
111
|
+
if not include_conflicts and (skills_dir / ".system" / skill).exists():
|
|
112
|
+
skipped += 1
|
|
113
|
+
print(f"skip: {skill}")
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
dst_skill_dir = skills_dir / skill
|
|
117
|
+
exists = dst_skill_dir.exists()
|
|
118
|
+
if exists:
|
|
119
|
+
updated += 1
|
|
120
|
+
print(f"update: {skill}")
|
|
121
|
+
else:
|
|
122
|
+
added += 1
|
|
123
|
+
print(f"add: {skill}")
|
|
124
|
+
|
|
125
|
+
if dry_run:
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
if exists:
|
|
129
|
+
shutil.rmtree(dst_skill_dir)
|
|
130
|
+
dst_skill_dir.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
|
|
132
|
+
for zip_name, inner in sorted(skill_entries[skill], key=lambda x: x[1]):
|
|
133
|
+
info = zf.getinfo(zip_name)
|
|
134
|
+
data = zf.read(zip_name)
|
|
135
|
+
dst = dst_skill_dir / inner
|
|
136
|
+
write_bytes_if_changed(dst, data, mode=zip_mode(info), dry_run=False)
|
|
137
|
+
|
|
138
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
total_skills = len(list(skills_dir.rglob("SKILL.md")))
|
|
140
|
+
return {"added": added, "updated": updated, "skipped": skipped, "total_skills": total_skills}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Bridge skill generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .utils import load_template, write_text_if_changed
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ensure_bridge_skill(*, codex_home: Path, dry_run: bool) -> bool:
|
|
11
|
+
"""Ensure claudekit-command-bridge skill exists."""
|
|
12
|
+
bridge_dir = codex_home / "skills" / "claudekit-command-bridge"
|
|
13
|
+
scripts_dir = bridge_dir / "scripts"
|
|
14
|
+
if not dry_run:
|
|
15
|
+
scripts_dir.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
changed = False
|
|
17
|
+
|
|
18
|
+
skill_md = load_template("bridge-skill.md")
|
|
19
|
+
resolve_script = load_template("bridge-resolve-command.py")
|
|
20
|
+
docs_init = load_template("bridge-docs-init.sh")
|
|
21
|
+
project_status = load_template("bridge-project-status.sh")
|
|
22
|
+
|
|
23
|
+
changed |= write_text_if_changed(bridge_dir / "SKILL.md", skill_md, dry_run=dry_run)
|
|
24
|
+
changed |= write_text_if_changed(
|
|
25
|
+
scripts_dir / "resolve-command.py", resolve_script, executable=True, dry_run=dry_run
|
|
26
|
+
)
|
|
27
|
+
changed |= write_text_if_changed(
|
|
28
|
+
scripts_dir / "docs-init.sh", docs_init, executable=True, dry_run=dry_run
|
|
29
|
+
)
|
|
30
|
+
changed |= write_text_if_changed(
|
|
31
|
+
scripts_dir / "project-status.sh", project_status, executable=True, dry_run=dry_run
|
|
32
|
+
)
|
|
33
|
+
return changed
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""All-in-one ClaudeKit -> Codex sync CLI."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import zipfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
from .asset_sync_dir import sync_assets_from_dir, sync_skills_from_dir
|
|
14
|
+
from .asset_sync_zip import sync_assets, sync_skills
|
|
15
|
+
from .bridge_generator import ensure_bridge_skill
|
|
16
|
+
from .config_enforcer import ensure_agents, enforce_config, enforce_multi_agent_flag
|
|
17
|
+
from .dep_bootstrapper import bootstrap_deps
|
|
18
|
+
from .path_normalizer import normalize_agent_tomls, normalize_files
|
|
19
|
+
from .prompt_exporter import export_prompts
|
|
20
|
+
from .runtime_verifier import verify_runtime
|
|
21
|
+
from .source_resolver import detect_claude_source, find_latest_zip, validate_source
|
|
22
|
+
from .sync_registry import load_registry, save_registry
|
|
23
|
+
from .utils import SyncError, eprint
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def print_summary(summary: Dict[str, Any]) -> None:
|
|
27
|
+
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
|
28
|
+
|
|
29
|
+
def parse_args() -> argparse.Namespace:
|
|
30
|
+
p = argparse.ArgumentParser(
|
|
31
|
+
description="All-in-one ClaudeKit -> Codex sync script."
|
|
32
|
+
)
|
|
33
|
+
p.add_argument("--zip", dest="zip_path", type=Path, help="Specific ClaudeKit zip path")
|
|
34
|
+
p.add_argument("--codex-home", type=Path, default=None, help="Codex home (default: $CODEX_HOME or ~/.codex)")
|
|
35
|
+
p.add_argument("--workspace", type=Path, default=Path.cwd(), help="Workspace root for AGENTS.md")
|
|
36
|
+
p.add_argument("--source-mode", choices=["auto", "live", "zip"], default="auto", help="Source mode")
|
|
37
|
+
p.add_argument("--source-dir", type=Path, default=None, help="Source directory for live mode")
|
|
38
|
+
p.add_argument("--include-mcp", action="store_true", help="Include MCP skills/prompts")
|
|
39
|
+
p.add_argument("--include-hooks", action="store_true", help="Include hooks")
|
|
40
|
+
p.add_argument("--include-conflicts", action="store_true", help="Include conflicting skills")
|
|
41
|
+
p.add_argument("--include-test-deps", action="store_true", help="Install test requirements")
|
|
42
|
+
p.add_argument("--skip-bootstrap", action="store_true", help="Skip dependency bootstrap")
|
|
43
|
+
p.add_argument("--skip-verify", action="store_true", help="Skip verification")
|
|
44
|
+
p.add_argument("--skip-agent-toml", action="store_true", help="Skip agent TOML normalization")
|
|
45
|
+
p.add_argument("--respect-edits", action="store_true", help="Backup user-edited files")
|
|
46
|
+
p.add_argument("--dry-run", action="store_true", help="Preview changes only")
|
|
47
|
+
return p.parse_args()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main() -> int:
|
|
51
|
+
args = parse_args()
|
|
52
|
+
codex_home = (args.codex_home or Path(os.environ.get("CODEX_HOME", "~/.codex"))).expanduser().resolve()
|
|
53
|
+
workspace = args.workspace.expanduser().resolve()
|
|
54
|
+
workspace.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
|
|
56
|
+
# Load registry for backup/respect-edits
|
|
57
|
+
registry = load_registry(codex_home)
|
|
58
|
+
|
|
59
|
+
source_mode = args.source_mode
|
|
60
|
+
use_live = source_mode == "live" or (source_mode == "auto" and args.source_dir)
|
|
61
|
+
|
|
62
|
+
if use_live:
|
|
63
|
+
source = args.source_dir or detect_claude_source()
|
|
64
|
+
validation = validate_source(source)
|
|
65
|
+
print(f"source: {source} (live)")
|
|
66
|
+
print(f"validation: {validation}")
|
|
67
|
+
registry["sourceDir"] = str(source)
|
|
68
|
+
else:
|
|
69
|
+
zip_path = find_latest_zip(args.zip_path)
|
|
70
|
+
print(f"zip: {zip_path}")
|
|
71
|
+
registry["sourceDir"] = None
|
|
72
|
+
|
|
73
|
+
print(f"codex_home: {codex_home}")
|
|
74
|
+
print(f"workspace: {workspace}")
|
|
75
|
+
print(
|
|
76
|
+
f"include_mcp={args.include_mcp} include_hooks={args.include_hooks} "
|
|
77
|
+
f"dry_run={args.dry_run} respect_edits={args.respect_edits}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
codex_home.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
|
|
82
|
+
if use_live:
|
|
83
|
+
assets_stats = sync_assets_from_dir(
|
|
84
|
+
source, codex_home=codex_home, include_hooks=args.include_hooks, dry_run=args.dry_run
|
|
85
|
+
)
|
|
86
|
+
skills_stats = sync_skills_from_dir(
|
|
87
|
+
source,
|
|
88
|
+
codex_home=codex_home,
|
|
89
|
+
include_mcp=args.include_mcp,
|
|
90
|
+
include_conflicts=args.include_conflicts,
|
|
91
|
+
dry_run=args.dry_run,
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
with zipfile.ZipFile(zip_path) as zf: # type: ignore
|
|
95
|
+
assets_stats = sync_assets(
|
|
96
|
+
zf, codex_home=codex_home, include_hooks=args.include_hooks, dry_run=args.dry_run
|
|
97
|
+
)
|
|
98
|
+
skills_stats = sync_skills(
|
|
99
|
+
zf,
|
|
100
|
+
codex_home=codex_home,
|
|
101
|
+
include_mcp=args.include_mcp,
|
|
102
|
+
include_conflicts=args.include_conflicts,
|
|
103
|
+
dry_run=args.dry_run,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
print(
|
|
107
|
+
f"assets: added={assets_stats['added']} updated={assets_stats['updated']} "
|
|
108
|
+
f"removed={assets_stats['removed']}"
|
|
109
|
+
)
|
|
110
|
+
print(
|
|
111
|
+
f"skills: added={skills_stats['added']} updated={skills_stats['updated']} "
|
|
112
|
+
f"skipped={skills_stats['skipped']}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
changed = normalize_files(codex_home=codex_home, include_mcp=args.include_mcp, dry_run=args.dry_run)
|
|
116
|
+
print(f"normalize_changed={changed}")
|
|
117
|
+
|
|
118
|
+
# Agent TOML normalization
|
|
119
|
+
agent_toml_changed = 0
|
|
120
|
+
if not args.skip_agent_toml:
|
|
121
|
+
agent_toml_changed = normalize_agent_tomls(codex_home=codex_home, dry_run=args.dry_run)
|
|
122
|
+
print(f"agent_toml_changed={agent_toml_changed}")
|
|
123
|
+
|
|
124
|
+
baseline_changed = 0
|
|
125
|
+
if ensure_agents(workspace=workspace, dry_run=args.dry_run):
|
|
126
|
+
baseline_changed += 1
|
|
127
|
+
print(f"upsert: {workspace / 'AGENTS.md'}")
|
|
128
|
+
if enforce_config(codex_home=codex_home, include_mcp=args.include_mcp, dry_run=args.dry_run):
|
|
129
|
+
baseline_changed += 1
|
|
130
|
+
print(f"upsert: {codex_home / 'config.toml'}")
|
|
131
|
+
if ensure_bridge_skill(codex_home=codex_home, dry_run=args.dry_run):
|
|
132
|
+
baseline_changed += 1
|
|
133
|
+
print(f"upsert: {codex_home / 'skills' / 'claudekit-command-bridge'}")
|
|
134
|
+
|
|
135
|
+
# Enable multi_agent flag
|
|
136
|
+
config_path = codex_home / "config.toml"
|
|
137
|
+
if enforce_multi_agent_flag(config_path, dry_run=args.dry_run):
|
|
138
|
+
print(f"upsert: multi_agent = true in {config_path}")
|
|
139
|
+
|
|
140
|
+
print(f"baseline_changed={baseline_changed}")
|
|
141
|
+
|
|
142
|
+
prompt_stats = export_prompts(codex_home=codex_home, include_mcp=args.include_mcp, dry_run=args.dry_run)
|
|
143
|
+
print(
|
|
144
|
+
f"prompts: added={prompt_stats['added']} updated={prompt_stats['updated']} "
|
|
145
|
+
f"total_generated={prompt_stats['total_generated']}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
bootstrap_stats = None
|
|
149
|
+
if not args.skip_bootstrap:
|
|
150
|
+
bootstrap_stats = bootstrap_deps(
|
|
151
|
+
codex_home=codex_home,
|
|
152
|
+
include_mcp=args.include_mcp,
|
|
153
|
+
include_test_deps=args.include_test_deps,
|
|
154
|
+
dry_run=args.dry_run,
|
|
155
|
+
)
|
|
156
|
+
print(
|
|
157
|
+
f"bootstrap: python_ok={bootstrap_stats['python_ok']} "
|
|
158
|
+
f"python_fail={bootstrap_stats['python_fail']}"
|
|
159
|
+
)
|
|
160
|
+
if (bootstrap_stats["python_fail"] or bootstrap_stats["node_fail"]) and not args.dry_run:
|
|
161
|
+
raise SyncError("Dependency bootstrap reported failures")
|
|
162
|
+
|
|
163
|
+
verify_stats = None
|
|
164
|
+
if not args.skip_verify:
|
|
165
|
+
verify_stats = verify_runtime(codex_home=codex_home, dry_run=args.dry_run)
|
|
166
|
+
print(f"verify: {verify_stats}")
|
|
167
|
+
|
|
168
|
+
# Save registry
|
|
169
|
+
if not args.dry_run:
|
|
170
|
+
save_registry(codex_home, registry)
|
|
171
|
+
|
|
172
|
+
summary = {
|
|
173
|
+
"source": str(source) if use_live else str(zip_path),
|
|
174
|
+
"source_mode": "live" if use_live else "zip",
|
|
175
|
+
"codex_home": str(codex_home),
|
|
176
|
+
"workspace": str(workspace),
|
|
177
|
+
"dry_run": args.dry_run,
|
|
178
|
+
"include_mcp": args.include_mcp,
|
|
179
|
+
"include_hooks": args.include_hooks,
|
|
180
|
+
"assets": assets_stats,
|
|
181
|
+
"skills": skills_stats,
|
|
182
|
+
"normalize_changed": changed,
|
|
183
|
+
"agent_toml_changed": agent_toml_changed,
|
|
184
|
+
"baseline_changed": baseline_changed,
|
|
185
|
+
"prompts": prompt_stats,
|
|
186
|
+
"bootstrap": bootstrap_stats,
|
|
187
|
+
"verify": verify_stats,
|
|
188
|
+
}
|
|
189
|
+
print_summary(summary)
|
|
190
|
+
print("done: claudekit all-in-one sync completed")
|
|
191
|
+
return 0
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == "__main__":
|
|
195
|
+
try:
|
|
196
|
+
raise SystemExit(main())
|
|
197
|
+
except SyncError as exc:
|
|
198
|
+
eprint(f"error: {exc}")
|
|
199
|
+
raise SystemExit(2)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Config enforcement for Codex."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ensure_agents(*, workspace: Path, dry_run: bool) -> bool:
|
|
11
|
+
"""Ensure AGENTS.md exists in workspace."""
|
|
12
|
+
from .utils import load_template, write_text_if_changed
|
|
13
|
+
target = workspace / "AGENTS.md"
|
|
14
|
+
template = load_template("agents-md.md")
|
|
15
|
+
return write_text_if_changed(target, template, dry_run=dry_run)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def enforce_config(*, codex_home: Path, include_mcp: bool, dry_run: bool) -> bool:
|
|
19
|
+
"""Enforce Codex config defaults."""
|
|
20
|
+
config = codex_home / "config.toml"
|
|
21
|
+
if config.exists():
|
|
22
|
+
text = config.read_text(encoding="utf-8")
|
|
23
|
+
else:
|
|
24
|
+
text = ""
|
|
25
|
+
orig = text
|
|
26
|
+
|
|
27
|
+
if re.search(r"^project_doc_max_bytes\s*=", text, flags=re.M):
|
|
28
|
+
text = re.sub(r"^project_doc_max_bytes\s*=.*$", "project_doc_max_bytes = 65536", text, flags=re.M)
|
|
29
|
+
else:
|
|
30
|
+
text = (text.rstrip("\n") + "\nproject_doc_max_bytes = 65536\n").lstrip("\n")
|
|
31
|
+
|
|
32
|
+
fallback_line = 'project_doc_fallback_filenames = ["AGENTS.md", "CLAUDE.md", "AGENTS.override.md"]'
|
|
33
|
+
if re.search(r"^project_doc_fallback_filenames\s*=", text, flags=re.M):
|
|
34
|
+
text = re.sub(r"^project_doc_fallback_filenames\s*=.*$", fallback_line, text, flags=re.M)
|
|
35
|
+
else:
|
|
36
|
+
text = text.rstrip("\n") + "\n" + fallback_line + "\n"
|
|
37
|
+
|
|
38
|
+
mcp_management_path = str((codex_home / "skills" / "mcp-management").resolve())
|
|
39
|
+
mcp_builder_path = str((codex_home / "skills" / "mcp-builder").resolve())
|
|
40
|
+
mcp_enabled = "true" if include_mcp else "false"
|
|
41
|
+
|
|
42
|
+
pattern = re.compile(r"\n\[\[skills\.config\]\]\n(?:[^\n]*\n)*?(?=\n\[\[skills\.config\]\]|\Z)", re.M)
|
|
43
|
+
blocks = pattern.findall("\n" + text)
|
|
44
|
+
kept: List[str] = []
|
|
45
|
+
for block in blocks:
|
|
46
|
+
if f'path = "{mcp_management_path}"' in block:
|
|
47
|
+
continue
|
|
48
|
+
if f'path = "{mcp_builder_path}"' in block:
|
|
49
|
+
continue
|
|
50
|
+
kept.append(block.rstrip("\n"))
|
|
51
|
+
|
|
52
|
+
base = pattern.sub("", "\n" + text).lstrip("\n").rstrip("\n")
|
|
53
|
+
for block in kept:
|
|
54
|
+
if block:
|
|
55
|
+
base += "\n\n" + block
|
|
56
|
+
|
|
57
|
+
base += f'\n\n[[skills.config]]\npath = "{mcp_management_path}"\nenabled = {mcp_enabled}\n'
|
|
58
|
+
base += f'\n[[skills.config]]\npath = "{mcp_builder_path}"\nenabled = {mcp_enabled}\n'
|
|
59
|
+
|
|
60
|
+
if base == orig:
|
|
61
|
+
return False
|
|
62
|
+
if not dry_run:
|
|
63
|
+
config.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
config.write_text(base, encoding="utf-8")
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def enforce_multi_agent_flag(config_path: Path, dry_run: bool) -> bool:
|
|
69
|
+
"""Ensure multi_agent and child_agents_md flags are set in config."""
|
|
70
|
+
text = config_path.read_text() if config_path.exists() else ""
|
|
71
|
+
orig = text
|
|
72
|
+
|
|
73
|
+
if "[features]" in text:
|
|
74
|
+
if "multi_agent" not in text:
|
|
75
|
+
text = text.replace("[features]", "[features]\nmulti_agent = true")
|
|
76
|
+
if "child_agents_md" not in text:
|
|
77
|
+
text = text.replace("[features]", "[features]\nchild_agents_md = true")
|
|
78
|
+
else:
|
|
79
|
+
text += "\n[features]\nmulti_agent = true\nchild_agents_md = true\n"
|
|
80
|
+
|
|
81
|
+
if text != orig and not dry_run:
|
|
82
|
+
config_path.write_text(text)
|
|
83
|
+
return text != orig
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _extract_description(toml_text: str) -> str:
|
|
87
|
+
"""Extract first meaningful sentence from developer_instructions."""
|
|
88
|
+
match = re.search(
|
|
89
|
+
r'developer_instructions\s*=\s*"""(.*?)"""',
|
|
90
|
+
toml_text,
|
|
91
|
+
re.DOTALL,
|
|
92
|
+
)
|
|
93
|
+
if not match:
|
|
94
|
+
return ""
|
|
95
|
+
body = match.group(1).strip()
|
|
96
|
+
for line in body.splitlines():
|
|
97
|
+
line = line.strip()
|
|
98
|
+
if line.startswith("#") or not line:
|
|
99
|
+
continue
|
|
100
|
+
# Strip markdown bold
|
|
101
|
+
line = re.sub(r"\*\*([^*]+)\*\*", r"\1", line)
|
|
102
|
+
# Take first sentence
|
|
103
|
+
dot = line.find(". ")
|
|
104
|
+
if dot > 0:
|
|
105
|
+
return line[: dot + 1]
|
|
106
|
+
return line[:120]
|
|
107
|
+
return ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def register_agents(*, codex_home: Path, dry_run: bool) -> int:
|
|
111
|
+
"""Register agent TOMLs as [agents.*] roles in config.toml."""
|
|
112
|
+
agents_dir = codex_home / "agents"
|
|
113
|
+
config_path = codex_home / "config.toml"
|
|
114
|
+
if not agents_dir.exists():
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
text = config_path.read_text(encoding="utf-8") if config_path.exists() else ""
|
|
118
|
+
added = 0
|
|
119
|
+
|
|
120
|
+
for toml_file in sorted(agents_dir.glob("*.toml")):
|
|
121
|
+
slug = toml_file.stem
|
|
122
|
+
section_header = f"[agents.{slug}]"
|
|
123
|
+
if section_header in text:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
content = toml_file.read_text(encoding="utf-8")
|
|
127
|
+
desc = _extract_description(content) or f"{slug.replace('_', ' ').title()} agent"
|
|
128
|
+
# Escape quotes in description
|
|
129
|
+
desc = desc.replace('"', '\\"')
|
|
130
|
+
|
|
131
|
+
text += (
|
|
132
|
+
f"\n{section_header}\n"
|
|
133
|
+
f'description = "{desc}"\n'
|
|
134
|
+
f'config_file = "agents/{toml_file.name}"\n'
|
|
135
|
+
)
|
|
136
|
+
added += 1
|
|
137
|
+
|
|
138
|
+
if added > 0 and not dry_run:
|
|
139
|
+
config_path.write_text(text, encoding="utf-8")
|
|
140
|
+
return added
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Constants for path normalization and sync operations."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Set, Tuple
|
|
4
|
+
|
|
5
|
+
ASSET_DIRS = {"agents", "commands", "output-styles", "rules", "scripts"}
|
|
6
|
+
ASSET_FILES = {
|
|
7
|
+
"CLAUDE.md",
|
|
8
|
+
".ck.json",
|
|
9
|
+
".ckignore",
|
|
10
|
+
".env.example",
|
|
11
|
+
".mcp.json.example",
|
|
12
|
+
"settings.json",
|
|
13
|
+
"metadata.json",
|
|
14
|
+
"statusline.cjs",
|
|
15
|
+
"statusline.sh",
|
|
16
|
+
"statusline.ps1",
|
|
17
|
+
}
|
|
18
|
+
ASSET_MANIFEST = ".sync-manifest-assets.txt"
|
|
19
|
+
PROMPT_MANIFEST = ".claudekit-generated-prompts.txt"
|
|
20
|
+
REGISTRY_FILE = ".claudekit-sync-registry.json"
|
|
21
|
+
|
|
22
|
+
EXCLUDED_SKILLS_ALWAYS: Set[str] = {"template-skill"}
|
|
23
|
+
MCP_SKILLS: Set[str] = {"mcp-builder", "mcp-management"}
|
|
24
|
+
CONFLICT_SKILLS: Set[str] = {"skill-creator"}
|
|
25
|
+
|
|
26
|
+
SKILL_MD_REPLACEMENTS: List[Tuple[str, str]] = [
|
|
27
|
+
("$HOME/.claude/skills/", "${CODEX_HOME:-$HOME/.codex}/skills/"),
|
|
28
|
+
("$HOME/.claude/scripts/", "${CODEX_HOME:-$HOME/.codex}/claudekit/scripts/"),
|
|
29
|
+
("$HOME/.claude/rules/", "${CODEX_HOME:-$HOME/.codex}/claudekit/rules/"),
|
|
30
|
+
("$HOME/.claude/", "${CODEX_HOME:-$HOME/.codex}/"),
|
|
31
|
+
("./.claude/skills/", "${CODEX_HOME:-$HOME/.codex}/skills/"),
|
|
32
|
+
(".claude/skills/", "${CODEX_HOME:-$HOME/.codex}/skills/"),
|
|
33
|
+
("./.claude/scripts/", "${CODEX_HOME:-$HOME/.codex}/claudekit/scripts/"),
|
|
34
|
+
(".claude/scripts/", "${CODEX_HOME:-$HOME/.codex}/claudekit/scripts/"),
|
|
35
|
+
("./.claude/rules/", "${CODEX_HOME:-$HOME/.codex}/claudekit/rules/"),
|
|
36
|
+
(".claude/rules/", "${CODEX_HOME:-$HOME/.codex}/claudekit/rules/"),
|
|
37
|
+
("~/.claude/.ck.json", "~/.codex/claudekit/.ck.json"),
|
|
38
|
+
("./.claude/.ck.json", "~/.codex/claudekit/.ck.json"),
|
|
39
|
+
(".claude/.ck.json", "~/.codex/claudekit/.ck.json"),
|
|
40
|
+
("~/.claude/", "~/.codex/"),
|
|
41
|
+
("./.claude/", "./.codex/"),
|
|
42
|
+
("<project>/.claude/", "<project>/.codex/"),
|
|
43
|
+
(".claude/", ".codex/"),
|
|
44
|
+
("`.claude`", "`.codex`"),
|
|
45
|
+
("$HOME/${CODEX_HOME:-$HOME/.codex}/", "${CODEX_HOME:-$HOME/.codex}/"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
PROMPT_REPLACEMENTS: List[Tuple[str, str]] = [
|
|
49
|
+
("$HOME/.claude/skills/", "${CODEX_HOME:-$HOME/.codex}/skills/"),
|
|
50
|
+
("$HOME/.claude/scripts/", "${CODEX_HOME:-$HOME/.codex}/claudekit/scripts/"),
|
|
51
|
+
("$HOME/.claude/rules/", "${CODEX_HOME:-$HOME/.codex}/claudekit/rules/"),
|
|
52
|
+
("$HOME/.claude/", "${CODEX_HOME:-$HOME/.codex}/"),
|
|
53
|
+
("./.claude/skills/", "~/.codex/skills/"),
|
|
54
|
+
(".claude/skills/", "~/.codex/skills/"),
|
|
55
|
+
("./.claude/scripts/", "~/.codex/claudekit/scripts/"),
|
|
56
|
+
(".claude/scripts/", "~/.codex/claudekit/scripts/"),
|
|
57
|
+
("./.claude/rules/", "~/.codex/claudekit/rules/"),
|
|
58
|
+
(".claude/rules/", "~/.codex/claudekit/rules/"),
|
|
59
|
+
("~/.claude/.ck.json", "~/.codex/claudekit/.ck.json"),
|
|
60
|
+
("./.claude/.ck.json", "~/.codex/claudekit/.ck.json"),
|
|
61
|
+
(".claude/.ck.json", "~/.codex/claudekit/.ck.json"),
|
|
62
|
+
("$HOME/${CODEX_HOME:-$HOME/.codex}/", "${CODEX_HOME:-$HOME/.codex}/"),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
AGENT_TOML_REPLACEMENTS: List[Tuple[str, str]] = [
|
|
66
|
+
("$HOME/.claude/skills/", "${CODEX_HOME:-$HOME/.codex}/skills/"),
|
|
67
|
+
("$HOME/.claude/scripts/", "${CODEX_HOME:-$HOME/.codex}/claudekit/scripts/"),
|
|
68
|
+
("$HOME/.claude/rules/", "${CODEX_HOME:-$HOME/.codex}/claudekit/rules/"),
|
|
69
|
+
("$HOME/.claude/.ck.json", "${CODEX_HOME:-$HOME/.codex}/claudekit/.ck.json"),
|
|
70
|
+
("$HOME/.claude/.mcp.json", "${CODEX_HOME:-$HOME/.codex}/claudekit/.mcp.json"),
|
|
71
|
+
("$HOME/.claude/", "${CODEX_HOME:-$HOME/.codex}/"),
|
|
72
|
+
("~/.claude/", "~/.codex/"),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
CLAUDE_SYNTAX_ADAPTATIONS: List[Tuple[str, str]] = [
|
|
76
|
+
("Task(Explore)", "the explore agent"),
|
|
77
|
+
("Task(researcher)", "the researcher agent"),
|
|
78
|
+
("Task(", "delegate to "),
|
|
79
|
+
("$HOME/.claude/skills/*", "${CODEX_HOME:-$HOME/.codex}/skills/*"),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
# Claude model → Codex model mapping (per developers.openai.com/codex/multi-agent)
|
|
83
|
+
CLAUDE_TO_CODEX_MODELS: dict[str, str] = {
|
|
84
|
+
"opus": "gpt-5.3-codex",
|
|
85
|
+
"sonnet": "gpt-5.3-codex",
|
|
86
|
+
"haiku": "gpt-5.3-codex-spark",
|
|
87
|
+
"inherit": "",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Reasoning effort per Claude model tier
|
|
91
|
+
CLAUDE_MODEL_REASONING_EFFORT: dict[str, str] = {
|
|
92
|
+
"opus": "xhigh",
|
|
93
|
+
"sonnet": "high",
|
|
94
|
+
"haiku": "medium",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Agents that should be read-only (no file writes needed)
|
|
98
|
+
READ_ONLY_AGENT_ROLES: Set[str] = {
|
|
99
|
+
"brainstormer",
|
|
100
|
+
"code_reviewer",
|
|
101
|
+
"researcher",
|
|
102
|
+
"project_manager",
|
|
103
|
+
"journal_writer",
|
|
104
|
+
}
|