kaizen-loop 0.1.0__tar.gz → 0.2.1__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.
Files changed (27) hide show
  1. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/.github/workflows/ci.yml +3 -0
  2. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/.github/workflows/release.yml +3 -0
  3. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/PKG-INFO +2 -1
  4. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/pyproject.toml +2 -1
  5. kaizen_loop-0.2.1/src/kaizen/__init__.py +1 -0
  6. kaizen_loop-0.2.1/src/kaizen/findings.py +44 -0
  7. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/src/kaizen/loop.py +2 -2
  8. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/src/kaizen/orchestrator.py +18 -12
  9. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/src/kaizen/review_prompt.py +22 -0
  10. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/src/kaizen/steps/__init__.py +2 -3
  11. kaizen_loop-0.1.0/src/kaizen/__init__.py +0 -1
  12. kaizen_loop-0.1.0/src/kaizen/findings.py +0 -53
  13. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/.gitignore +0 -0
  14. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/LICENSE +0 -0
  15. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/README.md +0 -0
  16. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/justfile +0 -0
  17. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/src/kaizen/__main__.py +0 -0
  18. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/src/kaizen/agent.py +0 -0
  19. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/src/kaizen/cli.py +0 -0
  20. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/src/kaizen/config.py +0 -0
  21. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/src/kaizen/git.py +0 -0
  22. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/src/kaizen/run.py +0 -0
  23. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/src/kaizen/steps/pr.py +0 -0
  24. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/src/kaizen/steps/push.py +0 -0
  25. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/src/kaizen/steps/review.py +0 -0
  26. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/src/kaizen/work_prompt.py +0 -0
  27. {kaizen_loop-0.1.0 → kaizen_loop-0.2.1}/tests/test_import.py +0 -0
@@ -6,6 +6,9 @@ on:
6
6
  pull_request:
7
7
  branches: [main]
8
8
 
9
+ env:
10
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
11
+
9
12
  jobs:
10
13
  lint:
11
14
  runs-on: ubuntu-latest
@@ -4,6 +4,9 @@ on:
4
4
  push:
5
5
  tags: ["v*"]
6
6
 
7
+ env:
8
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
9
+
7
10
  jobs:
8
11
  build:
9
12
  runs-on: ubuntu-latest
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kaizen-loop
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: Continuous code improvement: autonomous work → review → fix → ship
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.10
8
+ Requires-Dist: pydantic>=2.0
8
9
  Provides-Extra: dev
9
10
  Requires-Dist: pytest; extra == 'dev'
10
11
  Requires-Dist: ruff; extra == 'dev'
@@ -4,10 +4,11 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "kaizen-loop"
7
- version = "0.1.0"
7
+ version = "0.2.1"
8
8
  description = "Continuous code improvement: autonomous work → review → fix → ship"
9
9
  requires-python = ">=3.10"
10
10
  license = "MIT"
11
+ dependencies = ["pydantic>=2.0"]
11
12
 
12
13
  [project.optional-dependencies]
13
14
  dev = ["pytest", "ruff"]
@@ -0,0 +1 @@
1
+ __version__ = "0.2.1"
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import cached_property
4
+ from typing import Literal
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ Action = Literal["no-op", "auto-fix"]
9
+ Severity = Literal["info", "warning", "error"]
10
+ RiskLevel = Literal["low", "medium", "high"]
11
+
12
+
13
+ class Finding(BaseModel):
14
+ id: str
15
+ severity: Severity
16
+ file: str = ""
17
+ line: int = 0
18
+ description: str = ""
19
+ action: Action = "no-op"
20
+
21
+
22
+ class FindingsResult(BaseModel):
23
+ items: list[Finding] = Field(default_factory=list)
24
+ summary: str = ""
25
+ risk_level: RiskLevel = "low"
26
+ risk_rationale: str = ""
27
+
28
+ @cached_property
29
+ def has_auto_fix(self) -> bool:
30
+ return any(f.action == "auto-fix" for f in self.items)
31
+
32
+ @cached_property
33
+ def auto_fix_items(self) -> list[Finding]:
34
+ return [f for f in self.items if f.action == "auto-fix"]
35
+
36
+
37
+ def parse_findings(data: dict) -> FindingsResult:
38
+ mapped = {
39
+ "items": data.get("findings", []),
40
+ "summary": data.get("summary", ""),
41
+ "risk_level": data.get("risk_level", "low"),
42
+ "risk_rationale": data.get("risk_rationale", ""),
43
+ }
44
+ return FindingsResult.model_validate(mapped)
@@ -23,7 +23,7 @@ from kaizen.git import (
23
23
  slugify_prompt,
24
24
  )
