codeoptix 0.1.3__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.
Files changed (92) hide show
  1. codeoptix/__init__.py +8 -0
  2. codeoptix/acp/__init__.py +33 -0
  3. codeoptix/acp/agent.py +209 -0
  4. codeoptix/acp/bridge.py +402 -0
  5. codeoptix/acp/client_adapter.py +312 -0
  6. codeoptix/acp/code_extractor.py +125 -0
  7. codeoptix/acp/orchestrator.py +349 -0
  8. codeoptix/acp/registry.py +294 -0
  9. codeoptix/adapters/__init__.py +18 -0
  10. codeoptix/adapters/base.py +50 -0
  11. codeoptix/adapters/basic.py +195 -0
  12. codeoptix/adapters/claude_code.py +221 -0
  13. codeoptix/adapters/codex.py +327 -0
  14. codeoptix/adapters/factory.py +56 -0
  15. codeoptix/adapters/gemini_cli.py +370 -0
  16. codeoptix/artifacts/__init__.py +5 -0
  17. codeoptix/artifacts/manager.py +193 -0
  18. codeoptix/behaviors/__init__.py +45 -0
  19. codeoptix/behaviors/base.py +81 -0
  20. codeoptix/behaviors/insecure_code.py +129 -0
  21. codeoptix/behaviors/plan_drift.py +192 -0
  22. codeoptix/behaviors/vacuous_tests.py +198 -0
  23. codeoptix/cli.py +1468 -0
  24. codeoptix/evaluation/__init__.py +23 -0
  25. codeoptix/evaluation/bloom_integration.py +271 -0
  26. codeoptix/evaluation/engine.py +274 -0
  27. codeoptix/evaluation/evaluators.py +308 -0
  28. codeoptix/evaluation/scenario_generator.py +222 -0
  29. codeoptix/evolution/__init__.py +7 -0
  30. codeoptix/evolution/engine.py +206 -0
  31. codeoptix/evolution/gepa_integration.py +149 -0
  32. codeoptix/evolution/proposer.py +185 -0
  33. codeoptix/linters/__init__.py +13 -0
  34. codeoptix/linters/bandit_linter.py +172 -0
  35. codeoptix/linters/base.py +105 -0
  36. codeoptix/linters/coverage_linter.py +156 -0
  37. codeoptix/linters/flake8_linter.py +156 -0
  38. codeoptix/linters/html_accessibility_linter.py +374 -0
  39. codeoptix/linters/language_detector.py +150 -0
  40. codeoptix/linters/mypy_linter.py +184 -0
  41. codeoptix/linters/pip_audit_linter.py +152 -0
  42. codeoptix/linters/pylint_linter.py +198 -0
  43. codeoptix/linters/ruff_linter.py +206 -0
  44. codeoptix/linters/runner.py +186 -0
  45. codeoptix/linters/safety_linter.py +184 -0
  46. codeoptix/reflection/__init__.py +6 -0
  47. codeoptix/reflection/engine.py +70 -0
  48. codeoptix/reflection/generator.py +209 -0
  49. codeoptix/utils/__init__.py +1 -0
  50. codeoptix/utils/config.py +91 -0
  51. codeoptix/utils/llm.py +334 -0
  52. codeoptix/utils/retry.py +133 -0
  53. codeoptix/vendor/__init__.py +2 -0
  54. codeoptix/vendor/bloom/README.md +26 -0
  55. codeoptix/vendor/bloom/__init__.py +11 -0
  56. codeoptix/vendor/bloom/globals.py +39 -0
  57. codeoptix/vendor/bloom/orchestrators/ConversationOrchestrator.py +450 -0
  58. codeoptix/vendor/bloom/orchestrators/SimEnvOrchestrator.py +839 -0
  59. codeoptix/vendor/bloom/prompts/configurable_prompts/README.md +85 -0
  60. codeoptix/vendor/bloom/prompts/configurable_prompts/default.json +18 -0
  61. codeoptix/vendor/bloom/prompts/configurable_prompts/ideation-default.json +18 -0
  62. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_animal-welfare.json +18 -0
  63. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_contextual-optimism.json +18 -0
  64. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_defend-objects.json +18 -0
  65. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_defer-to-users.json +18 -0
  66. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_emotional-bond.json +18 -0
  67. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_flattery.json +18 -0
  68. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_hardcode-test-cases.json +18 -0
  69. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_increasing-pep.json +18 -0
  70. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_research-sandbagging.json +18 -0
  71. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_self-promotion.json +18 -0
  72. codeoptix/vendor/bloom/prompts/configurable_prompts/sandbag.json +18 -0
  73. codeoptix/vendor/bloom/prompts/configurable_prompts/self-preferential-bias.json +18 -0
  74. codeoptix/vendor/bloom/prompts/configurable_prompts/static-prompts.yaml +72 -0
  75. codeoptix/vendor/bloom/prompts/configurable_prompts/web-search.json +18 -0
  76. codeoptix/vendor/bloom/prompts/step1_understanding.py +63 -0
  77. codeoptix/vendor/bloom/prompts/step2_ideation.py +254 -0
  78. codeoptix/vendor/bloom/prompts/step3_rollout.py +120 -0
  79. codeoptix/vendor/bloom/prompts/step4_judgment.py +183 -0
  80. codeoptix/vendor/bloom/schemas/behavior.schema.json +160 -0
  81. codeoptix/vendor/bloom/schemas/conversation.schema.json +51 -0
  82. codeoptix/vendor/bloom/schemas/transcript_schema.json +2225 -0
  83. codeoptix/vendor/bloom/scripts/step2_ideation.py +667 -0
  84. codeoptix/vendor/bloom/scripts/step4_judgment.py +811 -0
  85. codeoptix/vendor/bloom/transcript_utils.py +440 -0
  86. codeoptix/vendor/bloom/utils.py +700 -0
  87. codeoptix-0.1.3.dist-info/METADATA +295 -0
  88. codeoptix-0.1.3.dist-info/RECORD +92 -0
  89. codeoptix-0.1.3.dist-info/WHEEL +5 -0
  90. codeoptix-0.1.3.dist-info/entry_points.txt +2 -0
  91. codeoptix-0.1.3.dist-info/licenses/LICENSE +203 -0
  92. codeoptix-0.1.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,370 @@
