codexopt 0.1.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.
- codexopt/__init__.py +4 -0
- codexopt/__main__.py +3 -0
- codexopt/applier.py +71 -0
- codexopt/artifacts.py +63 -0
- codexopt/benchmark.py +114 -0
- codexopt/cli.py +233 -0
- codexopt/config.py +131 -0
- codexopt/optimizer.py +317 -0
- codexopt/reporter.py +96 -0
- codexopt/scanner.py +163 -0
- codexopt/types.py +51 -0
- codexopt-0.1.0.dist-info/METADATA +475 -0
- codexopt-0.1.0.dist-info/RECORD +17 -0
- codexopt-0.1.0.dist-info/WHEEL +5 -0
- codexopt-0.1.0.dist-info/entry_points.txt +2 -0
- codexopt-0.1.0.dist-info/licenses/LICENSE +21 -0
- codexopt-0.1.0.dist-info/top_level.txt +1 -0
codexopt/__init__.py
ADDED
codexopt/__main__.py
ADDED
codexopt/applier.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from datetime import timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def apply_optimization_result(
|
|
10
|
+
optimization_result: dict[str, Any],
|
|
11
|
+
repo_root: Path,
|
|
12
|
+
backup_root: Path,
|
|
13
|
+
dry_run: bool = False,
|
|
14
|
+
) -> dict[str, Any]:
|
|
15
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
16
|
+
backup_dir = backup_root / timestamp
|
|
17
|
+
applied: list[str] = []
|
|
18
|
+
skipped: list[str] = []
|
|
19
|
+
|
|
20
|
+
if not dry_run:
|
|
21
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
|
|
23
|
+
for item in optimization_result.get("results", []):
|
|
24
|
+
path = Path(item["path"])
|
|
25
|
+
if item.get("delta", 0.0) <= 0.0:
|
|
26
|
+
skipped.append(f"{path} (no improvement)")
|
|
27
|
+
continue
|
|
28
|
+
if not path.exists():
|
|
29
|
+
skipped.append(f"{path} (missing)")
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
new_content = str(item.get("best_content", ""))
|
|
33
|
+
current = path.read_text(encoding="utf-8", errors="replace")
|
|
34
|
+
if current == new_content:
|
|
35
|
+
skipped.append(f"{path} (no changes)")
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
if dry_run:
|
|
39
|
+
applied.append(str(path))
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
rel = path.resolve().relative_to(repo_root.resolve())
|
|
44
|
+
except Exception:
|
|
45
|
+
rel = Path(path.name)
|
|
46
|
+
backup_path = backup_dir / rel
|
|
47
|
+
backup_path.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
backup_path.write_text(current, encoding="utf-8")
|
|
49
|
+
path.write_text(new_content, encoding="utf-8")
|
|
50
|
+
applied.append(str(path))
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
"dry_run": dry_run,
|
|
54
|
+
"backup_dir": str(backup_dir),
|
|
55
|
+
"applied_count": len(applied),
|
|
56
|
+
"skipped_count": len(skipped),
|
|
57
|
+
"applied": applied,
|
|
58
|
+
"skipped": skipped,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def print_apply_summary(result: dict[str, Any]) -> None:
|
|
63
|
+
print(f"dry_run: {result['dry_run']}")
|
|
64
|
+
print(f"applied_count: {result['applied_count']}")
|
|
65
|
+
print(f"skipped_count: {result['skipped_count']}")
|
|
66
|
+
if not result["dry_run"]:
|
|
67
|
+
print(f"backup_dir: {result['backup_dir']}")
|
|
68
|
+
for item in result["applied"]:
|
|
69
|
+
print(f"- applied: {item}")
|
|
70
|
+
for item in result["skipped"]:
|
|
71
|
+
print(f"- skipped: {item}")
|
codexopt/artifacts.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from datetime import timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
STATE_FILENAME = "state.json"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def ensure_output_dirs(root_dir: Path) -> None:
|
|
14
|
+
(root_dir / "runs").mkdir(parents=True, exist_ok=True)
|
|
15
|
+
(root_dir / "backups").mkdir(parents=True, exist_ok=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_state(root_dir: Path) -> dict[str, Any]:
|
|
19
|
+
state_path = root_dir / STATE_FILENAME
|
|
20
|
+
if not state_path.exists():
|
|
21
|
+
return {}
|
|
22
|
+
try:
|
|
23
|
+
return json.loads(state_path.read_text(encoding="utf-8"))
|
|
24
|
+
except Exception:
|
|
25
|
+
return {}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def save_state(root_dir: Path, state: dict[str, Any]) -> None:
|
|
29
|
+
state_path = root_dir / STATE_FILENAME
|
|
30
|
+
state_path.write_text(json.dumps(state, indent=2, sort_keys=True), encoding="utf-8")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def new_run_dir(root_dir: Path, kind: str) -> tuple[str, Path]:
|
|
34
|
+
ensure_output_dirs(root_dir)
|
|
35
|
+
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
|
|
36
|
+
run_id = f"{stamp}-{kind}"
|
|
37
|
+
run_dir = root_dir / "runs" / run_id
|
|
38
|
+
run_dir.mkdir(parents=True, exist_ok=False)
|
|
39
|
+
return run_id, run_dir
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def write_json(path: Path, data: dict[str, Any]) -> None:
|
|
43
|
+
path.write_text(json.dumps(data, indent=2, sort_keys=True), encoding="utf-8")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def read_json(path: Path) -> dict[str, Any]:
|
|
47
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def set_latest_run(root_dir: Path, state_key: str, run_id: str) -> None:
|
|
51
|
+
state = load_state(root_dir)
|
|
52
|
+
state[state_key] = run_id
|
|
53
|
+
save_state(root_dir, state)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def resolve_run_id(root_dir: Path, state_key: str, explicit_run_id: str | None) -> str | None:
|
|
57
|
+
if explicit_run_id:
|
|
58
|
+
return explicit_run_id
|
|
59
|
+
state = load_state(root_dir)
|
|
60
|
+
value = state.get(state_key)
|
|
61
|
+
if isinstance(value, str):
|
|
62
|
+
return value
|
|
63
|
+
return None
|
codexopt/benchmark.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .types import FileScore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _score_agents(entry: dict[str, Any]) -> FileScore:
|
|
9
|
+
score = 1.0
|
|
10
|
+
issues: list[str] = []
|
|
11
|
+
details: dict[str, Any] = {}
|
|
12
|
+
words = int(entry.get("words", 0))
|
|
13
|
+
tokens = int(entry.get("token_estimate", 0))
|
|
14
|
+
|
|
15
|
+
if words < 80:
|
|
16
|
+
score -= 0.2
|
|
17
|
+
issues.append("too_short")
|
|
18
|
+
if words > 2400:
|
|
19
|
+
score -= min(0.35, (words - 2400) / 10000.0)
|
|
20
|
+
issues.append("too_long")
|
|
21
|
+
if tokens > 7000:
|
|
22
|
+
score -= 0.15
|
|
23
|
+
issues.append("token_heavy")
|
|
24
|
+
if "empty_agents" in entry.get("issues", []):
|
|
25
|
+
score -= 0.6
|
|
26
|
+
issues.append("empty")
|
|
27
|
+
|
|
28
|
+
details["words"] = words
|
|
29
|
+
details["token_estimate"] = tokens
|
|
30
|
+
return FileScore(
|
|
31
|
+
path=entry["path"],
|
|
32
|
+
kind="agents",
|
|
33
|
+
score=max(0.0, min(1.0, score)),
|
|
34
|
+
issues=issues,
|
|
35
|
+
details=details,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _score_skill(entry: dict[str, Any]) -> FileScore:
|
|
40
|
+
score = 1.0
|
|
41
|
+
issues: list[str] = []
|
|
42
|
+
details: dict[str, Any] = {}
|
|
43
|
+
words = int(entry.get("words", 0))
|
|
44
|
+
entry_issues = set(entry.get("issues", []))
|
|
45
|
+
|
|
46
|
+
if "missing_frontmatter" in entry_issues:
|
|
47
|
+
score -= 0.6
|
|
48
|
+
issues.append("missing_frontmatter")
|
|
49
|
+
if "missing_name" in entry_issues:
|
|
50
|
+
score -= 0.2
|
|
51
|
+
issues.append("missing_name")
|
|
52
|
+
if "missing_description" in entry_issues:
|
|
53
|
+
score -= 0.2
|
|
54
|
+
issues.append("missing_description")
|
|
55
|
+
if "name_too_long" in entry_issues:
|
|
56
|
+
score -= 0.15
|
|
57
|
+
issues.append("name_too_long")
|
|
58
|
+
if "description_too_long" in entry_issues:
|
|
59
|
+
score -= 0.15
|
|
60
|
+
issues.append("description_too_long")
|
|
61
|
+
if words < 40:
|
|
62
|
+
score -= 0.15
|
|
63
|
+
issues.append("too_short")
|
|
64
|
+
if words > 1800:
|
|
65
|
+
score -= min(0.25, (words - 1800) / 10000.0)
|
|
66
|
+
issues.append("too_long")
|
|
67
|
+
|
|
68
|
+
details["words"] = words
|
|
69
|
+
details["frontmatter_present"] = entry.get("metadata", {}).get("frontmatter_present", False)
|
|
70
|
+
return FileScore(
|
|
71
|
+
path=entry["path"],
|
|
72
|
+
kind="skill",
|
|
73
|
+
score=max(0.0, min(1.0, score)),
|
|
74
|
+
issues=issues,
|
|
75
|
+
details=details,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def score_entry(entry: dict[str, Any]) -> FileScore:
|
|
80
|
+
kind = entry.get("kind")
|
|
81
|
+
if kind == "agents":
|
|
82
|
+
return _score_agents(entry)
|
|
83
|
+
return _score_skill(entry)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def run_benchmark(scan_result: dict[str, Any]) -> dict[str, Any]:
|
|
87
|
+
scores: list[FileScore] = [score_entry(entry) for entry in scan_result["entries"]]
|
|
88
|
+
if scores:
|
|
89
|
+
overall = sum(item.score for item in scores) / len(scores)
|
|
90
|
+
else:
|
|
91
|
+
overall = 0.0
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
"counts": scan_result["counts"],
|
|
95
|
+
"overall_score": round(overall, 4),
|
|
96
|
+
"files": [
|
|
97
|
+
{
|
|
98
|
+
"path": item.path,
|
|
99
|
+
"kind": item.kind,
|
|
100
|
+
"score": round(item.score, 4),
|
|
101
|
+
"issues": item.issues,
|
|
102
|
+
"details": item.details,
|
|
103
|
+
}
|
|
104
|
+
for item in scores
|
|
105
|
+
],
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def print_benchmark_summary(result: dict[str, Any]) -> None:
|
|
110
|
+
print(f"overall_score: {result['overall_score']:.4f}")
|
|
111
|
+
for file_result in result["files"]:
|
|
112
|
+
issues = ", ".join(file_result["issues"]) if file_result["issues"] else "ok"
|
|
113
|
+
print(f"- {file_result['kind']}: {file_result['path']}")
|
|
114
|
+
print(f" score={file_result['score']:.4f} issues={issues}")
|
codexopt/cli.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .applier import apply_optimization_result
|
|
9
|
+
from .applier import print_apply_summary
|
|
10
|
+
from .artifacts import new_run_dir
|
|
11
|
+
from .artifacts import read_json
|
|
12
|
+
from .artifacts import resolve_run_id
|
|
13
|
+
from .artifacts import set_latest_run
|
|
14
|
+
from .artifacts import write_json
|
|
15
|
+
from .benchmark import print_benchmark_summary
|
|
16
|
+
from .benchmark import run_benchmark
|
|
17
|
+
from .config import DEFAULT_CONFIG_FILENAME
|
|
18
|
+
from .config import load_config
|
|
19
|
+
from .config import write_default_config
|
|
20
|
+
from .optimizer import optimize_entries
|
|
21
|
+
from .optimizer import print_optimization_summary
|
|
22
|
+
from .reporter import build_markdown_report
|
|
23
|
+
from .scanner import print_scan_summary
|
|
24
|
+
from .scanner import scan_project
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _resolve_config(config_path: str | None) -> tuple[Any, Path]:
|
|
28
|
+
path = Path(config_path).resolve() if config_path else None
|
|
29
|
+
return load_config(path)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
33
|
+
path = Path(args.path or DEFAULT_CONFIG_FILENAME).resolve()
|
|
34
|
+
write_default_config(path, force=args.force)
|
|
35
|
+
print(f"wrote {path}")
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cmd_scan(args: argparse.Namespace) -> int:
|
|
40
|
+
cfg, _cfg_path = _resolve_config(args.config)
|
|
41
|
+
cwd = Path.cwd()
|
|
42
|
+
result = scan_project(cwd, cfg)
|
|
43
|
+
|
|
44
|
+
output_root = Path(cfg.output.root_dir)
|
|
45
|
+
run_id, run_dir = new_run_dir(output_root, "scan")
|
|
46
|
+
write_json(run_dir / "scan.json", result)
|
|
47
|
+
set_latest_run(output_root, "latest_scan_run", run_id)
|
|
48
|
+
|
|
49
|
+
print_scan_summary(result)
|
|
50
|
+
print(f"run_id: {run_id}")
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def cmd_benchmark(args: argparse.Namespace) -> int:
|
|
55
|
+
cfg, _cfg_path = _resolve_config(args.config)
|
|
56
|
+
cwd = Path.cwd()
|
|
57
|
+
scan_result = scan_project(cwd, cfg)
|
|
58
|
+
benchmark_result = run_benchmark(scan_result)
|
|
59
|
+
|
|
60
|
+
output_root = Path(cfg.output.root_dir)
|
|
61
|
+
run_id, run_dir = new_run_dir(output_root, "benchmark")
|
|
62
|
+
write_json(run_dir / "scan.json", scan_result)
|
|
63
|
+
write_json(run_dir / "benchmark.json", benchmark_result)
|
|
64
|
+
set_latest_run(output_root, "latest_scan_run", run_id)
|
|
65
|
+
set_latest_run(output_root, "latest_benchmark_run", run_id)
|
|
66
|
+
|
|
67
|
+
print_benchmark_summary(benchmark_result)
|
|
68
|
+
print(f"run_id: {run_id}")
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _optimize(args: argparse.Namespace, kind: str) -> int:
|
|
73
|
+
cfg, _cfg_path = _resolve_config(args.config)
|
|
74
|
+
cwd = Path.cwd()
|
|
75
|
+
|
|
76
|
+
agents_patterns = args.file if kind == "agents" and args.file else None
|
|
77
|
+
skills_patterns = args.glob if kind == "skill" and args.glob else None
|
|
78
|
+
|
|
79
|
+
scan_result = scan_project(
|
|
80
|
+
cwd=cwd,
|
|
81
|
+
config=cfg,
|
|
82
|
+
agents_files=agents_patterns,
|
|
83
|
+
skills_globs=skills_patterns,
|
|
84
|
+
)
|
|
85
|
+
result = optimize_entries(
|
|
86
|
+
entries=scan_result["entries"],
|
|
87
|
+
kind=kind,
|
|
88
|
+
engine=args.engine or cfg.optimization.engine,
|
|
89
|
+
min_delta=cfg.optimization.min_apply_delta,
|
|
90
|
+
reflection_model=args.reflection_model or cfg.optimization.reflection_model,
|
|
91
|
+
max_metric_calls=args.max_metric_calls or cfg.optimization.max_metric_calls,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
output_root = Path(cfg.output.root_dir)
|
|
95
|
+
run_label = "optimize-agents" if kind == "agents" else "optimize-skills"
|
|
96
|
+
run_id, run_dir = new_run_dir(output_root, run_label)
|
|
97
|
+
write_json(run_dir / "scan.json", scan_result)
|
|
98
|
+
write_json(run_dir / "optimize.json", result)
|
|
99
|
+
key = "latest_optimize_agents_run" if kind == "agents" else "latest_optimize_skills_run"
|
|
100
|
+
set_latest_run(output_root, key, run_id)
|
|
101
|
+
|
|
102
|
+
print_optimization_summary(result)
|
|
103
|
+
print(f"run_id: {run_id}")
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cmd_optimize_agents(args: argparse.Namespace) -> int:
|
|
108
|
+
return _optimize(args, "agents")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def cmd_optimize_skills(args: argparse.Namespace) -> int:
|
|
112
|
+
return _optimize(args, "skill")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def cmd_apply(args: argparse.Namespace) -> int:
|
|
116
|
+
cfg, _cfg_path = _resolve_config(args.config)
|
|
117
|
+
output_root = Path(cfg.output.root_dir)
|
|
118
|
+
|
|
119
|
+
if args.kind == "agents":
|
|
120
|
+
state_key = "latest_optimize_agents_run"
|
|
121
|
+
else:
|
|
122
|
+
state_key = "latest_optimize_skills_run"
|
|
123
|
+
|
|
124
|
+
run_id = resolve_run_id(output_root, state_key, args.run_id)
|
|
125
|
+
if not run_id:
|
|
126
|
+
print("no optimization run found", file=sys.stderr)
|
|
127
|
+
return 2
|
|
128
|
+
|
|
129
|
+
run_dir = output_root / "runs" / run_id
|
|
130
|
+
optimize_path = run_dir / "optimize.json"
|
|
131
|
+
if not optimize_path.exists():
|
|
132
|
+
print(f"missing optimize artifact: {optimize_path}", file=sys.stderr)
|
|
133
|
+
return 2
|
|
134
|
+
optimize_result = read_json(optimize_path)
|
|
135
|
+
apply_result = apply_optimization_result(
|
|
136
|
+
optimization_result=optimize_result,
|
|
137
|
+
repo_root=Path.cwd(),
|
|
138
|
+
backup_root=output_root / "backups",
|
|
139
|
+
dry_run=args.dry_run,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
apply_run_id, apply_run_dir = new_run_dir(output_root, "apply")
|
|
143
|
+
write_json(apply_run_dir / "apply.json", apply_result)
|
|
144
|
+
set_latest_run(output_root, "latest_apply_run", apply_run_id)
|
|
145
|
+
|
|
146
|
+
print_apply_summary(apply_result)
|
|
147
|
+
print(f"source_run_id: {run_id}")
|
|
148
|
+
print(f"run_id: {apply_run_id}")
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def cmd_report(args: argparse.Namespace) -> int:
|
|
153
|
+
cfg, _cfg_path = _resolve_config(args.config)
|
|
154
|
+
output_root = Path(cfg.output.root_dir)
|
|
155
|
+
report = build_markdown_report(output_root)
|
|
156
|
+
if args.output:
|
|
157
|
+
out = Path(args.output)
|
|
158
|
+
out.write_text(report, encoding="utf-8")
|
|
159
|
+
print(f"wrote {out}")
|
|
160
|
+
else:
|
|
161
|
+
print(report)
|
|
162
|
+
return 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
166
|
+
parser = argparse.ArgumentParser(prog="codexopt", description="Optimize Codex AGENTS.md and SKILL.md")
|
|
167
|
+
parser.add_argument("--config", help="Path to codexopt.yaml", default=None)
|
|
168
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
169
|
+
|
|
170
|
+
p_init = sub.add_parser("init", help="Write codexopt.yaml template")
|
|
171
|
+
p_init.add_argument("--path", default=None, help="Output config path")
|
|
172
|
+
p_init.add_argument("--force", action="store_true", help="Overwrite if exists")
|
|
173
|
+
p_init.set_defaults(func=cmd_init)
|
|
174
|
+
|
|
175
|
+
p_scan = sub.add_parser("scan", help="Scan AGENTS and SKILL files")
|
|
176
|
+
p_scan.set_defaults(func=cmd_scan)
|
|
177
|
+
|
|
178
|
+
p_bench = sub.add_parser("benchmark", help="Benchmark current instruction assets")
|
|
179
|
+
p_bench.set_defaults(func=cmd_benchmark)
|
|
180
|
+
|
|
181
|
+
p_opt = sub.add_parser("optimize", help="Optimize instruction assets")
|
|
182
|
+
opt_sub = p_opt.add_subparsers(dest="target", required=True)
|
|
183
|
+
|
|
184
|
+
p_opt_agents = opt_sub.add_parser("agents", help="Optimize AGENTS files")
|
|
185
|
+
p_opt_agents.add_argument(
|
|
186
|
+
"--file",
|
|
187
|
+
action="append",
|
|
188
|
+
help="AGENTS glob pattern (can be repeated). Defaults from config.",
|
|
189
|
+
)
|
|
190
|
+
p_opt_agents.add_argument("--engine", choices=["heuristic", "gepa"], default=None)
|
|
191
|
+
p_opt_agents.add_argument("--reflection-model", default=None)
|
|
192
|
+
p_opt_agents.add_argument("--max-metric-calls", type=int, default=None)
|
|
193
|
+
p_opt_agents.set_defaults(func=cmd_optimize_agents)
|
|
194
|
+
|
|
195
|
+
p_opt_skills = opt_sub.add_parser("skills", help="Optimize SKILL files")
|
|
196
|
+
p_opt_skills.add_argument(
|
|
197
|
+
"--glob",
|
|
198
|
+
action="append",
|
|
199
|
+
help="SKILL glob pattern (can be repeated). Defaults from config.",
|
|
200
|
+
)
|
|
201
|
+
p_opt_skills.add_argument("--engine", choices=["heuristic", "gepa"], default=None)
|
|
202
|
+
p_opt_skills.add_argument("--reflection-model", default=None)
|
|
203
|
+
p_opt_skills.add_argument("--max-metric-calls", type=int, default=None)
|
|
204
|
+
p_opt_skills.set_defaults(func=cmd_optimize_skills)
|
|
205
|
+
|
|
206
|
+
p_apply = sub.add_parser("apply", help="Apply best optimized candidates")
|
|
207
|
+
p_apply.add_argument("--kind", choices=["agents", "skills"], default="agents")
|
|
208
|
+
p_apply.add_argument("--run-id", default=None, help="Optimization run id to apply")
|
|
209
|
+
p_apply.add_argument("--dry-run", action="store_true")
|
|
210
|
+
p_apply.set_defaults(func=cmd_apply)
|
|
211
|
+
|
|
212
|
+
p_report = sub.add_parser("report", help="Render markdown report from latest runs")
|
|
213
|
+
p_report.add_argument("--output", default=None, help="Optional output markdown file")
|
|
214
|
+
p_report.set_defaults(func=cmd_report)
|
|
215
|
+
|
|
216
|
+
return parser
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def main(argv: list[str] | None = None) -> int:
|
|
220
|
+
parser = build_parser()
|
|
221
|
+
args = parser.parse_args(argv)
|
|
222
|
+
try:
|
|
223
|
+
return int(args.func(args))
|
|
224
|
+
except FileExistsError as exc:
|
|
225
|
+
print(str(exc), file=sys.stderr)
|
|
226
|
+
return 2
|
|
227
|
+
except Exception as exc: # pragma: no cover
|
|
228
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
229
|
+
return 1
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if __name__ == "__main__": # pragma: no cover
|
|
233
|
+
raise SystemExit(main())
|
codexopt/config.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import yaml
|
|
10
|
+
except Exception: # pragma: no cover
|
|
11
|
+
yaml = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_CONFIG_FILENAME = "codexopt.yaml"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class TargetsConfig:
|
|
19
|
+
agents_files: list[str] = field(
|
|
20
|
+
default_factory=lambda: ["AGENTS.md", "**/AGENTS.md", "**/AGENTS.override.md"]
|
|
21
|
+
)
|
|
22
|
+
skills_globs: list[str] = field(
|
|
23
|
+
default_factory=lambda: [".codex/skills/**/SKILL.md", "**/.codex/skills/**/SKILL.md"]
|
|
24
|
+
)
|
|
25
|
+
exclude_globs: list[str] = field(
|
|
26
|
+
default_factory=lambda: [
|
|
27
|
+
".git/**",
|
|
28
|
+
".codexopt/**",
|
|
29
|
+
".venv/**",
|
|
30
|
+
"node_modules/**",
|
|
31
|
+
"reference/**",
|
|
32
|
+
]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class OutputConfig:
|
|
38
|
+
root_dir: str = ".codexopt"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class OptimizationConfig:
|
|
43
|
+
engine: str = "heuristic"
|
|
44
|
+
min_apply_delta: float = 0.01
|
|
45
|
+
max_metric_calls: int = 60
|
|
46
|
+
reflection_model: str | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class CodexOptConfig:
|
|
51
|
+
version: int = 1
|
|
52
|
+
targets: TargetsConfig = field(default_factory=TargetsConfig)
|
|
53
|
+
output: OutputConfig = field(default_factory=OutputConfig)
|
|
54
|
+
optimization: OptimizationConfig = field(default_factory=OptimizationConfig)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def default_config() -> CodexOptConfig:
|
|
58
|
+
return CodexOptConfig()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _merge_dict(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
|
|
62
|
+
out = dict(base)
|
|
63
|
+
for key, value in patch.items():
|
|
64
|
+
if isinstance(value, dict) and isinstance(out.get(key), dict):
|
|
65
|
+
out[key] = _merge_dict(out[key], value)
|
|
66
|
+
else:
|
|
67
|
+
out[key] = value
|
|
68
|
+
return out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _config_to_dict(cfg: CodexOptConfig) -> dict[str, Any]:
|
|
72
|
+
return {
|
|
73
|
+
"version": cfg.version,
|
|
74
|
+
"targets": {
|
|
75
|
+
"agents_files": cfg.targets.agents_files,
|
|
76
|
+
"skills_globs": cfg.targets.skills_globs,
|
|
77
|
+
"exclude_globs": cfg.targets.exclude_globs,
|
|
78
|
+
},
|
|
79
|
+
"output": {"root_dir": cfg.output.root_dir},
|
|
80
|
+
"optimization": {
|
|
81
|
+
"engine": cfg.optimization.engine,
|
|
82
|
+
"min_apply_delta": cfg.optimization.min_apply_delta,
|
|
83
|
+
"max_metric_calls": cfg.optimization.max_metric_calls,
|
|
84
|
+
"reflection_model": cfg.optimization.reflection_model,
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _dict_to_config(data: dict[str, Any]) -> CodexOptConfig:
|
|
90
|
+
targets = data.get("targets", {})
|
|
91
|
+
output = data.get("output", {})
|
|
92
|
+
optimization = data.get("optimization", {})
|
|
93
|
+
return CodexOptConfig(
|
|
94
|
+
version=int(data.get("version", 1)),
|
|
95
|
+
targets=TargetsConfig(
|
|
96
|
+
agents_files=list(targets.get("agents_files", TargetsConfig().agents_files)),
|
|
97
|
+
skills_globs=list(targets.get("skills_globs", TargetsConfig().skills_globs)),
|
|
98
|
+
exclude_globs=list(targets.get("exclude_globs", TargetsConfig().exclude_globs)),
|
|
99
|
+
),
|
|
100
|
+
output=OutputConfig(root_dir=str(output.get("root_dir", ".codexopt"))),
|
|
101
|
+
optimization=OptimizationConfig(
|
|
102
|
+
engine=str(optimization.get("engine", "heuristic")),
|
|
103
|
+
min_apply_delta=float(optimization.get("min_apply_delta", 0.01)),
|
|
104
|
+
max_metric_calls=int(optimization.get("max_metric_calls", 60)),
|
|
105
|
+
reflection_model=optimization.get("reflection_model"),
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def load_config(path: Path | None = None) -> tuple[CodexOptConfig, Path]:
|
|
111
|
+
cfg_path = path or Path.cwd() / DEFAULT_CONFIG_FILENAME
|
|
112
|
+
cfg = default_config()
|
|
113
|
+
if not cfg_path.exists():
|
|
114
|
+
return cfg, cfg_path
|
|
115
|
+
if yaml is None:
|
|
116
|
+
raise RuntimeError("PyYAML is required to read codexopt.yaml")
|
|
117
|
+
|
|
118
|
+
raw = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or {}
|
|
119
|
+
merged = _merge_dict(_config_to_dict(cfg), raw)
|
|
120
|
+
return _dict_to_config(merged), cfg_path
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def write_default_config(path: Path, force: bool = False) -> None:
|
|
124
|
+
if path.exists() and not force:
|
|
125
|
+
raise FileExistsError(f"{path} already exists; pass --force to overwrite")
|
|
126
|
+
if yaml is None:
|
|
127
|
+
raise RuntimeError("PyYAML is required to write codexopt.yaml")
|
|
128
|
+
|
|
129
|
+
data = _config_to_dict(default_config())
|
|
130
|
+
rendered = yaml.safe_dump(data, sort_keys=False)
|
|
131
|
+
path.write_text(rendered, encoding="utf-8")
|