claude-code-generator 0.4.9__tar.gz → 0.4.11__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.4.9/src/claude_code_generator.egg-info → claude_code_generator-0.4.11}/PKG-INFO +2 -1
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/pyproject.toml +11 -7
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11/src/claude_code_generator.egg-info}/PKG-INFO +2 -1
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/claude_code_generator.egg-info/requires.txt +1 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/__init__.py +1 -1
- claude_code_generator-0.4.11/src/code_generator/orchestrator/ollama_budget.py +215 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_ollama_budget.py +86 -60
- claude_code_generator-0.4.9/src/code_generator/orchestrator/ollama_budget.py +0 -116
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/LICENSE +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/README.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/setup.cfg +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/claude_code_generator.egg-info/SOURCES.txt +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/claude_code_generator.egg-info/dependency_links.txt +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/claude_code_generator.egg-info/entry_points.txt +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/claude_code_generator.egg-info/top_level.txt +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/agents.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/cli.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/__init__.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/_bench_io.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/_crash_recovery.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/_detect.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/_dispatch.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/_resume.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/_validators.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/bench.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/bench_compare.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/bench_export.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/generate.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/init.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/optimize.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/review.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/status.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/effort.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/env.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/gh/__init__.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/gh/core.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/gh/issues.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/gh/labels.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/gh/milestones.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/git_ops.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/logging_setup.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/memory.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/__init__.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/_client_lifecycle.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/_comments.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/_memory_writers.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/_phase5_precommit.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/cycle_loop.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/cycle_prompts.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/phase0_complexity.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/phase1_plan.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/phase2_review.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/phase3_4_implement.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/phase5_closure.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/phase6_test.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/phase7_commit.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/preflight.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/__init__.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/hashes.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-cycle-specializer.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-optimize-requirements.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-phase-0-complexity.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-phase-1-planning.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-phase-2-batch-review.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-phase-2-issue-review.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-phase-3-implementation.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-phase-5-final-review.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-phase-6-test.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-phase-7-commit.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-review.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/repo_info.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/repomap.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/requirements_structure.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/__init__.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/_telemetry.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/batch.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/fake_runner.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/mcp.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/message_parsing.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/options.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/protocol.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/rate_limit.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/retry.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/sdk_runner.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/soft_reset.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/state_guard.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/subprocess_runner.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/types.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/utils.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/state.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/state_retention.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/templates/__init__.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/templates/angular.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/templates/base.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/templates/fastapi.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/templates/finance.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/templates/fullstack.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/templates/nestjs.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/templates/python-cli.md +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_agents.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_bench.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_bench_compare.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_bench_export.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_bench_fixture.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_bench_regression.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_changelog.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_claude_md.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_client_lifecycle.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_comments.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_commit_message.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_crash_recovery.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_cycle_loop.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_cycle_loop_multicycle.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_cycle_ollama_model.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_cycle_prompts.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_delta_planning.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_dependencies.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_detect.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_docs_no_default_max_turns.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_docs_ollama_model_guide.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_docs_ollama_pro.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_effective_model_routing.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_effort.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_env.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_generate.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_generate_ollama.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_generate_resume.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_gh.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_gh_labels.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_gh_milestones.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_gh_repo_threading.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_gh_submodules.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_git_ops.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_init.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_logging_setup.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_max_turns_cli_flag.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_mcp.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_memory.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_memory_writers.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_message_parsing.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_no_max_turns_in_call_sites.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_no_max_turns_literal.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_non_goals_grep_guard.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_ollama_rate_limit.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_optimize.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_options.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase0.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase1.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase2.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase2_batch.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase3_4.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase5.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase5_precommit.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase6.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase7.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase_mcp_regression.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase_token_logging.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_preflight.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_preflight_ollama.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_prompt_drift.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_prompt_prefix_snapshots.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_prompt_prefix_stability.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_prompts.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_rate_limit.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_repo_info.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_repomap.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_requirements_structure.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_retry.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_review.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_runner_protocol.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_runner_protocol_annotations.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_runner_types.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_runner_utils.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_sdk_runner.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_sdk_runner_shared.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_session_mode.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_state.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_state_guard.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_state_retention.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_status.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_subprocess_runner.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_telemetry.py +0 -0
- {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-code-generator
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.11
|
|
4
4
|
Summary: Orchestrator CLI that drives Claude Code end-to-end to generate whole projects from a requirements.md file.
|
|
5
5
|
Author: Silvio Baratto
|
|
6
6
|
License: MIT
|
|
@@ -32,6 +32,7 @@ Requires-Dist: tree-sitter>=0.23; extra == "repomap"
|
|
|
32
32
|
Requires-Dist: tree-sitter-language-pack>=0.5; extra == "repomap"
|
|
33
33
|
Provides-Extra: mcp
|
|
34
34
|
Requires-Dist: mcp<2.0,>=1.0; extra == "mcp"
|
|
35
|
+
Requires-Dist: serena-agent<2.0,>=1.1; extra == "mcp"
|
|
35
36
|
Provides-Extra: batch
|
|
36
37
|
Requires-Dist: anthropic>=0.35; extra == "batch"
|
|
37
38
|
Provides-Extra: all
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "claude-code-generator"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.11"
|
|
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" }
|
|
@@ -47,11 +47,13 @@ repomap = [
|
|
|
47
47
|
"tree-sitter-language-pack>=0.5",
|
|
48
48
|
]
|
|
49
49
|
mcp = [
|
|
50
|
-
# Python MCP client library
|
|
51
|
-
# are external and must be installed separately:
|
|
52
|
-
# cargo install codebase-memory-mcp (or brew install codebase-memory-mcp)
|
|
53
|
-
# pip install serena (or uvx serena for zero-install)
|
|
50
|
+
# Python MCP client library.
|
|
54
51
|
"mcp>=1.0,<2.0",
|
|
52
|
+
# Serena MCP server (provides the `serena` executable on PATH —
|
|
53
|
+
# picked up by runner/mcp.py's shutil.which("serena") fallback).
|
|
54
|
+
# The primary server, codebase-memory-mcp, is a Rust crate and still
|
|
55
|
+
# must be installed separately: `cargo install codebase-memory-mcp`.
|
|
56
|
+
"serena-agent>=1.1,<2.0",
|
|
55
57
|
]
|
|
56
58
|
batch = [
|
|
57
59
|
# Anthropic SDK for the Messages Batches API (§6).
|
|
@@ -59,8 +61,10 @@ batch = [
|
|
|
59
61
|
"anthropic>=0.35",
|
|
60
62
|
]
|
|
61
63
|
all = [
|
|
62
|
-
# Everything pip can install
|
|
63
|
-
#
|
|
64
|
+
# Everything pip can install: repomap (tree-sitter), MCP client + Serena
|
|
65
|
+
# MCP server, and the Anthropic Batches API. The primary MCP server
|
|
66
|
+
# codebase-memory-mcp is a Rust crate and must be installed separately:
|
|
67
|
+
# cargo install codebase-memory-mcp
|
|
64
68
|
"claude-code-generator[repomap,mcp,batch]",
|
|
65
69
|
]
|
|
66
70
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-code-generator
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.11
|
|
4
4
|
Summary: Orchestrator CLI that drives Claude Code end-to-end to generate whole projects from a requirements.md file.
|
|
5
5
|
Author: Silvio Baratto
|
|
6
6
|
License: MIT
|
|
@@ -32,6 +32,7 @@ Requires-Dist: tree-sitter>=0.23; extra == "repomap"
|
|
|
32
32
|
Requires-Dist: tree-sitter-language-pack>=0.5; extra == "repomap"
|
|
33
33
|
Provides-Extra: mcp
|
|
34
34
|
Requires-Dist: mcp<2.0,>=1.0; extra == "mcp"
|
|
35
|
+
Requires-Dist: serena-agent<2.0,>=1.1; extra == "mcp"
|
|
35
36
|
Provides-Extra: batch
|
|
36
37
|
Requires-Dist: anthropic>=0.35; extra == "batch"
|
|
37
38
|
Provides-Extra: all
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Per-cycle safety backstop for the Ollama codepath.
|
|
2
|
+
|
|
3
|
+
The pre-0.4.11 design treated ``OLLAMA_TURN_BUDGET`` as a hard abort trigger
|
|
4
|
+
— cycles were aborted after exactly 200 turns regardless of whether the
|
|
5
|
+
model was making progress. That is the wrong layer: actual fault-detection
|
|
6
|
+
already lives in two places and triggers on real malfunctions, not on an
|
|
7
|
+
arbitrary counter:
|
|
8
|
+
|
|
9
|
+
* :class:`~code_generator.runner.retry.CircuitBreaker` — trips after ``N``
|
|
10
|
+
consecutive failures on a single phase call. Already wrapped around
|
|
11
|
+
Phase 2 (per-issue / batched review), Phase 3/4 (TDD implementation),
|
|
12
|
+
and Phase 5 (closure).
|
|
13
|
+
* :func:`~code_generator.runner.rate_limit.handle_ollama_429` —
|
|
14
|
+
wait-and-resume on 429s returned by the Ollama daemon.
|
|
15
|
+
|
|
16
|
+
This module therefore degrades to a **non-blocking adaptive** backstop:
|
|
17
|
+
|
|
18
|
+
* The turn counter now emits a single **WARNING** when the cycle crosses
|
|
19
|
+
the soft threshold (default 500). The pipeline is **never** aborted on
|
|
20
|
+
the turn count alone. Weak open models are chatty by design; letting
|
|
21
|
+
them run is the right call.
|
|
22
|
+
* The wall-clock cap remains a **hard abort**, but the default is raised
|
|
23
|
+
to 4 hours. It exists purely to catch a stuck daemon or a model trapped
|
|
24
|
+
in a pathological loop the CircuitBreaker cannot see (e.g. infinite
|
|
25
|
+
``end_turn``→``continue`` cycle producing no tool calls).
|
|
26
|
+
|
|
27
|
+
Both thresholds are env-overridable:
|
|
28
|
+
|
|
29
|
+
* ``OLLAMA_SOFT_TURN_WARN`` (int, positive; default 500)
|
|
30
|
+
* ``OLLAMA_WALLCLOCK_BUDGET_SECONDS`` (int, positive; default 14400)
|
|
31
|
+
|
|
32
|
+
Backwards-compatible shim: the old ``OLLAMA_TURN_BUDGET`` env variable is
|
|
33
|
+
still honoured and maps onto the soft-warn threshold, so operators with
|
|
34
|
+
existing scripts see no behaviour change beyond the abort becoming a
|
|
35
|
+
warning.
|
|
36
|
+
|
|
37
|
+
Nothing is persisted in ``state.json`` — the tracker is per-run and
|
|
38
|
+
discarded on abort or clean completion.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import logging
|
|
44
|
+
import os
|
|
45
|
+
import time
|
|
46
|
+
from typing import TYPE_CHECKING
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from collections.abc import Callable
|
|
50
|
+
|
|
51
|
+
from code_generator.state import CycleState, State
|
|
52
|
+
|
|
53
|
+
_logger = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Threshold constants
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
_DEFAULT_SOFT_TURN_WARN = 500
|
|
60
|
+
_DEFAULT_WALLCLOCK_BUDGET_SECONDS = 14400 # 4 h
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _read_int_env(name: str, default: int) -> int:
|
|
64
|
+
"""Parse a positive int from the environment, falling back to *default*.
|
|
65
|
+
|
|
66
|
+
Invalid values (empty, non-numeric, zero, or negative) are silently
|
|
67
|
+
replaced with *default* — these are per-run tuning knobs, not safety
|
|
68
|
+
gates, so noisy error handling would surprise operators who meant well.
|
|
69
|
+
"""
|
|
70
|
+
raw = os.environ.get(name)
|
|
71
|
+
if raw is None or not raw.strip():
|
|
72
|
+
return default
|
|
73
|
+
try:
|
|
74
|
+
value = int(raw)
|
|
75
|
+
except ValueError:
|
|
76
|
+
return default
|
|
77
|
+
return value if value > 0 else default
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _resolve_soft_turn_warn() -> int:
|
|
81
|
+
"""Honour the legacy ``OLLAMA_TURN_BUDGET`` env var for backwards compat."""
|
|
82
|
+
legacy = _read_int_env("OLLAMA_TURN_BUDGET", 0)
|
|
83
|
+
if legacy:
|
|
84
|
+
return legacy
|
|
85
|
+
return _read_int_env("OLLAMA_SOFT_TURN_WARN", _DEFAULT_SOFT_TURN_WARN)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
OLLAMA_SOFT_TURN_WARN = _resolve_soft_turn_warn()
|
|
89
|
+
"""Soft warning threshold on per-cycle ``num_turns``.
|
|
90
|
+
|
|
91
|
+
Defaults to 500. Override via ``OLLAMA_SOFT_TURN_WARN`` (new name) or the
|
|
92
|
+
legacy ``OLLAMA_TURN_BUDGET`` (preserved for backwards compatibility). The
|
|
93
|
+
value is **non-blocking**: the pipeline only logs a WARNING once per cycle
|
|
94
|
+
when the cumulative turn count first crosses this threshold. It never
|
|
95
|
+
aborts.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
# Kept as a module-level alias so existing importers (tests, scripts) keep
|
|
99
|
+
# working. The semantics are now "soft warning threshold", not "abort".
|
|
100
|
+
OLLAMA_TURN_BUDGET = OLLAMA_SOFT_TURN_WARN
|
|
101
|
+
"""Backwards-compatible alias for :data:`OLLAMA_SOFT_TURN_WARN`."""
|
|
102
|
+
|
|
103
|
+
OLLAMA_WALLCLOCK_BUDGET_SECONDS = _read_int_env(
|
|
104
|
+
"OLLAMA_WALLCLOCK_BUDGET_SECONDS", _DEFAULT_WALLCLOCK_BUDGET_SECONDS
|
|
105
|
+
)
|
|
106
|
+
"""Hard wall-clock abort threshold (seconds) per cycle on the Ollama codepath.
|
|
107
|
+
|
|
108
|
+
Defaults to 14400 (4 h); override via ``OLLAMA_WALLCLOCK_BUDGET_SECONDS``.
|
|
109
|
+
This is the only hard abort enforced by this module — it exists to catch a
|
|
110
|
+
stuck daemon or a pathological loop the per-phase
|
|
111
|
+
:class:`~code_generator.runner.retry.CircuitBreaker` cannot see.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# Exception
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class OllamaBudgetExceeded(RuntimeError):
|
|
121
|
+
"""Raised when the Ollama per-cycle wall-clock backstop is exceeded.
|
|
122
|
+
|
|
123
|
+
Only fires on the wall-clock path. The turn counter now emits a WARNING
|
|
124
|
+
instead; real per-call failures are handled by the ``CircuitBreaker``
|
|
125
|
+
in :mod:`code_generator.runner.retry`.
|
|
126
|
+
|
|
127
|
+
Subclasses ``RuntimeError`` to match the existing safety-abort hierarchy
|
|
128
|
+
(e.g. :class:`~code_generator.runner.types.OverageAbort`).
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# Tracker
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class OllamaBudgetTracker:
|
|
138
|
+
"""Adaptive per-cycle safety backstop; a no-op on the Anthropic Max path.
|
|
139
|
+
|
|
140
|
+
Emits one WARNING when the cycle crosses the soft turn threshold; aborts
|
|
141
|
+
only on the wall-clock backstop. Does **not** block the pipeline on the
|
|
142
|
+
turn count — real failures are the responsibility of
|
|
143
|
+
:class:`~code_generator.runner.retry.CircuitBreaker` and the rate-limit
|
|
144
|
+
handlers in :mod:`code_generator.runner.rate_limit`.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def __init__(
|
|
148
|
+
self,
|
|
149
|
+
*,
|
|
150
|
+
provider_is_ollama: bool,
|
|
151
|
+
clock: Callable[[], float] | None = None,
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Initialise a tracker; call :meth:`start` at cycle kickoff.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
provider_is_ollama: When False every :meth:`check` call is a
|
|
157
|
+
no-op so the Anthropic Max path stays byte-for-byte unchanged.
|
|
158
|
+
clock: Injectable monotonic clock for deterministic tests;
|
|
159
|
+
defaults to ``time.monotonic``.
|
|
160
|
+
"""
|
|
161
|
+
self._active = provider_is_ollama
|
|
162
|
+
self._clock = clock or time.monotonic
|
|
163
|
+
self._start_time: float | None = None
|
|
164
|
+
self._turn_warning_emitted = False
|
|
165
|
+
|
|
166
|
+
def start(self) -> None:
|
|
167
|
+
"""Record the cycle start time. Idempotent; only the first call matters."""
|
|
168
|
+
if self._active and self._start_time is None:
|
|
169
|
+
self._start_time = self._clock()
|
|
170
|
+
|
|
171
|
+
def check(self, state: State, cycle: CycleState | None) -> None:
|
|
172
|
+
"""Warn on soft-turn threshold; raise only on wall-clock overflow."""
|
|
173
|
+
if not self._active:
|
|
174
|
+
return
|
|
175
|
+
self._check_turn_soft_warn(state, cycle)
|
|
176
|
+
self._check_wallclock_budget()
|
|
177
|
+
|
|
178
|
+
def _check_turn_soft_warn(self, state: State, cycle: CycleState | None) -> None:
|
|
179
|
+
"""Emit one WARNING the first time the cycle crosses the soft threshold.
|
|
180
|
+
|
|
181
|
+
Never raises. Subsequent checks in the same cycle are no-ops because
|
|
182
|
+
the warning flag is sticky until the tracker is discarded.
|
|
183
|
+
"""
|
|
184
|
+
if self._turn_warning_emitted:
|
|
185
|
+
return
|
|
186
|
+
total = _sum_num_turns(state, cycle)
|
|
187
|
+
if total > OLLAMA_SOFT_TURN_WARN:
|
|
188
|
+
_logger.warning(
|
|
189
|
+
"Ollama cycle has consumed %d turns (soft threshold: %d). "
|
|
190
|
+
"Letting it run — real failures are caught by the per-phase "
|
|
191
|
+
"CircuitBreaker in runner/retry.py. Raise the threshold via "
|
|
192
|
+
"OLLAMA_SOFT_TURN_WARN or the legacy OLLAMA_TURN_BUDGET env "
|
|
193
|
+
"var to silence this warning.",
|
|
194
|
+
total,
|
|
195
|
+
OLLAMA_SOFT_TURN_WARN,
|
|
196
|
+
)
|
|
197
|
+
self._turn_warning_emitted = True
|
|
198
|
+
|
|
199
|
+
def _check_wallclock_budget(self) -> None:
|
|
200
|
+
if self._start_time is None:
|
|
201
|
+
return
|
|
202
|
+
elapsed = self._clock() - self._start_time
|
|
203
|
+
if elapsed > OLLAMA_WALLCLOCK_BUDGET_SECONDS:
|
|
204
|
+
raise OllamaBudgetExceeded(
|
|
205
|
+
f"Ollama wall-clock budget exceeded: {elapsed:.0f}s > "
|
|
206
|
+
f"{OLLAMA_WALLCLOCK_BUDGET_SECONDS}s. "
|
|
207
|
+
"Raise OLLAMA_WALLCLOCK_BUDGET_SECONDS (env var) if real "
|
|
208
|
+
"workloads legitimately need more than 4 hours per cycle."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _sum_num_turns(state: State, cycle: CycleState | None) -> int:
|
|
213
|
+
"""Sum ``num_turns`` across every phase of the active cycle (or state)."""
|
|
214
|
+
source = cycle.token_usage if cycle is not None else state.token_usage
|
|
215
|
+
return sum(usage.num_turns for usage in source.values())
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
"""Tests for Ollama per-cycle
|
|
1
|
+
"""Tests for the Ollama per-cycle adaptive safety backstop (issue #220, 0.4.11).
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
As of 0.4.11 the turn counter is a **non-blocking soft warning** — the
|
|
4
|
+
pipeline is never aborted on turn count alone. Real per-call failures are
|
|
5
|
+
the job of :class:`~code_generator.runner.retry.CircuitBreaker` and the
|
|
6
|
+
rate-limit handlers. The wall-clock remains a hard abort (default 4 h).
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
turn count and wall-clock. The Anthropic Max path is untouched.
|
|
8
|
-
|
|
9
|
-
Turn accumulation reads from ``state.token_usage[<phase>].num_turns`` (or
|
|
10
|
-
``cycle.token_usage[<phase>].num_turns`` when a cycle is active), which
|
|
11
|
-
#204/#205 already populate on every phase run.
|
|
8
|
+
Backwards compatibility: ``OLLAMA_TURN_BUDGET`` is preserved as an alias
|
|
9
|
+
for :data:`OLLAMA_SOFT_TURN_WARN` so existing scripts keep importing.
|
|
12
10
|
"""
|
|
13
11
|
|
|
14
12
|
from __future__ import annotations
|
|
15
13
|
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
16
|
import pytest
|
|
17
17
|
|
|
18
18
|
from code_generator import state as _state
|
|
19
19
|
from code_generator.orchestrator.ollama_budget import (
|
|
20
|
+
OLLAMA_SOFT_TURN_WARN,
|
|
20
21
|
OLLAMA_TURN_BUDGET,
|
|
21
22
|
OLLAMA_WALLCLOCK_BUDGET_SECONDS,
|
|
22
23
|
OllamaBudgetExceeded,
|
|
@@ -29,12 +30,16 @@ from code_generator.runner.types import TokenUsage
|
|
|
29
30
|
# ---------------------------------------------------------------------------
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
class
|
|
33
|
-
def
|
|
34
|
-
assert
|
|
33
|
+
class TestThresholdConstants:
|
|
34
|
+
def test_soft_turn_warn_default_is_500(self) -> None:
|
|
35
|
+
assert OLLAMA_SOFT_TURN_WARN == 500
|
|
35
36
|
|
|
36
|
-
def
|
|
37
|
-
|
|
37
|
+
def test_turn_budget_alias_matches_soft_turn_warn(self) -> None:
|
|
38
|
+
"""Legacy ``OLLAMA_TURN_BUDGET`` must alias the new soft-warn constant."""
|
|
39
|
+
assert OLLAMA_TURN_BUDGET == OLLAMA_SOFT_TURN_WARN
|
|
40
|
+
|
|
41
|
+
def test_wallclock_budget_default_is_4_hours(self) -> None:
|
|
42
|
+
assert OLLAMA_WALLCLOCK_BUDGET_SECONDS == 14400
|
|
38
43
|
|
|
39
44
|
|
|
40
45
|
# ---------------------------------------------------------------------------
|
|
@@ -69,69 +74,78 @@ def _make_state_with_usage(
|
|
|
69
74
|
|
|
70
75
|
|
|
71
76
|
# ---------------------------------------------------------------------------
|
|
72
|
-
#
|
|
77
|
+
# Soft-turn warning
|
|
73
78
|
# ---------------------------------------------------------------------------
|
|
74
79
|
|
|
75
80
|
|
|
76
|
-
class
|
|
77
|
-
def
|
|
78
|
-
"""
|
|
81
|
+
class TestTurnSoftWarning:
|
|
82
|
+
def test_under_threshold_does_not_warn(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
83
|
+
"""Below the soft threshold → no WARNING emitted, no raise."""
|
|
79
84
|
st, cycle = _make_state_with_usage({}, cycle_turns={"phase1": 50, "phase2": 80})
|
|
80
85
|
tracker = OllamaBudgetTracker(provider_is_ollama=True)
|
|
81
86
|
|
|
82
|
-
|
|
87
|
+
with caplog.at_level(logging.WARNING):
|
|
88
|
+
tracker.check(st, cycle)
|
|
83
89
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
turn_warnings = [r for r in caplog.records if "turns" in r.message.lower()]
|
|
91
|
+
assert turn_warnings == []
|
|
92
|
+
|
|
93
|
+
def test_over_threshold_warns_without_raising(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
94
|
+
"""Crossing the soft threshold logs a WARNING; the pipeline continues."""
|
|
95
|
+
st, cycle = _make_state_with_usage(
|
|
96
|
+
{}, cycle_turns={"phase3_4": OLLAMA_SOFT_TURN_WARN + 100}
|
|
97
|
+
)
|
|
87
98
|
tracker = OllamaBudgetTracker(provider_is_ollama=True)
|
|
88
99
|
|
|
89
|
-
|
|
100
|
+
with caplog.at_level(logging.WARNING):
|
|
101
|
+
tracker.check(st, cycle) # must not raise
|
|
90
102
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
turn_warnings = [r for r in caplog.records if "consumed" in r.message.lower()]
|
|
104
|
+
assert len(turn_warnings) == 1
|
|
105
|
+
msg = turn_warnings[0].message
|
|
106
|
+
assert str(OLLAMA_SOFT_TURN_WARN + 100) in msg
|
|
107
|
+
assert str(OLLAMA_SOFT_TURN_WARN) in msg
|
|
108
|
+
|
|
109
|
+
def test_warning_is_emitted_only_once_per_cycle(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
110
|
+
"""Subsequent checks after the first warning must stay silent."""
|
|
111
|
+
st, cycle = _make_state_with_usage(
|
|
112
|
+
{}, cycle_turns={"phase3_4": OLLAMA_SOFT_TURN_WARN + 100}
|
|
113
|
+
)
|
|
94
114
|
tracker = OllamaBudgetTracker(provider_is_ollama=True)
|
|
95
115
|
|
|
96
|
-
with
|
|
116
|
+
with caplog.at_level(logging.WARNING):
|
|
117
|
+
tracker.check(st, cycle)
|
|
118
|
+
tracker.check(st, cycle)
|
|
97
119
|
tracker.check(st, cycle)
|
|
98
120
|
|
|
99
|
-
|
|
100
|
-
assert
|
|
101
|
-
assert str(OLLAMA_TURN_BUDGET) in msg
|
|
102
|
-
assert str(OLLAMA_TURN_BUDGET + 1) in msg
|
|
121
|
+
turn_warnings = [r for r in caplog.records if "consumed" in r.message.lower()]
|
|
122
|
+
assert len(turn_warnings) == 1
|
|
103
123
|
|
|
104
|
-
def test_turns_aggregate_across_phases(self) -> None:
|
|
105
|
-
"""num_turns from every phase sum toward the
|
|
124
|
+
def test_turns_aggregate_across_phases(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
125
|
+
"""num_turns from every phase sum toward the soft threshold."""
|
|
106
126
|
st, cycle = _make_state_with_usage(
|
|
107
127
|
{},
|
|
108
128
|
cycle_turns={
|
|
109
|
-
"phase0":
|
|
110
|
-
"phase1":
|
|
111
|
-
"phase2":
|
|
112
|
-
"phase3_4":
|
|
113
|
-
"phase5":
|
|
114
|
-
"phase6":
|
|
115
|
-
"phase7":
|
|
129
|
+
"phase0": 100,
|
|
130
|
+
"phase1": 80,
|
|
131
|
+
"phase2": 80,
|
|
132
|
+
"phase3_4": 200,
|
|
133
|
+
"phase5": 60,
|
|
134
|
+
"phase6": 20,
|
|
135
|
+
"phase7": 10,
|
|
116
136
|
},
|
|
117
137
|
)
|
|
118
|
-
# Sum =
|
|
138
|
+
# Sum = 550 > 500.
|
|
119
139
|
tracker = OllamaBudgetTracker(provider_is_ollama=True)
|
|
120
140
|
|
|
121
|
-
with
|
|
122
|
-
tracker.check(st, cycle)
|
|
123
|
-
|
|
124
|
-
def test_single_mode_uses_state_token_usage_when_cycle_is_none(self) -> None:
|
|
125
|
-
"""When ``cycle`` is None, total turns come from state.token_usage."""
|
|
126
|
-
st, _c = _make_state_with_usage({"phase0": OLLAMA_TURN_BUDGET + 5})
|
|
127
|
-
tracker = OllamaBudgetTracker(provider_is_ollama=True)
|
|
141
|
+
with caplog.at_level(logging.WARNING):
|
|
142
|
+
tracker.check(st, cycle) # must not raise
|
|
128
143
|
|
|
129
|
-
|
|
130
|
-
tracker.check(st, cycle=None)
|
|
144
|
+
assert any("550" in r.message for r in caplog.records)
|
|
131
145
|
|
|
132
146
|
|
|
133
147
|
# ---------------------------------------------------------------------------
|
|
134
|
-
# Wall-clock
|
|
148
|
+
# Wall-clock hard abort
|
|
135
149
|
# ---------------------------------------------------------------------------
|
|
136
150
|
|
|
137
151
|
|
|
@@ -145,7 +159,6 @@ class TestWallclockBudget:
|
|
|
145
159
|
clock=lambda: now,
|
|
146
160
|
)
|
|
147
161
|
tracker.start()
|
|
148
|
-
# Simulate time passage below the budget.
|
|
149
162
|
now += OLLAMA_WALLCLOCK_BUDGET_SECONDS - 10
|
|
150
163
|
|
|
151
164
|
tracker.check(st, cycle) # must not raise
|
|
@@ -153,7 +166,6 @@ class TestWallclockBudget:
|
|
|
153
166
|
def test_over_budget_raises_with_exact_message(self) -> None:
|
|
154
167
|
"""Elapsed > budget → OllamaBudgetExceeded naming wall-clock."""
|
|
155
168
|
st, cycle = _make_state_with_usage({}, cycle_turns={"phase0": 1})
|
|
156
|
-
# Use a small mutable clock to simulate time moving forward.
|
|
157
169
|
t = [1_000_000.0]
|
|
158
170
|
|
|
159
171
|
def _clock() -> float:
|
|
@@ -175,22 +187,36 @@ class TestWallclockBudget:
|
|
|
175
187
|
st, cycle = _make_state_with_usage({}, cycle_turns={"phase0": 1})
|
|
176
188
|
tracker = OllamaBudgetTracker(provider_is_ollama=True, clock=lambda: 1e12)
|
|
177
189
|
|
|
178
|
-
|
|
179
|
-
|
|
190
|
+
tracker.check(st, cycle) # must not raise
|
|
191
|
+
|
|
192
|
+
def test_extreme_turn_count_does_not_raise(self) -> None:
|
|
193
|
+
"""No amount of turns should raise on its own — only wall-clock hard-aborts."""
|
|
194
|
+
st, cycle = _make_state_with_usage(
|
|
195
|
+
{}, cycle_turns={"phase3_4": OLLAMA_SOFT_TURN_WARN * 100}
|
|
196
|
+
)
|
|
197
|
+
tracker = OllamaBudgetTracker(provider_is_ollama=True)
|
|
198
|
+
|
|
199
|
+
tracker.check(st, cycle) # must not raise
|
|
180
200
|
|
|
181
201
|
|
|
182
202
|
# ---------------------------------------------------------------------------
|
|
183
|
-
# Anthropic Max path —
|
|
203
|
+
# Anthropic Max path — thresholds do not fire
|
|
184
204
|
# ---------------------------------------------------------------------------
|
|
185
205
|
|
|
186
206
|
|
|
187
207
|
class TestAnthropicMaxUntouched:
|
|
188
|
-
def
|
|
189
|
-
"""provider_is_ollama=False →
|
|
190
|
-
st, cycle = _make_state_with_usage(
|
|
208
|
+
def test_anthropic_max_mode_skips_turn_warning(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
209
|
+
"""provider_is_ollama=False → no WARNING, no raise."""
|
|
210
|
+
st, cycle = _make_state_with_usage(
|
|
211
|
+
{}, cycle_turns={"phase3_4": OLLAMA_SOFT_TURN_WARN + 100}
|
|
212
|
+
)
|
|
191
213
|
tracker = OllamaBudgetTracker(provider_is_ollama=False)
|
|
192
214
|
|
|
193
|
-
|
|
215
|
+
with caplog.at_level(logging.WARNING):
|
|
216
|
+
tracker.check(st, cycle)
|
|
217
|
+
|
|
218
|
+
turn_warnings = [r for r in caplog.records if "consumed" in r.message.lower()]
|
|
219
|
+
assert turn_warnings == []
|
|
194
220
|
|
|
195
221
|
def test_anthropic_max_mode_skips_wallclock_budget(self) -> None:
|
|
196
222
|
"""provider_is_ollama=False → wall-clock breach does not raise."""
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
"""Per-cycle safety budgets for the Ollama codepath (issue #220).
|
|
2
|
-
|
|
3
|
-
Ollama does not emit Anthropic's ``RateLimitEvent.info.overage_status``, so
|
|
4
|
-
non-negotiable #4 (abort on overage) cannot fire on the Ollama codepath.
|
|
5
|
-
This module substitutes the safety invariant with two explicit per-cycle
|
|
6
|
-
budgets — a turn-count cap and a wall-clock cap — whose breaches raise
|
|
7
|
-
:class:`OllamaBudgetExceeded` with a message naming which budget was hit.
|
|
8
|
-
|
|
9
|
-
These caps are defensive defaults. Operators who regularly exceed them on
|
|
10
|
-
real workloads should tune the module-level constants in one place rather
|
|
11
|
-
than editing phase internals.
|
|
12
|
-
|
|
13
|
-
Nothing is persisted in ``state.json`` — budgets are per-run enforcement,
|
|
14
|
-
not cross-resume accounting. A fresh ``OllamaBudgetTracker`` is created
|
|
15
|
-
for every cycle and discarded on abort or clean completion.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
from __future__ import annotations
|
|
19
|
-
|
|
20
|
-
import time
|
|
21
|
-
from typing import TYPE_CHECKING
|
|
22
|
-
|
|
23
|
-
if TYPE_CHECKING:
|
|
24
|
-
from collections.abc import Callable
|
|
25
|
-
|
|
26
|
-
from code_generator.state import CycleState, State
|
|
27
|
-
|
|
28
|
-
# ---------------------------------------------------------------------------
|
|
29
|
-
# Budget constants
|
|
30
|
-
# ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
OLLAMA_TURN_BUDGET = 200
|
|
33
|
-
"""Maximum num_turns allowed per cycle on the Ollama codepath."""
|
|
34
|
-
|
|
35
|
-
OLLAMA_WALLCLOCK_BUDGET_SECONDS = 3600
|
|
36
|
-
"""Maximum wall-clock elapsed (seconds) per cycle on the Ollama codepath (1 hour)."""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# ---------------------------------------------------------------------------
|
|
40
|
-
# Exception
|
|
41
|
-
# ---------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class OllamaBudgetExceeded(RuntimeError):
|
|
45
|
-
"""Raised when the Ollama per-cycle turn or wall-clock budget is exceeded.
|
|
46
|
-
|
|
47
|
-
Subclasses ``RuntimeError`` to match the existing safety-abort hierarchy
|
|
48
|
-
(e.g. :class:`~code_generator.runner.types.OverageAbort`).
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# ---------------------------------------------------------------------------
|
|
53
|
-
# Tracker
|
|
54
|
-
# ---------------------------------------------------------------------------
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class OllamaBudgetTracker:
|
|
58
|
-
"""Cheap per-cycle budget checker; a no-op on the Anthropic Max path."""
|
|
59
|
-
|
|
60
|
-
def __init__(
|
|
61
|
-
self,
|
|
62
|
-
*,
|
|
63
|
-
provider_is_ollama: bool,
|
|
64
|
-
clock: Callable[[], float] | None = None,
|
|
65
|
-
) -> None:
|
|
66
|
-
"""Initialise a tracker; call :meth:`start` at cycle kickoff.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
provider_is_ollama: When False every :meth:`check` call is a
|
|
70
|
-
no-op so the Anthropic Max path stays byte-for-byte unchanged.
|
|
71
|
-
clock: Injectable monotonic clock for deterministic tests;
|
|
72
|
-
defaults to ``time.monotonic``.
|
|
73
|
-
"""
|
|
74
|
-
self._active = provider_is_ollama
|
|
75
|
-
self._clock = clock or time.monotonic
|
|
76
|
-
self._start_time: float | None = None
|
|
77
|
-
|
|
78
|
-
def start(self) -> None:
|
|
79
|
-
"""Record the cycle start time. Idempotent; only the first call matters."""
|
|
80
|
-
if self._active and self._start_time is None:
|
|
81
|
-
self._start_time = self._clock()
|
|
82
|
-
|
|
83
|
-
def check(self, state: State, cycle: CycleState | None) -> None:
|
|
84
|
-
"""Raise :class:`OllamaBudgetExceeded` when either budget is breached."""
|
|
85
|
-
if not self._active:
|
|
86
|
-
return
|
|
87
|
-
self._check_turn_budget(state, cycle)
|
|
88
|
-
self._check_wallclock_budget()
|
|
89
|
-
|
|
90
|
-
def _check_turn_budget(self, state: State, cycle: CycleState | None) -> None:
|
|
91
|
-
total = _sum_num_turns(state, cycle)
|
|
92
|
-
if total > OLLAMA_TURN_BUDGET:
|
|
93
|
-
raise OllamaBudgetExceeded(
|
|
94
|
-
f"Ollama turn-count budget exceeded: {total} > {OLLAMA_TURN_BUDGET}. "
|
|
95
|
-
"Raise OLLAMA_TURN_BUDGET in code_generator.orchestrator.ollama_budget "
|
|
96
|
-
"if real workloads need a higher cap."
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
def _check_wallclock_budget(self) -> None:
|
|
100
|
-
if self._start_time is None:
|
|
101
|
-
return
|
|
102
|
-
elapsed = self._clock() - self._start_time
|
|
103
|
-
if elapsed > OLLAMA_WALLCLOCK_BUDGET_SECONDS:
|
|
104
|
-
raise OllamaBudgetExceeded(
|
|
105
|
-
f"Ollama wall-clock budget exceeded: {elapsed:.0f}s > "
|
|
106
|
-
f"{OLLAMA_WALLCLOCK_BUDGET_SECONDS}s. "
|
|
107
|
-
"Raise OLLAMA_WALLCLOCK_BUDGET_SECONDS in "
|
|
108
|
-
"code_generator.orchestrator.ollama_budget if real workloads "
|
|
109
|
-
"need a higher cap."
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def _sum_num_turns(state: State, cycle: CycleState | None) -> int:
|
|
114
|
-
"""Sum ``num_turns`` across every phase of the active cycle (or state)."""
|
|
115
|
-
source = cycle.token_usage if cycle is not None else state.token_usage
|
|
116
|
-
return sum(usage.num_turns for usage in source.values())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|