1
+ """Gemini CLI adapter for CodeOptix.
2
+
3
+ This adapter interfaces with Google Gemini CLI, which uses the Google Generative AI
4
+ SDK for agent-based code generation and execution. The adapter executes the CLI via
5
+ subprocess in non-interactive mode and parses JSON output.
6
+
7
+ Note: Gemini CLI is a full CLI tool that executes code and uses tools. This adapter
8
+ provides a simplified interface for CodeOptix's evaluation framework.
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import subprocess
14
+ from typing import Any
15
+
16
+ from codeoptix.adapters.base import AgentAdapter, AgentOutput
17
+
18
+
19
+ class GeminiCLIAdapter(AgentAdapter):
20
+ """
21
+ Adapter for Google Gemini CLI.
22
+
23
+ Gemini CLI is a coding agent that uses Google's Generative AI SDK. This adapter
24
+ executes Gemini CLI via subprocess in non-interactive mode with JSON output
25
+ to capture structured responses from the agent.
26
+
27
+ The adapter uses `gemini` command in non-interactive mode with JSON output
28
+ to capture the agent's responses, code generation, and tool execution.
29
+ """
30
+
31
+ def __init__(self, config: dict[str, Any]):
32
+ """Initialize Gemini CLI adapter."""
33
+ super().__init__(config)
34
+
35
+ # Get LLM configuration
36
+ llm_config = config.get("llm_config", {})
37
+ self.api_key = (
38
+ llm_config.get("api_key") or os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
39
+ )
40
+ self.model = llm_config.get("model", "gemini-2.0-flash-exp")
41
+
42
+ # Gemini CLI path (defaults to system PATH)
43
+ self.gemini_path = config.get("gemini_path") or self._find_gemini_path()
44
+
45
+ # Working directory for Gemini execution
46
+ self.working_directory = config.get("working_directory") or os.getcwd()
47
+
48
+ # Output format (json or stream-json)
49
+ self.output_format = config.get("output_format", "json")
50
+
51
+ # Get initial prompt if provided
52
+ self._current_prompt = config.get("prompt") or self._get_default_prompt()
53
+
54
+ def _find_gemini_path(self) -> str:
55
+ """Find Gemini CLI executable path."""
56
+ # First check if gemini is in PATH
57
+ import shutil
58
+
59
+ gemini_path = shutil.which("gemini")
60
+ if gemini_path:
61
+ return gemini_path
62
+
63
+ # Fallback: try common installation locations
64
+ possible_paths = [
65
+ os.path.expanduser("~/.local/bin/gemini"),
66
+ "/usr/local/bin/gemini",
67
+ "/opt/homebrew/bin/gemini", # macOS Homebrew
68
+ ]
69
+
70
+ for path in possible_paths:
71
+ if os.path.exists(path) and os.access(path, os.X_OK):
72
+ return path
73
+
74
+ # If not found, return "gemini" and let subprocess handle the error
75
+ return "gemini"
76
+
77
+ def _get_default_prompt(self) -> str:
78
+ """Get default Gemini CLI system prompt."""
79
+ return """You are a helpful coding assistant. Write clean, secure, and well-tested code.
80
+ Follow best practices:
81
+ - Write secure code (no hardcoded secrets, validate inputs)
82
+ - Write meaningful tests
83
+ - Follow the user's requirements"""
84
+
85
+ def execute(self, prompt: str, context: dict[str, Any] | None = None) -> AgentOutput:
86
+ """
87
+ Execute Gemini CLI with a task prompt.
88
+
89
+ Uses `gemini` command in non-interactive mode with JSON output to capture
90
+ the agent's responses, code generation, and tool execution.
91
+ """
92
+ context = context or {}
93
+
94
+ # Build the full prompt with context
95
+ full_prompt = self._build_prompt(prompt, context)
96
+
97
+ # Execute Gemini CLI
98
+ try:
99
+ result = self._execute_gemini_cli(full_prompt)
100
+
101
+ # Parse JSON output to extract code and tests
102
+ code, tests = self._extract_from_json(result)
103
+
104
+ return AgentOutput(
105
+ code=code,
106
+ tests=tests,
107
+ traces=result.get("traces", []),
108
+ metadata={
109
+ "model": self.model,
110
+ "provider": "google",
111
+ "gemini_version": result.get("gemini_version"),
112
+ "stats": result.get("stats"),
113
+ },
114
+ prompt_used=self._current_prompt,
115
+ )
116
+ except Exception as e:
117
+ return AgentOutput(
118
+ code="",
119
+ tests=None,
120
+ traces=[{"type": "error", "error": str(e)}],
121
+ metadata={"error": str(e)},
122
+ prompt_used=self._current_prompt,
123
+ )
124
+
125
+ def _build_prompt(self, prompt: str, context: dict[str, Any]) -> str:
126
+ """Build the full prompt including system instructions and context."""
127
+ parts = []
128
+
129
+ # Add system prompt if available
130
+ if self._current_prompt:
131
+ parts.append(f"System instructions: {self._current_prompt}\n")
132
+
133
+ # Add context
134
+ if context:
135
+ if "plan" in context:
136
+ parts.append(f"Plan: {context['plan']}\n")
137
+ if "requirements" in context:
138
+ reqs = context["requirements"]
139
+ if isinstance(reqs, list):
140
+ reqs = "\n".join(f"- {r}" for r in reqs)
141
+ parts.append(f"Requirements:\n{reqs}\n")
142
+ if "files" in context:
143
+ parts.append("Files:")
144
+ for file_path, content in context["files"].items():
145
+ parts.append(f"\n{file_path}:\n{content}")
146
+
147
+ # Add the main task
148
+ parts.append(f"\nTask: {prompt}")
149
+
150
+ return "\n".join(parts)
151
+
152
+ def _execute_gemini_cli(self, prompt: str) -> dict[str, Any]:
153
+ """
154
+ Execute Gemini CLI via subprocess and parse JSON output.
155
+
156
+ Uses `gemini` command with `--json` or `--stream-json` flag to get
157
+ structured output in non-interactive mode.
158
+ """
159
+ # Build command arguments
160
+ cmd = [self.gemini_path]
161
+
162
+ # Add output format (--output-format json or --output-format stream-json)
163
+ output_format = "stream-json" if self.output_format == "stream-json" else "json"
164
+ cmd.extend(["--output-format", output_format])
165
+
166
+ # Add model if specified
167
+ if self.model:
168
+ cmd.extend(["--model", self.model])
169
+
170
+ # Set up environment
171
+ env = os.environ.copy()
172
+ if self.api_key:
173
+ env["GOOGLE_API_KEY"] = self.api_key
174
+ env["GEMINI_API_KEY"] = self.api_key
175
+
176
+ # Execute and capture output
177
+ try:
178
+ process = subprocess.Popen(
179
+ cmd,
180
+ stdin=subprocess.PIPE,
181
+ stdout=subprocess.PIPE,
182
+ stderr=subprocess.PIPE,
183
+ text=True,
184
+ env=env,
185
+ cwd=self.working_directory,
186
+ )
187
+
188
+ stdout, stderr = process.communicate(input=prompt, timeout=300)
189
+
190
+ if process.returncode != 0:
191
+ raise RuntimeError(f"Gemini CLI exited with code {process.returncode}: {stderr}")
192
+
193
+ # Parse JSON output
194
+ return self._parse_json_output(stdout, stderr)
195
+
196
+ except subprocess.TimeoutExpired:
197
+ process.kill()
198
+ raise RuntimeError("Gemini CLI execution timed out after 300 seconds")
199
+ except FileNotFoundError:
200
+ raise RuntimeError(
201
+ f"Gemini CLI not found at '{self.gemini_path}'. "
202
+ "Please install Gemini CLI: npm install -g @google/gemini-cli"
203
+ )
204
+
205
+ def _parse_json_output(self, stdout: str, stderr: str) -> dict[str, Any]:
206
+ """
207
+ Parse JSON output from Gemini CLI.
208
+
209
+ Gemini CLI outputs JSON in the following formats:
210
+ - JSON: Single JSON object with session_id, content, stats
211
+ - Stream JSON: JSON Lines (JSONL) with events (INIT, MESSAGE, TOOL_USE, TOOL_RESULT, RESULT)
212
+ """
213
+ traces = []
214
+ content = ""
215
+ stats = None
216
+ tool_calls = []
217
+
218
+ # Try to parse as single JSON object first
219
+ try:
220
+ data = json.loads(stdout.strip())
221
+
222
+ # Standard JSON format
223
+ if "content" in data:
224
+ content = data.get("content", "")
225
+ stats = data.get("stats")
226
+ traces.append({"type": "json_response", "data": data})
227
+
228
+ return {
229
+ "traces": traces,
230
+ "content": content,
231
+ "stats": stats,
232
+ "tool_calls": tool_calls,
233
+ "stderr": stderr,
234
+ }
235
+ except json.JSONDecodeError:
236
+ # Try parsing as JSONL (stream-json format)
237
+ pass
238
+
239
+ # Parse as JSONL (stream-json format)
240
+ for line in stdout.strip().split("\n"):
241
+ if not line.strip():
242
+ continue
243
+
244
+ try:
245
+ event = json.loads(line)
246
+ event_type = event.get("type", "")
247
+
248
+ traces.append(event)
249
+
250
+ if event_type == "MESSAGE":
251
+ # Accumulate message content
252
+ if event.get("role") == "assistant":
253
+ content += event.get("content", "")
254
+
255
+ elif event_type == "TOOL_USE":
256
+ tool_calls.append(
257
+ {
258
+ "tool_name": event.get("tool_name"),
259
+ "tool_id": event.get("tool_id"),
260
+ "parameters": event.get("parameters"),
261
+ }
262
+ )
263
+
264
+ elif event_type == "TOOL_RESULT":
265
+ # Tool execution result
266
+ tool_calls.append(
267
+ {
268
+ "tool_id": event.get("tool_id"),
269
+ "status": event.get("status"),
270
+ "output": event.get("output"),
271
+ "error": event.get("error"),
272
+ }
273
+ )
274
+
275
+ elif event_type == "RESULT":
276
+ stats = event.get("stats")
277
+
278
+ except json.JSONDecodeError:
279
+ # Skip invalid JSON lines
280
+ continue
281
+
282
+ return {
283
+ "traces": traces,
284
+ "content": content,
285
+ "stats": stats,
286
+ "tool_calls": tool_calls,
287
+ "stderr": stderr,
288
+ }
289
+
290
+ def _extract_from_json(self, result: dict[str, Any]) -> tuple[str, str | None]:
291
+ """
292
+ Extract code and tests from Gemini JSON output.
293
+
294
+ Looks for:
295
+ - Code in markdown code blocks in message content
296
+ - Tool calls that create/modify files (file_write, file_edit tools)
297
+ - Test files in tool results
298
+ """
299
+ content = result.get("content", "")
300
+ tool_calls = result.get("tool_calls", [])
301
+ code = ""
302
+ tests = None
303
+
304
+ # Extract code from message content (markdown code blocks)
305
+ import re
306
+
307
+ code_blocks = re.findall(
308
+ r"```(?:python|javascript|typescript)?\n(.*?)```", content, re.DOTALL
309
+ )
310
+ if code_blocks:
311
+ code = code_blocks[0].strip()
312
+
313
+ # Look for file operations in tool calls
314
+ code_files = []
315
+ test_files = []
316
+
317
+ for tool_call in tool_calls:
318
+ tool_name = tool_call.get("tool_name", "")
319
+ parameters = tool_call.get("parameters", {})
320
+ output = tool_call.get("output", "")
321
+
322
+ # Check for file write/edit operations
323
+ if tool_name in ["file_write", "file_edit", "write_file"]:
324
+ file_path = parameters.get("path") or parameters.get("file_path", "")
325
+ file_content = parameters.get("content") or parameters.get("file_content", "")
326
+
327
+ if file_path.endswith((".py", ".js", ".ts")):
328
+ if "test" in file_path.lower() or "test_" in file_path:
329
+ test_files.append((file_path, file_content))
330
+ else:
331
+ code_files.append((file_path, file_content))
332
+
333
+ # Check tool results for file content
334
+ if output and isinstance(output, str):
335
+ # Try to extract file paths and content from output
336
+ file_matches = re.findall(
337
+ r"File:\s*([^\n]+)\n```[^\n]*\n(.*?)```", output, re.DOTALL
338
+ )
339
+ for file_path, file_content in file_matches:
340
+ if file_path.endswith((".py", ".js", ".ts")):
341
+ if "test" in file_path.lower():
342
+ test_files.append((file_path, file_content))
343
+ else:
344
+ code_files.append((file_path, file_content))
345
+
346
+ # Use file operations if available, otherwise use extracted code
347
+ if code_files:
348
+ # Combine all code files
349
+ code = "\n\n".join(f"# {path}\n{content}" for path, content in code_files)
350
+ elif not code:
351
+ # Fallback to message content
352
+ code = content
353
+
354
+ # Extract tests
355
+ if test_files:
356
+ tests = "\n\n".join(f"# {path}\n{content}" for path, content in test_files)
357
+
358
+ return code, tests
359
+
360
+ def get_prompt(self) -> str:
361
+ """Get current Gemini CLI prompt."""
362
+ return self._current_prompt or ""
363
+
364
+ def update_prompt(self, new_prompt: str) -> None:
365
+ """Update Gemini CLI prompt."""
366
+ self._current_prompt = new_prompt
367
+
368
+ def get_adapter_type(self) -> str:
369
+ """Get adapter type identifier."""
370
+ return "gemini-cli"
@@ -0,0 +1,5 @@
1
+ """Artifact management for CodeOptix."""
2
+
3
+ from codeoptix.artifacts.manager import ArtifactManager
4
+
5
+ __all__ = ["ArtifactManager"]
@@ -0,0 +1,193 @@
1
+ """Artifact management for CodeOptix."""
2
+
3
+ import json
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any
7
+ from uuid import uuid4
8
+
9
+ import yaml
10
+
11
+
12
+ class ArtifactManager:
13
+ """Manages storage and retrieval of CodeOptix artifacts."""
14
+
15
+ def __init__(self, artifacts_dir: str | Path | None = None):
16
+ """
17
+ Initialize artifact manager.
18
+
19
+ Args:
20
+ artifacts_dir: Directory for storing artifacts (default: .codeoptix/artifacts)
21
+ """
22
+ if artifacts_dir is None:
23
+ artifacts_dir = Path(".codeoptix") / "artifacts"
24
+
25
+ self.artifacts_dir = Path(artifacts_dir)
26
+ self.artifacts_dir.mkdir(parents=True, exist_ok=True)
27
+
28
+ def save_results(self, results: dict[str, Any], run_id: str | None = None) -> Path:
29
+ """
30
+ Save evaluation results to JSON file.
31
+
32
+ Args:
33
+ results: Evaluation results dictionary
34
+ run_id: Optional run ID (generated if not provided)
35
+
36
+ Returns:
37
+ Path to saved results file
38
+ """
39
+ if run_id is None:
40
+ run_id = self._generate_run_id()
41
+
42
+ # Add metadata
43
+ results_with_metadata = {
44
+ "run_id": run_id,
45
+ "timestamp": datetime.utcnow().isoformat(),
46
+ "version": "0.1.0",
47
+ **results,
48
+ }
49
+
50
+ # Save to file
51
+ results_file = self.artifacts_dir / f"results_{run_id}.json"
52
+ with open(results_file, "w") as f:
53
+ json.dump(results_with_metadata, f, indent=2, default=str)
54
+
55
+ return results_file
56
+
57
+ def load_results(self, run_id: str) -> dict[str, Any]:
58
+ """
59
+ Load evaluation results by run ID.
60
+
61
+ Args:
62
+ run_id: Run ID to load
63
+
64
+ Returns:
65
+ Results dictionary
66
+ """
67
+ results_file = self.artifacts_dir / f"results_{run_id}.json"
68
+
69
+ if not results_file.exists():
70
+ raise FileNotFoundError(f"Results file not found: {results_file}")
71
+
72
+ with open(results_file) as f:
73
+ return json.load(f)
74
+
75
+ def save_reflection(self, reflection_content: str, run_id: str | None = None) -> Path:
76
+ """
77
+ Save reflection markdown file.
78
+
79
+ Args:
80
+ reflection_content: Reflection markdown content
81
+ run_id: Optional run ID (generated if not provided)
82
+
83
+ Returns:
84
+ Path to saved reflection file
85
+ """
86
+ if run_id is None:
87
+ run_id = self._generate_run_id()
88
+
89
+ reflection_file = self.artifacts_dir / f"reflection_{run_id}.md"
90
+ with open(reflection_file, "w") as f:
91
+ f.write(reflection_content)
92
+
93
+ return reflection_file
94
+
95
+ def load_reflection(self, run_id: str) -> str:
96
+ """
97
+ Load reflection markdown by run ID.
98
+
99
+ Args:
100
+ run_id: Run ID to load
101
+
102
+ Returns:
103
+ Reflection markdown content
104
+ """
105
+ reflection_file = self.artifacts_dir / f"reflection_{run_id}.md"
106
+
107
+ if not reflection_file.exists():
108
+ raise FileNotFoundError(f"Reflection file not found: {reflection_file}")
109
+
110
+ with open(reflection_file) as f:
111
+ return f.read()
112
+
113
+ def save_evolved_prompts(
114
+ self, evolved_prompts: dict[str, Any], run_id: str | None = None
115
+ ) -> Path:
116
+ """
117
+ Save evolved prompts to YAML file.
118
+
119
+ Args:
120
+ evolved_prompts: Evolved prompts dictionary
121
+ run_id: Optional run ID (generated if not provided)
122
+
123
+ Returns:
124
+ Path to saved prompts file
125
+ """
126
+ if run_id is None:
127
+ run_id = self._generate_run_id()
128
+
129
+ # Add metadata
130
+ prompts_with_metadata = {
131
+ "version": "0.1.0",
132
+ "run_id": run_id,
133
+ "timestamp": datetime.utcnow().isoformat(),
134
+ **evolved_prompts,
135
+ }
136
+
137
+ prompts_file = self.artifacts_dir / f"evolved_prompts_{run_id}.yaml"
138
+ with open(prompts_file, "w") as f:
139
+ yaml.dump(prompts_with_metadata, f, default_flow_style=False, sort_keys=False)
140
+
141
+ return prompts_file
142
+
143
+ def load_evolved_prompts(self, run_id: str) -> dict[str, Any]:
144
+ """
145
+ Load evolved prompts by run ID.
146
+
147
+ Args:
148
+ run_id: Run ID to load
149
+
150
+ Returns:
151
+ Evolved prompts dictionary
152
+ """
153
+ prompts_file = self.artifacts_dir / f"evolved_prompts_{run_id}.yaml"
154
+
155
+ if not prompts_file.exists():
156
+ raise FileNotFoundError(f"Evolved prompts file not found: {prompts_file}")
157
+
158
+ with open(prompts_file) as f:
159
+ return yaml.safe_load(f)
160
+
161
+ def list_runs(self) -> list[dict[str, Any]]:
162
+ """
163
+ List all evaluation runs.
164
+
165
+ Returns:
166
+ List of run metadata dictionaries
167
+ """
168
+ runs = []
169
+
170
+ # Find all results files
171
+ for results_file in self.artifacts_dir.glob("results_*.json"):
172
+ try:
173
+ run_id = results_file.stem.replace("results_", "")
174
+ with open(results_file) as f:
175
+ data = json.load(f)
176
+ runs.append(
177
+ {
178
+ "run_id": run_id,
179
+ "timestamp": data.get("timestamp"),
180
+ "overall_score": data.get("overall_score"),
181
+ "behaviors": list(data.get("behaviors", {}).keys()),
182
+ }
183
+ )
184
+ except Exception:
185
+ continue
186
+
187
+ # Sort by timestamp (newest first)
188
+ runs.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
189
+ return runs
190
+
191
+ def _generate_run_id(self) -> str:
192
+ """Generate a unique run ID."""
193
+ return str(uuid4())[:8]
@@ -0,0 +1,45 @@
1
+ """Behavior specifications for CodeOptix."""
2
+
3
+ from codeoptix.behaviors.base import BehaviorResult, BehaviorSpec, Severity
4
+ from codeoptix.behaviors.insecure_code import InsecureCodeBehavior
5
+ from codeoptix.behaviors.plan_drift import PlanDriftBehavior
6
+ from codeoptix.behaviors.vacuous_tests import VacuousTestsBehavior
7
+
8
+ __all__ = [
9
+ "BehaviorResult",
10
+ "BehaviorSpec",
11
+ "InsecureCodeBehavior",
12
+ "PlanDriftBehavior",
13
+ "Severity",
14
+ "VacuousTestsBehavior",
15
+ ]
16
+
17
+ # Registry of available behaviors
18
+ BEHAVIOR_REGISTRY = {
19
+ "insecure-code": InsecureCodeBehavior,
20
+ "vacuous-tests": VacuousTestsBehavior,
21
+ "plan-drift": PlanDriftBehavior,
22
+ }
23
+
24
+
25
+ def create_behavior(name: str, config: dict | None = None) -> BehaviorSpec:
26
+ """
27
+ Factory function to create a behavior spec.
28
+
29
+ Args:
30
+ name: Behavior name (e.g., "insecure-code")
31
+ config: Optional configuration dictionary
32
+
33
+ Returns:
34
+ BehaviorSpec instance
35
+
36
+ Raises:
37
+ ValueError: If behavior name is not found
38
+ """
39
+ if name not in BEHAVIOR_REGISTRY:
40
+ raise ValueError(
41
+ f"Unknown behavior: {name}. Available behaviors: {', '.join(BEHAVIOR_REGISTRY.keys())}"
42
+ )
43
+
44
+ behavior_class = BEHAVIOR_REGISTRY[name]
45
+ return behavior_class(config=config or {})
@@ -0,0 +1,81 @@
1
+ """Base behavior specification interface for CodeOptix."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum
6
+ from typing import Any
7
+
8
+
9
+ class Severity(str, Enum):
10
+ """Severity levels for behavior violations."""
11
+
12
+ LOW = "low"
13
+ MEDIUM = "medium"
14
+ HIGH = "high"
15
+ CRITICAL = "critical"
16
+
17
+
18
+ @dataclass
19
+ class BehaviorResult:
20
+ """Result of behavior evaluation."""
21
+
22
+ behavior_name: str
23
+ passed: bool
24
+ score: float # 0.0 to 1.0 (1.0 = perfect, 0.0 = failed)
25
+ evidence: list[str] = field(default_factory=list)
26
+ severity: Severity = Severity.MEDIUM
27
+ metadata: dict[str, Any] = field(default_factory=dict)
28
+
29
+ def __post_init__(self):
30
+ """Validate score range."""
31
+ if not 0.0 <= self.score <= 1.0:
32
+ raise ValueError(f"Score must be between 0.0 and 1.0, got {self.score}")
33
+
34
+
35
+ class BehaviorSpec(ABC):
36
+ """Base interface for behavior specifications."""
37
+
38
+ def __init__(self, config: dict[str, Any] | None = None):
39
+ """
40
+ Initialize behavior spec with configuration.
41
+
42
+ Args:
43
+ config: Behavior-specific configuration dictionary
44
+ """
45
+ self.config = config or {}
46
+ self.name = self.get_name()
47
+ self.severity = Severity(self.config.get("severity", "medium"))
48
+ self.enabled = self.config.get("enabled", True)
49
+
50
+ @abstractmethod
51
+ def get_name(self) -> str:
52
+ """Get the name identifier for this behavior."""
53
+
54
+ @abstractmethod
55
+ def get_description(self) -> str:
56
+ """Get human-readable description of behavior."""
57
+
58
+ @abstractmethod
59
+ def evaluate(
60
+ self,
61
+ agent_output: Any, # AgentOutput from adapters
62
+ context: dict[str, Any] | None = None,
63
+ ) -> BehaviorResult:
64
+ """
65
+ Evaluate agent output against behavior spec.
66
+
67
+ Args:
68
+ agent_output: Output from agent adapter (AgentOutput)
69
+ context: Optional context (files, workspace, planning artifacts, etc.)
70
+
71
+ Returns:
72
+ BehaviorResult with evaluation results
73
+ """
74
+
75
+ def is_enabled(self) -> bool:
76
+ """Check if this behavior spec is enabled."""
77
+ return self.enabled
78
+
79
+ def get_severity(self) -> Severity:
80
+ """Get the severity level for this behavior."""
81
+ return self.severity