codemate-team 0.1.0__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ABHINAV UNNIKRISHNAN
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: codemate-team
3
+ Version: 0.1.0
4
+ Summary: A deterministic supervisor that runs Claude Code and Codex CLI through a Plan -> Implement -> Review -> Test workflow
5
+ Author: Abhinav
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/abhinav-18max/codemate
8
+ Project-URL: Repository, https://github.com/abhinav-18max/codemate
9
+ Project-URL: Issues, https://github.com/abhinav-18max/codemate/issues
10
+ Keywords: ai,agents,claude,codex,cli,developer-tools,automation,code-review
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Build Tools
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.12
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: license-file
25
+
26
+ # codemate
27
+
28
+ `codemate` is a sequential AI development team runner.
29
+
30
+ It does not replace Codex CLI or Claude Code. It supervises them:
31
+
32
+ ```text
33
+ Plan -> Implement -> Review -> Test
34
+ ```
35
+
36
+ The CLI owns sequencing, git state, run artifacts, local test execution, and
37
+ policy checks. Agent CLIs only receive one bounded role at a time.
38
+
39
+ ## Install
40
+
41
+ The PyPI distribution is `codemate-team`; it installs a `codemate` command.
42
+
43
+ As a global CLI tool (recommended):
44
+
45
+ ```bash
46
+ uv tool install codemate-team
47
+ # or: pipx install codemate-team
48
+ codemate --help
49
+ ```
50
+
51
+ Run once without installing:
52
+
53
+ ```bash
54
+ uvx --from codemate-team codemate --help
55
+ ```
56
+
57
+ For local development from a checkout:
58
+
59
+ ```bash
60
+ uv run --with-editable . codemate --help
61
+ ```
62
+
63
+ ## Quick Start
64
+
65
+ Initialize a repo:
66
+
67
+ ```bash
68
+ codemate init
69
+ ```
70
+
71
+ Review and edit `team.yml`, then check local prerequisites:
72
+
73
+ ```bash
74
+ codemate doctor
75
+ ```
76
+
77
+ Run a task:
78
+
79
+ ```bash
80
+ codemate run "Fix the flaky checkout test"
81
+ ```
82
+
83
+ Inspect results:
84
+
85
+ ```bash
86
+ codemate status
87
+ codemate logs --step implement
88
+ codemate diff
89
+ ```
90
+
91
+ Accept or reset:
92
+
93
+ ```bash
94
+ codemate accept --commit --message "Fix flaky checkout test"
95
+ codemate reset
96
+ ```
97
+
98
+ ## Generated Project Files
99
+
100
+ `codemate init` creates:
101
+
102
+ ```text
103
+ team.yml
104
+ .team/
105
+ prompts/
106
+ schemas/
107
+ runs/
108
+ docs/
109
+ team.md
110
+ ```
111
+
112
+ `.team/runs/` and `.team/lock.json` are ignored so run artifacts do not pollute
113
+ normal source control status.
114
+
115
+ ## Safety Model
116
+
117
+ - The default config requires a clean worktree before a run starts.
118
+ - A run uses a branch under `ai/team/<run-id>`.
119
+ - Agent writes are checked against `policies.allow_paths` and
120
+ `policies.deny_paths`.
121
+ - Local command output is recorded by the CLI.
122
+ - Agent claims about tests are ignored; configured commands are the authority.
123
+ - Fix loops are bounded by `limits.max_fix_retries`.
124
+ - The CLI never pushes or merges by default.
125
+
126
+ ## Documentation
127
+
128
+ - [Overview](docs/README.md)
129
+ - [Architecture](docs/architecture.md)
130
+ - [Commands](docs/commands.md)
131
+ - [Configuration](docs/configuration.md)
132
+ - [Safety](docs/safety.md)
@@ -0,0 +1,107 @@
1
+ # codemate
2
+
3
+ `codemate` is a sequential AI development team runner.
4
+
5
+ It does not replace Codex CLI or Claude Code. It supervises them:
6
+
7
+ ```text
8
+ Plan -> Implement -> Review -> Test
9
+ ```
10
+
11
+ The CLI owns sequencing, git state, run artifacts, local test execution, and
12
+ policy checks. Agent CLIs only receive one bounded role at a time.
13
+
14
+ ## Install
15
+
16
+ The PyPI distribution is `codemate-team`; it installs a `codemate` command.
17
+
18
+ As a global CLI tool (recommended):
19
+
20
+ ```bash
21
+ uv tool install codemate-team
22
+ # or: pipx install codemate-team
23
+ codemate --help
24
+ ```
25
+
26
+ Run once without installing:
27
+
28
+ ```bash
29
+ uvx --from codemate-team codemate --help
30
+ ```
31
+
32
+ For local development from a checkout:
33
+
34
+ ```bash
35
+ uv run --with-editable . codemate --help
36
+ ```
37
+
38
+ ## Quick Start
39
+
40
+ Initialize a repo:
41
+
42
+ ```bash
43
+ codemate init
44
+ ```
45
+
46
+ Review and edit `team.yml`, then check local prerequisites:
47
+
48
+ ```bash
49
+ codemate doctor
50
+ ```
51
+
52
+ Run a task:
53
+
54
+ ```bash
55
+ codemate run "Fix the flaky checkout test"
56
+ ```
57
+
58
+ Inspect results:
59
+
60
+ ```bash
61
+ codemate status
62
+ codemate logs --step implement
63
+ codemate diff
64
+ ```
65
+
66
+ Accept or reset:
67
+
68
+ ```bash
69
+ codemate accept --commit --message "Fix flaky checkout test"
70
+ codemate reset
71
+ ```
72
+
73
+ ## Generated Project Files
74
+
75
+ `codemate init` creates:
76
+
77
+ ```text
78
+ team.yml
79
+ .team/
80
+ prompts/
81
+ schemas/
82
+ runs/
83
+ docs/
84
+ team.md
85
+ ```
86
+
87
+ `.team/runs/` and `.team/lock.json` are ignored so run artifacts do not pollute
88
+ normal source control status.
89
+
90
+ ## Safety Model
91
+
92
+ - The default config requires a clean worktree before a run starts.
93
+ - A run uses a branch under `ai/team/<run-id>`.
94
+ - Agent writes are checked against `policies.allow_paths` and
95
+ `policies.deny_paths`.
96
+ - Local command output is recorded by the CLI.
97
+ - Agent claims about tests are ignored; configured commands are the authority.
98
+ - Fix loops are bounded by `limits.max_fix_retries`.
99
+ - The CLI never pushes or merges by default.
100
+
101
+ ## Documentation
102
+
103
+ - [Overview](docs/README.md)
104
+ - [Architecture](docs/architecture.md)
105
+ - [Commands](docs/commands.md)
106
+ - [Configuration](docs/configuration.md)
107
+ - [Safety](docs/safety.md)
@@ -0,0 +1,3 @@
1
+ """Codemate sequential AI team runner."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ import subprocess
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class AgentRunInput:
13
+ run_id: str
14
+ step_id: str
15
+ cwd: Path
16
+ prompt: str
17
+ mode: str
18
+ expected_output: str
19
+ output_path: Path
20
+ raw_log_path: Path
21
+ schema_path: Path | None
22
+ config: dict[str, Any]
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class AgentRunResult:
27
+ ok: bool
28
+ output_path: Path
29
+ raw_log_path: Path
30
+ exit_code: int
31
+
32
+
33
+ class AgentAdapter:
34
+ def run(self, input: AgentRunInput) -> AgentRunResult:
35
+ raise NotImplementedError
36
+
37
+
38
+ class CodexCliAdapter(AgentAdapter):
39
+ def run(self, input: AgentRunInput) -> AgentRunResult:
40
+ command = str(input.config.get("command", "codex"))
41
+ _require_executable(command)
42
+ args = [
43
+ command,
44
+ "exec",
45
+ "--cd",
46
+ str(input.cwd),
47
+ "--sandbox",
48
+ str(input.config.get("sandbox", "workspace-write")),
49
+ "--ask-for-approval",
50
+ str(input.config.get("approval", "never")),
51
+ "--output-last-message",
52
+ str(input.output_path),
53
+ ]
54
+ if input.schema_path and input.schema_path.exists():
55
+ args.extend(["--output-schema", str(input.schema_path)])
56
+ args.append(input.prompt)
57
+ return _run_process(args, input)
58
+
59
+
60
+ class ClaudeCodeAdapter(AgentAdapter):
61
+ def run(self, input: AgentRunInput) -> AgentRunResult:
62
+ command = str(input.config.get("command", "claude"))
63
+ _require_executable(command)
64
+ permission_mode = _claude_permission_mode(input.mode, input.config)
65
+ output_format = str(input.config.get("output_format", "text"))
66
+ args = [command, "-p", input.prompt, "--permission-mode", permission_mode]
67
+ if output_format != "text":
68
+ args.extend(["--output-format", output_format])
69
+ result = _run_process(args, input)
70
+ # Claude Code streams its final answer to stdout (captured in the raw log)
71
+ # rather than to a dedicated output file, so derive the output ourselves.
72
+ if not input.output_path.read_text(errors="replace").strip():
73
+ raw = input.raw_log_path.read_text(errors="replace")
74
+ input.output_path.write_text(_extract_claude_message(raw, output_format))
75
+ return result
76
+
77
+
78
+ def adapter_for(config: dict[str, Any]) -> AgentAdapter:
79
+ provider = str(config.get("provider", "")).lower()
80
+ if provider == "codex-cli":
81
+ return CodexCliAdapter()
82
+ if provider == "claude-code":
83
+ return ClaudeCodeAdapter()
84
+ raise ValueError(f"Unsupported agent provider: {provider}")
85
+
86
+
87
+ def _extract_claude_message(raw: str, output_format: str) -> str:
88
+ """Pull the assistant's final message out of captured Claude Code output.
89
+
90
+ With `--output-format json` the CLI emits a result envelope; extract its
91
+ `result` field. Anything else (including malformed JSON) falls back to the
92
+ raw captured text so no output is ever silently dropped.
93
+ """
94
+ if output_format == "json":
95
+ try:
96
+ data = json.loads(raw.strip() or "{}")
97
+ except json.JSONDecodeError:
98
+ return raw
99
+ if isinstance(data, dict) and isinstance(data.get("result"), str):
100
+ return data["result"]
101
+ return raw
102
+
103
+
104
+ def _claude_permission_mode(mode: str, config: dict[str, Any]) -> str:
105
+ if mode in {"read_only", "review_only"}:
106
+ return "plan"
107
+ if mode == "write":
108
+ return str(config.get("write_permission_mode", "acceptEdits"))
109
+ return str(config.get("default_mode", "plan"))
110
+
111
+
112
+ def _run_process(args: list[str], input: AgentRunInput) -> AgentRunResult:
113
+ timeout = int(input.config.get("timeout_seconds", 900))
114
+ with input.raw_log_path.open("w") as raw_log:
115
+ try:
116
+ result = subprocess.run(
117
+ args,
118
+ cwd=input.cwd,
119
+ text=True,
120
+ stdout=raw_log,
121
+ stderr=subprocess.STDOUT,
122
+ timeout=timeout,
123
+ check=False,
124
+ )
125
+ exit_code = result.returncode
126
+ except subprocess.TimeoutExpired:
127
+ raw_log.write(f"\n[timeout after {timeout} seconds]\n")
128
+ exit_code = 124
129
+ if not input.output_path.exists():
130
+ input.output_path.write_text("")
131
+ return AgentRunResult(
132
+ ok=exit_code == 0,
133
+ output_path=input.output_path,
134
+ raw_log_path=input.raw_log_path,
135
+ exit_code=exit_code,
136
+ )
137
+
138
+
139
+ def _require_executable(command: str) -> None:
140
+ if shutil.which(command) is None:
141
+ raise FileNotFoundError(f"Required agent command not found: {command}")
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ def new_run_id() -> str:
12
+ return datetime.now().strftime("%Y-%m-%dT%H-%M-%S-%f")
13
+
14
+
15
+ def latest_run_dir(root: Path) -> Path:
16
+ runs = root / ".team" / "runs"
17
+ candidates = sorted([path for path in runs.glob("*") if path.is_dir()])
18
+ if not candidates:
19
+ raise FileNotFoundError("No runs found")
20
+ return candidates[-1]
21
+
22
+
23
+ @dataclass
24
+ class RunState:
25
+ run_id: str
26
+ task: str
27
+ flow: str
28
+ status: str
29
+ branch: str
30
+ base_branch: str
31
+ base_head: str
32
+ current_step: str | None = None
33
+ steps: list[dict[str, Any]] = field(default_factory=list)
34
+ changed_files: list[str] = field(default_factory=list)
35
+ reason: str | None = None
36
+
37
+ def to_dict(self) -> dict[str, Any]:
38
+ return {
39
+ "run_id": self.run_id,
40
+ "task": self.task,
41
+ "flow": self.flow,
42
+ "status": self.status,
43
+ "branch": self.branch,
44
+ "base_branch": self.base_branch,
45
+ "base_head": self.base_head,
46
+ "current_step": self.current_step,
47
+ "steps": self.steps,
48
+ "changed_files": self.changed_files,
49
+ "reason": self.reason,
50
+ }
51
+
52
+ @classmethod
53
+ def from_path(cls, path: Path) -> "RunState":
54
+ data = json.loads(path.read_text())
55
+ return cls(**data)
56
+
57
+
58
+ class RunArtifacts:
59
+ def __init__(self, root: Path, run_id: str) -> None:
60
+ self.root = root
61
+ self.run_id = run_id
62
+ self.dir = root / ".team" / "runs" / run_id
63
+ self.dir.mkdir(parents=True, exist_ok=True)
64
+
65
+ @property
66
+ def state_path(self) -> Path:
67
+ return self.dir / "state.json"
68
+
69
+ def write_state(self, state: RunState) -> None:
70
+ self.state_path.write_text(json.dumps(state.to_dict(), indent=2) + "\n")
71
+
72
+ def write_text(self, name: str, content: str) -> Path:
73
+ path = self.dir / name
74
+ path.write_text(content)
75
+ return path
76
+
77
+ def read_text(self, name: str, default: str = "") -> str:
78
+ path = self.dir / name
79
+ return path.read_text() if path.exists() else default
80
+
81
+
82
+ class RunLock:
83
+ def __init__(self, root: Path, run_id: str) -> None:
84
+ self.path = root / ".team" / "lock.json"
85
+ self.run_id = run_id
86
+
87
+ def __enter__(self) -> "RunLock":
88
+ if self.path.exists():
89
+ if self._is_stale():
90
+ self.path.unlink()
91
+ else:
92
+ raise RuntimeError(f"Another codemate run appears active: {self.path}")
93
+ self.path.parent.mkdir(parents=True, exist_ok=True)
94
+ self.path.write_text(
95
+ json.dumps(
96
+ {
97
+ "run_id": self.run_id,
98
+ "pid": os.getpid(),
99
+ "created_at": datetime.now().isoformat(timespec="seconds"),
100
+ },
101
+ indent=2,
102
+ )
103
+ + "\n"
104
+ )
105
+ return self
106
+
107
+ def _is_stale(self) -> bool:
108
+ try:
109
+ data = json.loads(self.path.read_text())
110
+ except (OSError, json.JSONDecodeError):
111
+ return False
112
+ pid = data.get("pid")
113
+ if not isinstance(pid, int) or pid <= 0:
114
+ return False
115
+ try:
116
+ os.kill(pid, 0)
117
+ except ProcessLookupError:
118
+ return True
119
+ except PermissionError:
120
+ return False
121
+ return False
122
+
123
+ def __exit__(self, _exc_type: object, _exc: object, _tb: object) -> None:
124
+ if self.path.exists():
125
+ self.path.unlink()