open-autoforge 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.
autoforge/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """AutoForge: Autonomous metric-driven agentic coding framework."""
2
+
3
+ __version__ = "0.1.0"
autoforge/__main__.py ADDED
@@ -0,0 +1,327 @@
1
+ """
2
+ AutoForge CLI entry point.
3
+
4
+ Commands:
5
+ run — Execute a workflow (measure -> act -> validate loop)
6
+ health — Run all metric adapters and produce a health dashboard
7
+ list — List available workflows and adapters
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import logging
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ from autoforge import __version__
19
+
20
+
21
+ def _setup_logging(verbose: bool = False) -> None:
22
+ level = logging.DEBUG if verbose else logging.INFO
23
+ logging.basicConfig(
24
+ level=level,
25
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
26
+ datefmt="%H:%M:%S",
27
+ force=True,
28
+ )
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Commands
33
+ # ---------------------------------------------------------------------------
34
+
35
+ def cmd_run(args: argparse.Namespace) -> int:
36
+ """Execute a workflow."""
37
+ from autoforge.registry import find_workflow_config, get_adapter
38
+ from autoforge.reporting import report_to_markdown, save_run_report
39
+ from autoforge.runner import WorkflowRunner
40
+
41
+ # Load workflow config
42
+ if args.config:
43
+ from autoforge.registry import load_workflow_config
44
+ config = load_workflow_config(args.config)
45
+ else:
46
+ config = find_workflow_config(args.workflow)
47
+
48
+ # Override budget from CLI
49
+ if args.max_iterations is not None:
50
+ config.budget.max_iterations = args.max_iterations
51
+ if args.max_tokens is not None:
52
+ config.budget.max_tokens = args.max_tokens
53
+ if args.max_time is not None:
54
+ config.budget.max_wall_clock_minutes = args.max_time
55
+
56
+ # Get the adapter
57
+ adapter_name = args.adapter or config.adapter or config.name
58
+ adapter = get_adapter(adapter_name)
59
+
60
+ # Resolve paths
61
+ repo_path = str(Path(args.repo).resolve())
62
+ target_path = str(Path(args.path).resolve()) if args.path else repo_path
63
+
64
+ # Create and run
65
+ runner = WorkflowRunner(
66
+ config=config,
67
+ adapter=adapter,
68
+ repo_path=repo_path,
69
+ target_path=target_path,
70
+ target_value=args.target,
71
+ test_command=args.test_command,
72
+ skip_tests=args.skip_tests,
73
+ skip_git=args.skip_git,
74
+ dry_run=args.dry_run,
75
+ agent_command=args.agent_command,
76
+ )
77
+
78
+ report = runner.run()
79
+
80
+ # Save report
81
+ output_dir = args.output or str(Path(repo_path) / ".autoforge" / "reports")
82
+ json_path, md_path = save_run_report(report, output_dir)
83
+
84
+ # Print summary
85
+ print()
86
+ print(report_to_markdown(report))
87
+ print()
88
+ print(f"Reports saved to:")
89
+ print(f" JSON: {json_path}")
90
+ print(f" Markdown: {md_path}")
91
+
92
+ return 0 if report.outcome.value == "target_met" else 1
93
+
94
+
95
+ def cmd_health(args: argparse.Namespace) -> int:
96
+ """Run health check across all (or specified) adapters."""
97
+ from autoforge.registry import get_adapter, list_adapters
98
+ from autoforge.reporting import format_health_dashboard
99
+ from autoforge.models import MetricResult
100
+
101
+ repo_path = str(Path(args.repo).resolve())
102
+ target_path = str(Path(args.path).resolve()) if args.path else repo_path
103
+
104
+ adapter_names = args.adapters.split(",") if args.adapters else list_adapters()
105
+
106
+ metrics: dict[str, MetricResult] = {}
107
+ for name in adapter_names:
108
+ try:
109
+ adapter = get_adapter(name)
110
+ if adapter.check_prerequisites(repo_path):
111
+ result = adapter.measure(repo_path, target_path)
112
+ metrics[name] = result
113
+ else:
114
+ logging.getLogger(__name__).warning(
115
+ "Skipping adapter '%s': prerequisites not met", name
116
+ )
117
+ except Exception as e:
118
+ logging.getLogger(__name__).error("Adapter '%s' failed: %s", name, e)
119
+
120
+ if not metrics:
121
+ print("No metrics collected. Check adapter prerequisites.")
122
+ return 1
123
+
124
+ dashboard = format_health_dashboard(metrics)
125
+
126
+ if args.format == "json":
127
+ data = {
128
+ name: {
129
+ "metric_name": r.metric_name,
130
+ "value": r.value,
131
+ "unit": r.unit,
132
+ "direction": r.direction.value,
133
+ "breakdown": r.breakdown,
134
+ }
135
+ for name, r in metrics.items()
136
+ }
137
+ print(json.dumps(data, indent=2))
138
+ else:
139
+ print(dashboard)
140
+
141
+ # Save if output specified
142
+ if args.output:
143
+ out = Path(args.output)
144
+ out.parent.mkdir(parents=True, exist_ok=True)
145
+ out.write_text(dashboard + "\n")
146
+ print(f"\nSaved to: {out}")
147
+
148
+ return 0
149
+
150
+
151
+ def cmd_list(args: argparse.Namespace) -> int:
152
+ """List available workflows and adapters."""
153
+ from autoforge.registry import list_adapters, list_workflows
154
+
155
+ print("Available Adapters:")
156
+ for name in list_adapters():
157
+ print(f" - {name}")
158
+
159
+ print()
160
+ print("Available Workflows:")
161
+ workflows = list_workflows()
162
+ if workflows:
163
+ for name in workflows:
164
+ print(f" - {name}")
165
+ else:
166
+ print(" (none found)")
167
+
168
+ return 0
169
+
170
+
171
+ # ---------------------------------------------------------------------------
172
+ # Argument Parser
173
+ # ---------------------------------------------------------------------------
174
+
175
+ def build_parser() -> argparse.ArgumentParser:
176
+ parser = argparse.ArgumentParser(
177
+ prog="autoforge",
178
+ description="AutoForge: Autonomous metric-driven agentic coding framework",
179
+ )
180
+ parser.add_argument(
181
+ "--version", "-V",
182
+ action="version",
183
+ version=f"%(prog)s {__version__}",
184
+ )
185
+ parser.add_argument(
186
+ "--verbose", "-v",
187
+ action="store_true",
188
+ help="Enable debug logging",
189
+ )
190
+
191
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
192
+
193
+ # --- run ---
194
+ run_p = subparsers.add_parser("run", help="Execute a workflow")
195
+ run_p.add_argument(
196
+ "workflow",
197
+ help="Workflow name (e.g., 'complexity_refactor') or path to YAML config",
198
+ )
199
+ run_p.add_argument(
200
+ "--path", "-p",
201
+ default=None,
202
+ help="Target path to improve (default: repo root)",
203
+ )
204
+ run_p.add_argument(
205
+ "--repo", "-r",
206
+ default=".",
207
+ help="Repository root (default: current directory)",
208
+ )
209
+ run_p.add_argument(
210
+ "--target", "-t",
211
+ type=float,
212
+ default=None,
213
+ help="Target metric value to achieve",
214
+ )
215
+ run_p.add_argument(
216
+ "--adapter", "-a",
217
+ default=None,
218
+ help="Metric adapter to use (default: inferred from workflow)",
219
+ )
220
+ run_p.add_argument(
221
+ "--config", "-c",
222
+ default=None,
223
+ help="Path to workflow YAML config file",
224
+ )
225
+ run_p.add_argument(
226
+ "--max-iterations",
227
+ type=int,
228
+ default=None,
229
+ help="Override max iterations",
230
+ )
231
+ run_p.add_argument(
232
+ "--max-tokens",
233
+ type=int,
234
+ default=None,
235
+ help="Override max token budget",
236
+ )
237
+ run_p.add_argument(
238
+ "--max-time",
239
+ type=int,
240
+ default=None,
241
+ help="Override max wall-clock time (minutes)",
242
+ )
243
+ run_p.add_argument(
244
+ "--test-command",
245
+ default=None,
246
+ help="Test command to run for regression guard",
247
+ )
248
+ run_p.add_argument(
249
+ "--skip-tests",
250
+ action="store_true",
251
+ help="Skip test validation between iterations",
252
+ )
253
+ run_p.add_argument(
254
+ "--skip-git",
255
+ action="store_true",
256
+ help="Skip git branch/commit management",
257
+ )
258
+ run_p.add_argument(
259
+ "--dry-run",
260
+ action="store_true",
261
+ help="Measure only, don't run agent",
262
+ )
263
+ run_p.add_argument(
264
+ "--agent-command",
265
+ default=None,
266
+ help="Custom agent command (overrides workflow agent.command; used as-is)",
267
+ )
268
+ run_p.add_argument(
269
+ "--output", "-o",
270
+ default=None,
271
+ help="Output directory for reports",
272
+ )
273
+ run_p.set_defaults(func=cmd_run)
274
+
275
+ # --- health ---
276
+ health_p = subparsers.add_parser("health", help="Run health check")
277
+ health_p.add_argument(
278
+ "--path", "-p",
279
+ default=None,
280
+ help="Target path to analyze (default: repo root)",
281
+ )
282
+ health_p.add_argument(
283
+ "--repo", "-r",
284
+ default=".",
285
+ help="Repository root (default: current directory)",
286
+ )
287
+ health_p.add_argument(
288
+ "--adapters",
289
+ default=None,
290
+ help="Comma-separated list of adapters to run (default: all)",
291
+ )
292
+ health_p.add_argument(
293
+ "--format", "-f",
294
+ choices=["text", "json"],
295
+ default="text",
296
+ help="Output format",
297
+ )
298
+ health_p.add_argument(
299
+ "--output", "-o",
300
+ default=None,
301
+ help="Save output to file",
302
+ )
303
+ health_p.set_defaults(func=cmd_health)
304
+
305
+ # --- list ---
306
+ list_p = subparsers.add_parser("list", help="List available workflows and adapters")
307
+ list_p.set_defaults(func=cmd_list)
308
+
309
+ return parser
310
+
311
+
312
+ def main() -> None:
313
+ parser = build_parser()
314
+ args = parser.parse_args()
315
+
316
+ _setup_logging(getattr(args, "verbose", False))
317
+
318
+ if not args.command:
319
+ parser.print_help()
320
+ sys.exit(1)
321
+
322
+ exit_code = args.func(args)
323
+ sys.exit(exit_code)
324
+
325
+
326
+ if __name__ == "__main__":
327
+ main()
@@ -0,0 +1 @@
1
+ """Metric adapters for AutoForge."""
@@ -0,0 +1,42 @@
1
+ """
2
+ Base metric adapter interface.
3
+
4
+ All metric adapters implement the MetricAdapter protocol defined in models.py.
5
+ This module provides a concrete base class with shared utilities.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import shutil
12
+ from abc import ABC, abstractmethod
13
+
14
+ from autoforge.models import MetricResult
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class BaseMetricAdapter(ABC):
20
+ """Abstract base class for metric adapters with shared utilities."""
21
+
22
+ name: str = ""
23
+ supported_languages: list[str] = []
24
+
25
+ def check_tool_available(self, tool_cmd: str) -> bool:
26
+ """Check if a CLI tool is available on PATH."""
27
+ return shutil.which(tool_cmd) is not None
28
+
29
+ @abstractmethod
30
+ def check_prerequisites(self, repo_path: str) -> bool:
31
+ """Verify tool is installed and repo is compatible."""
32
+ ...
33
+
34
+ @abstractmethod
35
+ def measure(self, repo_path: str, target_path: str) -> MetricResult:
36
+ """Run the metric tool and return normalized result."""
37
+ ...
38
+
39
+ @abstractmethod
40
+ def identify_targets(self, result: MetricResult, n: int) -> list[str]:
41
+ """Return top-n files to target for improvement (worst first)."""
42
+ ...
@@ -0,0 +1,124 @@
1
+ """
2
+ Complexity-accounting metric adapter.
3
+
4
+ Wraps the complexity-accounting tool (code-complexity-measure) to provide
5
+ Net Complexity Score (NCS) measurements through the standard MetricAdapter
6
+ interface.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ import subprocess
14
+ import sys
15
+ from datetime import datetime, timezone
16
+
17
+ from autoforge.adapters.base import BaseMetricAdapter
18
+ from autoforge.models import Direction, MetricResult
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class ComplexityAdapter(BaseMetricAdapter):
24
+ """Metric adapter for complexity-accounting (NCS measurement)."""
25
+
26
+ name = "complexity"
27
+ supported_languages = ["python", "go", "java", "javascript", "typescript", "rust", "cpp"]
28
+
29
+ def __init__(
30
+ self,
31
+ *,
32
+ no_churn: bool = True,
33
+ no_coupling: bool = False,
34
+ no_duplication: bool = False,
35
+ ncs_model: str = "multiplicative",
36
+ threshold: int | None = None,
37
+ include_tests: bool = False,
38
+ ):
39
+ self.no_churn = no_churn
40
+ self.no_coupling = no_coupling
41
+ self.no_duplication = no_duplication
42
+ self.ncs_model = ncs_model
43
+ self.threshold = threshold
44
+ self.include_tests = include_tests
45
+
46
+ def check_prerequisites(self, repo_path: str) -> bool:
47
+ """Check that complexity-accounting is importable."""
48
+ try:
49
+ import complexity_accounting # noqa: F401
50
+ return True
51
+ except ImportError:
52
+ logger.warning(
53
+ "complexity-accounting not installed. "
54
+ "Install with: pip install complexity-accounting"
55
+ )
56
+ return False
57
+
58
+ def _build_command(self, target_path: str) -> list[str]:
59
+ """Build the complexity-accounting scan command."""
60
+ cmd = [
61
+ sys.executable, "-m", "complexity_accounting",
62
+ "scan", target_path,
63
+ "--format", "json",
64
+ "--ncs-model", self.ncs_model,
65
+ ]
66
+ if self.no_churn:
67
+ cmd.append("--no-churn")
68
+ if self.no_coupling:
69
+ cmd.append("--no-coupling")
70
+ if self.no_duplication:
71
+ cmd.append("--no-duplication")
72
+ if self.threshold is not None:
73
+ cmd.extend(["--threshold", str(self.threshold)])
74
+ if self.include_tests:
75
+ cmd.append("--include-tests")
76
+ return cmd
77
+
78
+ def measure(self, repo_path: str, target_path: str) -> MetricResult:
79
+ """Run complexity-accounting scan and return NCS as MetricResult."""
80
+ cmd = self._build_command(target_path)
81
+ logger.info("Running: %s", " ".join(cmd))
82
+
83
+ result = subprocess.run(
84
+ cmd,
85
+ capture_output=True,
86
+ text=True,
87
+ cwd=repo_path,
88
+ timeout=300,
89
+ )
90
+
91
+ if result.returncode != 0:
92
+ raise RuntimeError(
93
+ f"complexity-accounting scan failed (exit {result.returncode}):\n"
94
+ f"{result.stderr}"
95
+ )
96
+
97
+ data = json.loads(result.stdout)
98
+ summary = data.get("summary", {})
99
+ ncs = summary.get("net_complexity_score", 0.0)
100
+
101
+ breakdown: dict[str, float] = {}
102
+ for file_info in data.get("files", []):
103
+ path = file_info.get("path", "")
104
+ avg_cog = file_info.get("avg_cognitive", 0.0)
105
+ breakdown[path] = avg_cog
106
+
107
+ return MetricResult(
108
+ metric_name="net_complexity_score",
109
+ value=ncs,
110
+ unit="score",
111
+ direction=Direction.MINIMIZE,
112
+ breakdown=breakdown,
113
+ tool="complexity-accounting",
114
+ timestamp=datetime.now(timezone.utc).isoformat(),
115
+ )
116
+
117
+ def identify_targets(self, result: MetricResult, n: int) -> list[str]:
118
+ """Return top-n files with highest average cognitive complexity."""
119
+ sorted_files = sorted(
120
+ result.breakdown.items(),
121
+ key=lambda kv: kv[1],
122
+ reverse=True,
123
+ )
124
+ return [path for path, _ in sorted_files[:n]]