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,371 @@
|
|
|
1
|
+
"""Multi-cycle driver: single-mode and multi-cycle orchestration loops.
|
|
2
|
+
|
|
3
|
+
Implements the phase 1→7 execution in dependency order for multi-cycle mode.
|
|
4
|
+
Each cycle gets a fresh SDK session (non-negotiable #6: no ``options.resume``
|
|
5
|
+
across cycle boundaries).
|
|
6
|
+
|
|
7
|
+
Topological iteration:
|
|
8
|
+
A cycle is eligible when all its ``depends_on`` cycle IDs have
|
|
9
|
+
``status == "completed"``. The loop iterates until no further eligible
|
|
10
|
+
cycles remain. Cycles with unmet dependencies after all eligible ones are
|
|
11
|
+
exhausted remain as ``"open"``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
from code_generator import gh
|
|
19
|
+
from code_generator import state as _state
|
|
20
|
+
from code_generator.logging_setup import setup_phase_logger
|
|
21
|
+
from code_generator.orchestrator import (
|
|
22
|
+
phase1_plan,
|
|
23
|
+
phase2_review,
|
|
24
|
+
phase3_4_implement,
|
|
25
|
+
phase5_closure,
|
|
26
|
+
phase6_test,
|
|
27
|
+
phase7_commit,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
import logging
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
from code_generator.state import CycleState, State
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Internal helpers
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _eligible_cycles(state: State, completed_ids: set[int]) -> list[CycleState]:
|
|
43
|
+
"""Return cycles whose dependencies are all completed and which are open.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
state: Root state with ``cycles`` list.
|
|
47
|
+
completed_ids: Set of cycle IDs already completed this run.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
List of :class:`~code_generator.state.CycleState` objects that are
|
|
51
|
+
eligible to run (open + all deps satisfied).
|
|
52
|
+
"""
|
|
53
|
+
return [
|
|
54
|
+
c
|
|
55
|
+
for c in state.cycles
|
|
56
|
+
if c.status == "open" and all(dep in completed_ids for dep in c.depends_on)
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Single-mode driver
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def run_single_mode(
|
|
66
|
+
state: State,
|
|
67
|
+
project_dir: Path,
|
|
68
|
+
*,
|
|
69
|
+
runner_module: Any,
|
|
70
|
+
logger: logging.Logger | None = None,
|
|
71
|
+
start_phase: int = 1,
|
|
72
|
+
) -> State:
|
|
73
|
+
"""Execute the single-mode pipeline: phases 1→7.
|
|
74
|
+
|
|
75
|
+
Non-negotiable #6 is respected by phase 3/4, which clears ``session_id``
|
|
76
|
+
before each issue. This function itself does not set ``options.resume``.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
state: Root state object (mutated in place and returned).
|
|
80
|
+
project_dir: Project root directory.
|
|
81
|
+
runner_module: Runner module returned by ``get_runner()``.
|
|
82
|
+
logger: Optional phase logger; one is created when omitted.
|
|
83
|
+
start_phase: First phase to execute. Phases below this number are
|
|
84
|
+
skipped. Defaults to 1 (full run).
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
The mutated state.
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
OverageAbort: Propagated unchanged from any phase.
|
|
91
|
+
RateLimitHit: Propagated unchanged from any phase.
|
|
92
|
+
TestsFailed: When phase 6 reports persistent test failures.
|
|
93
|
+
CircuitOpen: When phase 2's circuit breaker trips.
|
|
94
|
+
"""
|
|
95
|
+
log = logger or setup_phase_logger("cycle_loop", project_dir)
|
|
96
|
+
state_path = project_dir / ".code-generator" / "state.json"
|
|
97
|
+
|
|
98
|
+
log.info("run_single_mode: start_phase=%d", start_phase)
|
|
99
|
+
|
|
100
|
+
if start_phase <= 1:
|
|
101
|
+
await phase1_plan.run(
|
|
102
|
+
state, None, project_dir, runner_module=runner_module,
|
|
103
|
+
logger=setup_phase_logger("phase1", project_dir),
|
|
104
|
+
)
|
|
105
|
+
state.phase = 1
|
|
106
|
+
_state.save_state(state_path, state)
|
|
107
|
+
|
|
108
|
+
if start_phase <= 2:
|
|
109
|
+
await phase2_review.run(
|
|
110
|
+
state, None, project_dir, runner_module=runner_module,
|
|
111
|
+
logger=setup_phase_logger("phase2", project_dir),
|
|
112
|
+
)
|
|
113
|
+
state.phase = 2
|
|
114
|
+
_state.save_state(state_path, state)
|
|
115
|
+
|
|
116
|
+
if start_phase <= 3:
|
|
117
|
+
await phase3_4_implement.run(
|
|
118
|
+
state, None, project_dir, runner_module=runner_module,
|
|
119
|
+
logger=setup_phase_logger("phase3_4", project_dir),
|
|
120
|
+
)
|
|
121
|
+
state.phase = 3
|
|
122
|
+
_state.save_state(state_path, state)
|
|
123
|
+
|
|
124
|
+
if start_phase <= 4:
|
|
125
|
+
fix_result = await phase5_closure.run(
|
|
126
|
+
state, None, project_dir, runner_module=runner_module,
|
|
127
|
+
logger=setup_phase_logger("phase5", project_dir),
|
|
128
|
+
)
|
|
129
|
+
state.phase = 4
|
|
130
|
+
_state.save_state(state_path, state)
|
|
131
|
+
# Re-enter implementation for any fix issues detected by phase 5.
|
|
132
|
+
if fix_result.new_issues:
|
|
133
|
+
await phase3_4_implement.run(
|
|
134
|
+
state, None, project_dir, runner_module=runner_module,
|
|
135
|
+
logger=setup_phase_logger("phase3_4", project_dir),
|
|
136
|
+
)
|
|
137
|
+
_state.save_state(state_path, state)
|
|
138
|
+
|
|
139
|
+
if start_phase <= 5:
|
|
140
|
+
await phase6_test.run(
|
|
141
|
+
state, None, project_dir, runner_module=runner_module,
|
|
142
|
+
logger=setup_phase_logger("phase6", project_dir),
|
|
143
|
+
)
|
|
144
|
+
state.phase = 5
|
|
145
|
+
_state.save_state(state_path, state)
|
|
146
|
+
|
|
147
|
+
if start_phase <= 6:
|
|
148
|
+
await phase7_commit.run(
|
|
149
|
+
state, None, project_dir, runner_module=runner_module,
|
|
150
|
+
logger=setup_phase_logger("phase7", project_dir),
|
|
151
|
+
)
|
|
152
|
+
state.phase = 7
|
|
153
|
+
_state.save_state(state_path, state)
|
|
154
|
+
|
|
155
|
+
log.info("run_single_mode: complete")
|
|
156
|
+
return state
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
# Multi-cycle driver
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def run_multi_cycle(
|
|
165
|
+
state: State,
|
|
166
|
+
project_dir: Path,
|
|
167
|
+
*,
|
|
168
|
+
runner_module: Any,
|
|
169
|
+
logger: logging.Logger | None = None,
|
|
170
|
+
start_cycle: int | None = None,
|
|
171
|
+
start_phase: int = 1,
|
|
172
|
+
) -> State:
|
|
173
|
+
"""Execute cycles in dependency order, each with a fresh SDK session.
|
|
174
|
+
|
|
175
|
+
Eligible cycles are those whose ``depends_on`` cycle IDs are all
|
|
176
|
+
``"completed"``. The loop keeps running until no further eligible
|
|
177
|
+
cycles remain.
|
|
178
|
+
|
|
179
|
+
Non-negotiable #6: ``state.session_id`` is set to ``None`` at the entry
|
|
180
|
+
of every cycle and persisted before any phase runs.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
state: Root state object (mutated in place and returned).
|
|
184
|
+
project_dir: Project root directory.
|
|
185
|
+
runner_module: Runner module returned by ``get_runner()``.
|
|
186
|
+
logger: Optional phase logger; one is created when omitted.
|
|
187
|
+
start_cycle: When set, cycles with ``id < start_cycle`` are skipped
|
|
188
|
+
even if their dependencies are not completed. The phase resume
|
|
189
|
+
applies only to the *first* eligible cycle visited.
|
|
190
|
+
start_phase: Resume from this phase in the first eligible cycle.
|
|
191
|
+
Subsequent cycles always start at phase 1.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
The mutated state.
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
OverageAbort: Propagated unchanged — stops all cycles.
|
|
198
|
+
RateLimitHit: Propagated unchanged — stops all cycles.
|
|
199
|
+
TestsFailed: When phase 6 fails — stops remaining cycles.
|
|
200
|
+
CircuitOpen: Propagated unchanged — stops all cycles.
|
|
201
|
+
"""
|
|
202
|
+
log = logger or setup_phase_logger("cycle_loop", project_dir)
|
|
203
|
+
state_path = project_dir / ".code-generator" / "state.json"
|
|
204
|
+
|
|
205
|
+
log.info(
|
|
206
|
+
"run_multi_cycle: start_cycle=%s start_phase=%d total_cycles=%d",
|
|
207
|
+
start_cycle,
|
|
208
|
+
start_phase,
|
|
209
|
+
len(state.cycles),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Track which cycles have been completed during this run.
|
|
213
|
+
completed_ids: set[int] = {
|
|
214
|
+
c.id for c in state.cycles if c.status == "completed"
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
is_first_eligible = True
|
|
218
|
+
|
|
219
|
+
while True:
|
|
220
|
+
eligible = _eligible_cycles(state, completed_ids)
|
|
221
|
+
if not eligible:
|
|
222
|
+
break
|
|
223
|
+
|
|
224
|
+
for cycle in eligible:
|
|
225
|
+
# Honor --cycle N: skip cycles below the requested start.
|
|
226
|
+
if start_cycle is not None and cycle.id < start_cycle:
|
|
227
|
+
log.info("Skipping cycle %d (below start_cycle=%d).", cycle.id, start_cycle)
|
|
228
|
+
# Mark as completed so dependent cycles can run.
|
|
229
|
+
cycle.status = "completed"
|
|
230
|
+
completed_ids.add(cycle.id)
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
# Non-negotiable #6: fresh session at every cycle entry.
|
|
234
|
+
state.session_id = None
|
|
235
|
+
state.current_cycle = cycle.id
|
|
236
|
+
_state.save_state(state_path, state)
|
|
237
|
+
|
|
238
|
+
log.info("Starting cycle %d (%s).", cycle.id, cycle.name)
|
|
239
|
+
|
|
240
|
+
# start_phase applies only to the first cycle we actually run.
|
|
241
|
+
effective_start_phase = start_phase if is_first_eligible else 1
|
|
242
|
+
is_first_eligible = False
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
await _run_cycle_phases(
|
|
246
|
+
state=state,
|
|
247
|
+
cycle=cycle,
|
|
248
|
+
project_dir=project_dir,
|
|
249
|
+
runner_module=runner_module,
|
|
250
|
+
log=log,
|
|
251
|
+
start_phase=effective_start_phase,
|
|
252
|
+
)
|
|
253
|
+
except Exception as exc:
|
|
254
|
+
cycle.status = "failed"
|
|
255
|
+
state.last_error = str(exc)
|
|
256
|
+
_state.save_state(state_path, state)
|
|
257
|
+
log.error(
|
|
258
|
+
"Cycle %d (%s) failed: %s. Aborting further cycles.",
|
|
259
|
+
cycle.id,
|
|
260
|
+
cycle.name,
|
|
261
|
+
exc,
|
|
262
|
+
)
|
|
263
|
+
raise
|
|
264
|
+
|
|
265
|
+
# Cycle succeeded.
|
|
266
|
+
cycle.status = "completed"
|
|
267
|
+
completed_ids.add(cycle.id)
|
|
268
|
+
|
|
269
|
+
# Close the milestone for this cycle.
|
|
270
|
+
if cycle.milestone_number is not None:
|
|
271
|
+
try:
|
|
272
|
+
gh.close_milestone(cycle.milestone_number)
|
|
273
|
+
log.info(
|
|
274
|
+
"Closed milestone #%d for cycle %d.",
|
|
275
|
+
cycle.milestone_number,
|
|
276
|
+
cycle.id,
|
|
277
|
+
)
|
|
278
|
+
except gh.GhError as exc:
|
|
279
|
+
log.warning(
|
|
280
|
+
"Could not close milestone #%d: %s",
|
|
281
|
+
cycle.milestone_number,
|
|
282
|
+
exc,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
_state.save_state(state_path, state)
|
|
286
|
+
log.info("Cycle %d (%s) completed.", cycle.id, cycle.name)
|
|
287
|
+
|
|
288
|
+
log.info("run_multi_cycle: all eligible cycles complete.")
|
|
289
|
+
return state
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# ---------------------------------------------------------------------------
|
|
293
|
+
# Per-cycle phase runner
|
|
294
|
+
# ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
async def _run_cycle_phases(
|
|
298
|
+
state: State,
|
|
299
|
+
cycle: CycleState,
|
|
300
|
+
project_dir: Path,
|
|
301
|
+
runner_module: Any,
|
|
302
|
+
log: logging.Logger,
|
|
303
|
+
start_phase: int,
|
|
304
|
+
) -> None:
|
|
305
|
+
"""Run phases 1→7 for a single cycle.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
state: Root state (mutated in place).
|
|
309
|
+
cycle: The active cycle being executed.
|
|
310
|
+
project_dir: Project root directory.
|
|
311
|
+
runner_module: Runner module.
|
|
312
|
+
log: Logger for this cycle run.
|
|
313
|
+
start_phase: First phase to execute (1 = full, higher = resume).
|
|
314
|
+
"""
|
|
315
|
+
state_path = project_dir / ".code-generator" / "state.json"
|
|
316
|
+
|
|
317
|
+
if start_phase <= 1:
|
|
318
|
+
await phase1_plan.run(
|
|
319
|
+
state, cycle, project_dir, runner_module=runner_module,
|
|
320
|
+
logger=setup_phase_logger(f"cycle{cycle.id}_phase1", project_dir),
|
|
321
|
+
)
|
|
322
|
+
cycle.phase = 1
|
|
323
|
+
_state.save_state(state_path, state)
|
|
324
|
+
|
|
325
|
+
if start_phase <= 2:
|
|
326
|
+
await phase2_review.run(
|
|
327
|
+
state, cycle, project_dir, runner_module=runner_module,
|
|
328
|
+
logger=setup_phase_logger(f"cycle{cycle.id}_phase2", project_dir),
|
|
329
|
+
)
|
|
330
|
+
cycle.phase = 2
|
|
331
|
+
_state.save_state(state_path, state)
|
|
332
|
+
|
|
333
|
+
if start_phase <= 3:
|
|
334
|
+
await phase3_4_implement.run(
|
|
335
|
+
state, cycle, project_dir, runner_module=runner_module,
|
|
336
|
+
logger=setup_phase_logger(f"cycle{cycle.id}_phase3_4", project_dir),
|
|
337
|
+
)
|
|
338
|
+
cycle.phase = 3
|
|
339
|
+
_state.save_state(state_path, state)
|
|
340
|
+
|
|
341
|
+
if start_phase <= 4:
|
|
342
|
+
fix_result = await phase5_closure.run(
|
|
343
|
+
state, cycle, project_dir, runner_module=runner_module,
|
|
344
|
+
logger=setup_phase_logger(f"cycle{cycle.id}_phase5", project_dir),
|
|
345
|
+
)
|
|
346
|
+
cycle.phase = 4
|
|
347
|
+
_state.save_state(state_path, state)
|
|
348
|
+
if fix_result.new_issues:
|
|
349
|
+
await phase3_4_implement.run(
|
|
350
|
+
state, cycle, project_dir, runner_module=runner_module,
|
|
351
|
+
logger=setup_phase_logger(f"cycle{cycle.id}_phase3_4_fix", project_dir),
|
|
352
|
+
)
|
|
353
|
+
_state.save_state(state_path, state)
|
|
354
|
+
|
|
355
|
+
if start_phase <= 5:
|
|
356
|
+
await phase6_test.run(
|
|
357
|
+
state, cycle, project_dir, runner_module=runner_module,
|
|
358
|
+
logger=setup_phase_logger(f"cycle{cycle.id}_phase6", project_dir),
|
|
359
|
+
)
|
|
360
|
+
cycle.phase = 5
|
|
361
|
+
_state.save_state(state_path, state)
|
|
362
|
+
|
|
363
|
+
if start_phase <= 6:
|
|
364
|
+
await phase7_commit.run(
|
|
365
|
+
state, cycle, project_dir, runner_module=runner_module,
|
|
366
|
+
logger=setup_phase_logger(f"cycle{cycle.id}_phase7", project_dir),
|
|
367
|
+
)
|
|
368
|
+
cycle.phase = 7
|
|
369
|
+
_state.save_state(state_path, state)
|
|
370
|
+
|
|
371
|
+
log.info("Cycle %d phases 1-7 complete.", cycle.id)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Phase 0 — Complexity analysis and single vs. multi-cycle decision.
|
|
2
|
+
|
|
3
|
+
Uses Opus 4.6 to analyse the requirements file and populate
|
|
4
|
+
``state.mode`` and ``state.cycles``.
|
|
5
|
+
|
|
6
|
+
Non-negotiable #8: model is hard-coded to ``claude-opus-4-6``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
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
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from code_generator.state import State
|
|
24
|
+
|
|
25
|
+
_LOG = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# How many times to re-prompt when JSON is unparseable before falling back.
|
|
28
|
+
_MAX_JSON_RETRIES = 2
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_json(text: str) -> dict[str, Any] | None:
|
|
32
|
+
"""Extract and parse the first JSON object from *text*.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
text: Raw text that may contain prose around a JSON object.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Parsed dict or ``None`` when no valid JSON object is found.
|
|
39
|
+
"""
|
|
40
|
+
start = text.find("{")
|
|
41
|
+
end = text.rfind("}")
|
|
42
|
+
if start == -1 or end == -1 or end < start:
|
|
43
|
+
return None
|
|
44
|
+
try:
|
|
45
|
+
return json.loads(text[start : end + 1]) # type: ignore[no-any-return]
|
|
46
|
+
except json.JSONDecodeError:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _apply_result(state: State, data: dict[str, Any]) -> None:
|
|
51
|
+
"""Write parsed phase-0 data into *state* in place.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
state: The state object to mutate.
|
|
55
|
+
data: Dict with ``"mode"`` and optional ``"cycles"`` keys.
|
|
56
|
+
"""
|
|
57
|
+
mode = data.get("mode", "single")
|
|
58
|
+
if mode not in ("single", "multi-cycle"):
|
|
59
|
+
mode = "single"
|
|
60
|
+
state.mode = mode # type: ignore[assignment]
|
|
61
|
+
|
|
62
|
+
raw_cycles = data.get("cycles", [])
|
|
63
|
+
if isinstance(raw_cycles, list) and raw_cycles:
|
|
64
|
+
state.cycles = [
|
|
65
|
+
_state.CycleState(
|
|
66
|
+
id=c.get("id", idx + 1),
|
|
67
|
+
name=c.get("name", f"Cycle {idx + 1}"),
|
|
68
|
+
milestone_number=None,
|
|
69
|
+
milestone_title=c.get("milestone_title", f"Cycle {idx + 1}"),
|
|
70
|
+
status="open",
|
|
71
|
+
phase=0,
|
|
72
|
+
issues=[],
|
|
73
|
+
commit_sha=None,
|
|
74
|
+
depends_on=c.get("depends_on", []),
|
|
75
|
+
scope=c.get("scope", ""),
|
|
76
|
+
)
|
|
77
|
+
for idx, c in enumerate(raw_cycles)
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def run(
|
|
82
|
+
state: State,
|
|
83
|
+
project_dir: Path,
|
|
84
|
+
*,
|
|
85
|
+
runner_module: Any,
|
|
86
|
+
logger: logging.Logger | None = None,
|
|
87
|
+
) -> State:
|
|
88
|
+
"""Execute phase 0: complexity analysis.
|
|
89
|
+
|
|
90
|
+
Loads the phase-0 prompt, runs it via the rate-limit main loop,
|
|
91
|
+
parses JSON from the response, and writes ``mode`` and ``cycles``
|
|
92
|
+
into *state*.
|
|
93
|
+
|
|
94
|
+
If JSON parsing fails twice the response is re-prompted. After
|
|
95
|
+
``_MAX_JSON_RETRIES`` failures the mode falls back to ``"single"``
|
|
96
|
+
and an ERROR is logged.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
state: Current state (mutated in place and returned).
|
|
100
|
+
project_dir: The project root directory (cwd for the SDK).
|
|
101
|
+
runner_module: The runner module returned by ``get_runner()``.
|
|
102
|
+
logger: Optional phase logger; one is created when omitted.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
The mutated state with ``mode`` and ``cycles`` populated.
|
|
106
|
+
"""
|
|
107
|
+
if logger is None:
|
|
108
|
+
logger = setup_phase_logger("phase0", project_dir)
|
|
109
|
+
|
|
110
|
+
state_path = project_dir / ".code-generator" / "state.json"
|
|
111
|
+
prompt = load_prompt("prompt-phase-0-complexity.md")
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
from claude_agent_sdk import ClaudeAgentOptions # type: ignore[import-not-found]
|
|
115
|
+
except ImportError: # pragma: no cover
|
|
116
|
+
ClaudeAgentOptions = type( # type: ignore[assignment,misc]
|
|
117
|
+
"ClaudeAgentOptions",
|
|
118
|
+
(),
|
|
119
|
+
{"__init__": lambda self, **kw: self.__dict__.update(kw)},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
options = ClaudeAgentOptions(
|
|
123
|
+
model="claude-opus-4-6",
|
|
124
|
+
allowed_tools=["Read", "Glob", "Grep"],
|
|
125
|
+
cwd=str(project_dir),
|
|
126
|
+
max_turns=30,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
for attempt in range(_MAX_JSON_RETRIES + 1):
|
|
130
|
+
result = await rate_limit.main_loop(
|
|
131
|
+
runner_module,
|
|
132
|
+
prompt,
|
|
133
|
+
options,
|
|
134
|
+
state_path=state_path,
|
|
135
|
+
logger=logger,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
data = _parse_json(result.text)
|
|
139
|
+
if data is not None:
|
|
140
|
+
_apply_result(state, data)
|
|
141
|
+
logger.info("Phase 0 complete: mode=%s, cycles=%d", state.mode, len(state.cycles))
|
|
142
|
+
return state
|
|
143
|
+
|
|
144
|
+
logger.warning(
|
|
145
|
+
"Phase 0: JSON parse failed (attempt %d/%d). Text: %.200s",
|
|
146
|
+
attempt + 1,
|
|
147
|
+
_MAX_JSON_RETRIES + 1,
|
|
148
|
+
result.text,
|
|
149
|
+
)
|
|
150
|
+
if attempt < _MAX_JSON_RETRIES:
|
|
151
|
+
prompt = "Return ONLY valid JSON, no prose. " + prompt
|
|
152
|
+
|
|
153
|
+
# All retries exhausted — safe fallback.
|
|
154
|
+
logger.error(
|
|
155
|
+
"Phase 0: JSON parsing failed after %d retries. Falling back to single mode.",
|
|
156
|
+
_MAX_JSON_RETRIES,
|
|
157
|
+
)
|
|
158
|
+
state.mode = "single" # type: ignore[assignment]
|
|
159
|
+
return state
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Phase 1 — Planning: milestone creation and issue seeding.
|
|
2
|
+
|
|
3
|
+
Uses Opus 4.6 to read the requirements and create GitHub Issues
|
|
4
|
+
(via Bash tool calls inside the SDK session).
|
|
5
|
+
|
|
6
|
+
Non-negotiable #8: model is hard-coded to ``claude-opus-4-6``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from code_generator import gh
|
|
14
|
+
from code_generator import state as _state
|
|
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, State
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Phase1NoIssuesError(RuntimeError):
|
|
27
|
+
"""Phase 1 completed but created zero GitHub issues — abort the pipeline."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _agent_from_labels(labels: list[dict[str, Any]]) -> str: # type: ignore[type-arg]
|
|
31
|
+
"""Extract the agent name from a list of label dicts returned by gh.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
labels: List of dicts with at least a ``"name"`` key.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The agent name (without ``"agent:"`` prefix) or ``"python-pro"``
|
|
38
|
+
when no ``agent:`` label is present.
|
|
39
|
+
"""
|
|
40
|
+
for lbl in labels:
|
|
41
|
+
name: str = lbl.get("name", "")
|
|
42
|
+
if name.startswith("agent:"):
|
|
43
|
+
return name[len("agent:"):]
|
|
44
|
+
return "python-pro"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _build_issue_states(raw_issues: list[dict[str, Any]]) -> list[_state.IssueState]: # type: ignore[type-arg]
|
|
48
|
+
"""Convert raw gh issue dicts into IssueState objects.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
raw_issues: List of dicts as returned by ``gh.list_issues()``.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
List of :class:`~code_generator.state.IssueState` instances.
|
|
55
|
+
"""
|
|
56
|
+
return [
|
|
57
|
+
_state.IssueState(
|
|
58
|
+
number=issue["number"],
|
|
59
|
+
status="open",
|
|
60
|
+
agent=_agent_from_labels(issue.get("labels", [])),
|
|
61
|
+
)
|
|
62
|
+
for issue in raw_issues
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def run(
|
|
67
|
+
state: State,
|
|
68
|
+
cycle: CycleState | None,
|
|
69
|
+
project_dir: Path,
|
|
70
|
+
*,
|
|
71
|
+
runner_module: Any,
|
|
72
|
+
logger: logging.Logger | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Execute phase 1: planning.
|
|
75
|
+
|
|
76
|
+
Seeds standard labels, optionally creates a milestone (multi-cycle),
|
|
77
|
+
runs the planning prompt, then populates ``state.issues`` or
|
|
78
|
+
``cycle.issues`` from the newly-created GitHub issues.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
state: Root state object (mutated in place).
|
|
82
|
+
cycle: Active cycle (multi-cycle mode) or ``None`` (single mode).
|
|
83
|
+
project_dir: Project root directory.
|
|
84
|
+
runner_module: Runner module from ``get_runner()``.
|
|
85
|
+
logger: Optional phase logger.
|
|
86
|
+
"""
|
|
87
|
+
if logger is None:
|
|
88
|
+
logger = setup_phase_logger("phase1", project_dir)
|
|
89
|
+
|
|
90
|
+
state_path = project_dir / ".code-generator" / "state.json"
|
|
91
|
+
|
|
92
|
+
# Seed standard label taxonomy — idempotent.
|
|
93
|
+
gh.ensure_standard_labels()
|
|
94
|
+
|
|
95
|
+
# Multi-cycle: create the milestone if not yet done.
|
|
96
|
+
milestone_title: str | None = None
|
|
97
|
+
if cycle is not None:
|
|
98
|
+
milestone_title = cycle.milestone_title
|
|
99
|
+
if cycle.milestone_number is None:
|
|
100
|
+
cycle.milestone_number = gh.create_milestone(
|
|
101
|
+
cycle.milestone_title,
|
|
102
|
+
cycle.scope,
|
|
103
|
+
)
|
|
104
|
+
logger.info("Created milestone %r → #%d", cycle.milestone_title, cycle.milestone_number)
|
|
105
|
+
|
|
106
|
+
# Determine completed cycle names for the prompt.
|
|
107
|
+
completed_cycles: str = "none"
|
|
108
|
+
if state.cycles:
|
|
109
|
+
closed = [c.name for c in state.cycles if c.status == "closed"]
|
|
110
|
+
if closed:
|
|
111
|
+
completed_cycles = ", ".join(closed)
|
|
112
|
+
|
|
113
|
+
prompt = load_prompt(
|
|
114
|
+
"prompt-phase-1-planning.md",
|
|
115
|
+
REQUIREMENTS_PATH=".code-generator/requirements.md",
|
|
116
|
+
CYCLE_SCOPE=(
|
|
117
|
+
cycle.scope
|
|
118
|
+
if cycle is not None
|
|
119
|
+
else "single-cycle — implement all features from requirements.md"
|
|
120
|
+
),
|
|
121
|
+
MILESTONE_TITLE=milestone_title if milestone_title is not None else "",
|
|
122
|
+
COMPLETED_CYCLES=completed_cycles,
|
|
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-opus-4-6",
|
|
136
|
+
allowed_tools=["Read", "Bash", "Glob", "Grep"],
|
|
137
|
+
cwd=str(project_dir),
|
|
138
|
+
max_turns=60,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
await rate_limit.main_loop(
|
|
142
|
+
runner_module,
|
|
143
|
+
prompt,
|
|
144
|
+
options,
|
|
145
|
+
state_path=state_path,
|
|
146
|
+
logger=logger,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Fetch the issues that were created by the planning session.
|
|
150
|
+
raw_issues = gh.list_issues(
|
|
151
|
+
milestone=milestone_title,
|
|
152
|
+
state="open",
|
|
153
|
+
)
|
|
154
|
+
issue_states = _build_issue_states(raw_issues)
|
|
155
|
+
|
|
156
|
+
if not issue_states:
|
|
157
|
+
raise Phase1NoIssuesError(
|
|
158
|
+
"Phase 1 finished without creating any GitHub issues. "
|
|
159
|
+
"Opus likely misread the task (e.g. audited existing code instead "
|
|
160
|
+
"of planning new work). Inspect .code-generator/logs/phase1.log, "
|
|
161
|
+
"adjust prompt-phase-1-planning.md if needed, and re-run."
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if cycle is not None:
|
|
165
|
+
cycle.issues = issue_states
|
|
166
|
+
else:
|
|
167
|
+
state.issues = issue_states
|
|
168
|
+
|
|
169
|
+
_state.save_state(state_path, state)
|
|
170
|
+
logger.info("Phase 1 complete: %d issues created.", len(issue_states))
|