coding-agents 0.0.1.dev0__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.
@@ -0,0 +1,106 @@
1
+ """Autonomous Skill Synthesis and Pattern Mining from Verified Trajectories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ from typing import Any, Dict, List, Optional
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class MinedToolStep(BaseModel):
12
+ tool_name: str
13
+ arguments: Dict[str, Any]
14
+ output_summary: str
15
+
16
+
17
+ class CandidateSkill(BaseModel):
18
+ name: str
19
+ description: str
20
+ trigger_pattern: str
21
+ steps: List[MinedToolStep] = Field(default_factory=list)
22
+ parameter_names: List[str] = Field(default_factory=list)
23
+ skill_md_content: str = ""
24
+
25
+
26
+ class SkillMiner:
27
+ """Extracts repeatable procedures from verified successful execution trajectories."""
28
+
29
+ @classmethod
30
+ def mine_trajectory(
31
+ cls,
32
+ task_description: str,
33
+ turn_records: List[Dict[str, Any]],
34
+ skill_name_hint: Optional[str] = None,
35
+ ) -> CandidateSkill:
36
+ steps: List[MinedToolStep] = []
37
+ param_candidates: set[str] = set()
38
+
39
+ for turn in turn_records:
40
+ calls = turn.get("tool_calls", [])
41
+ results = turn.get("tool_results", [])
42
+ for i, call in enumerate(calls):
43
+ tool_name = call.get("name", "")
44
+ args = call.get("arguments", {})
45
+ res = results[i] if i < len(results) else {}
46
+ output = res.get("output", "")[:200]
47
+
48
+ # Identify potential file path parameters
49
+ for k, v in args.items():
50
+ if isinstance(v, str) and ("/" in v or "." in v):
51
+ param_candidates.add(v)
52
+
53
+ steps.append(MinedToolStep(
54
+ tool_name=tool_name,
55
+ arguments=args,
56
+ output_summary=output,
57
+ ))
58
+
59
+ clean_name = skill_name_hint or re.sub(r"[^a-z0-9_-]", "-", task_description.lower())[:24].strip("-")
60
+ clean_name = clean_name or "extracted-skill"
61
+
62
+ # Synthesize SKILL.md markdown
63
+ skill_md = [
64
+ "---",
65
+ f"name: {clean_name}",
66
+ f'description: "Procedural skill mined from successful task: {task_description}"',
67
+ "---",
68
+ f"# Procedure: {clean_name}",
69
+ "",
70
+ "## When to Use",
71
+ f"- Trigger on tasks matching: `{task_description}`",
72
+ "",
73
+ "## Ordered Steps",
74
+ ]
75
+
76
+ for idx, step in enumerate(steps, start=1):
77
+ skill_md.append(f"{idx}. Execute `{step.tool_name}` with arguments: `{step.arguments}`")
78
+
79
+ skill_md.append("")
80
+ skill_md.append("## Verification Checklist")
81
+ skill_md.append("- [ ] Run verification pipeline to confirm all assertions pass.")
82
+
83
+ candidate = CandidateSkill(
84
+ name=clean_name,
85
+ description=f"Auto-mined skill for {task_description}",
86
+ trigger_pattern=task_description,
87
+ steps=steps,
88
+ parameter_names=sorted(list(param_candidates)),
89
+ skill_md_content="\n".join(skill_md),
90
+ )
91
+ return candidate
92
+
93
+ @classmethod
94
+ def export_skill_package(cls, candidate: CandidateSkill, base_output_dir: str) -> str:
95
+ """Write candidate skill into standard .agents/skills/<name>/ package."""
96
+ skill_dir = os.path.join(base_output_dir, candidate.name)
97
+ os.makedirs(skill_dir, exist_ok=True)
98
+ os.makedirs(os.path.join(skill_dir, "scripts"), exist_ok=True)
99
+ os.makedirs(os.path.join(skill_dir, "references"), exist_ok=True)
100
+
101
+ skill_md_path = os.path.join(skill_dir, "SKILL.md")
102
+ with open(skill_md_path, "w", encoding="utf-8") as f:
103
+ f.write(candidate.skill_md_content)
104
+
105
+ return skill_dir
106
+
coding_agents/py.typed ADDED
@@ -0,0 +1,2 @@
1
+ # Marker file for PEP 561 inline types
2
+
@@ -0,0 +1,7 @@
1
+ """Sandbox and policy governance modules."""
2
+
3
+ from coding_agents.sandbox.policy import PolicyGate
4
+ from coding_agents.sandbox.worktree import WorktreeSandbox
5
+
6
+ __all__ = ["PolicyGate", "WorktreeSandbox"]
7
+
@@ -0,0 +1,61 @@
1
+ """Policy governance gates and immutable test defenses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fnmatch
6
+ import os
7
+ from typing import List, Optional, Tuple
8
+
9
+
10
+ class PolicyGate:
11
+ """Enforces execution boundaries, command deny-lists, and test file immutability."""
12
+
13
+ DEFAULT_DENY_PATTERNS: List[str] = [
14
+ "rm -rf /*",
15
+ "rm -rf ~*",
16
+ ":(){ :|:& };:",
17
+ "mkfs*",
18
+ "dd if=*",
19
+ "chmod -R 777 /*",
20
+ ]
21
+
22
+ DEFAULT_IMMUTABLE_PATHS: List[str] = [
23
+ "tests/*",
24
+ "test/*",
25
+ "spec/*",
26
+ ".github/workflows/*",
27
+ ]
28
+
29
+ def __init__(
30
+ self,
31
+ deny_patterns: Optional[List[str]] = None,
32
+ immutable_paths: Optional[List[str]] = None,
33
+ lock_tests_during_repair: bool = True,
34
+ ) -> None:
35
+ self.deny_patterns = deny_patterns or self.DEFAULT_DENY_PATTERNS
36
+ self.immutable_paths = immutable_paths or self.DEFAULT_IMMUTABLE_PATHS
37
+ self.lock_tests_during_repair = lock_tests_during_repair
38
+
39
+ def check_command(self, cmd: str) -> Tuple[bool, Optional[str]]:
40
+ """Verify if a shell command violates security policies."""
41
+ cleaned = cmd.strip()
42
+ for pattern in self.deny_patterns:
43
+ if fnmatch.fnmatch(cleaned, pattern) or pattern in cleaned:
44
+ return False, f"Policy violation: Command '{cmd}' matches forbidden pattern '{pattern}'"
45
+ return True, None
46
+
47
+ def check_file_mutation(self, file_path: str) -> Tuple[bool, Optional[str]]:
48
+ """Verify if a file path is protected by test immutability rules."""
49
+ if not self.lock_tests_during_repair:
50
+ return True, None
51
+
52
+ normalized = file_path.replace("\\", "/").lstrip("./")
53
+ for pattern in self.immutable_paths:
54
+ if fnmatch.fnmatch(normalized, pattern) or fnmatch.fnmatch(os.path.basename(normalized), pattern):
55
+ return (
56
+ False,
57
+ f"Policy violation: File '{file_path}' is protected by immutable test boundary. "
58
+ "You cannot edit test assertions during a bug-fix repair loop.",
59
+ )
60
+ return True, None
61
+
@@ -0,0 +1,61 @@
1
+ """Ephemeral Git worktree sandboxing for safe workspace isolation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import uuid
9
+ from typing import Optional
10
+
11
+
12
+ class WorktreeSandbox:
13
+ """Manages ephemeral git worktree checkouts for isolated agent execution."""
14
+
15
+ def __init__(self, repo_dir: str = ".", base_ref: str = "HEAD") -> None:
16
+ self.repo_dir = os.path.abspath(repo_dir)
17
+ self.base_ref = base_ref
18
+ self.worktree_dir: Optional[str] = None
19
+ self.branch_name: Optional[str] = None
20
+
21
+ def __enter__(self) -> WorktreeSandbox:
22
+ self.setup()
23
+ return self
24
+
25
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
26
+ self.cleanup()
27
+
28
+ def setup(self) -> str:
29
+ """Create an ephemeral worktree in a temporary directory."""
30
+ unique_id = uuid.uuid4().hex[:8]
31
+ self.branch_name = f"agent-sandbox-{unique_id}"
32
+ self.worktree_dir = os.path.join(self.repo_dir, ".worktrees", self.branch_name)
33
+
34
+ os.makedirs(os.path.dirname(self.worktree_dir), exist_ok=True)
35
+
36
+ cmd = [
37
+ "git", "worktree", "add", "-b", self.branch_name,
38
+ self.worktree_dir, self.base_ref
39
+ ]
40
+ subprocess.run(cmd, cwd=self.repo_dir, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
41
+ return self.worktree_dir
42
+
43
+ def cleanup(self) -> None:
44
+ """Remove worktree and delete the ephemeral branch."""
45
+ if self.worktree_dir and os.path.exists(self.worktree_dir):
46
+ subprocess.run(
47
+ ["git", "worktree", "remove", "--force", self.worktree_dir],
48
+ cwd=self.repo_dir,
49
+ stdout=subprocess.PIPE,
50
+ stderr=subprocess.PIPE,
51
+ )
52
+ shutil.rmtree(self.worktree_dir, ignore_errors=True)
53
+
54
+ if self.branch_name:
55
+ subprocess.run(
56
+ ["git", "branch", "-D", self.branch_name],
57
+ cwd=self.repo_dir,
58
+ stdout=subprocess.PIPE,
59
+ stderr=subprocess.PIPE,
60
+ )
61
+
@@ -0,0 +1,8 @@
1
+ """Tool interfaces and reference implementations."""
2
+
3
+ from coding_agents.tools.base import BaseTool
4
+ from coding_agents.tools.patch_editor import PatchEditor
5
+ from coding_agents.tools.bash_tool import BashTool
6
+
7
+ __all__ = ["BaseTool", "PatchEditor", "BashTool"]
8
+
@@ -0,0 +1,55 @@
1
+ """Base protocol and interfaces for tools and skills."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any, Dict
7
+ from pydantic import BaseModel
8
+ from coding_agents.core.models import ToolResult
9
+
10
+
11
+ class BaseTool(ABC):
12
+ """Abstract base class for all agent tools."""
13
+
14
+ @property
15
+ @abstractmethod
16
+ def name(self) -> str:
17
+ """Tool name presented to the model."""
18
+ pass
19
+
20
+ @property
21
+ @abstractmethod
22
+ def description(self) -> str:
23
+ """Instructional schema docstring for the model."""
24
+ pass
25
+
26
+ @property
27
+ @abstractmethod
28
+ def parameters_schema(self) -> Dict[str, Any]:
29
+ """JSON schema defining accepted parameters."""
30
+ pass
31
+
32
+ @abstractmethod
33
+ def execute(self, **kwargs: Any) -> ToolResult:
34
+ """Execute the tool in the environment and return structured result."""
35
+ pass
36
+
37
+ def to_openai_schema(self) -> Dict[str, Any]:
38
+ """Format as standard OpenAI function definition."""
39
+ return {
40
+ "type": "function",
41
+ "function": {
42
+ "name": self.name,
43
+ "description": self.description,
44
+ "parameters": self.parameters_schema,
45
+ },
46
+ }
47
+
48
+ def to_anthropic_schema(self) -> Dict[str, Any]:
49
+ """Format as Anthropic tool definition."""
50
+ return {
51
+ "name": self.name,
52
+ "description": self.description,
53
+ "input_schema": self.parameters_schema,
54
+ }
55
+
@@ -0,0 +1,103 @@
1
+ """Safe subprocess execution tool with output truncation and timeouts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ from typing import Any, Dict, Optional
8
+ from coding_agents.tools.base import BaseTool
9
+ from coding_agents.core.models import ToolResult
10
+
11
+
12
+ class BashTool(BaseTool):
13
+ """Subprocess execution tool bounded by timeout and token/byte caps."""
14
+
15
+ def __init__(
16
+ self,
17
+ working_dir: str = ".",
18
+ timeout_seconds: int = 60,
19
+ max_output_bytes: int = 32_000,
20
+ ) -> None:
21
+ self.working_dir = os.path.abspath(working_dir)
22
+ self.timeout_seconds = timeout_seconds
23
+ self.max_output_bytes = max_output_bytes
24
+
25
+ @property
26
+ def name(self) -> str:
27
+ return "bash"
28
+
29
+ @property
30
+ def description(self) -> str:
31
+ return (
32
+ "Run a command in the bash shell. "
33
+ "Returns stdout, stderr, and the process exit code. "
34
+ "Output is automatically truncated if it exceeds maximum byte limits."
35
+ )
36
+
37
+ @property
38
+ def parameters_schema(self) -> Dict[str, Any]:
39
+ return {
40
+ "type": "object",
41
+ "properties": {
42
+ "command": {
43
+ "type": "string",
44
+ "description": "The exact shell command line to run",
45
+ },
46
+ },
47
+ "required": ["command"],
48
+ }
49
+
50
+ def execute(self, command: str, **kwargs: Any) -> ToolResult:
51
+ try:
52
+ proc = subprocess.run(
53
+ command,
54
+ shell=True,
55
+ cwd=self.working_dir,
56
+ stdout=subprocess.PIPE,
57
+ stderr=subprocess.PIPE,
58
+ text=True,
59
+ timeout=self.timeout_seconds,
60
+ )
61
+ stdout = proc.stdout
62
+ stderr = proc.stderr
63
+ exit_code = proc.returncode
64
+ except subprocess.TimeoutExpired:
65
+ return ToolResult(
66
+ name=self.name,
67
+ output="",
68
+ error=f"Error: Command timed out after {self.timeout_seconds} seconds.",
69
+ exit_code=124,
70
+ )
71
+ except Exception as e:
72
+ return ToolResult(
73
+ name=self.name,
74
+ output="",
75
+ error=f"Error executing command: {str(e)}",
76
+ exit_code=1,
77
+ )
78
+
79
+ combined = stdout
80
+ if stderr:
81
+ combined += f"\n[stderr]\n{stderr}"
82
+
83
+ truncated = False
84
+ if len(combined.encode("utf-8")) > self.max_output_bytes:
85
+ # Keep first 20% and last 80% of allowed budget
86
+ head_bytes = int(self.max_output_bytes * 0.2)
87
+ tail_bytes = int(self.max_output_bytes * 0.8)
88
+ combined_bytes = combined.encode("utf-8", errors="replace")
89
+ combined = (
90
+ combined_bytes[:head_bytes].decode("utf-8", errors="replace")
91
+ + f"\n\n... [Truncated {len(combined_bytes) - self.max_output_bytes} bytes] ...\n\n"
92
+ + combined_bytes[-tail_bytes:].decode("utf-8", errors="replace")
93
+ )
94
+ truncated = True
95
+
96
+ return ToolResult(
97
+ name=self.name,
98
+ output=combined,
99
+ error=stderr if exit_code != 0 else None,
100
+ exit_code=exit_code,
101
+ truncated=truncated,
102
+ )
103
+
@@ -0,0 +1,111 @@
1
+ """Exact-match string replacement tool with uniqueness collision protection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any, Dict
7
+ from coding_agents.tools.base import BaseTool
8
+ from coding_agents.core.models import ToolResult
9
+
10
+
11
+ class PatchEditor(BaseTool):
12
+ """File editing tool using strict exact-match substring replacement."""
13
+
14
+ def __init__(self, working_dir: str = ".") -> None:
15
+ self.working_dir = os.path.abspath(working_dir)
16
+
17
+ @property
18
+ def name(self) -> str:
19
+ return "edit_file_exact"
20
+
21
+ @property
22
+ def description(self) -> str:
23
+ return (
24
+ "Replace an exact substring in a file with new content. "
25
+ "The target string must be unique in the file to prevent ambiguous edits. "
26
+ "Include sufficient surrounding context lines to ensure uniqueness."
27
+ )
28
+
29
+ @property
30
+ def parameters_schema(self) -> Dict[str, Any]:
31
+ return {
32
+ "type": "object",
33
+ "properties": {
34
+ "path": {
35
+ "type": "string",
36
+ "description": "Relative or absolute path to the file to edit",
37
+ },
38
+ "old_str": {
39
+ "type": "string",
40
+ "description": "Exact substring to replace. Must occur exactly once in the file.",
41
+ },
42
+ "new_str": {
43
+ "type": "string",
44
+ "description": "Replacement string.",
45
+ },
46
+ },
47
+ "required": ["path", "old_str", "new_str"],
48
+ }
49
+
50
+ def execute(self, path: str, old_str: str, new_str: str, **kwargs: Any) -> ToolResult:
51
+ full_path = os.path.join(self.working_dir, path) if not os.path.isabs(path) else path
52
+
53
+ if not os.path.exists(full_path):
54
+ return ToolResult(
55
+ name=self.name,
56
+ output="",
57
+ error=f"Error: File '{path}' does not exist.",
58
+ exit_code=1,
59
+ )
60
+
61
+ try:
62
+ with open(full_path, "r", encoding="utf-8") as f:
63
+ content = f.read()
64
+ except Exception as e:
65
+ return ToolResult(
66
+ name=self.name,
67
+ output="",
68
+ error=f"Error reading '{path}': {str(e)}",
69
+ exit_code=1,
70
+ )
71
+
72
+ occurrences = content.count(old_str)
73
+ if occurrences == 0:
74
+ return ToolResult(
75
+ name=self.name,
76
+ output="",
77
+ error=(
78
+ f"Error: 'old_str' not found in '{path}'. "
79
+ "Make sure whitespace and indentation match exactly."
80
+ ),
81
+ exit_code=1,
82
+ )
83
+ elif occurrences > 1:
84
+ return ToolResult(
85
+ name=self.name,
86
+ output="",
87
+ error=(
88
+ f"Error: 'old_str' found {occurrences} times in '{path}'. "
89
+ "Ambiguous edit rejected. Add more surrounding context to disambiguate."
90
+ ),
91
+ exit_code=1,
92
+ )
93
+
94
+ new_content = content.replace(old_str, new_str, 1)
95
+ try:
96
+ with open(full_path, "w", encoding="utf-8") as f:
97
+ f.write(new_content)
98
+ except Exception as e:
99
+ return ToolResult(
100
+ name=self.name,
101
+ output="",
102
+ error=f"Error writing '{path}': {str(e)}",
103
+ exit_code=1,
104
+ )
105
+
106
+ return ToolResult(
107
+ name=self.name,
108
+ output=f"Successfully edited '{path}'. Replaced 1 occurrence.",
109
+ exit_code=0,
110
+ )
111
+
@@ -0,0 +1,15 @@
1
+ """Verification pipelines, fault localization, and cycle detection."""
2
+
3
+ from coding_agents.verification.pipeline import VerificationPipeline, VerificationManifest, StageResult
4
+ from coding_agents.verification.fault_localizer import FaultLocalizer, DiagnosticFrame
5
+ from coding_agents.verification.oscillation import OscillationDetector
6
+
7
+ __all__ = [
8
+ "VerificationPipeline",
9
+ "VerificationManifest",
10
+ "StageResult",
11
+ "FaultLocalizer",
12
+ "DiagnosticFrame",
13
+ "OscillationDetector",
14
+ ]
15
+
@@ -0,0 +1,51 @@
1
+ """Structured diagnostic fault extraction from test and compiler outputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import List, Optional
7
+ from pydantic import BaseModel
8
+
9
+
10
+ class DiagnosticFrame(BaseModel):
11
+ """A localized error frame."""
12
+ file_path: str
13
+ line_number: Optional[int] = None
14
+ error_type: str
15
+ message: str
16
+ raw_snippet: Optional[str] = None
17
+
18
+
19
+ class FaultLocalizer:
20
+ """Extracts compact, actionable error records from raw compiler and test runner output."""
21
+
22
+ @classmethod
23
+ def parse_pytest_output(cls, output: str) -> List[DiagnosticFrame]:
24
+ frames: List[DiagnosticFrame] = []
25
+
26
+ # Match pytest failure headers: FAILURES / ERRORS
27
+ failure_pattern = re.compile(r"_{3,}\s*(.*?)\s*_{3,}\n(.*?)(?=\n_{3,}|\n={3,}|\Z)", re.DOTALL)
28
+ for match in failure_pattern.finditer(output):
29
+ test_name = match.group(1).strip()
30
+ trace_body = match.group(2).strip()
31
+
32
+ # Locate file and line: File "...", line 123, in ...
33
+ file_match = re.search(r'File "([^"]+)", line (\d+), in (\w+)', trace_body)
34
+ # Locate exception line
35
+ exc_match = re.search(r"(\w+Error|\w+Exception|AssertionError):\s*(.*)", trace_body)
36
+
37
+ file_path = file_match.group(1) if file_match else test_name
38
+ line_no = int(file_match.group(2)) if file_match else None
39
+ error_type = exc_match.group(1) if exc_match else "AssertionFailure"
40
+ msg = exc_match.group(2).strip() if exc_match else "Test assertion failed"
41
+
42
+ frames.append(DiagnosticFrame(
43
+ file_path=file_path,
44
+ line_number=line_no,
45
+ error_type=error_type,
46
+ message=f"[{test_name}] {error_type}: {msg}",
47
+ raw_snippet=trace_body[-300:] if len(trace_body) > 300 else trace_body,
48
+ ))
49
+
50
+ return frames
51
+
@@ -0,0 +1,46 @@
1
+ """Cryptographic AST and diff state hashing for cycle detection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import hashlib
7
+ from typing import List, Optional, Set
8
+
9
+
10
+ class OscillationDetector:
11
+ """Detects cyclic repair states where an agent alternates between identical code mutations."""
12
+
13
+ def __init__(self) -> None:
14
+ self.seen_state_hashes: Set[str] = set()
15
+ self.hash_history: List[str] = []
16
+
17
+ @staticmethod
18
+ def hash_diff(diff_text: str) -> str:
19
+ """Compute normalized cryptographic hash of candidate diff."""
20
+ normalized = "\n".join(line.rstrip() for line in diff_text.strip().splitlines() if line.strip())
21
+ return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
22
+
23
+ @staticmethod
24
+ def hash_python_ast(source_code: str) -> Optional[str]:
25
+ """Compute structural AST hash invariant to whitespace and formatting changes."""
26
+ try:
27
+ tree = ast.parse(source_code)
28
+ dumped = ast.dump(tree, annotate_fields=False, include_attributes=False)
29
+ return hashlib.sha256(dumped.encode("utf-8")).hexdigest()
30
+ except SyntaxError:
31
+ return None
32
+
33
+ def record_and_check(self, diff_or_code: str, is_ast: bool = False) -> bool:
34
+ """Return True if the current state was already visited in this session."""
35
+ if is_ast:
36
+ state_hash = self.hash_python_ast(diff_or_code) or self.hash_diff(diff_or_code)
37
+ else:
38
+ state_hash = self.hash_diff(diff_or_code)
39
+
40
+ if state_hash in self.seen_state_hashes:
41
+ return True
42
+
43
+ self.seen_state_hashes.add(state_hash)
44
+ self.hash_history.append(state_hash)
45
+ return False
46
+