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.
Files changed (49) hide show
  1. claude_code_generator-0.1.0.dist-info/METADATA +176 -0
  2. claude_code_generator-0.1.0.dist-info/RECORD +49 -0
  3. claude_code_generator-0.1.0.dist-info/WHEEL +5 -0
  4. claude_code_generator-0.1.0.dist-info/entry_points.txt +2 -0
  5. claude_code_generator-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. claude_code_generator-0.1.0.dist-info/top_level.txt +1 -0
  7. code_generator/__init__.py +3 -0
  8. code_generator/agents.py +177 -0
  9. code_generator/cli.py +49 -0
  10. code_generator/commands/__init__.py +1 -0
  11. code_generator/commands/generate.py +252 -0
  12. code_generator/commands/init.py +72 -0
  13. code_generator/commands/review.py +117 -0
  14. code_generator/commands/status.py +83 -0
  15. code_generator/env.py +55 -0
  16. code_generator/gh.py +331 -0
  17. code_generator/logging_setup.py +73 -0
  18. code_generator/orchestrator/__init__.py +4 -0
  19. code_generator/orchestrator/cycle_loop.py +371 -0
  20. code_generator/orchestrator/phase0_complexity.py +159 -0
  21. code_generator/orchestrator/phase1_plan.py +170 -0
  22. code_generator/orchestrator/phase2_review.py +126 -0
  23. code_generator/orchestrator/phase3_4_implement.py +164 -0
  24. code_generator/orchestrator/phase5_closure.py +154 -0
  25. code_generator/orchestrator/phase6_test.py +98 -0
  26. code_generator/orchestrator/phase7_commit.py +167 -0
  27. code_generator/prompts/__init__.py +86 -0
  28. code_generator/prompts/prompt-phase-0-complexity.md +85 -0
  29. code_generator/prompts/prompt-phase-1-planning.md +209 -0
  30. code_generator/prompts/prompt-phase-2-issue-review.md +84 -0
  31. code_generator/prompts/prompt-phase-3-implementation.md +191 -0
  32. code_generator/prompts/prompt-phase-5-final-review.md +135 -0
  33. code_generator/prompts/prompt-phase-6-test.md +102 -0
  34. code_generator/prompts/prompt-phase-7-commit.md +103 -0
  35. code_generator/prompts/prompt-review.md +124 -0
  36. code_generator/runner/__init__.py +26 -0
  37. code_generator/runner/rate_limit.py +113 -0
  38. code_generator/runner/retry.py +165 -0
  39. code_generator/runner/sdk_runner.py +267 -0
  40. code_generator/runner/subprocess_runner.py +200 -0
  41. code_generator/state.py +178 -0
  42. code_generator/templates/__init__.py +1 -0
  43. code_generator/templates/angular.md +12 -0
  44. code_generator/templates/base.md +28 -0
  45. code_generator/templates/fastapi.md +12 -0
  46. code_generator/templates/finance.md +9 -0
  47. code_generator/templates/fullstack.md +24 -0
  48. code_generator/templates/nestjs.md +9 -0
  49. 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))