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,252 @@
1
+ """generate command — orchestrate a full project generation pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Annotated
9
+
10
+ import typer
11
+
12
+ from code_generator import env
13
+ from code_generator import state as state_module
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Pause guard
17
+ # ---------------------------------------------------------------------------
18
+
19
+
20
+ def _check_paused(st: state_module.State) -> bool:
21
+ """Return True and print a message when the state is paused for rate limit.
22
+
23
+ Args:
24
+ st: Root state to inspect.
25
+
26
+ Returns:
27
+ True when the state indicates an active rate-limit pause.
28
+ """
29
+ if st.paused_until is not None and st.paused_until > time.time():
30
+ remaining_s = st.paused_until - time.time()
31
+ remaining_m = remaining_s / 60
32
+ typer.echo(
33
+ f"Rate-limit pause active — {remaining_m:.1f} minutes remaining. "
34
+ "Re-run after the reset window expires.",
35
+ err=False,
36
+ )
37
+ return True
38
+ return False
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Orchestration dispatchers
43
+ # ---------------------------------------------------------------------------
44
+
45
+
46
+ async def _dispatch_async(
47
+ st: state_module.State,
48
+ project_dir: Path,
49
+ *,
50
+ dry_run: bool,
51
+ start_phase: int,
52
+ start_cycle: int | None,
53
+ mode: str,
54
+ ) -> None:
55
+ """Async entry point for the orchestration pipeline.
56
+
57
+ Args:
58
+ st: Root state object.
59
+ project_dir: Project root directory.
60
+ dry_run: When True, only phases 0 and 1 run.
61
+ start_phase: Resume from this phase (1 = full run).
62
+ start_cycle: Resume from this cycle (multi-cycle only).
63
+ mode: ``"auto"``, ``"single"``, or ``"multi-cycle"``.
64
+ """
65
+ from code_generator.logging_setup import setup_phase_logger
66
+ from code_generator.orchestrator import cycle_loop, phase0_complexity
67
+ from code_generator.runner import get_runner
68
+
69
+ runner_module = get_runner()
70
+ logger = setup_phase_logger("generate", project_dir)
71
+
72
+ # Phase 0: determine mode when not already known.
73
+ needs_phase0 = mode == "auto" and st.mode not in ("single", "multi-cycle")
74
+ if mode == "auto":
75
+ needs_phase0 = st.mode not in ("single", "multi-cycle")
76
+ else:
77
+ needs_phase0 = False
78
+ # Force the mode from the flag.
79
+ st.mode = mode # type: ignore[assignment]
80
+
81
+ if needs_phase0:
82
+ await phase0_complexity.run(
83
+ st, project_dir, runner_module=runner_module, logger=logger
84
+ )
85
+
86
+ # Dry-run: planning only (phase 0 + phase 1).
87
+ if dry_run:
88
+ from code_generator.orchestrator import phase1_plan
89
+
90
+ logger.info("--dry-run: running phase 1 (planning only).")
91
+ await phase1_plan.run(
92
+ st, None, project_dir, runner_module=runner_module, logger=logger
93
+ )
94
+ return
95
+
96
+ # Dispatch to cycle loop.
97
+ if st.mode == "multi-cycle":
98
+ await cycle_loop.run_multi_cycle(
99
+ st,
100
+ project_dir,
101
+ runner_module=runner_module,
102
+ logger=logger,
103
+ start_cycle=start_cycle,
104
+ start_phase=start_phase,
105
+ )
106
+ else:
107
+ await cycle_loop.run_single_mode(
108
+ st,
109
+ project_dir,
110
+ runner_module=runner_module,
111
+ logger=logger,
112
+ start_phase=start_phase,
113
+ )
114
+
115
+
116
+ def _dispatch_orchestrator(
117
+ st: state_module.State,
118
+ project_dir: Path,
119
+ *,
120
+ dry_run: bool,
121
+ phase: int | None,
122
+ continue_: bool,
123
+ mode: str,
124
+ cycle: int | None,
125
+ ) -> None:
126
+ """Resolve resume parameters and dispatch the async orchestrator.
127
+
128
+ Args:
129
+ st: Root state.
130
+ project_dir: Project root directory.
131
+ dry_run: When True, only phase 0/1 planning runs.
132
+ phase: Resume from this explicit phase number.
133
+ continue_: Resume from the last completed phase/cycle in state.
134
+ mode: ``"auto"``, ``"single"``, or ``"multi-cycle"``.
135
+ cycle: Resume from this specific cycle (multi-cycle only).
136
+ """
137
+ # Resolve effective mode (honor existing state when --mode auto).
138
+ effective_mode = mode
139
+ if mode == "auto" and st.mode in ("single", "multi-cycle"):
140
+ effective_mode = st.mode
141
+
142
+ # Resolve start_phase and start_cycle.
143
+ start_phase = 1
144
+ start_cycle: int | None = None
145
+
146
+ if continue_:
147
+ # Resume from where the previous run left off.
148
+ if effective_mode == "multi-cycle":
149
+ start_cycle = st.current_cycle
150
+ start_phase = st.phase or 1
151
+ else:
152
+ start_phase = st.phase or 1
153
+
154
+ elif phase is not None:
155
+ start_phase = phase
156
+
157
+ if cycle is not None:
158
+ # --cycle N overrides any continue-derived start_cycle.
159
+ start_cycle = cycle
160
+ start_phase = phase or 1
161
+
162
+ asyncio.run(
163
+ _dispatch_async(
164
+ st,
165
+ project_dir,
166
+ dry_run=dry_run,
167
+ start_phase=start_phase,
168
+ start_cycle=start_cycle,
169
+ mode=effective_mode,
170
+ )
171
+ )
172
+
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Typer command
176
+ # ---------------------------------------------------------------------------
177
+
178
+
179
+ def generate_command(
180
+ dry_run: Annotated[
181
+ bool,
182
+ typer.Option("--dry-run/--no-dry-run", help="Print what would happen without doing it."),
183
+ ] = False,
184
+ phase: Annotated[
185
+ int | None,
186
+ typer.Option("--phase", help="Run only a specific phase number."),
187
+ ] = None,
188
+ continue_: Annotated[
189
+ bool,
190
+ typer.Option("--continue/--no-continue", help="Resume a paused generation run."),
191
+ ] = False,
192
+ mode: Annotated[
193
+ str,
194
+ typer.Option("--mode", help="Generation mode: auto, single, or multi-cycle."),
195
+ ] = "auto",
196
+ cycle: Annotated[
197
+ int | None,
198
+ typer.Option("--cycle", help="Target a specific cycle (multi-cycle mode only)."),
199
+ ] = None,
200
+ ) -> None:
201
+ """Run the full code-generation pipeline."""
202
+ env.assert_safe_environment()
203
+
204
+ if phase is not None and continue_:
205
+ typer.echo(
206
+ "ERROR: --phase and --continue are mutually exclusive.",
207
+ err=True,
208
+ )
209
+ raise typer.Exit(code=2)
210
+
211
+ if cycle is not None and mode not in ("multi-cycle",) and not continue_:
212
+ typer.echo(
213
+ "ERROR: --cycle is only valid with --mode multi-cycle or --continue.",
214
+ err=True,
215
+ )
216
+ raise typer.Exit(code=2)
217
+
218
+ project_dir = Path.cwd()
219
+ cg_dir = project_dir / ".code-generator"
220
+
221
+ if not cg_dir.exists():
222
+ typer.echo(
223
+ "ERROR: .code-generator/ not found. Run `code-generator init` first.",
224
+ err=True,
225
+ )
226
+ raise typer.Exit(code=1)
227
+
228
+ st = state_module.load_state(cg_dir / "state.json")
229
+
230
+ # Rate-limit pause guard: inform user and exit cleanly.
231
+ if _check_paused(st):
232
+ raise typer.Exit(code=0)
233
+
234
+ try:
235
+ _dispatch_orchestrator(
236
+ st,
237
+ project_dir,
238
+ dry_run=dry_run,
239
+ phase=phase,
240
+ continue_=continue_,
241
+ mode=mode,
242
+ cycle=cycle,
243
+ )
244
+ except NotImplementedError as exc:
245
+ typer.echo(f"ERROR: {exc}", err=True)
246
+ raise typer.Exit(code=1) from exc
247
+ except Exception as exc: # noqa: BLE001
248
+ from rich.console import Console
249
+ from rich.traceback import Traceback
250
+
251
+ Console(stderr=True).print(Traceback())
252
+ raise typer.Exit(code=1) from exc
@@ -0,0 +1,72 @@
1
+ """init command — scaffold .code-generator/ with a requirements.md template."""
2
+
3
+ import importlib.resources
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from code_generator import state as state_module
10
+
11
+ _VALID_TEMPLATES = frozenset(
12
+ {"fastapi", "angular", "nestjs", "fullstack", "python-cli", "finance"}
13
+ )
14
+
15
+
16
+ def _read_template(name: str) -> str:
17
+ return (
18
+ importlib.resources.files("code_generator.templates")
19
+ .joinpath(name + ".md")
20
+ .read_text(encoding="utf-8")
21
+ )
22
+
23
+
24
+ def init_command(
25
+ template: Annotated[
26
+ str | None,
27
+ typer.Option(
28
+ "--template",
29
+ help=(
30
+ "Append a variant section. One of: "
31
+ "fastapi, angular, nestjs, fullstack, python-cli, finance."
32
+ ),
33
+ ),
34
+ ] = None,
35
+ force: Annotated[
36
+ bool,
37
+ typer.Option("--force", help="Overwrite an existing .code-generator/ directory."),
38
+ ] = False,
39
+ ) -> None:
40
+ """Scaffold .code-generator/ with a ready-to-fill requirements.md."""
41
+ if template is not None and template not in _VALID_TEMPLATES:
42
+ typer.echo(
43
+ f"ERROR: unknown template {template!r}. "
44
+ f"Valid values: {', '.join(sorted(_VALID_TEMPLATES))}",
45
+ err=True,
46
+ )
47
+ raise typer.Exit(code=1)
48
+
49
+ project_dir = Path.cwd()
50
+ cg_dir = project_dir / ".code-generator"
51
+
52
+ if cg_dir.exists() and not force:
53
+ typer.echo(
54
+ "ERROR: .code-generator/ already exists. Use --force to overwrite.",
55
+ err=True,
56
+ )
57
+ raise typer.Exit(code=1)
58
+
59
+ cg_dir.mkdir(parents=True, exist_ok=True)
60
+
61
+ content = _read_template("base")
62
+ if template is not None:
63
+ content += _read_template(template)
64
+
65
+ (cg_dir / "requirements.md").write_text(content, encoding="utf-8")
66
+
67
+ fresh_state = state_module.load_state(cg_dir / "state.json.missing")
68
+ state_module.save_state(cg_dir / "state.json", fresh_state)
69
+
70
+ typer.echo(f"Initialized .code-generator/ in {project_dir}")
71
+ if template:
72
+ typer.echo(f"Template applied: {template}")
@@ -0,0 +1,117 @@
1
+ """review command — run a standalone code-review session via Claude."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from pathlib import Path
7
+ from typing import Annotated
8
+
9
+ import typer
10
+
11
+ from code_generator import env
12
+ from code_generator.logging_setup import setup_phase_logger
13
+ from code_generator.prompts import load_prompt
14
+ from code_generator.runner import get_runner, rate_limit
15
+ from code_generator.runner.retry import CircuitOpen
16
+ from code_generator.runner.sdk_runner import OverageAbort, RunResult
17
+
18
+ try:
19
+ from claude_agent_sdk import ClaudeAgentOptions
20
+
21
+ _SDK_AVAILABLE = True
22
+ except ImportError:
23
+ _SDK_AVAILABLE = False
24
+ ClaudeAgentOptions = None # type: ignore[assignment,misc]
25
+
26
+
27
+ def _build_options() -> object:
28
+ """Build ClaudeAgentOptions for the review session.
29
+
30
+ Returns:
31
+ ClaudeAgentOptions if the SDK is available, else a duck-typed fallback.
32
+ """
33
+ if _SDK_AVAILABLE and ClaudeAgentOptions is not None:
34
+ return ClaudeAgentOptions(
35
+ model="claude-opus-4-6",
36
+ allowed_tools=["Read", "Grep", "Glob", "Bash"],
37
+ max_turns=30,
38
+ cwd=str(Path.cwd()),
39
+ )
40
+
41
+ # Fallback duck-typed options for when SDK is absent (subprocess runner).
42
+ class _FallbackOptions:
43
+ model = "claude-opus-4-6"
44
+ allowed_tools = ["Read", "Grep", "Glob", "Bash"]
45
+ max_turns = 30
46
+ cwd = str(Path.cwd())
47
+ permission_mode: str | None = None
48
+ resume: str | None = None
49
+
50
+ return _FallbackOptions()
51
+
52
+
53
+ async def _run_review_session(prompt: str, project_dir: Path) -> RunResult:
54
+ """Execute the review session via the main runner loop.
55
+
56
+ Args:
57
+ prompt: The fully rendered review prompt.
58
+ project_dir: Root of the project being reviewed (used for state + logs).
59
+
60
+ Returns:
61
+ RunResult from the completed session.
62
+
63
+ Raises:
64
+ OverageAbort: When overage billing is detected.
65
+ CircuitOpen: When the circuit breaker trips on transient failures.
66
+ """
67
+ runner = get_runner()
68
+ logger = setup_phase_logger("review", project_dir)
69
+ options = _build_options()
70
+ state_path = project_dir / ".code-generator" / "state.json"
71
+
72
+ return await rate_limit.main_loop(
73
+ runner,
74
+ prompt,
75
+ options,
76
+ state_path=state_path,
77
+ logger=logger,
78
+ )
79
+
80
+
81
+ def review_command(
82
+ create_issues: Annotated[
83
+ bool,
84
+ typer.Option(
85
+ "--create-issues/--no-create-issues",
86
+ help="Automatically create GitHub issues for findings.",
87
+ ),
88
+ ] = False,
89
+ severity: Annotated[
90
+ str,
91
+ typer.Option(
92
+ "--severity",
93
+ help="Minimum severity to report: low, medium, high, or critical.",
94
+ ),
95
+ ] = "low",
96
+ ) -> None:
97
+ """Run a standalone code review against the current repository."""
98
+ env.assert_safe_environment()
99
+
100
+ prompt = load_prompt(
101
+ "prompt-review.md",
102
+ CREATE_ISSUES=str(create_issues).lower(),
103
+ SEVERITY_FILTER=severity,
104
+ )
105
+
106
+ project_dir = Path.cwd()
107
+
108
+ try:
109
+ result = asyncio.run(_run_review_session(prompt, project_dir))
110
+ typer.echo(result.text)
111
+ raise typer.Exit(code=0)
112
+ except OverageAbort as exc:
113
+ typer.echo(f"ERROR: {exc}", err=True)
114
+ raise typer.Exit(code=2) from exc
115
+ except CircuitOpen as exc:
116
+ typer.echo(f"ERROR: {exc}", err=True)
117
+ raise typer.Exit(code=1) from exc
@@ -0,0 +1,83 @@
1
+ """status command — display current project generation state."""
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from code_generator import state as state_module
10
+
11
+
12
+ def status_command() -> None:
13
+ """Show the current generation state for this project."""
14
+ project_dir = Path.cwd()
15
+ state_path = project_dir / ".code-generator" / "state.json"
16
+
17
+ if not state_path.exists():
18
+ typer.echo(
19
+ "no .code-generator/state.json in this directory — "
20
+ "run `code-generator init` first"
21
+ )
22
+ raise typer.Exit(code=0)
23
+
24
+ st = state_module.load_state(state_path)
25
+ console = Console(file=typer.get_text_stream("stdout"))
26
+
27
+ _render_summary_table(console, st)
28
+
29
+ if st.mode == "multi-cycle" and st.cycles:
30
+ _render_cycles_table(console, st)
31
+
32
+
33
+ def _pause_remaining(state: state_module.State) -> str:
34
+ import time
35
+
36
+ if state.paused_until is None:
37
+ return "-"
38
+ remaining = state.paused_until - time.time()
39
+ if remaining <= 0:
40
+ return "-"
41
+ minutes = remaining / 60
42
+ return f"{minutes:.1f} minutes remaining"
43
+
44
+
45
+ def _render_summary_table(console: Console, st: state_module.State) -> None:
46
+ table = Table(title="code-generator status")
47
+ table.add_column("Field", style="bold")
48
+ table.add_column("Value")
49
+
50
+ open_issues = sum(1 for i in st.issues if i.status == "open")
51
+ closed_issues = sum(1 for i in st.issues if i.status == "closed")
52
+
53
+ table.add_row("mode", st.mode)
54
+ table.add_row("current cycle", str(st.current_cycle) if st.current_cycle is not None else "-")
55
+ table.add_row("current phase", str(st.phase) if st.phase is not None else "-")
56
+ table.add_row("open issues", str(open_issues))
57
+ table.add_row("closed issues", str(closed_issues))
58
+ table.add_row("rate-limit pause", _pause_remaining(st))
59
+ table.add_row("last error", st.last_error if st.last_error else "-")
60
+
61
+ console.print(table)
62
+
63
+
64
+ def _render_cycles_table(console: Console, st: state_module.State) -> None:
65
+ table = Table(title="cycles")
66
+ table.add_column("id")
67
+ table.add_column("name")
68
+ table.add_column("status")
69
+ table.add_column("phase")
70
+ table.add_column("closed/total")
71
+
72
+ for cycle in st.cycles:
73
+ closed = sum(1 for i in cycle.issues if i.status == "closed")
74
+ total = len(cycle.issues)
75
+ table.add_row(
76
+ str(cycle.id),
77
+ cycle.name,
78
+ cycle.status,
79
+ str(cycle.phase),
80
+ f"{closed}/{total}",
81
+ )
82
+
83
+ console.print(table)
code_generator/env.py ADDED
@@ -0,0 +1,55 @@
1
+ """Environment safety module.
2
+
3
+ Strips dangerous environment variables before any Claude Code invocation
4
+ to guarantee zero API credit usage (Max subscription only).
5
+
6
+ Non-negotiable constraint #1: no API keys must reach the Claude CLI.
7
+ Reference: ITAL-IA/kb/run-deep-research.py build_agent_env().
8
+ """
9
+
10
+ import os
11
+ import sys
12
+
13
+ DANGEROUS_VARS: tuple[str, ...] = (
14
+ "ANTHROPIC_API_KEY",
15
+ "ANTHROPIC_AUTH_TOKEN",
16
+ "ANTHROPIC_BEDROCK_API_KEY",
17
+ "ANTHROPIC_VERTEX_PROJECT_ID",
18
+ "CLAUDE_CODE_USE_BEDROCK",
19
+ "CLAUDE_CODE_USE_VERTEX",
20
+ )
21
+
22
+
23
+ def strip_dangerous_env() -> None:
24
+ """Remove every dangerous variable from the current process environment."""
25
+ for var in DANGEROUS_VARS:
26
+ os.environ.pop(var, None)
27
+
28
+
29
+ def build_agent_env() -> dict[str, str]:
30
+ """Return a sanitized copy of the environment suitable for subprocess.run(env=...).
31
+
32
+ The returned dict contains every variable except those in DANGEROUS_VARS.
33
+ The calling process's os.environ is not mutated.
34
+ """
35
+ return {k: v for k, v in os.environ.items() if k not in DANGEROUS_VARS}
36
+
37
+
38
+ def assert_safe_environment() -> None:
39
+ """Strip dangerous env vars from the current process, warning about each.
40
+
41
+ The process environment is mutated in place so every downstream SDK and
42
+ subprocess call inherits a clean environment — matching the pattern from
43
+ ITAL-IA/kb/run-deep-research.py build_agent_env(). The user does not need
44
+ to unset these variables in their shell.
45
+ """
46
+ offenders = [var for var in DANGEROUS_VARS if var in os.environ]
47
+ if not offenders:
48
+ return
49
+
50
+ listed = ", ".join(offenders)
51
+ print(
52
+ f"Using Max subscription (ignoring {listed} for this process).",
53
+ file=sys.stderr,
54
+ )
55
+ strip_dangerous_env()