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
code_generator/gh.py ADDED
@@ -0,0 +1,331 @@
1
+ """Thin subprocess wrapper around the `gh` CLI.
2
+
3
+ All GitHub operations in the orchestrator go through this module.
4
+ No direct subprocess.run() for gh is allowed outside this file.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import subprocess
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Exception
14
+ # ---------------------------------------------------------------------------
15
+
16
+
17
+ class GhError(RuntimeError):
18
+ """Raised when a `gh` subprocess exits with a non-zero return code.
19
+
20
+ Attributes:
21
+ returncode: The exit code from the gh process.
22
+ stderr: The stderr output from the gh process.
23
+ """
24
+
25
+ def __init__(self, returncode: int, stderr: str) -> None:
26
+ self.returncode = returncode
27
+ self.stderr = stderr
28
+ super().__init__(f"gh exited with code {returncode}: {stderr!r}")
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Internal helper
33
+ # ---------------------------------------------------------------------------
34
+
35
+
36
+ def _run(argv: list[str], *, input: str | None = None) -> str: # noqa: A002
37
+ """Run a gh sub-command and return stdout on success.
38
+
39
+ Args:
40
+ argv: Full argument list including the ``gh`` binary, e.g.
41
+ ``["gh", "issue", "list"]``.
42
+ input: Optional string piped to stdin.
43
+
44
+ Returns:
45
+ Stripped stdout string.
46
+
47
+ Raises:
48
+ GhError: When the process exits with a non-zero return code.
49
+ """
50
+ result = subprocess.run(
51
+ argv,
52
+ capture_output=True,
53
+ text=True,
54
+ check=False,
55
+ input=input,
56
+ )
57
+ if result.returncode != 0:
58
+ raise GhError(result.returncode, result.stderr)
59
+ return result.stdout.strip()
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # Repository discovery helper
64
+ # ---------------------------------------------------------------------------
65
+
66
+
67
+ def _repo_name_with_owner() -> str:
68
+ """Return the ``owner/repo`` string for the current repository.
69
+
70
+ Returns:
71
+ A string like ``"SilvioBaratto/code-generator"``.
72
+
73
+ Raises:
74
+ GhError: When gh cannot determine the repo.
75
+ """
76
+ return _run(["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Public API
81
+ # ---------------------------------------------------------------------------
82
+
83
+
84
+ def create_issue(
85
+ title: str,
86
+ body: str,
87
+ labels: list[str],
88
+ *,
89
+ milestone: str | None = None,
90
+ assignee: str = "@me",
91
+ ) -> int:
92
+ """Create a GitHub issue and return its number.
93
+
94
+ Args:
95
+ title: Issue title.
96
+ body: Issue body (Markdown).
97
+ labels: List of label names to attach.
98
+ milestone: Optional milestone title to assign the issue to.
99
+ assignee: GitHub username or ``"@me"`` (default).
100
+
101
+ Returns:
102
+ The integer issue number parsed from the returned URL.
103
+
104
+ Raises:
105
+ GhError: When gh fails.
106
+ """
107
+ argv = [
108
+ "gh", "issue", "create",
109
+ "--title", title,
110
+ "--body", body,
111
+ "--label", ",".join(labels),
112
+ "--assignee", assignee,
113
+ ]
114
+ if milestone is not None:
115
+ argv += ["--milestone", milestone]
116
+
117
+ url = _run(argv)
118
+ # URL looks like: https://github.com/owner/repo/issues/42
119
+ return int(url.rstrip("/").split("/")[-1])
120
+
121
+
122
+ def view_issue(number: int) -> dict: # type: ignore[type-arg]
123
+ """Fetch a single issue as a dict.
124
+
125
+ Args:
126
+ number: The issue number.
127
+
128
+ Returns:
129
+ Parsed JSON dict with keys ``title``, ``body``, ``state``,
130
+ ``labels``, and ``number``.
131
+
132
+ Raises:
133
+ GhError: When gh fails.
134
+ """
135
+ raw = _run([
136
+ "gh", "issue", "view", str(number),
137
+ "--json", "title,body,state,labels,number",
138
+ ])
139
+ return json.loads(raw) # type: ignore[no-any-return]
140
+
141
+
142
+ def close_issue(
143
+ number: int,
144
+ *,
145
+ reason: str = "completed",
146
+ comment: str | None = None,
147
+ ) -> None:
148
+ """Close an issue.
149
+
150
+ Args:
151
+ number: The issue number.
152
+ reason: Close reason — ``"completed"`` or ``"not planned"``.
153
+ comment: Optional comment to post alongside closure.
154
+
155
+ Raises:
156
+ GhError: When gh fails.
157
+ """
158
+ argv = ["gh", "issue", "close", str(number), "--reason", reason]
159
+ if comment is not None:
160
+ argv += ["--comment", comment]
161
+ _run(argv)
162
+
163
+
164
+ def list_issues(
165
+ *,
166
+ milestone: str | None = None,
167
+ state: str = "open",
168
+ ) -> list[dict]: # type: ignore[type-arg]
169
+ """List issues, optionally filtered by milestone.
170
+
171
+ Args:
172
+ milestone: Milestone title to filter by.
173
+ state: Issue state — ``"open"``, ``"closed"``, or ``"all"``.
174
+
175
+ Returns:
176
+ List of dicts with keys ``number``, ``title``, ``state``,
177
+ ``labels``.
178
+
179
+ Raises:
180
+ GhError: When gh fails.
181
+ """
182
+ argv = [
183
+ "gh", "issue", "list",
184
+ "--state", state,
185
+ "--json", "number,title,state,labels",
186
+ ]
187
+ if milestone is not None:
188
+ argv += ["--milestone", milestone]
189
+
190
+ raw = _run(argv)
191
+ return json.loads(raw) # type: ignore[no-any-return]
192
+
193
+
194
+ def create_milestone(title: str, description: str = "") -> int:
195
+ """Create a GitHub milestone via the API and return its number.
196
+
197
+ Args:
198
+ title: Milestone title.
199
+ description: Optional description.
200
+
201
+ Returns:
202
+ The integer milestone number from the API response.
203
+
204
+ Raises:
205
+ GhError: When gh fails.
206
+ """
207
+ nwo = _repo_name_with_owner()
208
+ raw = _run([
209
+ "gh", "api",
210
+ "--method", "POST",
211
+ f"repos/{nwo}/milestones",
212
+ "-f", f"title={title}",
213
+ "-f", f"description={description}",
214
+ "-f", "state=open",
215
+ ])
216
+ data = json.loads(raw)
217
+ return int(data["number"])
218
+
219
+
220
+ def close_milestone(number: int) -> None:
221
+ """Close a GitHub milestone via the API.
222
+
223
+ Args:
224
+ number: The milestone number.
225
+
226
+ Raises:
227
+ GhError: When gh fails.
228
+ """
229
+ nwo = _repo_name_with_owner()
230
+ _run([
231
+ "gh", "api",
232
+ "--method", "PATCH",
233
+ f"repos/{nwo}/milestones/{number}",
234
+ "-f", "state=closed",
235
+ ])
236
+
237
+
238
+ def ensure_label(name: str, color: str, description: str) -> None:
239
+ """Create a label, ignoring "already exists" errors.
240
+
241
+ Args:
242
+ name: Label name, e.g. ``"priority:high"``.
243
+ color: Hex colour without leading ``#``, e.g. ``"FF0000"``.
244
+ description: Human-readable description.
245
+
246
+ Raises:
247
+ GhError: On any error other than "already exists".
248
+ """
249
+ try:
250
+ _run([
251
+ "gh", "label", "create", name,
252
+ "--color", color,
253
+ "--description", description,
254
+ ])
255
+ except GhError as exc:
256
+ if "already exists" in exc.stderr.lower():
257
+ return
258
+ raise
259
+
260
+
261
+ def add_label(issue_number: int, label: str) -> None:
262
+ """Add a label to an existing issue.
263
+
264
+ Args:
265
+ issue_number: The issue number.
266
+ label: Label name to add.
267
+
268
+ Raises:
269
+ GhError: When gh fails.
270
+ """
271
+ _run(["gh", "issue", "edit", str(issue_number), "--add-label", label])
272
+
273
+
274
+ def ensure_standard_labels() -> None:
275
+ """Seed the repository with the standard label taxonomy.
276
+
277
+ Creates all labels defined in requirements.md §8. Idempotent —
278
+ "already exists" errors are silently ignored.
279
+
280
+ Labels created:
281
+ - ``priority:{high,medium,low}``
282
+ - ``area:{cli,orchestrator,runner,state,prompts,gh,tests,docs,api,
283
+ frontend,database,auth,infra}``
284
+ - ``phase:{plan,implement,review,test}``
285
+ - ``agent:<name>`` for every agent in the §7 selection matrix
286
+
287
+ Raises:
288
+ GhError: On unexpected gh failures.
289
+ """
290
+ # priority labels
291
+ for priority, color in [
292
+ ("high", "FF0000"),
293
+ ("medium", "FBCA04"),
294
+ ("low", "0E8A16"),
295
+ ]:
296
+ ensure_label(f"priority:{priority}", color, f"{priority.capitalize()} priority")
297
+
298
+ # area labels
299
+ area_color = "0052CC"
300
+ for area in [
301
+ "cli", "orchestrator", "runner", "state", "prompts",
302
+ "gh", "tests", "docs", "api", "frontend", "database", "auth", "infra",
303
+ ]:
304
+ ensure_label(f"area:{area}", area_color, f"Area: {area}")
305
+
306
+ # phase labels
307
+ phase_color = "D4C5F9"
308
+ for phase in ["plan", "implement", "review", "test"]:
309
+ ensure_label(f"phase:{phase}", phase_color, f"Phase: {phase}")
310
+
311
+ # agent labels (color: 7057FF per spec)
312
+ agent_color = "7057FF"
313
+ agents = [
314
+ # Angular
315
+ "frontend-developer",
316
+ "typescript-pro",
317
+ "tailwind-patterns",
318
+ "ui-ux-designer",
319
+ # FastAPI
320
+ "python-pro",
321
+ "fastapi-expert-agent",
322
+ "supabase-connection-expert",
323
+ # NestJS
324
+ "nestjs-expert",
325
+ # BAML
326
+ "baml-expert-agent",
327
+ # Portfolio/Finance
328
+ "riskfolio-expert",
329
+ ]
330
+ for agent in agents:
331
+ ensure_label(f"agent:{agent}", agent_color, f"Agent: {agent}")
@@ -0,0 +1,73 @@
1
+ """Per-phase structured logging for the code-generator orchestrator.
2
+
3
+ Each phase gets its own log file under .code-generator/logs/ and a shared
4
+ RichHandler for colour-coded console output. The setup is idempotent: calling
5
+ setup_phase_logger() twice for the same phase does not duplicate handlers.
6
+ """
7
+
8
+ import logging
9
+ from pathlib import Path
10
+
11
+ from rich.logging import RichHandler
12
+
13
+ _FILE_FORMATTER = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
14
+
15
+
16
+ def setup_phase_logger(
17
+ phase_name: str,
18
+ project_dir: Path,
19
+ *,
20
+ level: int = logging.INFO,
21
+ ) -> logging.Logger:
22
+ """Configure and return a logger for a single orchestration phase.
23
+
24
+ The logger writes to both a phase-specific log file and the console via
25
+ Rich. A second call for the same phase_name returns the existing logger
26
+ without adding duplicate handlers.
27
+
28
+ Args:
29
+ phase_name: Short identifier for the phase (e.g. ``"planning"``).
30
+ project_dir: Root directory of the user's project. Logs are written to
31
+ ``project_dir / ".code-generator" / "logs" / f"{phase_name}.log"``.
32
+ level: Python logging level applied to the logger. Defaults to INFO.
33
+
34
+ Returns:
35
+ Configured :class:`logging.Logger` instance.
36
+ """
37
+ logger = logging.getLogger(f"code_generator.phase.{phase_name}")
38
+
39
+ if _already_configured(logger):
40
+ return logger
41
+
42
+ logger.setLevel(level)
43
+ logger.propagate = False
44
+
45
+ _attach_file_handler(logger, project_dir, phase_name)
46
+ _attach_rich_handler(logger)
47
+
48
+ return logger
49
+
50
+
51
+ def _already_configured(logger: logging.Logger) -> bool:
52
+ """Return True when the logger already has handlers attached."""
53
+ return bool(logger.handlers)
54
+
55
+
56
+ def _attach_file_handler(
57
+ logger: logging.Logger,
58
+ project_dir: Path,
59
+ phase_name: str,
60
+ ) -> None:
61
+ log_dir = project_dir / ".code-generator" / "logs"
62
+ log_dir.mkdir(parents=True, exist_ok=True)
63
+
64
+ handler = logging.FileHandler(log_dir / f"{phase_name}.log", encoding="utf-8")
65
+ handler.setLevel(logging.DEBUG)
66
+ handler.setFormatter(_FILE_FORMATTER)
67
+ logger.addHandler(handler)
68
+
69
+
70
+ def _attach_rich_handler(logger: logging.Logger) -> None:
71
+ handler = RichHandler(show_time=False, show_path=False)
72
+ handler.setLevel(logging.INFO)
73
+ logger.addHandler(handler)
@@ -0,0 +1,4 @@
1
+ """Orchestrator package — one module per pipeline phase.
2
+
3
+ Phase modules expose a single async ``run(...)`` coroutine each.
4
+ """