25
25
  from kaizen.orchestrator import Orchestrator
26
- from kaizen.review_prompt import build_fix_prompt
26
+ from kaizen.review_prompt import FIX_SCHEMA, build_fix_prompt
27
27
  from kaizen.run import RunInfo, setup_run, update_run_head, update_run_pr_url, update_run_status
28
28
  from kaizen.steps.pr import PRStep
29
29
  from kaizen.steps.push import PushStep
@@ -178,7 +178,7 @@ def run_loop(
178
178
  print(f"\n auto-fixing {len(auto_fix_items)} issues...")
179
179
  fix_prompt = build_fix_prompt([_finding_to_dict(f) for f in auto_fix_items])
180
180
  try:
181
- agent.run(fix_prompt, ctx.work_dir, repo_dir=cwd)
181
+ agent.run(fix_prompt, ctx.work_dir, schema=FIX_SCHEMA, repo_dir=cwd)
182
182
  commit_all(f"kaizen: fix {len(auto_fix_items)} review findings", ctx.work_dir)
183
183
  current_head = head_commit(ctx.work_dir)
184
184
  update_run_head(ctx.run_info.run_dir, current_head)
@@ -1,5 +1,7 @@
1
1
  import time
2
2
 
3
+ from pydantic import BaseModel, Field
4
+
3
5
  from kaizen.agent import OpenCodeAgent
4
6
  from kaizen.config import load_config
5
7
  from kaizen.git import (
@@ -25,6 +27,14 @@ WORK_SCHEMA = {
25
27
  }
26
28
 
27
29
 
30
+ class WorkOutput(BaseModel):
31
+ success: bool
32
+ summary: str
33
+ key_changes_made: list[str] = Field(default_factory=list)
34
+ key_learnings: list[str] = Field(default_factory=list)
35
+ should_fully_stop: bool = False
36
+
37
+
28
38
  class Orchestrator:
29
39
  def __init__(
30
40
  self,
@@ -93,17 +103,13 @@ class Orchestrator:
93
103
  break
94
104
  continue
95
105
 
96
- success = bool(result.output.get("success"))
97
- summary = str(result.output.get("summary", ""))
98
- changes = result.output.get("key_changes_made") or []
99
- learnings = result.output.get("key_learnings") or []
100
- should_stop = bool(result.output.get("should_fully_stop", False))
106
+ work = WorkOutput.model_validate(result.output)
101
107
 
102
108
  self.total_input_tokens += result.input_tokens
103
109
  self.total_output_tokens += result.output_tokens
104
110
 
105
- if success:
106
- commit_msg = f"kaizen {self.iteration}: {summary}"
111
+ if work.success:
112
+ commit_msg = f"kaizen {self.iteration}: {work.summary}"
107
113
  try:
108
114
  commit_all(commit_msg, self.cwd)
109
115
  except RuntimeError as e:
@@ -117,9 +123,9 @@ class Orchestrator:
117
123
  self.consecutive_failures = 0
118
124
  append_notes(
119
125
  self.run_info.run_dir + "/notes.md",
120
- self.iteration, summary, changes, learnings,
126
+ self.iteration, work.summary, work.key_changes_made, work.key_learnings,
121
127
  )
122
- print(f" committed: {summary}")
128
+ print(f" committed: {work.summary}")
123
129
 
124
130
  if self.push_remote:
125
131
  try:
@@ -133,11 +139,11 @@ class Orchestrator:
133
139
  reset_hard(self.cwd)
134
140
  append_notes(
135
141
  self.run_info.run_dir + "/notes.md",
136
- self.iteration, f"[FAIL] {summary}", [], learnings,
142
+ self.iteration, f"[FAIL] {work.summary}", [], work.key_learnings,
137
143
  )
138
- print(f" failed: {summary}")
144
+ print(f" failed: {work.summary}")
139
145
 
140
- if self.stop_when and should_stop:
146
+ if self.stop_when and work.should_fully_stop:
141
147
  print(f" stop condition met: {self.stop_when}")
142
148
  status = "stopped"
143
149
  break
@@ -1,3 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, Field
4
+
1
5
  REVIEW_SCHEMA = {
2
6
  "type": "object",
3
7
  "additionalProperties": False,
@@ -25,6 +29,21 @@ REVIEW_SCHEMA = {
25
29
  "required": ["findings", "summary", "risk_level"],
26
30
  }
27
31
 
32
+ FIX_SCHEMA = {
33
+ "type": "object",
34
+ "additionalProperties": False,
35
+ "properties": {
36
+ "changes_made": {"type": "array", "items": {"type": "string"}},
37
+ "summary": {"type": "string"},
38
+ },
39
+ "required": ["changes_made", "summary"],
40
+ }
41
+
42
+
43
+ class FixOutput(BaseModel):
44
+ changes_made: list[str] = Field(default_factory=list)
45
+ summary: str = ""
46
+
28
47
 
29
48
  def build_review_prompt(diff: str, intent: str = "") -> str:
30
49
  intent_section = ""
@@ -62,5 +81,8 @@ def build_fix_prompt(findings_items: list[dict]) -> str:
62
81
  lines.append(f"1. [{f['severity']}]{loc} — {f['description']} ({f['action']})")
63
82
 
64
83
  lines.append("\nAfter fixing, run any available tests or linters to verify.")
84
+ lines.append("\n## Output\n\nReturn structured output with:")
85
+ lines.append("- changes_made: array of descriptions of fixes applied")
86
+ lines.append("- summary: one-sentence summary of fixes")
65
87
 
66
88
  return "\n".join(lines)
@@ -1,10 +1,9 @@
1
- from dataclasses import dataclass
1
+ from pydantic import BaseModel
2
2
 
3
3
  from kaizen.findings import FindingsResult
4
4
 
5
5
 
6
- @dataclass
7
- class StepOutcome:
6
+ class StepOutcome(BaseModel):
8
7
  findings: FindingsResult | None = None
9
8
  skipped: bool = False
10
9
  skip_remaining: bool = False
@@ -1 +0,0 @@
1
- __version__ = "0.1.0"
@@ -1,53 +0,0 @@
1
- from dataclasses import dataclass, field
2
-
3
- ACTION_NOOP = "no-op"
4
- ACTION_AUTO_FIX = "auto-fix"
5
-
6
- SEVERITY_INFO = "info"
7
- SEVERITY_WARNING = "warning"
8
- SEVERITY_ERROR = "error"
9
-
10
-
11
- @dataclass
12
- class Finding:
13
- id: str
14
- severity: str
15
- file: str = ""
16
- line: int = 0
17
- description: str = ""
18
- action: str = ACTION_NOOP
19
-
20
-
21
- @dataclass
22
- class FindingsResult:
23
- items: list[Finding] = field(default_factory=list)
24
- summary: str = ""
25
- risk_level: str = "low"
26
- risk_rationale: str = ""
27
-
28
- @property
29
- def has_auto_fix(self) -> bool:
30
- return any(f.action == ACTION_AUTO_FIX for f in self.items)
31
-
32
- @property
33
- def auto_fix_items(self) -> list[Finding]:
34
- return [f for f in self.items if f.action == ACTION_AUTO_FIX]
35
-
36
-
37
- def parse_findings(data: dict) -> FindingsResult:
38
- items = []
39
- for i, f in enumerate(data.get("findings", [])):
40
- items.append(Finding(
41
- id=f.get("id", f"f{i + 1}"),
42
- severity=f.get("severity", SEVERITY_INFO),
43
- file=f.get("file", ""),
44
- line=f.get("line", 0),
45
- description=f.get("description", ""),
46
- action=f.get("action", ACTION_NOOP),
47
- ))
48
- return FindingsResult(
49
- items=items,
50
- summary=data.get("summary", ""),
51
- risk_level=data.get("risk_level", "low"),
52
- risk_rationale=data.get("risk_rationale", ""),
53
- )
File without changes
File without changes
File without changes
File without changes