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
codeoptix/cli.py ADDED
@@ -0,0 +1,1468 @@
1
+ """CLI interface for CodeOptiX."""
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+
8
+ import click
9
+ from acp import run_agent
10
+
11
+ from codeoptix.acp import (
12
+ ACPAgentRegistry,
13
+ ACPQualityBridge,
14
+ CodeOptiXAgent,
15
+ MultiAgentJudge,
16
+ )
17
+ from codeoptix.adapters.factory import create_adapter
18
+ from codeoptix.artifacts import ArtifactManager
19
+ from codeoptix.evaluation import EvaluationEngine
20
+ from codeoptix.evolution import EvolutionEngine
21
+ from codeoptix.linters import LinterRunner
22
+ from codeoptix.reflection import ReflectionEngine
23
+ from codeoptix.utils.llm import LLMProvider, create_llm_client
24
+
25
+
26
+ @click.group()
27
+ @click.version_option(version="0.1.3")
28
+ def main():
29
+ """CodeOptiX - Agentic Code Optimization & Deep Evaluation for Superior Coding Agent Experience.
30
+
31
+ The universal code optimization engine that improves coding agent experience with deep evaluations and optimization. When AI coding agents dazzle with impressive code but leave you wondering about quality, maintainability, security, and reliability, CodeOptiX ensures proper behavior through evaluations, reflection, and self-improvement.
32
+
33
+ Built by Superagentic AI - Advancing AI agent optimization and autonomous systems.
34
+ """
35
+
36
+
37
+ @main.command()
38
+ @click.option("--agent", required=True, help="Agent type (claude-code, codex, gemini-cli)")
39
+ @click.option(
40
+ "--behaviors",
41
+ required=True,
42
+ help="Comma-separated behavior names (e.g., insecure-code,vacuous-tests)",
43
+ )
44
+ @click.option("--output", default="results.json", help="Output file for results")
45
+ @click.option("--config", type=click.Path(exists=True), help="Path to config file (JSON/YAML)")
46
+ @click.option(
47
+ "--llm-provider",
48
+ default="openai",
49
+ help="LLM provider for evaluation (anthropic, openai, google, ollama)",
50
+ )
51
+ @click.option("--llm-api-key", help="API key for LLM (or set environment variable)")
52
+ @click.option(
53
+ "--context",
54
+ type=click.Path(exists=True),
55
+ help="Path to context file (JSON) with plan/requirements",
56
+ )
57
+ @click.option(
58
+ "--fail-on-failure", is_flag=True, help="Exit with non-zero code if any behavior fails"
59
+ )
60
+ def eval(agent, behaviors, output, config, llm_provider, llm_api_key, context, fail_on_failure):
61
+ """Evaluate agent against behavior specifications."""
62
+ import sys
63
+
64
+ click.echo("šŸ” CodeOptiX Evaluation")
65
+ click.echo("=" * 60)
66
+
67
+ # Parse behaviors
68
+ behavior_list = [b.strip() for b in behaviors.split(",") if b.strip()]
69
+
70
+ if not behavior_list:
71
+ click.echo(
72
+ "āŒ Error: No behaviors specified. Please provide at least one behavior.", err=True
73
+ )
74
+ click.echo(" Example: --behaviors insecure-code", err=True)
75
+ click.echo(
76
+ " Available behaviors: insecure-code, vacuous-tests, plan-drift",
77
+ err=True,
78
+ )
79
+ sys.exit(1)
80
+
81
+ # Validate behavior names (keep in sync with evaluation engine)
82
+ valid_behaviors = [
83
+ "insecure-code",
84
+ "vacuous-tests",
85
+ "plan-drift",
86
+ "api-smoke",
87
+ "contract-compliance",
88
+ "db-validation",
89
+ ]
90
+ invalid_behaviors = [b for b in behavior_list if b not in valid_behaviors]
91
+ if invalid_behaviors:
92
+ click.echo(f"āŒ Error: Invalid behavior name(s): {', '.join(invalid_behaviors)}", err=True)
93
+ click.echo(f" Available behaviors: {', '.join(valid_behaviors)}", err=True)
94
+ sys.exit(1)
95
+
96
+ click.echo(f"šŸ“Š Agent: {agent}")
97
+ click.echo(f"šŸ“‹ Behavior(s): {', '.join(behavior_list)}")
98
+ if len(behavior_list) == 1:
99
+ click.echo(" [INFO] Single behavior mode - perfect for getting started!")
100
+
101
+ # Load config if provided
102
+ eval_config = {}
103
+ if config:
104
+ config_path = Path(config)
105
+ if not config_path.exists():
106
+ click.echo(f"āŒ Error: Config file not found: {config}", err=True)
107
+ click.echo(" Please check the file path and try again.", err=True)
108
+ sys.exit(1)
109
+
110
+ try:
111
+ if config_path.suffix == ".json":
112
+ with open(config_path) as f:
113
+ eval_config = json.load(f)
114
+ elif config_path.suffix in [".yaml", ".yml"]:
115
+ import yaml
116
+
117
+ with open(config_path) as f:
118
+ eval_config = yaml.safe_load(f)
119
+ else:
120
+ click.echo(
121
+ f"āŒ Error: Unsupported config file format: {config_path.suffix}", err=True
122
+ )
123
+ click.echo(" Supported formats: .json, .yaml, .yml", err=True)
124
+ sys.exit(1)
125
+ except json.JSONDecodeError as e:
126
+ click.echo(f"āŒ Error: Invalid JSON in config file: {e}", err=True)
127
+ sys.exit(1)
128
+ except Exception as e:
129
+ click.echo(f"āŒ Error: Failed to load config file: {e}", err=True)
130
+ sys.exit(1)
131
+
132
+ # Load context if provided
133
+ eval_context = {}
134
+ if context:
135
+ context_path = Path(context)
136
+ if not context_path.exists():
137
+ click.echo(f"āŒ Error: Context file not found: {context}", err=True)
138
+ sys.exit(1)
139
+ try:
140
+ with open(context_path) as f:
141
+ eval_context = json.load(f)
142
+ except json.JSONDecodeError as e:
143
+ click.echo(f"āŒ Error: Invalid JSON in context file: {e}", err=True)
144
+ sys.exit(1)
145
+ except Exception as e:
146
+ click.echo(f"āŒ Error: Failed to load context file: {e}", err=True)
147
+ sys.exit(1)
148
+
149
+ # Normalize provider name and decide if we need an API key
150
+ llm_provider = (
151
+ eval_config.get("llm_provider")
152
+ or llm_provider
153
+ or os.getenv("CODEOPTIX_LLM_PROVIDER", "openai")
154
+ ).lower()
155
+ is_ollama = llm_provider == "ollama"
156
+
157
+ # Create adapter
158
+ adapter_config = eval_config.get("adapter", {})
159
+ if not adapter_config.get("llm_config"):
160
+ # Default LLM config
161
+ api_key = llm_api_key or os.getenv(f"{llm_provider.upper()}_API_KEY")
162
+ if not api_key and not is_ollama:
163
+ click.echo(f"āŒ Error: API key required for {llm_provider}", err=True)
164
+ click.echo(
165
+ f" Set {llm_provider.upper()}_API_KEY environment variable or use --llm-api-key",
166
+ err=True,
167
+ )
168
+ click.echo("", err=True)
169
+ click.echo("šŸ’” Tip: Without an API key, you can use basic static analysis:", err=True)
170
+ click.echo(" codeoptix lint --path ./src", err=True)
171
+ click.echo(
172
+ " This runs linters (ruff, bandit, flake8, etc.) without requiring API keys.",
173
+ err=True,
174
+ )
175
+ sys.exit(1)
176
+
177
+ adapter_config["llm_config"] = {
178
+ "provider": llm_provider,
179
+ # Ollama does not need an API key; other providers still do.
180
+ "api_key": api_key if not is_ollama else None,
181
+ }
182
+
183
+ try:
184
+ adapter = create_adapter(agent, adapter_config)
185
+ click.echo(f"āœ… Adapter created: {adapter.get_adapter_type()}")
186
+ except ValueError as e:
187
+ click.echo(f"āŒ Error: {e}", err=True)
188
+ click.echo(" Available agents: claude-code, codex, gemini-cli", err=True)
189
+ sys.exit(1)
190
+ except Exception as e:
191
+ click.echo(f"āŒ Error: Failed to create adapter: {e}", err=True)
192
+ if "api_key" in str(e).lower() or "authentication" in str(e).lower():
193
+ click.echo(
194
+ " šŸ’” Tip: Check your API key is correct and has sufficient credits", err=True
195
+ )
196
+ sys.exit(1)
197
+
198
+ # Create LLM client for evaluation
199
+ if not is_ollama:
200
+ click.echo("🧠 Using local Ollama provider.")
201
+
202
+ try:
203
+ llm_config = adapter_config["llm_config"]
204
+ llm_client = create_llm_client(
205
+ LLMProvider(llm_config["provider"]), llm_config.get("api_key"), llm_config.get("model")
206
+ )
207
+ except Exception as e:
208
+ click.echo(f"āŒ Error: Failed to create LLM client: {e}", err=True)
209
+ if "api_key" in str(e).lower():
210
+ click.echo(" šŸ’” Tip: Verify your API key is correct", err=True)
211
+ if is_ollama:
212
+ click.echo(
213
+ " šŸ’” Tip: Ensure `ollama serve` is running and the model is pulled (e.g. `ollama pull gpt-oss:120b`).",
214
+ err=True,
215
+ )
216
+ sys.exit(1)
217
+
218
+ # Create evaluation engine
219
+ eval_engine_config = eval_config.get("evaluation", {})
220
+ try:
221
+ eval_engine = EvaluationEngine(adapter, llm_client, config=eval_engine_config)
222
+ except Exception as e:
223
+ click.echo(f"āŒ Error: Failed to create evaluation engine: {e}", err=True)
224
+ sys.exit(1)
225
+
226
+ # Run evaluation
227
+ click.echo("\nšŸš€ Running evaluation...")
228
+ try:
229
+ results = eval_engine.evaluate_behaviors(behavior_names=behavior_list, context=eval_context)
230
+
231
+ if not results or "behaviors" not in results:
232
+ click.echo("āŒ Error: Evaluation returned no results", err=True)
233
+ click.echo(" This might indicate an issue with the evaluation engine", err=True)
234
+ sys.exit(1)
235
+
236
+ # Save results
237
+ artifact_manager = ArtifactManager()
238
+ results_file = artifact_manager.save_results(results)
239
+
240
+ # Also save to specified output if different
241
+ if output != str(results_file.name):
242
+ try:
243
+ with open(output, "w") as f:
244
+ json.dump(results, f, indent=2, default=str)
245
+ except Exception as e:
246
+ click.echo(f"āš ļø Warning: Failed to save to {output}: {e}", err=True)
247
+ click.echo(f" Results saved to: {results_file}", err=True)
248
+
249
+ click.echo("\n" + "=" * 60)
250
+ click.echo("āœ… Evaluation Complete!")
251
+ click.echo("=" * 60)
252
+ click.echo(f"šŸ“Š Overall Score: {results.get('overall_score', 0.0):.2%}")
253
+ click.echo(f"šŸ“ Results: {results_file}")
254
+ click.echo(f"šŸ†” Run ID: {results.get('run_id', 'unknown')}")
255
+
256
+ # Show behavior results
257
+ behaviors_data = results.get("behaviors", {})
258
+ if behaviors_data:
259
+ click.echo("\nšŸ“‹ Behavior Results:")
260
+ for behavior_name, behavior_data in behaviors_data.items():
261
+ passed = behavior_data.get("passed", True)
262
+ score = behavior_data.get("score", 0.0)
263
+ emoji = "āœ…" if passed else "āŒ"
264
+ click.echo(f" {emoji} {behavior_name}: {score:.2%}")
265
+
266
+ # Check for failures if --fail-on-failure is set
267
+ if fail_on_failure:
268
+ failed_behaviors = [
269
+ name for name, data in behaviors_data.items() if not data.get("passed", True)
270
+ ]
271
+
272
+ if failed_behaviors:
273
+ click.echo(
274
+ f"\nāŒ {len(failed_behaviors)} behavior(s) failed: {', '.join(failed_behaviors)}",
275
+ err=True,
276
+ )
277
+ click.echo(" Exiting with error code (--fail-on-failure)", err=True)
278
+ sys.exit(1)
279
+ else:
280
+ click.echo("\nāœ… All behaviors passed!")
281
+
282
+ except KeyboardInterrupt:
283
+ click.echo("\nāš ļø Evaluation interrupted by user", err=True)
284
+ sys.exit(130)
285
+ except Exception as e:
286
+ click.echo(f"\nāŒ Error: Evaluation failed: {e}", err=True)
287
+ if hasattr(e, "__cause__") and e.__cause__:
288
+ click.echo(f" Caused by: {e.__cause__!s}", err=True)
289
+ click.echo("\nšŸ’” Troubleshooting tips:", err=True)
290
+ click.echo(" - Check your API key is valid and has credits", err=True)
291
+ click.echo(" - Verify the agent type is correct", err=True)
292
+ click.echo(" - Try with a single behavior first: --behaviors insecure-code", err=True)
293
+ click.echo(" - Check the documentation: https://codeoptix.ai/docs", err=True)
294
+ sys.exit(1)
295
+
296
+
297
+ @main.command()
298
+ @click.option("--input", required=True, help="Path to results JSON file or run ID")
299
+ @click.option("--output", help="Output file for reflection (default: reflection_{run_id}.md)")
300
+ @click.option("--agent-name", help="Agent name for reflection report")
301
+ def reflect(input, output, agent_name):
302
+ """Generate reflection report from evaluation results."""
303
+ click.echo("šŸ“ Generating reflection report...")
304
+
305
+ artifact_manager = ArtifactManager()
306
+
307
+ # Load results
308
+ input_path = Path(input)
309
+ if input_path.exists():
310
+ # Load from file
311
+ with open(input_path) as f:
312
+ results = json.load(f)
313
+ run_id = results.get("run_id")
314
+ else:
315
+ # Assume it's a run ID
316
+ run_id = input
317
+ try:
318
+ results = artifact_manager.load_results(run_id)
319
+ except FileNotFoundError:
320
+ click.echo(f"āŒ Results not found for run ID: {run_id}", err=True)
321
+ raise click.Abort()
322
+
323
+ # Generate reflection
324
+ reflection_engine = ReflectionEngine(artifact_manager)
325
+
326
+ try:
327
+ reflection = reflection_engine.reflect(results=results, agent_name=agent_name, save=True)
328
+
329
+ # Save to specified output if provided
330
+ if output:
331
+ with open(output, "w") as f:
332
+ f.write(reflection)
333
+ click.echo(f"āœ… Reflection saved to: {output}")
334
+ else:
335
+ reflection_file = artifact_manager.artifacts_dir / f"reflection_{run_id}.md"
336
+ click.echo(f"āœ… Reflection saved to: {reflection_file}")
337
+
338
+ click.echo(f" Run ID: {run_id}")
339
+
340
+ except Exception as e:
341
+ click.echo(f"āŒ Reflection generation failed: {e}", err=True)
342
+ raise click.Abort()
343
+
344
+
345
+ @main.command()
346
+ @click.option("--input", required=True, help="Path to results JSON file or run ID")
347
+ @click.option(
348
+ "--reflection", help="Path to reflection markdown file (auto-generated if not provided)"
349
+ )
350
+ @click.option(
351
+ "--output", help="Output file for evolved prompts (default: evolved_prompts_{run_id}.yaml)"
352
+ )
353
+ @click.option("--iterations", default=3, help="Number of evolution iterations")
354
+ @click.option("--config", type=click.Path(exists=True), help="Path to config file (JSON/YAML)")
355
+ @click.option(
356
+ "--llm-provider",
357
+ default="openai",
358
+ help="LLM provider for evolution (anthropic, openai, google, ollama)",
359
+ )
360
+ @click.option("--llm-api-key", help="API key for LLM (or set environment variable)")
361
+ def evolve(input, reflection, output, iterations, config, llm_provider, llm_api_key):
362
+ """Evolve agent prompts based on evaluation results."""
363
+ click.echo("🧬 Evolving agent prompts...")
364
+
365
+ artifact_manager = ArtifactManager()
366
+
367
+ # Load results
368
+ input_path = Path(input)
369
+ if input_path.exists():
370
+ with open(input_path) as f:
371
+ results = json.load(f)
372
+ run_id = results.get("run_id")
373
+ else:
374
+ run_id = input
375
+ try:
376
+ results = artifact_manager.load_results(run_id)
377
+ except FileNotFoundError:
378
+ click.echo(f"āŒ Results not found for run ID: {run_id}", err=True)
379
+ raise click.Abort()
380
+
381
+ # Load or generate reflection
382
+ if reflection:
383
+ reflection_path = Path(reflection)
384
+ if reflection_path.exists():
385
+ with open(reflection_path) as f:
386
+ reflection_content = f.read()
387
+ else:
388
+ click.echo("āš ļø Reflection file not found, generating...")
389
+ reflection_engine = ReflectionEngine(artifact_manager)
390
+ reflection_content = reflection_engine.reflect_from_run_id(run_id)
391
+ else:
392
+ # Auto-generate reflection
393
+ click.echo("šŸ“ Generating reflection...")
394
+ reflection_engine = ReflectionEngine(artifact_manager)
395
+ reflection_content = reflection_engine.reflect_from_run_id(run_id)
396
+
397
+ # Load config
398
+ evolve_config = {}
399
+ if config:
400
+ config_path = Path(config)
401
+ if config_path.suffix == ".json":
402
+ with open(config_path) as f:
403
+ evolve_config = json.load(f)
404
+ elif config_path.suffix in [".yaml", ".yml"]:
405
+ import yaml
406
+
407
+ with open(config_path) as f:
408
+ evolve_config = yaml.safe_load(f)
409
+
410
+ # Set iterations
411
+ evolution_config = evolve_config.get("evolution", {})
412
+ evolution_config["max_iterations"] = iterations
413
+
414
+ # Get agent type and config from results
415
+ metadata = results.get("metadata", {})
416
+ agent_type = metadata.get("agent", "claude-code")
417
+
418
+ # Get LLM provider from command line or config
419
+ if llm_provider == "openai": # default, check config
420
+ llm_provider = evolve_config.get("llm_provider", "openai")
421
+ # llm_api_key param takes precedence, then config, then env
422
+ llm_api_key = (
423
+ llm_api_key
424
+ or evolve_config.get("llm_api_key")
425
+ or os.getenv(f"{llm_provider.upper()}_API_KEY")
426
+ )
427
+
428
+ is_ollama = llm_provider == "ollama"
429
+ if not llm_api_key and not is_ollama:
430
+ click.echo(
431
+ f"āŒ LLM API key required. Set {llm_provider.upper()}_API_KEY or use --config", err=True
432
+ )
433
+ click.echo("", err=True)
434
+ click.echo("šŸ’” Tip: Without an API key, you can use basic static analysis:", err=True)
435
+ click.echo(" codeoptix lint --path ./src", err=True)
436
+ click.echo(
437
+ " This runs linters (ruff, bandit, flake8, etc.) without requiring API keys.",
438
+ err=True,
439
+ )
440
+ raise click.Abort()
441
+
442
+ try:
443
+ # Create adapter
444
+ adapter_config = evolve_config.get("adapter", {})
445
+ if not adapter_config.get("llm_config"):
446
+ adapter_config["llm_config"] = {
447
+ "provider": llm_provider,
448
+ "api_key": llm_api_key,
449
+ }
450
+
451
+ adapter = create_adapter(agent_type, adapter_config)
452
+ click.echo(f"āœ… Created adapter: {adapter.get_adapter_type()}")
453
+
454
+ # Create LLM client (use same config as adapter for consistency)
455
+ llm_config = adapter_config["llm_config"]
456
+ llm_client = create_llm_client(
457
+ LLMProvider(llm_config["provider"]), llm_config.get("api_key"), llm_config.get("model")
458
+ )
459
+
460
+ # Create evaluation engine
461
+ eval_engine_config = evolve_config.get("evaluation", {})
462
+ eval_engine = EvaluationEngine(adapter, llm_client, config=eval_engine_config)
463
+
464
+ # Create evolution engine
465
+ evolution_engine = EvolutionEngine(
466
+ adapter=adapter,
467
+ evaluation_engine=eval_engine,
468
+ llm_client=llm_client,
469
+ artifact_manager=artifact_manager,
470
+ config=evolution_config,
471
+ )
472
+
473
+ # Run evolution
474
+ click.echo(f"🧬 Running evolution ({iterations} iterations)...")
475
+ evolved = evolution_engine.evolve(
476
+ evaluation_results=results,
477
+ reflection=reflection_content,
478
+ behavior_names=list(results.get("behaviors", {}).keys()),
479
+ )
480
+
481
+ # Save to specified output if provided
482
+ if output:
483
+ import yaml
484
+
485
+ with open(output, "w") as f:
486
+ yaml.dump(evolved, f, default_flow_style=False, sort_keys=False)
487
+ click.echo(f"āœ… Evolved prompts saved to: {output}")
488
+ else:
489
+ evolved_file = artifact_manager.artifacts_dir / f"evolved_prompts_{run_id}.yaml"
490
+ click.echo(f"āœ… Evolved prompts saved to: {evolved_file}")
491
+
492
+ click.echo(f" Improvement: {evolved['metadata']['improvement']:.2f}")
493
+ click.echo(f" Final score: {evolved['metadata']['final_score']:.2f}/1.0")
494
+ click.echo(f" Run ID: {run_id}")
495
+
496
+ except Exception as e:
497
+ click.echo(f"āŒ Evolution failed: {e}", err=True)
498
+ import traceback
499
+
500
+ click.echo(traceback.format_exc(), err=True)
501
+ raise click.Abort()
502
+
503
+
504
+ @main.command()
505
+ @click.option("--agent", required=True, help="Agent type")
506
+ @click.option("--behaviors", required=True, help="Comma-separated behavior names")
507
+ @click.option("--evolve", is_flag=True, help="Run evolution after evaluation")
508
+ @click.option("--config", type=click.Path(exists=True), help="Path to config file")
509
+ def run(agent, behaviors, evolve, config):
510
+ """Run full pipeline: evaluate → reflect → evolve (optional)."""
511
+ click.echo("šŸš€ Running full CodeOptiX pipeline...")
512
+
513
+ # Step 1: Evaluate
514
+ click.echo("\n" + "=" * 60)
515
+ click.echo("STEP 1: Evaluation")
516
+ click.echo("=" * 60)
517
+
518
+ # Create temporary results file
519
+ import tempfile
520
+
521
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
522
+ temp_results = f.name
523
+
524
+ try:
525
+ # Run eval command
526
+ from click.testing import CliRunner
527
+
528
+ runner = CliRunner()
529
+
530
+ result = runner.invoke(
531
+ eval,
532
+ [
533
+ "--agent",
534
+ agent,
535
+ "--behaviors",
536
+ behaviors,
537
+ "--output",
538
+ temp_results,
539
+ "--config",
540
+ config if config else "",
541
+ ],
542
+ )
543
+
544
+ if result.exit_code != 0:
545
+ click.echo(f"āŒ Evaluation failed: {result.output}", err=True)
546
+ raise click.Abort()
547
+
548
+ # Step 2: Reflect
549
+ click.echo("\n" + "=" * 60)
550
+ click.echo("STEP 2: Reflection")
551
+ click.echo("=" * 60)
552
+
553
+ result = runner.invoke(
554
+ reflect,
555
+ [
556
+ "--input",
557
+ temp_results,
558
+ ],
559
+ )
560
+
561
+ if result.exit_code != 0:
562
+ click.echo(f"āŒ Reflection failed: {result.output}", err=True)
563
+ raise click.Abort()
564
+
565
+ # Step 3: Evolve (if requested)
566
+ if evolve:
567
+ click.echo("\n" + "=" * 60)
568
+ click.echo("STEP 3: Evolution")
569
+ click.echo("=" * 60)
570
+
571
+ result = runner.invoke(
572
+ evolve,
573
+ [
574
+ "--input",
575
+ temp_results,
576
+ ],
577
+ )
578
+
579
+ if result.exit_code != 0:
580
+ click.echo(f"āš ļø Evolution failed: {result.output}", err=True)
581
+
582
+ click.echo("\n" + "=" * 60)
583
+ click.echo("āœ… Pipeline complete!")
584
+ click.echo("=" * 60)
585
+
586
+ finally:
587
+ # Clean up temp file
588
+ if os.path.exists(temp_results):
589
+ os.unlink(temp_results)
590
+
591
+
592
+ @main.command()
593
+ @click.option("--agent", required=True, help="Agent type (claude-code, codex, gemini-cli)")
594
+ @click.option(
595
+ "--behaviors", required=True, help="Comma-separated behavior names (e.g., insecure-code)"
596
+ )
597
+ @click.option("--config", type=click.Path(exists=True), help="Path to config file (JSON/YAML)")
598
+ @click.option(
599
+ "--llm-provider",
600
+ default="openai",
601
+ help="LLM provider for evaluation (anthropic, openai, google, ollama)",
602
+ )
603
+ @click.option("--llm-api-key", help="API key for LLM (or set environment variable)")
604
+ @click.option(
605
+ "--fail-on-failure",
606
+ is_flag=True,
607
+ default=True,
608
+ help="Exit with non-zero code if any behavior fails (default: true)",
609
+ )
610
+ @click.option(
611
+ "--output-format",
612
+ default="json",
613
+ type=click.Choice(["json", "summary"]),
614
+ help="Output format (default: json)",
615
+ )
616
+ def ci(agent, behaviors, config, llm_provider, llm_api_key, fail_on_failure, output_format):
617
+ """
618
+ Run CodeOptiX in CI/CD mode.
619
+
620
+ Optimized for CI/CD pipelines with:
621
+ - Non-interactive execution
622
+ - Exit codes for automation
623
+ - Summary output format
624
+ - Fail-fast behavior
625
+ """
626
+ import sys
627
+
628
+ click.echo("šŸ” CodeOptiX CI/CD Check")
629
+ click.echo("=" * 60)
630
+
631
+ # Parse behaviors
632
+ behavior_list = [b.strip() for b in behaviors.split(",")]
633
+
634
+ if not behavior_list:
635
+ click.echo("āŒ Error: At least one behavior must be specified", err=True)
636
+ sys.exit(1)
637
+
638
+ # Load config if provided
639
+ config_dict = {}
640
+ if config:
641
+ config_path = Path(config)
642
+ if config_path.suffix == ".json":
643
+ with open(config_path) as f:
644
+ config_dict = json.load(f)
645
+ elif config_path.suffix in [".yaml", ".yml"]:
646
+ import yaml
647
+
648
+ with open(config_path) as f:
649
+ config_dict = yaml.safe_load(f)
650
+
651
+ # Get API key
652
+ api_key = llm_api_key or os.getenv(f"{llm_provider.upper()}_API_KEY")
653
+ if not api_key:
654
+ click.echo(
655
+ f"āŒ Error: API key required. Set {llm_provider.upper()}_API_KEY environment variable or use --llm-api-key",
656
+ err=True,
657
+ )
658
+ click.echo("", err=True)
659
+ click.echo("šŸ’” Tip: Without an API key, you can use basic static analysis:", err=True)
660
+ click.echo(" codeoptix lint --path ./src", err=True)
661
+ click.echo(
662
+ " This runs linters (ruff, bandit, flake8, etc.) without requiring API keys.",
663
+ err=True,
664
+ )
665
+ sys.exit(1)
666
+
667
+ try:
668
+ # Create adapter
669
+ adapter_config = config_dict.get("adapter", {})
670
+ if not adapter_config.get("llm_config"):
671
+ adapter_config["llm_config"] = {
672
+ "provider": llm_provider,
673
+ "api_key": api_key,
674
+ }
675
+
676
+ adapter = create_adapter(agent, adapter_config)
677
+
678
+ # Create LLM client
679
+ llm_provider_enum = LLMProvider[llm_provider.upper()]
680
+ llm_client = create_llm_client(llm_provider_enum, api_key=api_key)
681
+
682
+ # Create evaluation engine
683
+ eval_config = config_dict.get("evaluation", {})
684
+ eval_engine = EvaluationEngine(adapter, llm_client, config=eval_config)
685
+
686
+ # Run evaluation
687
+ click.echo(f"šŸ“Š Evaluating {len(behavior_list)} behavior(s): {', '.join(behavior_list)}")
688
+
689
+ results = eval_engine.evaluate_behaviors(
690
+ behavior_names=behavior_list, context=config_dict.get("context", {})
691
+ )
692
+
693
+ # Save results
694
+ artifact_manager = ArtifactManager()
695
+ run_id = artifact_manager.save_results(results)
696
+
697
+ # Display results
698
+ overall_score = results.get("overall_score", 0.0)
699
+ behaviors_data = results.get("behaviors", {})
700
+
701
+ if output_format == "summary":
702
+ click.echo("\n" + "=" * 60)
703
+ click.echo("šŸ“Š Evaluation Summary")
704
+ click.echo("=" * 60)
705
+ click.echo(f"Overall Score: {overall_score:.2%}")
706
+ click.echo(f"Run ID: {run_id}")
707
+ click.echo()
708
+
709
+ for behavior_name, behavior_data in behaviors_data.items():
710
+ passed = behavior_data.get("passed", True)
711
+ score = behavior_data.get("score", 0.0)
712
+ emoji = "āœ…" if passed else "āŒ"
713
+ click.echo(f"{emoji} {behavior_name}: {score:.2%}")
714
+
715
+ if not passed and behavior_data.get("evidence"):
716
+ evidence = behavior_data["evidence"][:3]
717
+ for ev in evidence:
718
+ click.echo(f" āš ļø {ev}")
719
+ click.echo("=" * 60)
720
+ else:
721
+ # JSON output
722
+ click.echo(
723
+ json.dumps(
724
+ {
725
+ "run_id": run_id,
726
+ "overall_score": overall_score,
727
+ "behaviors": {
728
+ name: {
729
+ "passed": data.get("passed", True),
730
+ "score": data.get("score", 0.0),
731
+ "evidence": data.get("evidence", [])[:3],
732
+ }
733
+ for name, data in behaviors_data.items()
734
+ },
735
+ },
736
+ indent=2,
737
+ )
738
+ )
739
+
740
+ # Check for failures
741
+ failed_behaviors = [
742
+ name for name, data in behaviors_data.items() if not data.get("passed", True)
743
+ ]
744
+
745
+ if failed_behaviors:
746
+ if fail_on_failure:
747
+ click.echo(
748
+ f"\nāŒ {len(failed_behaviors)} behavior(s) failed: {', '.join(failed_behaviors)}",
749
+ err=True,
750
+ )
751
+ sys.exit(1)
752
+ else:
753
+ click.echo(
754
+ f"\nāš ļø {len(failed_behaviors)} behavior(s) failed: {', '.join(failed_behaviors)}",
755
+ err=True,
756
+ )
757
+ else:
758
+ click.echo("\nāœ… All behaviors passed!")
759
+
760
+ except Exception as e:
761
+ click.echo(f"āŒ Error: {e!s}", err=True)
762
+ if hasattr(e, "__cause__") and e.__cause__:
763
+ click.echo(f" Caused by: {e.__cause__!s}", err=True)
764
+ sys.exit(1)
765
+
766
+
767
+ def _get_install_command(linter_name: str) -> str | None:
768
+ """Get install command for a linter."""
769
+ install_commands = {
770
+ "bandit": "pip install bandit",
771
+ "pylint": "pip install pylint",
772
+ "flake8": "pip install flake8",
773
+ "ruff": "pip install ruff or uv tool install ruff",
774
+ "mypy": "pip install mypy",
775
+ "safety": "pip install safety",
776
+ "pip-audit": "pip install pip-audit",
777
+ "coverage": "pip install coverage",
778
+ "html-accessibility": "No installation needed (built-in)",
779
+ }
780
+ return install_commands.get(linter_name)
781
+
782
+
783
+ @main.command()
784
+ @click.option("--path", type=click.Path(exists=True), help="Path to code (file or directory)")
785
+ @click.option(
786
+ "--linters", help="Comma-separated linter names (default: auto-detect from language and config)"
787
+ )
788
+ @click.option(
789
+ "--output",
790
+ default="summary",
791
+ type=click.Choice(["json", "summary"]),
792
+ help="Output format (default: summary)",
793
+ )
794
+ @click.option("--fail-on-issues", is_flag=True, help="Exit with non-zero code if issues found")
795
+ @click.option(
796
+ "--no-auto-detect", is_flag=True, help="Disable auto-detection of language and linters"
797
+ )
798
+ @click.option("--list-linters", is_flag=True, help="List all available linters and exit")
799
+ def lint(path, linters, output, fail_on_issues, no_auto_detect, list_linters):
800
+ """
801
+ Run linters on code (no API key required).
802
+
803
+ This command runs static analysis linters on your code without requiring
804
+ any API keys. Perfect for quick code quality checks.
805
+
806
+ Examples:
807
+ codeoptix lint --path ./src
808
+ codeoptix lint --path ./src --linters bandit,flake8
809
+ codeoptix lint --path ./src --output summary
810
+ """
811
+ import sys
812
+
813
+ # List linters if requested (check this first, before path validation)
814
+ if list_linters:
815
+ runner = LinterRunner()
816
+ available = runner.get_available_linters()
817
+ all_linters = runner.get_all_linters()
818
+
819
+ click.echo("Available Linters (Zero New Dependencies):")
820
+ click.echo("=" * 60)
821
+ click.echo("\nCode Quality:")
822
+ for linter in ["ruff", "pylint", "flake8"]:
823
+ status = "āœ…" if linter in available else "āŒ"
824
+ click.echo(f" {status} {linter}")
825
+
826
+ click.echo("\nType Checking:")
827
+ for linter in ["mypy"]:
828
+ status = "āœ…" if linter in available else "āŒ"
829
+ click.echo(f" {status} {linter}")
830
+
831
+ click.echo("\nSecurity:")
832
+ for linter in ["bandit", "safety", "pip-audit"]:
833
+ status = "āœ…" if linter in available else "āŒ"
834
+ click.echo(f" {status} {linter}")
835
+
836
+ click.echo("\nTesting:")
837
+ for linter in ["coverage"]:
838
+ status = "āœ…" if linter in available else "āŒ"
839
+ click.echo(f" {status} {linter}")
840
+
841
+ click.echo("\nAccessibility:")
842
+ for linter in ["html-accessibility"]:
843
+ status = "āœ…" if linter in available else "āŒ"
844
+ click.echo(f" {status} {linter} (custom, no dependency)")
845
+
846
+ click.echo(f"\nTotal: {len(available)}/{len(all_linters)} linters available")
847
+ click.echo("\nInstall missing linters:")
848
+ for linter in all_linters:
849
+ if linter not in available:
850
+ cmd = _get_install_command(linter)
851
+ if cmd:
852
+ click.echo(f" {cmd}")
853
+ return
854
+
855
+ if not path:
856
+ click.echo("āŒ Error: --path is required (or use --list-linters)", err=True)
857
+ sys.exit(1)
858
+
859
+ click.echo("šŸ” CodeOptiX Linter Check")
860
+ click.echo("=" * 60)
861
+ click.echo(f"šŸ“ Path: {path}")
862
+
863
+ try:
864
+ # Create linter runner
865
+ runner = LinterRunner()
866
+
867
+ # Auto-detect or use specified linters
868
+ if linters:
869
+ linter_list = [l.strip() for l in linters.split(",") if l.strip()]
870
+ else:
871
+ # Auto-detect from language and existing configs
872
+ # Find Python files if directory
873
+ from pathlib import Path
874
+
875
+ from codeoptix.linters.language_detector import LanguageDetector
876
+
877
+ path_obj = Path(path)
878
+ if path_obj.is_dir():
879
+ python_files = list(path_obj.rglob("*.py"))[:10] # Sample first 10
880
+ file_paths = [str(f) for f in python_files]
881
+ elif path_obj.is_file() and path_obj.suffix == ".py":
882
+ file_paths = [str(path_obj)]
883
+ else:
884
+ file_paths = []
885
+
886
+ # Detect language and find configs
887
+ if file_paths:
888
+ detected_languages = LanguageDetector.detect_languages(file_paths)
889
+ config_files = LanguageDetector.find_config_files(path)
890
+
891
+ click.echo(
892
+ f"🌐 Detected languages: {', '.join(detected_languages) if detected_languages else 'unknown'}"
893
+ )
894
+
895
+ # Get recommended linters
896
+ linter_list = []
897
+ for lang in detected_languages:
898
+ linter_list.extend(LanguageDetector.get_linters_for_language(lang))
899
+
900
+ # Prioritize linters with existing configs
901
+ configured_linters = [l for l in linter_list if l in config_files]
902
+ if configured_linters:
903
+ linter_list = configured_linters + [
904
+ l for l in linter_list if l not in configured_linters
905
+ ]
906
+
907
+ # Remove duplicates while preserving order
908
+ seen = set()
909
+ linter_list = [l for l in linter_list if not (l in seen or seen.add(l))]
910
+
911
+ if not linter_list:
912
+ # Fallback to common Python linters
913
+ linter_list = ["ruff", "bandit"] # Ruff first (fastest)
914
+ else:
915
+ linter_list = ["ruff", "bandit"] # Default for non-Python
916
+
917
+ click.echo(f"šŸ”§ Linters: {', '.join(linter_list) if linter_list else 'auto-detect'}")
918
+
919
+ # Check available linters
920
+ available = runner.get_available_linters()
921
+ all_linters = runner.get_all_linters()
922
+ requested_available = [l for l in linter_list if l in available]
923
+
924
+ if not requested_available:
925
+ click.echo("āŒ Error: No requested linters are available", err=True)
926
+ click.echo(f" Available linters: {', '.join(available)}", err=True)
927
+ click.echo(" Install missing linters:", err=True)
928
+ for linter in linter_list:
929
+ if linter not in available:
930
+ install_cmd = _get_install_command(linter)
931
+ if install_cmd:
932
+ click.echo(f" {install_cmd}", err=True)
933
+ sys.exit(1)
934
+
935
+ if len(requested_available) < len(linter_list):
936
+ missing = [l for l in linter_list if l not in available]
937
+ click.echo(f"āš ļø Warning: Some linters not available: {', '.join(missing)}", err=True)
938
+ click.echo(" These linters will be skipped. Install them to use:", err=True)
939
+ for linter in missing:
940
+ install_cmd = _get_install_command(linter)
941
+ if install_cmd:
942
+ click.echo(f" {install_cmd}", err=True)
943
+
944
+ # Run linters
945
+ click.echo("šŸš€ Running linters...")
946
+ results = runner.run_linters(
947
+ path,
948
+ linter_names=requested_available if requested_available else None,
949
+ auto_detect=not no_auto_detect,
950
+ )
951
+
952
+ # Display results
953
+ summary = results.get("summary", {})
954
+ total_issues = summary.get("total_issues", 0)
955
+
956
+ if output == "summary":
957
+ click.echo("\n" + "=" * 60)
958
+ click.echo("šŸ“Š Linter Results Summary")
959
+ click.echo("=" * 60)
960
+ click.echo(f"Total Issues: {total_issues}")
961
+ click.echo(f" Critical: {summary.get('critical', 0)}")
962
+ click.echo(f" High: {summary.get('high', 0)}")
963
+ click.echo(f" Medium: {summary.get('medium', 0)}")
964
+ click.echo(f" Low: {summary.get('low', 0)}")
965
+ click.echo(f"\nExecution Time: {results.get('execution_time', 0):.2f}s")
966
+
967
+ # Show issues by linter
968
+ linter_results = results.get("results", {})
969
+ for linter_name, linter_result in linter_results.items():
970
+ if isinstance(linter_result, dict):
971
+ issue_count = linter_result.get("issue_count", 0)
972
+ if issue_count > 0:
973
+ click.echo(f"\n{linter_name}: {issue_count} issue(s)")
974
+
975
+ # Show top issues
976
+ issues = results.get("issues", [])
977
+ if issues:
978
+ click.echo("\nāš ļø Top Issues:")
979
+ for issue in issues[:10]: # Show top 10
980
+ severity = issue.get("severity", "low").upper()
981
+ file = issue.get("file", "unknown")
982
+ line = issue.get("line", "?")
983
+ message = issue.get("message", "Unknown")
984
+ click.echo(f" [{severity}] {file}:{line} - {message}")
985
+
986
+ click.echo("=" * 60)
987
+ else:
988
+ # JSON output
989
+ click.echo(json.dumps(results, indent=2, default=str))
990
+
991
+ # Check for failures
992
+ if results.get("errors"):
993
+ click.echo("\nāš ļø Errors:", err=True)
994
+ for error in results["errors"]:
995
+ click.echo(f" {error}", err=True)
996
+
997
+ if total_issues > 0:
998
+ if fail_on_issues:
999
+ click.echo(f"\nāŒ Found {total_issues} issue(s)", err=True)
1000
+ sys.exit(1)
1001
+ else:
1002
+ click.echo(f"\nāš ļø Found {total_issues} issue(s)")
1003
+ else:
1004
+ click.echo("\nāœ… No issues found!")
1005
+
1006
+ except Exception as e:
1007
+ click.echo(f"āŒ Error: {e}", err=True)
1008
+ if hasattr(e, "__cause__") and e.__cause__:
1009
+ click.echo(f" Caused by: {e.__cause__!s}", err=True)
1010
+ sys.exit(1)
1011
+
1012
+
1013
+ @main.command()
1014
+ @click.option("--base", default="main", help="Base branch (default: main)")
1015
+ @click.option("--head", help="Head branch or commit (default: current branch)")
1016
+ @click.option(
1017
+ "--linters", help="Comma-separated linter names (default: auto-detect from language and config)"
1018
+ )
1019
+ @click.option(
1020
+ "--output", default="summary", type=click.Choice(["json", "summary"]), help="Output format"
1021
+ )
1022
+ @click.option("--fail-on-issues", is_flag=True, help="Exit with non-zero code if issues found")
1023
+ @click.option(
1024
+ "--no-auto-detect", is_flag=True, help="Disable auto-detection of language and linters"
1025
+ )
1026
+ def check(base, head, linters, output, fail_on_issues, no_auto_detect):
1027
+ """
1028
+ Check code changes in git (no API key required).
1029
+
1030
+ This command analyzes code changes between git branches/commits using
1031
+ linters. Perfect for CI/CD pipelines and PR checks.
1032
+
1033
+ Examples:
1034
+ codeoptix check --base main --head feature-branch
1035
+ codeoptix check --base main --head HEAD
1036
+ codeoptix check --linters bandit,flake8
1037
+ """
1038
+ import subprocess
1039
+ import sys
1040
+
1041
+ click.echo("šŸ” CodeOptiX Git Check")
1042
+ click.echo("=" * 60)
1043
+
1044
+ # Get git diff
1045
+ try:
1046
+ # Determine head
1047
+ if not head:
1048
+ result = subprocess.run(
1049
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
1050
+ capture_output=True,
1051
+ text=True,
1052
+ check=True,
1053
+ )
1054
+ head = result.stdout.strip()
1055
+
1056
+ click.echo(f"šŸ“Š Comparing: {base}..{head}")
1057
+
1058
+ # Get changed files
1059
+ result = subprocess.run(
1060
+ ["git", "diff", "--name-only", base, head],
1061
+ capture_output=True,
1062
+ text=True,
1063
+ check=True,
1064
+ )
1065
+
1066
+ changed_files = [f.strip() for f in result.stdout.split("\n") if f.strip()]
1067
+
1068
+ if not changed_files:
1069
+ click.echo("āœ… No files changed")
1070
+ return
1071
+
1072
+ # Filter Python files
1073
+ python_files = [f for f in changed_files if f.endswith(".py")]
1074
+
1075
+ if not python_files:
1076
+ click.echo("ā„¹ļø No Python files changed")
1077
+ return
1078
+
1079
+ click.echo(f"šŸ“ Changed Python files: {len(python_files)}")
1080
+ click.echo()
1081
+
1082
+ # Auto-detect or use specified linters
1083
+ from codeoptix.linters.language_detector import LanguageDetector
1084
+
1085
+ if linters:
1086
+ linter_list = [l.strip() for l in linters.split(",") if l.strip()]
1087
+ else:
1088
+ # Auto-detect from changed files
1089
+ detected_languages = LanguageDetector.detect_languages(python_files)
1090
+ config_files = LanguageDetector.find_config_files(".")
1091
+
1092
+ click.echo(
1093
+ f"🌐 Detected languages: {', '.join(detected_languages) if detected_languages else 'Python'}"
1094
+ )
1095
+
1096
+ # Get recommended linters
1097
+ linter_list = []
1098
+ for lang in detected_languages:
1099
+ linter_list.extend(LanguageDetector.get_linters_for_language(lang))
1100
+
1101
+ # Prioritize linters with existing configs
1102
+ configured_linters = [l for l in linter_list if l in config_files]
1103
+ if configured_linters:
1104
+ linter_list = configured_linters + [
1105
+ l for l in linter_list if l not in configured_linters
1106
+ ]
1107
+
1108
+ # Remove duplicates
1109
+ seen = set()
1110
+ linter_list = [l for l in linter_list if not (l in seen or seen.add(l))]
1111
+
1112
+ if not linter_list:
1113
+ linter_list = ["ruff", "bandit"] # Default
1114
+
1115
+ click.echo(f"šŸ”§ Linters: {', '.join(linter_list)}")
1116
+
1117
+ # Run linters on changed files
1118
+ runner = LinterRunner()
1119
+
1120
+ # Get current directory as base path
1121
+ import os
1122
+
1123
+ base_path = os.getcwd()
1124
+
1125
+ results = runner.run_linters(
1126
+ base_path,
1127
+ linter_names=linter_list,
1128
+ files=python_files,
1129
+ auto_detect=not no_auto_detect,
1130
+ )
1131
+
1132
+ # Filter issues to only changed files
1133
+ all_issues = results.get("issues", [])
1134
+ filtered_issues = [
1135
+ issue
1136
+ for issue in all_issues
1137
+ if any(issue.get("file", "").endswith(f) for f in python_files)
1138
+ ]
1139
+
1140
+ # Update summary
1141
+ summary = results.get("summary", {}).copy()
1142
+ summary["total_issues"] = len(filtered_issues)
1143
+ summary["critical"] = sum(1 for i in filtered_issues if i.get("severity") == "critical")
1144
+ summary["high"] = sum(1 for i in filtered_issues if i.get("severity") == "high")
1145
+ summary["medium"] = sum(1 for i in filtered_issues if i.get("severity") == "medium")
1146
+ summary["low"] = sum(1 for i in filtered_issues if i.get("severity") == "low")
1147
+
1148
+ results["summary"] = summary
1149
+ results["issues"] = filtered_issues
1150
+
1151
+ # Display results
1152
+ total_issues = summary.get("total_issues", 0)
1153
+
1154
+ if output == "summary":
1155
+ click.echo("=" * 60)
1156
+ click.echo("šŸ“Š Code Check Results")
1157
+ click.echo("=" * 60)
1158
+ click.echo(f"Total Issues: {total_issues}")
1159
+ click.echo(f" Critical: {summary.get('critical', 0)}")
1160
+ click.echo(f" High: {summary.get('high', 0)}")
1161
+ click.echo(f" Medium: {summary.get('medium', 0)}")
1162
+ click.echo(f" Low: {summary.get('low', 0)}")
1163
+
1164
+ if filtered_issues:
1165
+ click.echo("\nāš ļø Issues in Changed Files:")
1166
+ for issue in filtered_issues[:20]: # Show top 20
1167
+ severity = issue.get("severity", "low").upper()
1168
+ file = issue.get("file", "unknown")
1169
+ line = issue.get("line", "?")
1170
+ message = issue.get("message", "Unknown")
1171
+ click.echo(f" [{severity}] {file}:{line} - {message}")
1172
+
1173
+ click.echo("=" * 60)
1174
+ else:
1175
+ click.echo(json.dumps(results, indent=2, default=str))
1176
+
1177
+ if total_issues > 0:
1178
+ if fail_on_issues:
1179
+ click.echo(f"\nāŒ Found {total_issues} issue(s) in changed files", err=True)
1180
+ sys.exit(1)
1181
+ else:
1182
+ click.echo(f"\nāš ļø Found {total_issues} issue(s) in changed files")
1183
+ else:
1184
+ click.echo("\nāœ… No issues found in changed files!")
1185
+
1186
+ except subprocess.CalledProcessError as e:
1187
+ click.echo(f"āŒ Git error: {e.stderr}", err=True)
1188
+ sys.exit(1)
1189
+ except FileNotFoundError:
1190
+ click.echo("āŒ Error: Git not found. Please install git.", err=True)
1191
+ sys.exit(1)
1192
+ except Exception as e:
1193
+ click.echo(f"āŒ Error: {e}", err=True)
1194
+ sys.exit(1)
1195
+
1196
+
1197
+ @main.command()
1198
+ def list_runs():
1199
+ """List all evaluation runs."""
1200
+ artifact_manager = ArtifactManager()
1201
+
1202
+ runs = artifact_manager.list_runs()
1203
+
1204
+ if not runs:
1205
+ click.echo("No evaluation runs found.")
1206
+ return
1207
+
1208
+ click.echo(f"Found {len(runs)} evaluation run(s):\n")
1209
+
1210
+ for run in runs:
1211
+ click.echo(f"Run ID: {run['run_id']}")
1212
+ click.echo(f" Timestamp: {run.get('timestamp', 'unknown')}")
1213
+ click.echo(f" Score: {run.get('overall_score', 0.0):.2f}/1.0")
1214
+ click.echo(f" Behaviors: {', '.join(run.get('behaviors', []))}")
1215
+ click.echo()
1216
+
1217
+
1218
+ @main.group()
1219
+ def acp():
1220
+ """ACP (Agent Client Protocol) integration commands."""
1221
+
1222
+
1223
+ @acp.command()
1224
+ def register():
1225
+ """Register CodeOptiX as an ACP agent (for use with editors like Zed, JetBrains, Neovim)."""
1226
+ click.echo("šŸš€ Starting CodeOptiX as ACP agent...")
1227
+ click.echo("šŸ“ CodeOptiX will be available to ACP-compatible editors")
1228
+ click.echo("šŸ’” Connect from your editor using ACP protocol")
1229
+ click.echo()
1230
+
1231
+ # Create CodeOptiX agent
1232
+ agent = CodeOptiXAgent()
1233
+
1234
+ # Run agent (this blocks and handles ACP protocol)
1235
+ try:
1236
+ asyncio.run(run_agent(agent))
1237
+ except KeyboardInterrupt:
1238
+ click.echo("\nšŸ‘‹ CodeOptiX ACP agent stopped")
1239
+
1240
+
1241
+ @acp.command()
1242
+ @click.option("--agent-command", help="Command to spawn ACP agent (e.g., 'python agent.py')")
1243
+ @click.option("--agent-name", help="Name of agent in registry (alternative to agent-command)")
1244
+ @click.option(
1245
+ "--auto-eval/--no-auto-eval", default=True, help="Automatically evaluate code quality"
1246
+ )
1247
+ @click.option("--cwd", help="Working directory for the agent")
1248
+ @click.option("--behaviors", help="Comma-separated behavior names to evaluate")
1249
+ def bridge(
1250
+ agent_command: str | None,
1251
+ agent_name: str | None,
1252
+ auto_eval: bool,
1253
+ cwd: str | None,
1254
+ behaviors: str | None,
1255
+ ):
1256
+ """Use CodeOptiX as a quality bridge between editor and agent via ACP."""
1257
+ if not agent_command and not agent_name:
1258
+ click.echo("āŒ Error: Either --agent-command or --agent-name must be provided", err=True)
1259
+ raise click.Abort()
1260
+
1261
+ click.echo("šŸŒ‰ Starting CodeOptiX ACP Quality Bridge...")
1262
+ if agent_command:
1263
+ click.echo(f"šŸ¤– Agent command: {agent_command}")
1264
+ if agent_name:
1265
+ click.echo(f"šŸ¤– Agent name: {agent_name}")
1266
+ click.echo(f"šŸ” Auto-evaluation: {auto_eval}")
1267
+ click.echo()
1268
+
1269
+ # Parse behaviors
1270
+ behavior_list = behaviors.split(",") if behaviors else None
1271
+
1272
+ # Create evaluation engine if auto_eval
1273
+ evaluation_engine = None
1274
+ llm_client = None
1275
+ if auto_eval:
1276
+ from codeoptix.adapters.factory import create_adapter
1277
+ from codeoptix.evaluation import EvaluationEngine
1278
+ from codeoptix.utils.llm import LLMProvider, create_llm_client
1279
+
1280
+ # Create a dummy adapter for evaluation
1281
+ adapter = create_adapter("claude-code", {})
1282
+ llm_client = create_llm_client(LLMProvider.OPENAI)
1283
+ evaluation_engine = EvaluationEngine(adapter, llm_client)
1284
+
1285
+ # Create registry if using agent_name
1286
+ registry = None
1287
+ if agent_name:
1288
+ registry = ACPAgentRegistry()
1289
+ # Agent should be pre-registered, but we'll handle it
1290
+
1291
+ # Parse agent command if provided
1292
+ agent_cmd = agent_command.split() if agent_command else None
1293
+
1294
+ # Create quality bridge
1295
+ bridge = ACPQualityBridge(
1296
+ agent_command=agent_cmd,
1297
+ agent_name=agent_name,
1298
+ evaluation_engine=evaluation_engine,
1299
+ llm_client=llm_client,
1300
+ auto_eval=auto_eval,
1301
+ registry=registry,
1302
+ behaviors=behavior_list,
1303
+ )
1304
+
1305
+ async def run_bridge():
1306
+ await bridge.connect(cwd=cwd)
1307
+ click.echo("āœ… Quality bridge connected!")
1308
+ click.echo("šŸ’” CodeOptiX will now evaluate all agent interactions")
1309
+ # Keep bridge running
1310
+ try:
1311
+ await asyncio.Event().wait()
1312
+ except KeyboardInterrupt:
1313
+ click.echo("\nšŸ‘‹ Quality bridge stopped")
1314
+ await bridge.close()
1315
+
1316
+ try:
1317
+ asyncio.run(run_bridge())
1318
+ except KeyboardInterrupt:
1319
+ click.echo("\nšŸ‘‹ CodeOptiX quality bridge stopped")
1320
+
1321
+
1322
+ @acp.command()
1323
+ @click.option("--agent-command", help="Command to spawn ACP agent")
1324
+ @click.option("--agent-name", help="Name of agent in registry")
1325
+ @click.option("--prompt", required=True, help="Prompt to send to agent")
1326
+ @click.option("--cwd", help="Working directory")
1327
+ def connect(agent_command: str | None, agent_name: str | None, prompt: str, cwd: str | None):
1328
+ """Connect to an ACP agent and send a prompt."""
1329
+ if not agent_command and not agent_name:
1330
+ click.echo("āŒ Error: Either --agent-command or --agent-name must be provided", err=True)
1331
+ raise click.Abort()
1332
+
1333
+ if agent_command:
1334
+ click.echo(f"šŸ”Œ Connecting to ACP agent: {agent_command}")
1335
+ if agent_name:
1336
+ click.echo(f"šŸ”Œ Connecting to ACP agent: {agent_name}")
1337
+
1338
+ # Parse agent command if provided
1339
+ agent_cmd = agent_command.split() if agent_command else None
1340
+
1341
+ # Create registry if using agent_name
1342
+ registry = None
1343
+ if agent_name:
1344
+ registry = ACPAgentRegistry()
1345
+
1346
+ # Create bridge and send prompt
1347
+ bridge = ACPQualityBridge(
1348
+ agent_command=agent_cmd,
1349
+ agent_name=agent_name,
1350
+ auto_eval=True,
1351
+ registry=registry,
1352
+ )
1353
+
1354
+ async def run_connect():
1355
+ await bridge.connect(cwd=cwd)
1356
+ click.echo("āœ… Connected!")
1357
+ click.echo(f"šŸ“¤ Sending prompt: {prompt[:50]}...")
1358
+ result = await bridge.prompt(prompt)
1359
+ click.echo(f"āœ… Response: {result}")
1360
+ await bridge.close()
1361
+
1362
+ try:
1363
+ asyncio.run(run_connect())
1364
+ except Exception as e:
1365
+ click.echo(f"āŒ Error: {e}", err=True)
1366
+ raise click.Abort()
1367
+
1368
+
1369
+ @acp.group()
1370
+ def registry():
1371
+ """Manage ACP agent registry."""
1372
+
1373
+
1374
+ @registry.command("list")
1375
+ def registry_list():
1376
+ """List all registered ACP agents."""
1377
+ registry = ACPAgentRegistry()
1378
+ agents = registry.list_agents()
1379
+
1380
+ if not agents:
1381
+ click.echo("No agents registered.")
1382
+ return
1383
+
1384
+ click.echo(f"Registered ACP agents ({len(agents)}):\n")
1385
+ for agent_name in agents:
1386
+ config = registry.get_agent(agent_name)
1387
+ click.echo(f" • {agent_name}")
1388
+ if config and config.description:
1389
+ click.echo(f" {config.description}")
1390
+ if config and config.command:
1391
+ click.echo(f" Command: {' '.join(config.command)}")
1392
+
1393
+
1394
+ @registry.command("add")
1395
+ @click.option("--name", required=True, help="Agent name")
1396
+ @click.option("--command", required=True, help="Command to spawn agent (e.g., 'python agent.py')")
1397
+ @click.option("--cwd", help="Working directory")
1398
+ @click.option("--description", help="Agent description")
1399
+ def registry_add(name: str, command: str, cwd: str | None, description: str | None):
1400
+ """Register a new ACP agent."""
1401
+ registry = ACPAgentRegistry()
1402
+ registry.register(
1403
+ name=name,
1404
+ command=command.split(),
1405
+ cwd=cwd,
1406
+ description=description or "",
1407
+ )
1408
+ click.echo(f"āœ… Registered agent: {name}")
1409
+
1410
+
1411
+ @registry.command("remove")
1412
+ @click.option("--name", required=True, help="Agent name")
1413
+ def registry_remove(name: str):
1414
+ """Unregister an ACP agent."""
1415
+ registry = ACPAgentRegistry()
1416
+ registry.unregister(name)
1417
+ click.echo(f"āœ… Unregistered agent: {name}")
1418
+
1419
+
1420
+ @acp.command()
1421
+ @click.option("--generate-agent", required=True, help="Agent name for code generation")
1422
+ @click.option("--judge-agent", required=True, help="Agent name for code judgment")
1423
+ @click.option("--prompt", required=True, help="Prompt for code generation")
1424
+ def judge(generate_agent: str, judge_agent: str, prompt: str):
1425
+ """Use multi-agent judge: generate with one agent, judge with another."""
1426
+ click.echo("āš–ļø Starting Multi-Agent Judge...")
1427
+ click.echo(f"šŸ¤– Generate agent: {generate_agent}")
1428
+ click.echo(f"āš–ļø Judge agent: {judge_agent}")
1429
+ click.echo()
1430
+
1431
+ # Create registry
1432
+ registry = ACPAgentRegistry()
1433
+
1434
+ # Create evaluation engine
1435
+ from codeoptix.adapters.factory import create_adapter
1436
+ from codeoptix.evaluation import EvaluationEngine
1437
+ from codeoptix.utils.llm import LLMProvider, create_llm_client
1438
+
1439
+ adapter = create_adapter("claude-code", {})
1440
+ llm_client = create_llm_client(LLMProvider.OPENAI)
1441
+ evaluation_engine = EvaluationEngine(adapter, llm_client)
1442
+
1443
+ # Create multi-agent judge
1444
+ judge = MultiAgentJudge(
1445
+ registry=registry,
1446
+ generate_agent=generate_agent,
1447
+ judge_agent=judge_agent,
1448
+ evaluation_engine=evaluation_engine,
1449
+ llm_client=llm_client,
1450
+ )
1451
+
1452
+ async def run_judge():
1453
+ result = await judge.generate_and_judge(prompt)
1454
+ click.echo("āœ… Multi-agent judge complete!")
1455
+ click.echo(f"\nšŸ“ Generated Code:\n{result.get('generated_code', 'N/A')}")
1456
+ click.echo(f"\nāš–ļø Judgment:\n{result.get('judgment', 'N/A')}")
1457
+ if result.get("evaluation_results"):
1458
+ click.echo(f"\nšŸ” Evaluation Results:\n{result['evaluation_results']}")
1459
+
1460
+ try:
1461
+ asyncio.run(run_judge())
1462
+ except Exception as e:
1463
+ click.echo(f"āŒ Error: {e}", err=True)
1464
+ raise click.Abort()
1465
+
1466
+
1467
+ if __name__ == "__main__":
1468
+ main()