claude-code-generator 0.5.3__tar.gz → 0.5.5__tar.gz
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.5.3/src/claude_code_generator.egg-info → claude_code_generator-0.5.5}/PKG-INFO +1 -1
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/pyproject.toml +1 -1
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5/src/claude_code_generator.egg-info}/PKG-INFO +1 -1
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/claude_code_generator.egg-info/SOURCES.txt +1 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/__init__.py +1 -1
- claude_code_generator-0.5.5/src/code_generator/cli.py +93 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/logging_setup.py +21 -6
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/message_parsing.py +122 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/sdk_runner.py +49 -2
- claude_code_generator-0.5.5/tests/test_cli_io_logging.py +73 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_message_parsing.py +165 -0
- claude_code_generator-0.5.3/src/code_generator/cli.py +0 -53
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/LICENSE +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/README.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/setup.cfg +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/claude_code_generator.egg-info/dependency_links.txt +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/claude_code_generator.egg-info/entry_points.txt +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/claude_code_generator.egg-info/requires.txt +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/claude_code_generator.egg-info/top_level.txt +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/agents.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/commands/__init__.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/commands/_bench_io.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/commands/_crash_recovery.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/commands/_detect.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/commands/_dispatch.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/commands/_resume.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/commands/_validators.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/commands/bench.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/commands/bench_compare.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/commands/bench_export.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/commands/generate.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/commands/init.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/commands/optimize.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/commands/review.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/commands/status.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/effort.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/env.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/gh/__init__.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/gh/core.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/gh/issues.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/gh/labels.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/gh/milestones.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/git_ops.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/memory.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/orchestrator/__init__.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/orchestrator/_client_lifecycle.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/orchestrator/_comments.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/orchestrator/_memory_writers.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/orchestrator/_phase5_precommit.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/orchestrator/cycle_loop.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/orchestrator/cycle_prompts.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/orchestrator/ollama_budget.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/orchestrator/phase0_complexity.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/orchestrator/phase1_plan.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/orchestrator/phase2_review.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/orchestrator/phase3_4_implement.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/orchestrator/phase5_closure.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/orchestrator/phase6_test.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/orchestrator/phase7_commit.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/preflight.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/prompts/__init__.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/prompts/hashes.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/prompts/prompt-cycle-specializer.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/prompts/prompt-optimize-requirements.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/prompts/prompt-phase-0-complexity.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/prompts/prompt-phase-1-planning.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/prompts/prompt-phase-2-batch-review.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/prompts/prompt-phase-3-implementation.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/prompts/prompt-phase-5-final-review.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/prompts/prompt-phase-6-test.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/prompts/prompt-phase-7-commit.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/prompts/prompt-review.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/repo_info.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/requirements_structure.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/__init__.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/_telemetry.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/batch.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/fake_runner.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/mcp.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/options.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/protocol.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/rate_limit.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/retry.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/soft_reset.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/state_guard.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/subprocess_runner.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/types.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/utils.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/state.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/state_retention.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/templates/__init__.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/templates/angular.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/templates/base.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/templates/fastapi.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/templates/finance.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/templates/fullstack.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/templates/nestjs.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/templates/python-cli.md +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_agents.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_bench.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_bench_compare.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_bench_export.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_bench_fixture.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_bench_regression.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_changelog.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_claude_md.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_client_lifecycle.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_comments.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_commit_message.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_crash_recovery.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_cycle_loop.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_cycle_loop_multicycle.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_cycle_ollama_model.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_cycle_prompts.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_delta_planning.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_dependencies.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_detect.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_dispatch_graph_report.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_docs_no_default_max_turns.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_docs_ollama_model_guide.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_docs_ollama_pro.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_effective_model_routing.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_effort.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_env.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_generate.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_generate_ollama.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_generate_resume.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_gh.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_gh_labels.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_gh_milestones.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_gh_repo_threading.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_gh_submodules.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_git_ops.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_init.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_logging_setup.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_max_turns_cli_flag.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_mcp.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_memory.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_memory_writers.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_no_max_turns_in_call_sites.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_no_max_turns_literal.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_non_goals_grep_guard.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_ollama_budget.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_ollama_rate_limit.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_optimize.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_options.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_phase0.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_phase1.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_phase2.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_phase3_4.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_phase5.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_phase5_precommit.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_phase6.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_phase7.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_phase_mcp_regression.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_phase_token_logging.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_preflight.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_preflight_ollama.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_prompt_drift.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_prompt_prefix_snapshots.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_prompt_prefix_stability.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_prompts.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_rate_limit.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_repo_info.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_requirements_structure.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_retry.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_review.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_runner_protocol.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_runner_protocol_annotations.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_runner_types.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_runner_utils.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_sdk_runner.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_sdk_runner_shared.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_session_mode.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_state.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_state_guard.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_state_retention.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_status.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_subprocess_runner.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_telemetry.py +0 -0
- {claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/tests/test_version.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "claude-code-generator"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.5"
|
|
8
8
|
description = "Orchestrator CLI that drives Claude Code end-to-end to generate whole projects from a requirements.md file."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Entry-point CLI for code-generator.
|
|
2
|
+
|
|
3
|
+
Exposes a Typer application with --version and subcommands:
|
|
4
|
+
init, status, generate, review.
|
|
5
|
+
|
|
6
|
+
At import time we reconfigure stdout/stderr for line-buffered output (the
|
|
7
|
+
in-process equivalent of ``PYTHONUNBUFFERED=1``) so phase progress shows up
|
|
8
|
+
in the terminal in real time even when the CLI is invoked through buffering
|
|
9
|
+
wrappers like ``conda run``. The default log level is DEBUG, overridable
|
|
10
|
+
via the ``LOGLEVEL`` env var (set ``LOGLEVEL=INFO`` for the quieter behaviour
|
|
11
|
+
that earlier 0.4.x releases shipped with).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import contextlib
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
from typing import Annotated
|
|
21
|
+
|
|
22
|
+
import typer
|
|
23
|
+
|
|
24
|
+
import code_generator
|
|
25
|
+
from code_generator.commands.bench import bench_app
|
|
26
|
+
from code_generator.commands.generate import generate_app
|
|
27
|
+
from code_generator.commands.init import init_command
|
|
28
|
+
from code_generator.commands.optimize import optimize_command
|
|
29
|
+
from code_generator.commands.review import review_command
|
|
30
|
+
from code_generator.commands.status import status_command
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _configure_io_and_logging() -> None:
|
|
34
|
+
"""Force line-buffered stdout/stderr and apply LOGLEVEL (default DEBUG).
|
|
35
|
+
|
|
36
|
+
Equivalent to running with ``PYTHONUNBUFFERED=1 LOGLEVEL=DEBUG`` from
|
|
37
|
+
the shell, but always-on so users don't have to remember the env vars.
|
|
38
|
+
Both can still be overridden externally — ``LOGLEVEL=INFO`` reverts to
|
|
39
|
+
the quieter default.
|
|
40
|
+
|
|
41
|
+
Idempotent: safe to call multiple times (the ``reconfigure`` call is a
|
|
42
|
+
no-op when line buffering is already on, and ``logging.basicConfig`` is
|
|
43
|
+
a no-op once root handlers exist).
|
|
44
|
+
"""
|
|
45
|
+
# Line-buffered console I/O: each ``\n`` flushes immediately. The
|
|
46
|
+
# `contextlib.suppress` wrapper handles streams that are already buffered
|
|
47
|
+
# as needed or are non-seekable (e.g. piped to another process).
|
|
48
|
+
for stream in (sys.stdout, sys.stderr):
|
|
49
|
+
with contextlib.suppress(AttributeError, OSError):
|
|
50
|
+
stream.reconfigure(line_buffering=True) # type: ignore[union-attr]
|
|
51
|
+
|
|
52
|
+
level_name = os.environ.get("LOGLEVEL", "DEBUG").upper()
|
|
53
|
+
level = getattr(logging, level_name, logging.DEBUG)
|
|
54
|
+
logging.getLogger("code_generator").setLevel(level)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_configure_io_and_logging()
|
|
58
|
+
|
|
59
|
+
app = typer.Typer(
|
|
60
|
+
name="code-generator",
|
|
61
|
+
help="Orchestrate Claude Code to generate whole projects from a requirements.md.",
|
|
62
|
+
no_args_is_help=True,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _version_callback(value: bool) -> None:
|
|
67
|
+
if value:
|
|
68
|
+
typer.echo(f"code-generator {code_generator.__version__}")
|
|
69
|
+
raise typer.Exit()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.callback()
|
|
73
|
+
def root(
|
|
74
|
+
version: Annotated[
|
|
75
|
+
bool | None,
|
|
76
|
+
typer.Option(
|
|
77
|
+
"--version",
|
|
78
|
+
"-V",
|
|
79
|
+
help="Show version and exit.",
|
|
80
|
+
callback=_version_callback,
|
|
81
|
+
is_eager=True,
|
|
82
|
+
),
|
|
83
|
+
] = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""code-generator CLI root."""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
app.add_typer(bench_app, name="bench")
|
|
89
|
+
app.add_typer(generate_app, name="generate")
|
|
90
|
+
app.command(name="init")(init_command)
|
|
91
|
+
app.command(name="status")(status_command)
|
|
92
|
+
app.command(name="optimize")(optimize_command)
|
|
93
|
+
app.command(name="review")(review_command)
|
{claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/logging_setup.py
RENAMED
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
Each phase gets its own log file under .code-generator/logs/ and a shared
|
|
4
4
|
RichHandler for colour-coded console output. The setup is idempotent: calling
|
|
5
5
|
setup_phase_logger() twice for the same phase does not duplicate handlers.
|
|
6
|
+
|
|
7
|
+
The default console level is DEBUG, overridable via the ``LOGLEVEL`` env var
|
|
8
|
+
(``LOGLEVEL=INFO`` for the quieter behaviour shipped in 0.4.x). The file
|
|
9
|
+
handler always writes at DEBUG so post-mortem inspection retains everything.
|
|
6
10
|
"""
|
|
7
11
|
|
|
8
12
|
import logging
|
|
13
|
+
import os
|
|
9
14
|
from pathlib import Path
|
|
10
15
|
|
|
11
16
|
from rich.logging import RichHandler
|
|
@@ -13,11 +18,19 @@ from rich.logging import RichHandler
|
|
|
13
18
|
_FILE_FORMATTER = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
|
|
14
19
|
|
|
15
20
|
|
|
21
|
+
def _resolve_level(default: int = logging.DEBUG) -> int:
|
|
22
|
+
"""Read the ``LOGLEVEL`` env var, falling back to *default* on miss/typo."""
|
|
23
|
+
name = os.environ.get("LOGLEVEL", "").upper()
|
|
24
|
+
if not name:
|
|
25
|
+
return default
|
|
26
|
+
return getattr(logging, name, default)
|
|
27
|
+
|
|
28
|
+
|
|
16
29
|
def setup_phase_logger(
|
|
17
30
|
phase_name: str,
|
|
18
31
|
project_dir: Path,
|
|
19
32
|
*,
|
|
20
|
-
level: int =
|
|
33
|
+
level: int | None = None,
|
|
21
34
|
) -> logging.Logger:
|
|
22
35
|
"""Configure and return a logger for a single orchestration phase.
|
|
23
36
|
|
|
@@ -29,21 +42,23 @@ def setup_phase_logger(
|
|
|
29
42
|
phase_name: Short identifier for the phase (e.g. ``"planning"``).
|
|
30
43
|
project_dir: Root directory of the user's project. Logs are written to
|
|
31
44
|
``project_dir / ".code-generator" / "logs" / f"{phase_name}.log"``.
|
|
32
|
-
level: Python logging level applied to the logger.
|
|
45
|
+
level: Python logging level applied to the logger. Defaults to the
|
|
46
|
+
value of the ``LOGLEVEL`` env var, or DEBUG if unset.
|
|
33
47
|
|
|
34
48
|
Returns:
|
|
35
49
|
Configured :class:`logging.Logger` instance.
|
|
36
50
|
"""
|
|
51
|
+
effective_level = level if level is not None else _resolve_level()
|
|
37
52
|
logger = logging.getLogger(f"code_generator.phase.{phase_name}")
|
|
38
53
|
|
|
39
54
|
if _already_configured(logger):
|
|
40
55
|
return logger
|
|
41
56
|
|
|
42
|
-
logger.setLevel(
|
|
57
|
+
logger.setLevel(effective_level)
|
|
43
58
|
logger.propagate = False
|
|
44
59
|
|
|
45
60
|
_attach_file_handler(logger, project_dir, phase_name)
|
|
46
|
-
_attach_rich_handler(logger)
|
|
61
|
+
_attach_rich_handler(logger, effective_level)
|
|
47
62
|
|
|
48
63
|
return logger
|
|
49
64
|
|
|
@@ -67,9 +82,9 @@ def _attach_file_handler(
|
|
|
67
82
|
logger.addHandler(handler)
|
|
68
83
|
|
|
69
84
|
|
|
70
|
-
def _attach_rich_handler(logger: logging.Logger) -> None:
|
|
85
|
+
def _attach_rich_handler(logger: logging.Logger, level: int) -> None:
|
|
71
86
|
handler = RichHandler(show_time=False, show_path=False)
|
|
72
|
-
handler.setLevel(
|
|
87
|
+
handler.setLevel(level)
|
|
73
88
|
logger.addHandler(handler)
|
|
74
89
|
|
|
75
90
|
|
|
@@ -94,6 +94,128 @@ def is_result_message(msg: Any) -> bool:
|
|
|
94
94
|
)
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
def is_system_message(msg: Any) -> bool:
|
|
98
|
+
"""Return True when msg looks like a SDK ``SystemMessage``.
|
|
99
|
+
|
|
100
|
+
Duck-typed: SystemMessage carries a ``subtype`` (e.g. ``"init"``) and an
|
|
101
|
+
optional ``data`` dict, while not having the discriminating attributes of
|
|
102
|
+
AssistantMessage (``model``) or ResultMessage (``is_error``).
|
|
103
|
+
"""
|
|
104
|
+
return (
|
|
105
|
+
hasattr(msg, "subtype")
|
|
106
|
+
and not hasattr(msg, "model")
|
|
107
|
+
and not hasattr(msg, "is_error")
|
|
108
|
+
and not hasattr(msg, "rate_limit_info")
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def is_user_message(msg: Any) -> bool:
|
|
113
|
+
"""Return True when msg looks like a SDK ``UserMessage``.
|
|
114
|
+
|
|
115
|
+
Duck-typed: UserMessage has ``content`` (a list of blocks) and a
|
|
116
|
+
``role`` of ``"user"``, but not the AssistantMessage ``model`` field.
|
|
117
|
+
"""
|
|
118
|
+
role = getattr(msg, "role", None)
|
|
119
|
+
return (
|
|
120
|
+
role == "user"
|
|
121
|
+
and hasattr(msg, "content")
|
|
122
|
+
and not hasattr(msg, "model")
|
|
123
|
+
and not hasattr(msg, "is_error")
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def render_system_message(msg: Any) -> str:
|
|
128
|
+
"""Format a SystemMessage's ``subtype`` + ``data`` payload for DEBUG logs.
|
|
129
|
+
|
|
130
|
+
Full payload — no truncation. Large ``data`` values can produce long
|
|
131
|
+
lines; that is intentional so users running with ``LOGLEVEL=DEBUG`` see
|
|
132
|
+
exactly what the SDK emitted.
|
|
133
|
+
"""
|
|
134
|
+
subtype = getattr(msg, "subtype", None)
|
|
135
|
+
data = getattr(msg, "data", None)
|
|
136
|
+
if subtype is None and data is None:
|
|
137
|
+
return type(msg).__name__
|
|
138
|
+
|
|
139
|
+
if isinstance(data, dict):
|
|
140
|
+
previews = [f"{k}={v!r}" for k, v in data.items()]
|
|
141
|
+
body = " ".join(previews) if previews else "<empty>"
|
|
142
|
+
elif data is None:
|
|
143
|
+
body = "<no data>"
|
|
144
|
+
else:
|
|
145
|
+
body = repr(data)
|
|
146
|
+
|
|
147
|
+
return f"subtype={subtype} {body}"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def render_blocks(content: Any) -> str:
|
|
151
|
+
"""Render an SDK message's ``content`` (list of blocks) verbatim.
|
|
152
|
+
|
|
153
|
+
Handles every block shape the agent SDK currently emits:
|
|
154
|
+
- **TextBlock** (``.text``) → ``text: <body>``
|
|
155
|
+
- **ToolUseBlock** (``.name``, ``.input``) → ``tool_use(name): {args}``
|
|
156
|
+
- **ToolResultBlock** (``.tool_use_id``, ``.content``) → ``tool_result(id): <body>``
|
|
157
|
+
- **ThinkingBlock** (``.thinking``) → ``thinking: <body>``
|
|
158
|
+
- Anything else falls through to the class name.
|
|
159
|
+
|
|
160
|
+
No truncation: tool results, prompts, and thinking blocks render in
|
|
161
|
+
full. Long lines are intentional so DEBUG logs are lossless.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
content: The message's ``content`` attribute. Lists are walked
|
|
165
|
+
block-by-block; non-lists are repr'd directly.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
A single string suitable for a DEBUG log line.
|
|
169
|
+
"""
|
|
170
|
+
if not isinstance(content, list):
|
|
171
|
+
return repr(content)
|
|
172
|
+
|
|
173
|
+
if not content:
|
|
174
|
+
return "<empty>"
|
|
175
|
+
|
|
176
|
+
return " | ".join(_render_block(block) for block in content)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _render_block(block: Any) -> str:
|
|
180
|
+
"""Render a single content block — see :func:`render_blocks`."""
|
|
181
|
+
if hasattr(block, "text") and isinstance(block.text, str):
|
|
182
|
+
return f"text: {block.text}"
|
|
183
|
+
if hasattr(block, "name") and hasattr(block, "input"):
|
|
184
|
+
return f"tool_use({block.name}): {block.input!r}"
|
|
185
|
+
if hasattr(block, "tool_use_id"):
|
|
186
|
+
result = getattr(block, "content", "<no content>")
|
|
187
|
+
is_error = bool(getattr(block, "is_error", False))
|
|
188
|
+
body = _render_tool_result(result)
|
|
189
|
+
prefix = "tool_result_error" if is_error else "tool_result"
|
|
190
|
+
return f"{prefix}({block.tool_use_id}): {body}"
|
|
191
|
+
if hasattr(block, "thinking") and isinstance(block.thinking, str):
|
|
192
|
+
return f"thinking: {block.thinking}"
|
|
193
|
+
return type(block).__name__
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _render_tool_result(content: Any) -> str:
|
|
197
|
+
"""Render a ToolResultBlock's ``.content`` (str | list[block] | other) verbatim."""
|
|
198
|
+
if isinstance(content, str):
|
|
199
|
+
return content
|
|
200
|
+
if isinstance(content, list):
|
|
201
|
+
chunks: list[str] = []
|
|
202
|
+
for item in content:
|
|
203
|
+
text = getattr(item, "text", None)
|
|
204
|
+
if isinstance(text, str):
|
|
205
|
+
chunks.append(text)
|
|
206
|
+
elif isinstance(item, dict) and isinstance(item.get("text"), str):
|
|
207
|
+
chunks.append(item["text"])
|
|
208
|
+
else:
|
|
209
|
+
chunks.append(repr(item))
|
|
210
|
+
return " / ".join(chunks) if chunks else "<empty>"
|
|
211
|
+
return repr(content)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def render_prompt(prompt: str) -> str:
|
|
215
|
+
"""Return the outgoing prompt verbatim for DEBUG logging (no truncation)."""
|
|
216
|
+
return prompt
|
|
217
|
+
|
|
218
|
+
|
|
97
219
|
def is_context_edit_message(msg: Any) -> bool:
|
|
98
220
|
"""Return True when msg looks like a CompactionResult or ContextEditResult.
|
|
99
221
|
|
{claude_code_generator-0.5.3 → claude_code_generator-0.5.5}/src/code_generator/runner/sdk_runner.py
RENAMED
|
@@ -18,6 +18,11 @@ from code_generator.runner.message_parsing import (
|
|
|
18
18
|
is_context_edit_message,
|
|
19
19
|
is_rate_limit_event,
|
|
20
20
|
is_result_message,
|
|
21
|
+
is_system_message,
|
|
22
|
+
is_user_message,
|
|
23
|
+
render_blocks,
|
|
24
|
+
render_prompt,
|
|
25
|
+
render_system_message,
|
|
21
26
|
)
|
|
22
27
|
from code_generator.runner.types import (
|
|
23
28
|
ApiUpstreamError,
|
|
@@ -145,7 +150,14 @@ async def _drain_messages(
|
|
|
145
150
|
chunk.rstrip(),
|
|
146
151
|
)
|
|
147
152
|
else:
|
|
148
|
-
|
|
153
|
+
# No text content — almost always a tool_use block. Render it
|
|
154
|
+
# at DEBUG so users can trace exactly which tool the model
|
|
155
|
+
# invoked and with what arguments.
|
|
156
|
+
logger.debug(
|
|
157
|
+
"msg #%d: assistant (tool_use) %s",
|
|
158
|
+
msg_count,
|
|
159
|
+
render_blocks(getattr(msg, "content", None)),
|
|
160
|
+
)
|
|
149
161
|
# Some SDK versions attach session_id to AssistantMessage.
|
|
150
162
|
if session_id is None:
|
|
151
163
|
session_id = getattr(msg, "session_id", None)
|
|
@@ -202,8 +214,33 @@ async def _drain_messages(
|
|
|
202
214
|
f"tokens_out={usage.output}): {tail}"
|
|
203
215
|
)
|
|
204
216
|
break
|
|
217
|
+
elif is_system_message(msg):
|
|
218
|
+
logger.debug("msg #%d: SystemMessage %s", msg_count, render_system_message(msg))
|
|
219
|
+
elif is_user_message(msg):
|
|
220
|
+
logger.debug(
|
|
221
|
+
"msg #%d: UserMessage %s",
|
|
222
|
+
msg_count,
|
|
223
|
+
render_blocks(getattr(msg, "content", None)),
|
|
224
|
+
)
|
|
205
225
|
else:
|
|
206
|
-
|
|
226
|
+
# Catch-all for less common message types (TaskStartedMessage,
|
|
227
|
+
# TaskFinishedMessage, etc.). Render any ``content`` /
|
|
228
|
+
# ``data`` / ``message`` payload we can find so the type-name
|
|
229
|
+
# alone is never the only signal.
|
|
230
|
+
payload = (
|
|
231
|
+
getattr(msg, "content", None)
|
|
232
|
+
or getattr(msg, "data", None)
|
|
233
|
+
or getattr(msg, "message", None)
|
|
234
|
+
)
|
|
235
|
+
if payload is not None:
|
|
236
|
+
logger.debug(
|
|
237
|
+
"msg #%d: %s %s",
|
|
238
|
+
msg_count,
|
|
239
|
+
type(msg).__name__,
|
|
240
|
+
render_blocks(payload),
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
logger.debug("msg #%d: %s", msg_count, type(msg).__name__)
|
|
207
244
|
|
|
208
245
|
return "".join(text_parts), session_id, usage or TokenUsage()
|
|
209
246
|
|
|
@@ -256,6 +293,11 @@ async def run(
|
|
|
256
293
|
getattr(options, "effort", None) or "default",
|
|
257
294
|
getattr(options, "max_turns", "?"),
|
|
258
295
|
)
|
|
296
|
+
logger.debug(
|
|
297
|
+
"SDK prompt (%d chars):\n%s",
|
|
298
|
+
len(prompt),
|
|
299
|
+
render_prompt(prompt),
|
|
300
|
+
)
|
|
259
301
|
|
|
260
302
|
t0 = time.monotonic()
|
|
261
303
|
async with ClaudeSDKClient(options=options) as client:
|
|
@@ -311,6 +353,11 @@ async def run_with_shared_client(
|
|
|
311
353
|
getattr(options, "effort", None) or "default",
|
|
312
354
|
getattr(options, "max_turns", "?"),
|
|
313
355
|
)
|
|
356
|
+
logger.debug(
|
|
357
|
+
"SDK prompt (%d chars):\n%s",
|
|
358
|
+
len(prompt),
|
|
359
|
+
render_prompt(prompt),
|
|
360
|
+
)
|
|
314
361
|
|
|
315
362
|
t0 = time.monotonic()
|
|
316
363
|
await client.query(prompt)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Tests for ``cli._configure_io_and_logging``.
|
|
2
|
+
|
|
3
|
+
The CLI applies two startup defaults so users don't need to set env vars
|
|
4
|
+
manually: line-buffered stdout/stderr (PYTHONUNBUFFERED=1) and DEBUG-level
|
|
5
|
+
console logging (LOGLEVEL=DEBUG). Both are overridable via ``LOGLEVEL``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from code_generator import cli
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestConfigureIoAndLogging:
|
|
20
|
+
"""``_configure_io_and_logging`` sets sane defaults at CLI startup."""
|
|
21
|
+
|
|
22
|
+
def test_default_loglevel_is_debug(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
23
|
+
"""When ``LOGLEVEL`` is unset, the code_generator namespace uses DEBUG."""
|
|
24
|
+
monkeypatch.delenv("LOGLEVEL", raising=False)
|
|
25
|
+
cli._configure_io_and_logging()
|
|
26
|
+
|
|
27
|
+
assert logging.getLogger("code_generator").level == logging.DEBUG
|
|
28
|
+
|
|
29
|
+
def test_loglevel_env_overrides_default(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
30
|
+
"""``LOGLEVEL=INFO`` reverts to the quieter 0.4.x default."""
|
|
31
|
+
monkeypatch.setenv("LOGLEVEL", "INFO")
|
|
32
|
+
cli._configure_io_and_logging()
|
|
33
|
+
|
|
34
|
+
assert logging.getLogger("code_generator").level == logging.INFO
|
|
35
|
+
|
|
36
|
+
def test_unknown_loglevel_falls_back_to_debug(
|
|
37
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Typos like ``LOGLEVEL=NOPE`` fall back to DEBUG, not raise."""
|
|
40
|
+
monkeypatch.setenv("LOGLEVEL", "NOPE")
|
|
41
|
+
cli._configure_io_and_logging()
|
|
42
|
+
|
|
43
|
+
assert logging.getLogger("code_generator").level == logging.DEBUG
|
|
44
|
+
|
|
45
|
+
def test_idempotent_when_called_twice(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
46
|
+
"""Calling twice in a row does not raise (e.g. on `--version` re-import)."""
|
|
47
|
+
monkeypatch.setenv("LOGLEVEL", "INFO")
|
|
48
|
+
cli._configure_io_and_logging()
|
|
49
|
+
cli._configure_io_and_logging() # must not raise
|
|
50
|
+
|
|
51
|
+
assert logging.getLogger("code_generator").level == logging.INFO
|
|
52
|
+
|
|
53
|
+
def test_stdout_is_line_buffered_after_setup(
|
|
54
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
55
|
+
) -> None:
|
|
56
|
+
"""sys.stdout / sys.stderr must report line buffering after setup.
|
|
57
|
+
|
|
58
|
+
On terminals where stdout is already line-buffered, this is a no-op;
|
|
59
|
+
when running through a pipe (default block buffering), the call
|
|
60
|
+
switches the stream to line buffering.
|
|
61
|
+
"""
|
|
62
|
+
import sys
|
|
63
|
+
|
|
64
|
+
monkeypatch.delenv("LOGLEVEL", raising=False)
|
|
65
|
+
cli._configure_io_and_logging()
|
|
66
|
+
|
|
67
|
+
# `line_buffering` attribute exists on TextIOWrapper. After
|
|
68
|
+
# reconfigure(line_buffering=True), it must be True. Streams that
|
|
69
|
+
# don't support the attribute (rare) are intentionally tolerated.
|
|
70
|
+
for stream in (sys.stdout, sys.stderr):
|
|
71
|
+
buffering = getattr(stream, "line_buffering", None)
|
|
72
|
+
if buffering is not None:
|
|
73
|
+
assert buffering is True, f"{stream} not line-buffered"
|
|
@@ -483,3 +483,168 @@ def test_handle_rate_limit_event_rejected_without_resets_at_logs_warning(
|
|
|
483
483
|
handle_rate_limit_event(event, logger, state_path)
|
|
484
484
|
|
|
485
485
|
assert any("no resets_at" in r.message for r in caplog.records)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
# ---------------------------------------------------------------------------
|
|
489
|
+
# render_blocks / render_system_message / render_prompt / new predicates
|
|
490
|
+
# ---------------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
class FakeToolUseBlock:
|
|
494
|
+
def __init__(self, name: str, input_data: dict) -> None:
|
|
495
|
+
self.name = name
|
|
496
|
+
self.input = input_data
|
|
497
|
+
self.id = "tu-x"
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
class FakeToolResultBlock:
|
|
501
|
+
def __init__(self, tool_use_id: str, content, is_error: bool = False) -> None:
|
|
502
|
+
self.tool_use_id = tool_use_id
|
|
503
|
+
self.content = content
|
|
504
|
+
self.is_error = is_error
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
class FakeThinkingBlock:
|
|
508
|
+
def __init__(self, thinking: str) -> None:
|
|
509
|
+
self.thinking = thinking
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
class FakeSystemMessage:
|
|
513
|
+
def __init__(self, subtype: str, data: dict | None = None) -> None:
|
|
514
|
+
self.subtype = subtype
|
|
515
|
+
self.data = data
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
class FakeUserMessage:
|
|
519
|
+
def __init__(self, content) -> None:
|
|
520
|
+
self.role = "user"
|
|
521
|
+
self.content = content
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def test_render_blocks_handles_text_block() -> None:
|
|
525
|
+
from code_generator.runner.message_parsing import render_blocks
|
|
526
|
+
|
|
527
|
+
rendered = render_blocks([FakeTextBlock("hello world")])
|
|
528
|
+
assert rendered == "text: hello world"
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def test_render_blocks_handles_tool_use_block() -> None:
|
|
532
|
+
from code_generator.runner.message_parsing import render_blocks
|
|
533
|
+
|
|
534
|
+
rendered = render_blocks([FakeToolUseBlock("Read", {"file_path": "/tmp/x.py"})])
|
|
535
|
+
assert rendered.startswith("tool_use(Read): ")
|
|
536
|
+
assert "/tmp/x.py" in rendered
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def test_render_blocks_handles_tool_result_block_str() -> None:
|
|
540
|
+
from code_generator.runner.message_parsing import render_blocks
|
|
541
|
+
|
|
542
|
+
rendered = render_blocks([FakeToolResultBlock("tu-1", "result body")])
|
|
543
|
+
assert rendered == "tool_result(tu-1): result body"
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def test_render_blocks_marks_tool_result_errors() -> None:
|
|
547
|
+
from code_generator.runner.message_parsing import render_blocks
|
|
548
|
+
|
|
549
|
+
rendered = render_blocks(
|
|
550
|
+
[FakeToolResultBlock("tu-2", "boom", is_error=True)]
|
|
551
|
+
)
|
|
552
|
+
assert rendered.startswith("tool_result_error(tu-2): ")
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def test_render_blocks_handles_thinking_block() -> None:
|
|
556
|
+
from code_generator.runner.message_parsing import render_blocks
|
|
557
|
+
|
|
558
|
+
rendered = render_blocks([FakeThinkingBlock("planning step…")])
|
|
559
|
+
assert rendered == "thinking: planning step…"
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def test_render_blocks_renders_long_text_in_full() -> None:
|
|
563
|
+
"""No truncation — full text is preserved so DEBUG logs are lossless."""
|
|
564
|
+
from code_generator.runner.message_parsing import render_blocks
|
|
565
|
+
|
|
566
|
+
long = "x" * 5000
|
|
567
|
+
rendered = render_blocks([FakeTextBlock(long)])
|
|
568
|
+
assert rendered == f"text: {long}"
|
|
569
|
+
assert "truncated" not in rendered
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def test_render_blocks_joins_multiple_with_pipe() -> None:
|
|
573
|
+
from code_generator.runner.message_parsing import render_blocks
|
|
574
|
+
|
|
575
|
+
rendered = render_blocks(
|
|
576
|
+
[
|
|
577
|
+
FakeTextBlock("first"),
|
|
578
|
+
FakeToolUseBlock("Read", {"path": "a.py"}),
|
|
579
|
+
]
|
|
580
|
+
)
|
|
581
|
+
assert " | " in rendered
|
|
582
|
+
assert "text: first" in rendered
|
|
583
|
+
assert "tool_use(Read)" in rendered
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def test_render_blocks_empty_list_returns_marker() -> None:
|
|
587
|
+
from code_generator.runner.message_parsing import render_blocks
|
|
588
|
+
|
|
589
|
+
assert render_blocks([]) == "<empty>"
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def test_render_system_message_with_subtype_and_data() -> None:
|
|
593
|
+
from code_generator.runner.message_parsing import render_system_message
|
|
594
|
+
|
|
595
|
+
msg = FakeSystemMessage("init", {"model": "claude-opus-4-7", "session_id": "s1"})
|
|
596
|
+
rendered = render_system_message(msg)
|
|
597
|
+
assert rendered.startswith("subtype=init")
|
|
598
|
+
assert "model=" in rendered
|
|
599
|
+
assert "session_id=" in rendered
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def test_render_system_message_subtype_only() -> None:
|
|
603
|
+
from code_generator.runner.message_parsing import render_system_message
|
|
604
|
+
|
|
605
|
+
msg = FakeSystemMessage("compaction", None)
|
|
606
|
+
rendered = render_system_message(msg)
|
|
607
|
+
assert rendered.startswith("subtype=compaction")
|
|
608
|
+
assert "no data" in rendered
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def test_is_system_message_predicate() -> None:
|
|
612
|
+
from code_generator.runner.message_parsing import is_system_message
|
|
613
|
+
|
|
614
|
+
assert is_system_message(FakeSystemMessage("init"))
|
|
615
|
+
assert not is_system_message(FakeAssistantMessage("hi"))
|
|
616
|
+
assert not is_system_message(FakeUserMessage([FakeTextBlock("hi")]))
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def test_is_user_message_predicate() -> None:
|
|
620
|
+
from code_generator.runner.message_parsing import is_user_message
|
|
621
|
+
|
|
622
|
+
assert is_user_message(FakeUserMessage([FakeTextBlock("hi")]))
|
|
623
|
+
assert not is_user_message(FakeSystemMessage("init"))
|
|
624
|
+
assert not is_user_message(FakeAssistantMessage("hi"))
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def test_render_prompt_renders_long_input_verbatim() -> None:
|
|
628
|
+
"""No truncation — prompts log in full so the model's exact input is auditable."""
|
|
629
|
+
from code_generator.runner.message_parsing import render_prompt
|
|
630
|
+
|
|
631
|
+
long = "y" * 5000
|
|
632
|
+
rendered = render_prompt(long)
|
|
633
|
+
assert rendered == long
|
|
634
|
+
assert "truncated" not in rendered
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def test_render_prompt_passthrough_short_input() -> None:
|
|
638
|
+
from code_generator.runner.message_parsing import render_prompt
|
|
639
|
+
|
|
640
|
+
assert render_prompt("hello") == "hello"
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def test_render_blocks_renders_long_tool_result_in_full() -> None:
|
|
644
|
+
"""Tool results render verbatim — no body cap."""
|
|
645
|
+
from code_generator.runner.message_parsing import render_blocks
|
|
646
|
+
|
|
647
|
+
long = "z" * 5000
|
|
648
|
+
rendered = render_blocks([FakeToolResultBlock("tu-9", long)])
|
|
649
|
+
assert rendered == f"tool_result(tu-9): {long}"
|
|
650
|
+
assert "truncated" not in rendered
|