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,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()
|