kaizen-loop 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.
- kaizen/__init__.py +1 -0
- kaizen/__main__.py +4 -0
- kaizen/agent.py +349 -0
- kaizen/cli.py +92 -0
- kaizen/config.py +31 -0
- kaizen/findings.py +53 -0
- kaizen/git.py +208 -0
- kaizen/loop.py +248 -0
- kaizen/orchestrator.py +159 -0
- kaizen/review_prompt.py +66 -0
- kaizen/run.py +93 -0
- kaizen/steps/__init__.py +11 -0
- kaizen/steps/pr.py +96 -0
- kaizen/steps/push.py +23 -0
- kaizen/steps/review.py +55 -0
- kaizen/work_prompt.py +43 -0
- kaizen_loop-0.1.0.dist-info/METADATA +10 -0
- kaizen_loop-0.1.0.dist-info/RECORD +21 -0
- kaizen_loop-0.1.0.dist-info/WHEEL +4 -0
- kaizen_loop-0.1.0.dist-info/entry_points.txt +2 -0
- kaizen_loop-0.1.0.dist-info/licenses/LICENSE +21 -0
kaizen/run.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import uuid
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class RunInfo:
|
|
9
|
+
run_id: str
|
|
10
|
+
run_dir: str
|
|
11
|
+
prompt: str
|
|
12
|
+
branch: str
|
|
13
|
+
base_commit: str
|
|
14
|
+
head_commit: str
|
|
15
|
+
worktree_path: str | None = None
|
|
16
|
+
repo_cwd: str | None = None
|
|
17
|
+
pr_url: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def runs_home() -> str:
|
|
21
|
+
return os.path.expanduser("~/.kaizen/runs")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def setup_run(
|
|
25
|
+
prompt: str,
|
|
26
|
+
branch: str,
|
|
27
|
+
base_commit: str,
|
|
28
|
+
head_commit: str,
|
|
29
|
+
worktree_path: str | None = None,
|
|
30
|
+
repo_cwd: str | None = None,
|
|
31
|
+
) -> RunInfo:
|
|
32
|
+
run_id = uuid.uuid4().hex[:8]
|
|
33
|
+
run_dir = os.path.join(runs_home(), run_id)
|
|
34
|
+
os.makedirs(run_dir, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
Path(os.path.join(run_dir, "prompt.md")).write_text(prompt + "\n")
|
|
37
|
+
Path(os.path.join(run_dir, "branch")).write_text(branch + "\n")
|
|
38
|
+
Path(os.path.join(run_dir, "base-commit")).write_text(base_commit + "\n")
|
|
39
|
+
Path(os.path.join(run_dir, "head-commit")).write_text(head_commit + "\n")
|
|
40
|
+
Path(os.path.join(run_dir, "status")).write_text("pending\n")
|
|
41
|
+
|
|
42
|
+
if worktree_path:
|
|
43
|
+
Path(os.path.join(run_dir, "worktree")).write_text(worktree_path + "\n")
|
|
44
|
+
if repo_cwd:
|
|
45
|
+
Path(os.path.join(run_dir, "repo-cwd")).write_text(repo_cwd + "\n")
|
|
46
|
+
|
|
47
|
+
notes_path = os.path.join(run_dir, "notes.md")
|
|
48
|
+
if not os.path.exists(notes_path):
|
|
49
|
+
Path(notes_path).write_text(
|
|
50
|
+
f"# kaizen run: {run_id}\n\n"
|
|
51
|
+
f"Objective: {prompt}\n\n"
|
|
52
|
+
"## Iteration Log\n"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return RunInfo(
|
|
56
|
+
run_id=run_id,
|
|
57
|
+
run_dir=run_dir,
|
|
58
|
+
prompt=prompt,
|
|
59
|
+
branch=branch,
|
|
60
|
+
base_commit=base_commit,
|
|
61
|
+
head_commit=head_commit,
|
|
62
|
+
worktree_path=worktree_path,
|
|
63
|
+
repo_cwd=repo_cwd,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def update_run_status(run_dir: str, status: str) -> None:
|
|
68
|
+
Path(os.path.join(run_dir, "status")).write_text(status + "\n")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def update_run_head(run_dir: str, head: str) -> None:
|
|
72
|
+
Path(os.path.join(run_dir, "head-commit")).write_text(head + "\n")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def update_run_pr_url(run_dir: str, pr_url: str) -> None:
|
|
76
|
+
Path(os.path.join(run_dir, "pr-url")).write_text(pr_url + "\n")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def append_notes(
|
|
80
|
+
notes_path: str, iteration: int, summary: str,
|
|
81
|
+
changes: list[str], learnings: list[str],
|
|
82
|
+
) -> None:
|
|
83
|
+
lines = [f"\n### Iteration {iteration}\n", f"**Summary:** {summary}\n"]
|
|
84
|
+
if changes:
|
|
85
|
+
lines.append("**Changes:**")
|
|
86
|
+
lines.extend(f"- {c}" for c in changes)
|
|
87
|
+
lines.append("")
|
|
88
|
+
if learnings:
|
|
89
|
+
lines.append("**Learnings:**")
|
|
90
|
+
lines.extend(f"- {item}" for item in learnings)
|
|
91
|
+
lines.append("")
|
|
92
|
+
with open(notes_path, "a") as f:
|
|
93
|
+
f.write("\n".join(lines))
|
kaizen/steps/__init__.py
ADDED
kaizen/steps/pr.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
from kaizen.findings import FindingsResult
|
|
7
|
+
from kaizen.steps import StepOutcome
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PRStep:
|
|
11
|
+
def name(self) -> str:
|
|
12
|
+
return "pr"
|
|
13
|
+
|
|
14
|
+
def execute(
|
|
15
|
+
self, work_dir: str, branch: str, base_branch: str,
|
|
16
|
+
findings: FindingsResult | None = None,
|
|
17
|
+
) -> StepOutcome:
|
|
18
|
+
title = f"{branch}: changes"
|
|
19
|
+
body = self._build_body(branch, findings)
|
|
20
|
+
|
|
21
|
+
existing = self._find_existing_pr(branch)
|
|
22
|
+
if existing:
|
|
23
|
+
print(f" updating PR: {existing}")
|
|
24
|
+
self._update_pr(existing, title, body)
|
|
25
|
+
return StepOutcome(pr_url=existing)
|
|
26
|
+
|
|
27
|
+
url = self._create_pr(branch, base_branch, title, body, work_dir)
|
|
28
|
+
if url:
|
|
29
|
+
print(f" PR created: {url}")
|
|
30
|
+
return StepOutcome(pr_url=url)
|
|
31
|
+
|
|
32
|
+
return StepOutcome(skipped=True)
|
|
33
|
+
|
|
34
|
+
def _build_body(self, branch: str, findings: FindingsResult | None) -> str:
|
|
35
|
+
lines = ["## What Changed", ""]
|
|
36
|
+
if findings and findings.summary:
|
|
37
|
+
lines.append(findings.summary)
|
|
38
|
+
else:
|
|
39
|
+
lines.append(f"Changes on branch `{branch}`.")
|
|
40
|
+
lines.append("")
|
|
41
|
+
if findings:
|
|
42
|
+
lines.append("## Risk Assessment")
|
|
43
|
+
lines.append("")
|
|
44
|
+
lines.append(f"**Risk:** {findings.risk_level}")
|
|
45
|
+
if findings.risk_rationale:
|
|
46
|
+
lines.append(findings.risk_rationale)
|
|
47
|
+
lines.append("")
|
|
48
|
+
lines.append("---")
|
|
49
|
+
lines.append("*Validated through kaizen pipeline*")
|
|
50
|
+
return "\n".join(lines)
|
|
51
|
+
|
|
52
|
+
def _find_existing_pr(self, branch: str) -> str:
|
|
53
|
+
try:
|
|
54
|
+
result = subprocess.run(
|
|
55
|
+
["gh", "pr", "list", "--head", branch, "--json", "url", "--limit", "1"],
|
|
56
|
+
capture_output=True, text=True, timeout=30,
|
|
57
|
+
)
|
|
58
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
59
|
+
prs = json.loads(result.stdout)
|
|
60
|
+
if prs:
|
|
61
|
+
return prs[0].get("url", "")
|
|
62
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
63
|
+
pass
|
|
64
|
+
return ""
|
|
65
|
+
|
|
66
|
+
def _create_pr(
|
|
67
|
+
self, branch: str, base_branch: str, title: str, body: str, work_dir: str,
|
|
68
|
+
) -> str:
|
|
69
|
+
try:
|
|
70
|
+
result = subprocess.run(
|
|
71
|
+
[
|
|
72
|
+
"gh", "pr", "create",
|
|
73
|
+
"--head", branch, "--base", base_branch,
|
|
74
|
+
"--title", title, "--body", body,
|
|
75
|
+
],
|
|
76
|
+
cwd=work_dir,
|
|
77
|
+
capture_output=True, text=True, timeout=30,
|
|
78
|
+
)
|
|
79
|
+
if result.returncode == 0:
|
|
80
|
+
for line in result.stdout.splitlines():
|
|
81
|
+
if "pull" in line.lower():
|
|
82
|
+
return line.strip()
|
|
83
|
+
return result.stdout.strip()
|
|
84
|
+
print(f" gh pr create failed: {result.stderr.strip()}")
|
|
85
|
+
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
|
86
|
+
print(f" gh not available: {e}")
|
|
87
|
+
return ""
|
|
88
|
+
|
|
89
|
+
def _update_pr(self, pr_url: str, title: str, body: str) -> None:
|
|
90
|
+
try:
|
|
91
|
+
subprocess.run(
|
|
92
|
+
["gh", "pr", "edit", pr_url, "--title", title, "--body", body],
|
|
93
|
+
capture_output=True, text=True, timeout=30,
|
|
94
|
+
)
|
|
95
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
96
|
+
pass
|
kaizen/steps/push.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from kaizen.steps import StepOutcome
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PushStep:
|
|
9
|
+
def name(self) -> str:
|
|
10
|
+
return "push"
|
|
11
|
+
|
|
12
|
+
def execute(self, work_dir: str, branch: str) -> StepOutcome:
|
|
13
|
+
from kaizen.git import force_push_with_lease
|
|
14
|
+
|
|
15
|
+
print(f" pushing {branch} to origin...")
|
|
16
|
+
try:
|
|
17
|
+
force_push_with_lease(work_dir, "origin", branch)
|
|
18
|
+
print(" pushed successfully")
|
|
19
|
+
except RuntimeError as e:
|
|
20
|
+
print(f" push failed: {e}", file=sys.stderr)
|
|
21
|
+
return StepOutcome(skipped=True)
|
|
22
|
+
|
|
23
|
+
return StepOutcome()
|
kaizen/steps/review.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from kaizen.agent import OpenCodeAgent
|
|
6
|
+
from kaizen.findings import parse_findings
|
|
7
|
+
from kaizen.review_prompt import REVIEW_SCHEMA, build_review_prompt
|
|
8
|
+
from kaizen.steps import StepOutcome
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReviewStep:
|
|
12
|
+
def name(self) -> str:
|
|
13
|
+
return "review"
|
|
14
|
+
|
|
15
|
+
def execute(
|
|
16
|
+
self, work_dir: str, base_commit: str, head_commit: str,
|
|
17
|
+
agent: OpenCodeAgent, intent: str = "", repo_dir: str | None = None,
|
|
18
|
+
) -> StepOutcome:
|
|
19
|
+
from kaizen.git import get_diff
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
diff = get_diff(base_commit, head_commit, work_dir)
|
|
23
|
+
except RuntimeError as e:
|
|
24
|
+
print(f" Could not get diff: {e}", file=sys.stderr)
|
|
25
|
+
return StepOutcome(skipped=True)
|
|
26
|
+
|
|
27
|
+
if not diff.strip():
|
|
28
|
+
print(" No diff found, skipping review")
|
|
29
|
+
return StepOutcome(skipped=True)
|
|
30
|
+
|
|
31
|
+
max_diff_size = 50000
|
|
32
|
+
if len(diff) > max_diff_size:
|
|
33
|
+
diff = diff[:max_diff_size] + "\n... (truncated)"
|
|
34
|
+
|
|
35
|
+
prompt = build_review_prompt(diff, intent=intent)
|
|
36
|
+
result = agent.run(prompt, work_dir, schema=REVIEW_SCHEMA, repo_dir=repo_dir)
|
|
37
|
+
|
|
38
|
+
findings = parse_findings(result.output)
|
|
39
|
+
|
|
40
|
+
if not findings.items:
|
|
41
|
+
print(f" Clean: {findings.summary}")
|
|
42
|
+
return StepOutcome(findings=findings)
|
|
43
|
+
|
|
44
|
+
print(f" Risk: {findings.risk_level} — {findings.risk_rationale}")
|
|
45
|
+
print(f" {findings.summary}\n")
|
|
46
|
+
print(f" {'ID':<6} {'SEV':<9} {'ACTION':<12} DESCRIPTION")
|
|
47
|
+
print(f" {'-' * 6} {'-' * 9} {'-' * 12} {'-' * 40}")
|
|
48
|
+
for f in findings.items:
|
|
49
|
+
desc = f.description[:60]
|
|
50
|
+
print(f" {f.id:<6} {f.severity:<9} {f.action:<12} {desc}")
|
|
51
|
+
print()
|
|
52
|
+
|
|
53
|
+
return StepOutcome(
|
|
54
|
+
findings=findings,
|
|
55
|
+
)
|
kaizen/work_prompt.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
def build_iteration_prompt(
|
|
2
|
+
n: int,
|
|
3
|
+
run_id: str,
|
|
4
|
+
prompt: str,
|
|
5
|
+
stop_when: str | None = None,
|
|
6
|
+
) -> str:
|
|
7
|
+
output_fields = [
|
|
8
|
+
"- success: true if you made a meaningful contribution. false means changes should be discarded",
|
|
9
|
+
"- summary: concise one-sentence summary of the accomplishment",
|
|
10
|
+
"- key_changes_made: array of descriptions of key changes",
|
|
11
|
+
"- key_learnings: array of new learnings informative for future iterations",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
if stop_when is not None:
|
|
15
|
+
output_fields.append(
|
|
16
|
+
"- should_fully_stop: set true ONLY when the stop condition is fully met"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
stop_section = ""
|
|
20
|
+
if stop_when is not None:
|
|
21
|
+
stop_section = (
|
|
22
|
+
"\n\n## Stop Condition\n\n"
|
|
23
|
+
f"The user configured: {stop_when}\n"
|
|
24
|
+
"If this condition is fully met, set should_fully_stop=true."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
fields_text = "\n".join(output_fields)
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
"You are working autonomously towards an objective.\n"
|
|
31
|
+
f"This is iteration {n}. Each iteration makes one incremental step.\n\n"
|
|
32
|
+
"## Instructions\n\n"
|
|
33
|
+
f"1. Read .kaizen/runs/{run_id}/notes.md to understand prior work. Do NOT modify notes.md\n"
|
|
34
|
+
"2. Identify the next smallest verifiable unit of work\n"
|
|
35
|
+
"3. If a solution didn't move the needle, document learnings and set success=false\n"
|
|
36
|
+
"4. If you made code changes, run build/tests/linters if available. Do NOT make git commits\n"
|
|
37
|
+
"5. Stop any background processes before finishing\n\n"
|
|
38
|
+
"## Output\n\n"
|
|
39
|
+
"When finished, the structured output tool will prompt you for these fields:\n"
|
|
40
|
+
f"{fields_text}\n"
|
|
41
|
+
f"{stop_section}\n\n"
|
|
42
|
+
f"## Objective\n\n{prompt}"
|
|
43
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kaizen-loop
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Continuous code improvement: autonomous work → review → fix → ship
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
10
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
kaizen/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
kaizen/__main__.py,sha256=nIfRxBDvGRUzGKsjJ3_g-IbMsLZGGNJbviPAcKmXtOs,67
|
|
3
|
+
kaizen/agent.py,sha256=YYgpvujk3nhCa8LxbM5fX3r0lPC-M01YtWUByWRBwc8,11080
|
|
4
|
+
kaizen/cli.py,sha256=NVw--X4tQh90Wd2ZMue0nArdMKXPrQkrr7uNYhnXnSY,2989
|
|
5
|
+
kaizen/config.py,sha256=Hb5SmctueA6PqLsJMjBgdSURQ_qBfBC5eOHNYe5ZW4w,761
|
|
6
|
+
kaizen/findings.py,sha256=GLsNdxx8z8KpCpsUOUGSwqYv7uwV7HKkI91lf5IW-3k,1369
|
|
7
|
+
kaizen/git.py,sha256=nwu7MigAVe_lAdK6xx1jQq1Cx5OC0I4uejpnUM7kBCM,6100
|
|
8
|
+
kaizen/loop.py,sha256=YzHFwcN4dlsQ_USpWKou-yJbu8o01vtH-sxygfusO8U,7456
|
|
9
|
+
kaizen/orchestrator.py,sha256=DgR-CYJZnqvKDq6RyQ_cf0_xr-qaAd5Lt2yEtouvBA8,6191
|
|
10
|
+
kaizen/review_prompt.py,sha256=4eiZbtDeks94q7aRbCu7LW__sKBjDUtKJlWV7EmbW-k,2572
|
|
11
|
+
kaizen/run.py,sha256=rZMB3PXhVW6YgVLcq5OK9tAWcACvM6lKD5oB3tYarF8,2675
|
|
12
|
+
kaizen/work_prompt.py,sha256=Umai3rYxWG566dQFuq4s8VmAMaavaeNjSGs1GJZSeGc,1758
|
|
13
|
+
kaizen/steps/__init__.py,sha256=H8bo43GmWfiLZMBJxSf62hRoBNaEFWXeGVD_FTS6NDI,233
|
|
14
|
+
kaizen/steps/pr.py,sha256=mWOdt_BCx_AP83eawJEI0M4ZZtRpv_HL0RnBR_CFLQ4,3391
|
|
15
|
+
kaizen/steps/push.py,sha256=BZYjK-7FtXXyVkD4x7OTGabH02LmkQEimMuc78jr0Fc,608
|
|
16
|
+
kaizen/steps/review.py,sha256=TY0cOxIH4CKOjqrDpwL2--pA90oZjNMdrI_-ajz5kV4,1816
|
|
17
|
+
kaizen_loop-0.1.0.dist-info/METADATA,sha256=TFLrj68Ty4b4Z97bd7dvT5lEDE1EuVNbWxYsefnYtHY,301
|
|
18
|
+
kaizen_loop-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
19
|
+
kaizen_loop-0.1.0.dist-info/entry_points.txt,sha256=JsUdOanDcNnxHkJbRYekf5rYHopnDOzeXsh1r9tcAIc,43
|
|
20
|
+
kaizen_loop-0.1.0.dist-info/licenses/LICENSE,sha256=ESYyLizI0WWtxMeS7rGVcX3ivMezm-HOd5WdeOh-9oU,1056
|
|
21
|
+
kaizen_loop-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
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.
|