claude-code-generator 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.
- claude_code_generator-0.1.0.dist-info/METADATA +176 -0
- claude_code_generator-0.1.0.dist-info/RECORD +49 -0
- claude_code_generator-0.1.0.dist-info/WHEEL +5 -0
- claude_code_generator-0.1.0.dist-info/entry_points.txt +2 -0
- claude_code_generator-0.1.0.dist-info/licenses/LICENSE +21 -0
- claude_code_generator-0.1.0.dist-info/top_level.txt +1 -0
- code_generator/__init__.py +3 -0
- code_generator/agents.py +177 -0
- code_generator/cli.py +49 -0
- code_generator/commands/__init__.py +1 -0
- code_generator/commands/generate.py +252 -0
- code_generator/commands/init.py +72 -0
- code_generator/commands/review.py +117 -0
- code_generator/commands/status.py +83 -0
- code_generator/env.py +55 -0
- code_generator/gh.py +331 -0
- code_generator/logging_setup.py +73 -0
- code_generator/orchestrator/__init__.py +4 -0
- code_generator/orchestrator/cycle_loop.py +371 -0
- code_generator/orchestrator/phase0_complexity.py +159 -0
- code_generator/orchestrator/phase1_plan.py +170 -0
- code_generator/orchestrator/phase2_review.py +126 -0
- code_generator/orchestrator/phase3_4_implement.py +164 -0
- code_generator/orchestrator/phase5_closure.py +154 -0
- code_generator/orchestrator/phase6_test.py +98 -0
- code_generator/orchestrator/phase7_commit.py +167 -0
- code_generator/prompts/__init__.py +86 -0
- code_generator/prompts/prompt-phase-0-complexity.md +85 -0
- code_generator/prompts/prompt-phase-1-planning.md +209 -0
- code_generator/prompts/prompt-phase-2-issue-review.md +84 -0
- code_generator/prompts/prompt-phase-3-implementation.md +191 -0
- code_generator/prompts/prompt-phase-5-final-review.md +135 -0
- code_generator/prompts/prompt-phase-6-test.md +102 -0
- code_generator/prompts/prompt-phase-7-commit.md +103 -0
- code_generator/prompts/prompt-review.md +124 -0
- code_generator/runner/__init__.py +26 -0
- code_generator/runner/rate_limit.py +113 -0
- code_generator/runner/retry.py +165 -0
- code_generator/runner/sdk_runner.py +267 -0
- code_generator/runner/subprocess_runner.py +200 -0
- code_generator/state.py +178 -0
- code_generator/templates/__init__.py +1 -0
- code_generator/templates/angular.md +12 -0
- code_generator/templates/base.md +28 -0
- code_generator/templates/fastapi.md +12 -0
- code_generator/templates/finance.md +9 -0
- code_generator/templates/fullstack.md +24 -0
- code_generator/templates/nestjs.md +9 -0
- code_generator/templates/python-cli.md +9 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Phase 2 — Per-issue review with circuit breaker.
|
|
2
|
+
|
|
3
|
+
Each open issue is reviewed by Opus 4.6 in its own invocation.
|
|
4
|
+
If the circuit breaker trips for a specific issue, a ``needs-manual-review``
|
|
5
|
+
label is added and processing continues with the next issue.
|
|
6
|
+
|
|
7
|
+
Non-negotiable #8: model is hard-coded to ``claude-opus-4-6``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from code_generator import gh
|
|
15
|
+
from code_generator.logging_setup import setup_phase_logger
|
|
16
|
+
from code_generator.prompts import load_prompt
|
|
17
|
+
from code_generator.runner import rate_limit, retry
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
import logging
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from code_generator.state import CycleState, IssueState, State
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_issues(state: State, cycle: CycleState | None) -> list[IssueState]:
|
|
27
|
+
"""Return the list of issues for the active scope.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
state: Root state.
|
|
31
|
+
cycle: Active cycle or ``None`` for single mode.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of IssueState objects from the cycle or the root state.
|
|
35
|
+
"""
|
|
36
|
+
if cycle is not None:
|
|
37
|
+
return cycle.issues
|
|
38
|
+
return state.issues
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def run(
|
|
42
|
+
state: State,
|
|
43
|
+
cycle: CycleState | None,
|
|
44
|
+
project_dir: Path,
|
|
45
|
+
*,
|
|
46
|
+
runner_module: Any,
|
|
47
|
+
logger: logging.Logger | None = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Execute phase 2: per-issue review.
|
|
50
|
+
|
|
51
|
+
Iterates open issues and reviews each with a fresh Opus 4.6 session.
|
|
52
|
+
A :class:`~code_generator.runner.retry.CircuitBreaker` with
|
|
53
|
+
``max_failures=3`` wraps each issue. When the breaker trips, the issue
|
|
54
|
+
is tagged ``needs-manual-review`` and skipped.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
state: Root state object.
|
|
58
|
+
cycle: Active cycle (multi-cycle) or ``None`` (single mode).
|
|
59
|
+
project_dir: Project root directory.
|
|
60
|
+
runner_module: Runner module from ``get_runner()``.
|
|
61
|
+
logger: Optional phase logger.
|
|
62
|
+
"""
|
|
63
|
+
if logger is None:
|
|
64
|
+
logger = setup_phase_logger("phase2", project_dir)
|
|
65
|
+
|
|
66
|
+
state_path = project_dir / ".code-generator" / "state.json"
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
from claude_agent_sdk import ClaudeAgentOptions # type: ignore[import-not-found]
|
|
70
|
+
except ImportError: # pragma: no cover
|
|
71
|
+
ClaudeAgentOptions = type( # type: ignore[assignment,misc]
|
|
72
|
+
"ClaudeAgentOptions",
|
|
73
|
+
(),
|
|
74
|
+
{"__init__": lambda self, **kw: self.__dict__.update(kw)},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
issues = _get_issues(state, cycle)
|
|
78
|
+
|
|
79
|
+
for issue in issues:
|
|
80
|
+
if issue.status != "open":
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
logger.info("Phase 2: reviewing issue #%d (agent=%s).", issue.number, issue.agent)
|
|
84
|
+
|
|
85
|
+
prompt = load_prompt(
|
|
86
|
+
"prompt-phase-2-issue-review.md",
|
|
87
|
+
ISSUE_NUMBER=str(issue.number),
|
|
88
|
+
PRIMARY_AGENT=issue.agent,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
options = ClaudeAgentOptions(
|
|
92
|
+
model="claude-opus-4-6",
|
|
93
|
+
allowed_tools=["Read", "Bash", "Glob", "Grep"],
|
|
94
|
+
cwd=str(project_dir),
|
|
95
|
+
max_turns=15,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
breaker = retry.CircuitBreaker(max_failures=3)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
await retry.with_backoff(
|
|
102
|
+
lambda p=prompt, o=options: rate_limit.main_loop(
|
|
103
|
+
runner_module,
|
|
104
|
+
p,
|
|
105
|
+
o,
|
|
106
|
+
state_path=state_path,
|
|
107
|
+
logger=logger,
|
|
108
|
+
),
|
|
109
|
+
breaker=breaker,
|
|
110
|
+
)
|
|
111
|
+
except retry.CircuitOpen:
|
|
112
|
+
logger.error(
|
|
113
|
+
"Phase 2: circuit breaker opened for issue #%d — tagging for manual review.",
|
|
114
|
+
issue.number,
|
|
115
|
+
)
|
|
116
|
+
try:
|
|
117
|
+
gh.add_label(issue.number, "needs-manual-review")
|
|
118
|
+
except gh.GhError as exc:
|
|
119
|
+
logger.warning(
|
|
120
|
+
"Could not add needs-manual-review label to #%d: %s",
|
|
121
|
+
issue.number,
|
|
122
|
+
exc,
|
|
123
|
+
)
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
logger.info("Phase 2: issue #%d reviewed.", issue.number)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Phases 3 & 4 — Implementation loop (TDD, fresh session per issue).
|
|
2
|
+
|
|
3
|
+
Each open issue gets its own SDK session (non-negotiable #6: no ``resume``).
|
|
4
|
+
The issue agent is taken from state; skills default to ``solid-principles``
|
|
5
|
+
augmented by whatever ``agents.detect()`` returns for the requirements file.
|
|
6
|
+
|
|
7
|
+
Non-negotiable #8: model is hard-coded to ``claude-sonnet-4-6``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from code_generator import agents as _agents
|
|
15
|
+
from code_generator import state as _state
|
|
16
|
+
from code_generator.logging_setup import setup_phase_logger
|
|
17
|
+
from code_generator.prompts import load_prompt
|
|
18
|
+
from code_generator.runner import rate_limit
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import logging
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from code_generator.state import CycleState, IssueState, State
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_issues(state: State, cycle: CycleState | None) -> list[IssueState]:
|
|
28
|
+
if cycle is not None:
|
|
29
|
+
return cycle.issues
|
|
30
|
+
return state.issues
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _default_skills(project_dir: Path) -> str:
|
|
34
|
+
"""Derive skill names from the requirements file (or use generic fallback).
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
project_dir: Project root; the requirements file lives at
|
|
38
|
+
``<project_dir>/.code-generator/requirements.md``.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Comma-joined skill names string.
|
|
42
|
+
"""
|
|
43
|
+
req_path = project_dir / ".code-generator" / "requirements.md"
|
|
44
|
+
selection = _agents.detect(req_path)
|
|
45
|
+
skills = selection.skills or ("solid-principles",)
|
|
46
|
+
return ",".join(skills)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def run(
|
|
50
|
+
state: State,
|
|
51
|
+
cycle: CycleState | None,
|
|
52
|
+
project_dir: Path,
|
|
53
|
+
*,
|
|
54
|
+
runner_module: Any,
|
|
55
|
+
logger: logging.Logger | None = None,
|
|
56
|
+
max_retries: int = 3,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Execute phases 3 and 4: implementation loop.
|
|
59
|
+
|
|
60
|
+
For each open issue:
|
|
61
|
+
|
|
62
|
+
1. Builds a fresh set of options (no ``resume`` — non-negotiable #6).
|
|
63
|
+
2. Loads ``prompt-phase-3-implementation.md`` with the issue number,
|
|
64
|
+
agent, and skills.
|
|
65
|
+
3. Runs via ``rate_limit.main_loop``.
|
|
66
|
+
4. On success: marks the issue ``closed`` in state and persists.
|
|
67
|
+
5. On failure: retries up to *max_retries* times with an escalating
|
|
68
|
+
error context appended to the prompt. After all retries the issue
|
|
69
|
+
is left open and an ERROR is logged.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
state: Root state object (mutated in place).
|
|
73
|
+
cycle: Active cycle (multi-cycle) or ``None`` (single mode).
|
|
74
|
+
project_dir: Project root directory.
|
|
75
|
+
runner_module: Runner module from ``get_runner()``.
|
|
76
|
+
logger: Optional phase logger.
|
|
77
|
+
max_retries: Maximum implementation retries per issue (default 3).
|
|
78
|
+
"""
|
|
79
|
+
if logger is None:
|
|
80
|
+
logger = setup_phase_logger("phase3_4", project_dir)
|
|
81
|
+
|
|
82
|
+
state_path = project_dir / ".code-generator" / "state.json"
|
|
83
|
+
skills = _default_skills(project_dir)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
from claude_agent_sdk import ClaudeAgentOptions # type: ignore[import-not-found]
|
|
87
|
+
except ImportError: # pragma: no cover
|
|
88
|
+
ClaudeAgentOptions = type( # type: ignore[assignment,misc]
|
|
89
|
+
"ClaudeAgentOptions",
|
|
90
|
+
(),
|
|
91
|
+
{"__init__": lambda self, **kw: self.__dict__.update(kw)},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
issues = _get_issues(state, cycle)
|
|
95
|
+
|
|
96
|
+
for issue in issues:
|
|
97
|
+
if issue.status != "open":
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
agent = issue.agent or "python-pro"
|
|
101
|
+
logger.info("Phase 3/4: implementing issue #%d (agent=%s).", issue.number, agent)
|
|
102
|
+
|
|
103
|
+
base_prompt = load_prompt(
|
|
104
|
+
"prompt-phase-3-implementation.md",
|
|
105
|
+
ISSUE_NUMBER=str(issue.number),
|
|
106
|
+
PRIMARY_AGENT=agent,
|
|
107
|
+
SKILLS=skills,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
success = False
|
|
111
|
+
last_err: str = ""
|
|
112
|
+
|
|
113
|
+
for attempt in range(max_retries):
|
|
114
|
+
# Non-negotiable #6: fresh session per issue — never set resume.
|
|
115
|
+
# Clear session_id before each attempt.
|
|
116
|
+
state.session_id = None
|
|
117
|
+
_state.save_state(state_path, state)
|
|
118
|
+
|
|
119
|
+
prompt = base_prompt
|
|
120
|
+
if attempt > 0:
|
|
121
|
+
prompt = (
|
|
122
|
+
f"{base_prompt}\n\nPrevious attempt failed: {last_err}. "
|
|
123
|
+
"Be more explicit this time."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
options = ClaudeAgentOptions(
|
|
127
|
+
model="claude-sonnet-4-6",
|
|
128
|
+
allowed_tools=["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
|
|
129
|
+
cwd=str(project_dir),
|
|
130
|
+
max_turns=80,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
await rate_limit.main_loop(
|
|
135
|
+
runner_module,
|
|
136
|
+
prompt,
|
|
137
|
+
options,
|
|
138
|
+
state_path=state_path,
|
|
139
|
+
logger=logger,
|
|
140
|
+
)
|
|
141
|
+
success = True
|
|
142
|
+
break
|
|
143
|
+
except Exception as exc: # noqa: BLE001
|
|
144
|
+
last_err = str(exc)
|
|
145
|
+
logger.warning(
|
|
146
|
+
"Phase 3/4: attempt %d/%d failed for issue #%d: %s",
|
|
147
|
+
attempt + 1,
|
|
148
|
+
max_retries,
|
|
149
|
+
issue.number,
|
|
150
|
+
last_err,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if success:
|
|
154
|
+
issue.status = "closed"
|
|
155
|
+
# Non-negotiable #6: clear session_id after each issue.
|
|
156
|
+
state.session_id = None
|
|
157
|
+
_state.save_state(state_path, state)
|
|
158
|
+
logger.info("Phase 3/4: issue #%d closed.", issue.number)
|
|
159
|
+
else:
|
|
160
|
+
logger.error(
|
|
161
|
+
"Phase 3/4: issue #%d could not be implemented after %d retries.",
|
|
162
|
+
issue.number,
|
|
163
|
+
max_retries,
|
|
164
|
+
)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Phase 5 — Closure and cross-module review.
|
|
2
|
+
|
|
3
|
+
Runs Opus 4.6 to review the full codebase, close any straggling issues,
|
|
4
|
+
and optionally create new ``fix:`` issues. After the session, re-fetches
|
|
5
|
+
open issues and returns a :class:`FixResult` so the orchestrator can
|
|
6
|
+
re-enter phases 3/4 for any newly-created fix issues.
|
|
7
|
+
|
|
8
|
+
Non-negotiable #8: model is hard-coded to ``claude-opus-4-6``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
from code_generator import gh
|
|
17
|
+
from code_generator import state as _state
|
|
18
|
+
from code_generator.logging_setup import setup_phase_logger
|
|
19
|
+
from code_generator.prompts import load_prompt
|
|
20
|
+
from code_generator.runner import rate_limit
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
import logging
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from code_generator.state import CycleState, IssueState, State
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class FixResult:
|
|
31
|
+
"""Returned by :func:`run` to signal new fix issues to the orchestrator.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
new_issues: List of IssueState objects for issues created during
|
|
35
|
+
phase 5 (identified by the ``fix:`` title prefix).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
new_issues: list[IssueState] = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _known_numbers(state: State, cycle: CycleState | None) -> set[int]:
|
|
42
|
+
"""Return the set of issue numbers already in state/cycle.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
state: Root state.
|
|
46
|
+
cycle: Active cycle or ``None``.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Set of known issue numbers.
|
|
50
|
+
"""
|
|
51
|
+
issues = cycle.issues if cycle is not None else state.issues
|
|
52
|
+
return {i.number for i in issues}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _agent_from_labels(labels: list[dict[str, Any]]) -> str: # type: ignore[type-arg]
|
|
56
|
+
for lbl in labels:
|
|
57
|
+
name: str = lbl.get("name", "")
|
|
58
|
+
if name.startswith("agent:"):
|
|
59
|
+
return name[len("agent:"):]
|
|
60
|
+
return "python-pro"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def run(
|
|
64
|
+
state: State,
|
|
65
|
+
cycle: CycleState | None,
|
|
66
|
+
project_dir: Path,
|
|
67
|
+
*,
|
|
68
|
+
runner_module: Any,
|
|
69
|
+
logger: logging.Logger | None = None,
|
|
70
|
+
) -> FixResult:
|
|
71
|
+
"""Execute phase 5: overall review and closure.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
state: Root state object (mutated in place).
|
|
75
|
+
cycle: Active cycle (multi-cycle) or ``None`` (single mode).
|
|
76
|
+
project_dir: Project root directory.
|
|
77
|
+
runner_module: Runner module from ``get_runner()``.
|
|
78
|
+
logger: Optional phase logger.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
:class:`FixResult` listing any new fix issues discovered after the
|
|
82
|
+
session. An empty list means no re-entry into phases 3/4 is needed.
|
|
83
|
+
"""
|
|
84
|
+
if logger is None:
|
|
85
|
+
logger = setup_phase_logger("phase5", project_dir)
|
|
86
|
+
|
|
87
|
+
state_path = project_dir / ".code-generator" / "state.json"
|
|
88
|
+
|
|
89
|
+
prompt = load_prompt(
|
|
90
|
+
"prompt-phase-5-final-review.md",
|
|
91
|
+
CYCLE_SCOPE=(
|
|
92
|
+
cycle.scope
|
|
93
|
+
if cycle is not None
|
|
94
|
+
else "single-cycle — implement all features from requirements.md"
|
|
95
|
+
),
|
|
96
|
+
MILESTONE_TITLE=cycle.milestone_title if cycle is not None else "",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
from claude_agent_sdk import ClaudeAgentOptions # type: ignore[import-not-found]
|
|
101
|
+
except ImportError: # pragma: no cover
|
|
102
|
+
ClaudeAgentOptions = type( # type: ignore[assignment,misc]
|
|
103
|
+
"ClaudeAgentOptions",
|
|
104
|
+
(),
|
|
105
|
+
{"__init__": lambda self, **kw: self.__dict__.update(kw)},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
options = ClaudeAgentOptions(
|
|
109
|
+
model="claude-opus-4-6",
|
|
110
|
+
allowed_tools=["Read", "Bash", "Glob", "Grep"],
|
|
111
|
+
cwd=str(project_dir),
|
|
112
|
+
max_turns=30,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
await rate_limit.main_loop(
|
|
116
|
+
runner_module,
|
|
117
|
+
prompt,
|
|
118
|
+
options,
|
|
119
|
+
state_path=state_path,
|
|
120
|
+
logger=logger,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Re-fetch open issues to detect any new fix: issues.
|
|
124
|
+
known = _known_numbers(state, cycle)
|
|
125
|
+
milestone_title = cycle.milestone_title if cycle is not None else None
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
open_issues = gh.list_issues(milestone=milestone_title, state="open")
|
|
129
|
+
except gh.GhError as exc:
|
|
130
|
+
logger.warning("Phase 5: could not list issues after session: %s", exc)
|
|
131
|
+
return FixResult()
|
|
132
|
+
|
|
133
|
+
new_issue_states: list[_state.IssueState] = []
|
|
134
|
+
for raw in open_issues:
|
|
135
|
+
num = raw["number"]
|
|
136
|
+
if num in known:
|
|
137
|
+
continue
|
|
138
|
+
# Only pick up new issues — they may be fix: issues created by the session.
|
|
139
|
+
new_state = _state.IssueState(
|
|
140
|
+
number=num,
|
|
141
|
+
status="open",
|
|
142
|
+
agent=_agent_from_labels(raw.get("labels", [])),
|
|
143
|
+
)
|
|
144
|
+
new_issue_states.append(new_state)
|
|
145
|
+
|
|
146
|
+
if new_issue_states:
|
|
147
|
+
if cycle is not None:
|
|
148
|
+
cycle.issues.extend(new_issue_states)
|
|
149
|
+
else:
|
|
150
|
+
state.issues.extend(new_issue_states)
|
|
151
|
+
_state.save_state(state_path, state)
|
|
152
|
+
logger.info("Phase 5: %d new fix issues detected.", len(new_issue_states))
|
|
153
|
+
|
|
154
|
+
return FixResult(new_issues=new_issue_states)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Phase 6 — Test suite runner.
|
|
2
|
+
|
|
3
|
+
Runs Sonnet 4.6 to execute the test suite and fix failures iteratively
|
|
4
|
+
(max 3 attempts, controlled by the prompt). Raises :class:`TestsFailed`
|
|
5
|
+
if the session text contains the sentinel string ``"TESTS FAILED"``.
|
|
6
|
+
|
|
7
|
+
Non-negotiable #8: model is hard-coded to ``claude-sonnet-4-6``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from code_generator.logging_setup import setup_phase_logger
|
|
15
|
+
from code_generator.prompts import load_prompt
|
|
16
|
+
from code_generator.runner import rate_limit
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
import logging
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from code_generator.state import CycleState, State
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Sentinel emitted by the prompt when all retries are exhausted.
|
|
26
|
+
_FAIL_SENTINEL = "TESTS FAILED"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestsFailed(RuntimeError): # noqa: N818
|
|
30
|
+
"""Raised when the test-runner session reports persistent test failures.
|
|
31
|
+
|
|
32
|
+
The orchestrator should skip phase 7 (commit) when this is raised.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
__test__ = False # pytest: not a test class
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def run(
|
|
39
|
+
state: State,
|
|
40
|
+
cycle: CycleState | None,
|
|
41
|
+
project_dir: Path,
|
|
42
|
+
*,
|
|
43
|
+
runner_module: Any,
|
|
44
|
+
logger: logging.Logger | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Execute phase 6: test suite runner.
|
|
47
|
+
|
|
48
|
+
Loads the phase-6 prompt (with ``MAX_RETRIES="3"``), runs it via
|
|
49
|
+
the rate-limit loop, and inspects the result text for the sentinel.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
state: Root state object.
|
|
53
|
+
cycle: Active cycle (multi-cycle) or ``None`` (single mode).
|
|
54
|
+
project_dir: Project root directory.
|
|
55
|
+
runner_module: Runner module from ``get_runner()``.
|
|
56
|
+
logger: Optional phase logger.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
TestsFailed: When the result text contains ``"TESTS FAILED"``.
|
|
60
|
+
"""
|
|
61
|
+
if logger is None:
|
|
62
|
+
logger = setup_phase_logger("phase6", project_dir)
|
|
63
|
+
|
|
64
|
+
state_path = project_dir / ".code-generator" / "state.json"
|
|
65
|
+
|
|
66
|
+
prompt = load_prompt("prompt-phase-6-test.md", MAX_RETRIES="3")
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
from claude_agent_sdk import ClaudeAgentOptions # type: ignore[import-not-found]
|
|
70
|
+
except ImportError: # pragma: no cover
|
|
71
|
+
ClaudeAgentOptions = type( # type: ignore[assignment,misc]
|
|
72
|
+
"ClaudeAgentOptions",
|
|
73
|
+
(),
|
|
74
|
+
{"__init__": lambda self, **kw: self.__dict__.update(kw)},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
options = ClaudeAgentOptions(
|
|
78
|
+
model="claude-sonnet-4-6",
|
|
79
|
+
allowed_tools=["Read", "Edit", "Bash"],
|
|
80
|
+
cwd=str(project_dir),
|
|
81
|
+
max_turns=40,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
result = await rate_limit.main_loop(
|
|
85
|
+
runner_module,
|
|
86
|
+
prompt,
|
|
87
|
+
options,
|
|
88
|
+
state_path=state_path,
|
|
89
|
+
logger=logger,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if _FAIL_SENTINEL in result.text:
|
|
93
|
+
logger.error("Phase 6: test runner reported persistent failures.")
|
|
94
|
+
raise TestsFailed(
|
|
95
|
+
"Test suite failed after all retries. Inspect the output above and fix manually."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
logger.info("Phase 6 complete: all tests passed.")
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Phase 7 — Commit message generation and git push.
|
|
2
|
+
|
|
3
|
+
Uses Haiku 4.5 to generate a commit message, then performs
|
|
4
|
+
``git add . → git commit -m <msg> → git push origin <branch>``
|
|
5
|
+
with one rebase-and-retry on push rejection.
|
|
6
|
+
|
|
7
|
+
Non-negotiable #8: model is hard-coded to ``claude-haiku-4-5``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import subprocess
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from code_generator.logging_setup import setup_phase_logger
|
|
16
|
+
from code_generator.prompts import load_prompt
|
|
17
|
+
from code_generator.runner import rate_limit
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
import logging
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from code_generator.state import CycleState, IssueState, State
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _current_branch(project_dir: Path) -> str:
|
|
27
|
+
"""Return the current git branch name.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
project_dir: Directory to run git in.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Branch name string.
|
|
34
|
+
"""
|
|
35
|
+
result = subprocess.run(
|
|
36
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
37
|
+
cwd=project_dir,
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
check=True,
|
|
41
|
+
)
|
|
42
|
+
return result.stdout.strip()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _has_staged_changes(project_dir: Path) -> bool:
|
|
46
|
+
"""Return True when the git index has staged changes.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
project_dir: Directory to run git in.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
``True`` if there are staged changes, ``False`` otherwise.
|
|
53
|
+
"""
|
|
54
|
+
result = subprocess.run(
|
|
55
|
+
["git", "diff", "--cached", "--quiet"],
|
|
56
|
+
cwd=project_dir,
|
|
57
|
+
check=False,
|
|
58
|
+
)
|
|
59
|
+
# Exit code 0 means no diff (nothing staged); 1 means there are changes.
|
|
60
|
+
return result.returncode != 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _closed_issue_numbers(state: State, cycle: CycleState | None) -> list[int]:
|
|
64
|
+
"""Collect issue numbers marked as closed in the current scope.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
state: Root state.
|
|
68
|
+
cycle: Active cycle or ``None`` for single mode.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Sorted list of closed issue numbers.
|
|
72
|
+
"""
|
|
73
|
+
issues: list[IssueState] = cycle.issues if cycle is not None else state.issues
|
|
74
|
+
return sorted(i.number for i in issues if i.status == "closed")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def run(
|
|
78
|
+
state: State,
|
|
79
|
+
cycle: CycleState | None,
|
|
80
|
+
project_dir: Path,
|
|
81
|
+
*,
|
|
82
|
+
runner_module: Any,
|
|
83
|
+
logger: logging.Logger | None = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Execute phase 7: commit and push.
|
|
86
|
+
|
|
87
|
+
Steps:
|
|
88
|
+
1. ``git add .``
|
|
89
|
+
2. ``git diff --cached --quiet`` — skip when nothing staged.
|
|
90
|
+
3. Run Haiku 4.5 via the rate-limit loop to generate a commit message.
|
|
91
|
+
4. ``git commit -m <msg>``
|
|
92
|
+
5. ``git push origin <branch>``; on non-zero exit: ``git pull --rebase
|
|
93
|
+
&& git push origin <branch>`` (one retry).
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
state: Root state object.
|
|
97
|
+
cycle: Active cycle (multi-cycle) or ``None`` (single mode).
|
|
98
|
+
project_dir: Project root directory.
|
|
99
|
+
runner_module: Runner module from ``get_runner()``.
|
|
100
|
+
logger: Optional phase logger.
|
|
101
|
+
"""
|
|
102
|
+
if logger is None:
|
|
103
|
+
logger = setup_phase_logger("phase7", project_dir)
|
|
104
|
+
|
|
105
|
+
state_path = project_dir / ".code-generator" / "state.json"
|
|
106
|
+
|
|
107
|
+
# Stage everything.
|
|
108
|
+
subprocess.run(["git", "add", "."], cwd=project_dir, check=True)
|
|
109
|
+
|
|
110
|
+
# Skip commit when there is nothing staged.
|
|
111
|
+
if not _has_staged_changes(project_dir):
|
|
112
|
+
logger.info("Phase 7: nothing staged — skipping commit and push.")
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
closed_numbers = _closed_issue_numbers(state, cycle)
|
|
116
|
+
issues_closed = ", ".join(f"#{n}" for n in closed_numbers) if closed_numbers else "none"
|
|
117
|
+
cycle_name = cycle.name if cycle is not None else ""
|
|
118
|
+
|
|
119
|
+
prompt = load_prompt(
|
|
120
|
+
"prompt-phase-7-commit.md",
|
|
121
|
+
CYCLE_NAME=cycle_name,
|
|
122
|
+
ISSUES_CLOSED=issues_closed,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
from claude_agent_sdk import ClaudeAgentOptions # type: ignore[import-not-found]
|
|
127
|
+
except ImportError: # pragma: no cover
|
|
128
|
+
ClaudeAgentOptions = type( # type: ignore[assignment,misc]
|
|
129
|
+
"ClaudeAgentOptions",
|
|
130
|
+
(),
|
|
131
|
+
{"__init__": lambda self, **kw: self.__dict__.update(kw)},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
options = ClaudeAgentOptions(
|
|
135
|
+
model="claude-haiku-4-5",
|
|
136
|
+
allowed_tools=["Read", "Bash"],
|
|
137
|
+
cwd=str(project_dir),
|
|
138
|
+
max_turns=5,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
result = await rate_limit.main_loop(
|
|
142
|
+
runner_module,
|
|
143
|
+
prompt,
|
|
144
|
+
options,
|
|
145
|
+
state_path=state_path,
|
|
146
|
+
logger=logger,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
commit_msg = result.text.strip()
|
|
150
|
+
if not commit_msg:
|
|
151
|
+
commit_msg = f"chore: cycle {cycle_name or 'single'} complete"
|
|
152
|
+
|
|
153
|
+
subprocess.run(["git", "commit", "-m", commit_msg], cwd=project_dir, check=True)
|
|
154
|
+
|
|
155
|
+
branch = _current_branch(project_dir)
|
|
156
|
+
|
|
157
|
+
push_result = subprocess.run(
|
|
158
|
+
["git", "push", "origin", branch],
|
|
159
|
+
cwd=project_dir,
|
|
160
|
+
check=False,
|
|
161
|
+
)
|
|
162
|
+
if push_result.returncode != 0:
|
|
163
|
+
logger.warning("Phase 7: push rejected — attempting rebase and retry.")
|
|
164
|
+
subprocess.run(["git", "pull", "--rebase"], cwd=project_dir, check=True)
|
|
165
|
+
subprocess.run(["git", "push", "origin", branch], cwd=project_dir, check=True)
|
|
166
|
+
|
|
167
|
+
logger.info("Phase 7 complete: committed and pushed to %s.", branch)
|