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.
Files changed (183) hide show
  1. {claude_code_generator-0.4.9/src/claude_code_generator.egg-info → claude_code_generator-0.4.11}/PKG-INFO +2 -1
  2. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/pyproject.toml +11 -7
  3. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11/src/claude_code_generator.egg-info}/PKG-INFO +2 -1
  4. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/claude_code_generator.egg-info/requires.txt +1 -0
  5. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/__init__.py +1 -1
  6. claude_code_generator-0.4.11/src/code_generator/orchestrator/ollama_budget.py +215 -0
  7. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_ollama_budget.py +86 -60
  8. claude_code_generator-0.4.9/src/code_generator/orchestrator/ollama_budget.py +0 -116
  9. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/LICENSE +0 -0
  10. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/README.md +0 -0
  11. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/setup.cfg +0 -0
  12. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/claude_code_generator.egg-info/SOURCES.txt +0 -0
  13. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/claude_code_generator.egg-info/dependency_links.txt +0 -0
  14. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/claude_code_generator.egg-info/entry_points.txt +0 -0
  15. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/claude_code_generator.egg-info/top_level.txt +0 -0
  16. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/agents.py +0 -0
  17. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/cli.py +0 -0
  18. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/__init__.py +0 -0
  19. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/_bench_io.py +0 -0
  20. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/_crash_recovery.py +0 -0
  21. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/_detect.py +0 -0
  22. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/_dispatch.py +0 -0
  23. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/_resume.py +0 -0
  24. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/_validators.py +0 -0
  25. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/bench.py +0 -0
  26. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/bench_compare.py +0 -0
  27. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/bench_export.py +0 -0
  28. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/generate.py +0 -0
  29. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/init.py +0 -0
  30. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/optimize.py +0 -0
  31. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/review.py +0 -0
  32. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/commands/status.py +0 -0
  33. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/effort.py +0 -0
  34. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/env.py +0 -0
  35. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/gh/__init__.py +0 -0
  36. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/gh/core.py +0 -0
  37. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/gh/issues.py +0 -0
  38. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/gh/labels.py +0 -0
  39. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/gh/milestones.py +0 -0
  40. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/git_ops.py +0 -0
  41. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/logging_setup.py +0 -0
  42. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/memory.py +0 -0
  43. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/__init__.py +0 -0
  44. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/_client_lifecycle.py +0 -0
  45. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/_comments.py +0 -0
  46. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/_memory_writers.py +0 -0
  47. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/_phase5_precommit.py +0 -0
  48. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/cycle_loop.py +0 -0
  49. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/cycle_prompts.py +0 -0
  50. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/phase0_complexity.py +0 -0
  51. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/phase1_plan.py +0 -0
  52. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/phase2_review.py +0 -0
  53. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/phase3_4_implement.py +0 -0
  54. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/phase5_closure.py +0 -0
  55. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/phase6_test.py +0 -0
  56. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/orchestrator/phase7_commit.py +0 -0
  57. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/preflight.py +0 -0
  58. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/__init__.py +0 -0
  59. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/hashes.py +0 -0
  60. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-cycle-specializer.md +0 -0
  61. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-optimize-requirements.md +0 -0
  62. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-phase-0-complexity.md +0 -0
  63. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-phase-1-planning.md +0 -0
  64. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-phase-2-batch-review.md +0 -0
  65. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-phase-2-issue-review.md +0 -0
  66. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-phase-3-implementation.md +0 -0
  67. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-phase-5-final-review.md +0 -0
  68. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-phase-6-test.md +0 -0
  69. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-phase-7-commit.md +0 -0
  70. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/prompts/prompt-review.md +0 -0
  71. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/repo_info.py +0 -0
  72. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/repomap.py +0 -0
  73. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/requirements_structure.py +0 -0
  74. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/__init__.py +0 -0
  75. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/_telemetry.py +0 -0
  76. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/batch.py +0 -0
  77. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/fake_runner.py +0 -0
  78. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/mcp.py +0 -0
  79. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/message_parsing.py +0 -0
  80. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/options.py +0 -0
  81. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/protocol.py +0 -0
  82. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/rate_limit.py +0 -0
  83. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/retry.py +0 -0
  84. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/sdk_runner.py +0 -0
  85. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/soft_reset.py +0 -0
  86. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/state_guard.py +0 -0
  87. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/subprocess_runner.py +0 -0
  88. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/types.py +0 -0
  89. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/runner/utils.py +0 -0
  90. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/state.py +0 -0
  91. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/state_retention.py +0 -0
  92. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/templates/__init__.py +0 -0
  93. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/templates/angular.md +0 -0
  94. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/templates/base.md +0 -0
  95. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/templates/fastapi.md +0 -0
  96. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/templates/finance.md +0 -0
  97. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/templates/fullstack.md +0 -0
  98. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/templates/nestjs.md +0 -0
  99. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/src/code_generator/templates/python-cli.md +0 -0
  100. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_agents.py +0 -0
  101. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_bench.py +0 -0
  102. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_bench_compare.py +0 -0
  103. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_bench_export.py +0 -0
  104. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_bench_fixture.py +0 -0
  105. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_bench_regression.py +0 -0
  106. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_changelog.py +0 -0
  107. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_claude_md.py +0 -0
  108. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_client_lifecycle.py +0 -0
  109. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_comments.py +0 -0
  110. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_commit_message.py +0 -0
  111. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_crash_recovery.py +0 -0
  112. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_cycle_loop.py +0 -0
  113. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_cycle_loop_multicycle.py +0 -0
  114. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_cycle_ollama_model.py +0 -0
  115. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_cycle_prompts.py +0 -0
  116. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_delta_planning.py +0 -0
  117. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_dependencies.py +0 -0
  118. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_detect.py +0 -0
  119. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_docs_no_default_max_turns.py +0 -0
  120. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_docs_ollama_model_guide.py +0 -0
  121. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_docs_ollama_pro.py +0 -0
  122. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_effective_model_routing.py +0 -0
  123. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_effort.py +0 -0
  124. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_env.py +0 -0
  125. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_generate.py +0 -0
  126. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_generate_ollama.py +0 -0
  127. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_generate_resume.py +0 -0
  128. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_gh.py +0 -0
  129. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_gh_labels.py +0 -0
  130. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_gh_milestones.py +0 -0
  131. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_gh_repo_threading.py +0 -0
  132. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_gh_submodules.py +0 -0
  133. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_git_ops.py +0 -0
  134. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_init.py +0 -0
  135. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_logging_setup.py +0 -0
  136. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_max_turns_cli_flag.py +0 -0
  137. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_mcp.py +0 -0
  138. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_memory.py +0 -0
  139. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_memory_writers.py +0 -0
  140. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_message_parsing.py +0 -0
  141. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_no_max_turns_in_call_sites.py +0 -0
  142. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_no_max_turns_literal.py +0 -0
  143. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_non_goals_grep_guard.py +0 -0
  144. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_ollama_rate_limit.py +0 -0
  145. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_optimize.py +0 -0
  146. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_options.py +0 -0
  147. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase0.py +0 -0
  148. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase1.py +0 -0
  149. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase2.py +0 -0
  150. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase2_batch.py +0 -0
  151. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase3_4.py +0 -0
  152. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase5.py +0 -0
  153. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase5_precommit.py +0 -0
  154. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase6.py +0 -0
  155. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase7.py +0 -0
  156. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase_mcp_regression.py +0 -0
  157. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_phase_token_logging.py +0 -0
  158. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_preflight.py +0 -0
  159. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_preflight_ollama.py +0 -0
  160. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_prompt_drift.py +0 -0
  161. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_prompt_prefix_snapshots.py +0 -0
  162. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_prompt_prefix_stability.py +0 -0
  163. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_prompts.py +0 -0
  164. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_rate_limit.py +0 -0
  165. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_repo_info.py +0 -0
  166. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_repomap.py +0 -0
  167. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_requirements_structure.py +0 -0
  168. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_retry.py +0 -0
  169. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_review.py +0 -0
  170. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_runner_protocol.py +0 -0
  171. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_runner_protocol_annotations.py +0 -0
  172. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_runner_types.py +0 -0
  173. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_runner_utils.py +0 -0
  174. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_sdk_runner.py +0 -0
  175. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_sdk_runner_shared.py +0 -0
  176. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_session_mode.py +0 -0
  177. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_state.py +0 -0
  178. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_state_guard.py +0 -0
  179. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_state_retention.py +0 -0
  180. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_status.py +0 -0
  181. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_subprocess_runner.py +0 -0
  182. {claude_code_generator-0.4.9 → claude_code_generator-0.4.11}/tests/test_telemetry.py +0 -0
  183. {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.9
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.9"
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 — the codebase-memory-mcp and serena binaries
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. External binaries (codebase-memory-mcp Rust
63
- # crate, serena) must be installed separately see README.
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.9
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
@@ -15,6 +15,7 @@ ruff
15
15
 
16
16
  [mcp]
17
17
  mcp<2.0,>=1.0
18
+ serena-agent<2.0,>=1.1
18
19
 
19
20
  [repomap]
20
21
  tree-sitter>=0.23
@@ -1,3 +1,3 @@
1
1
  """code-generator: orchestrator CLI for end-to-end project generation."""
2
2
 
3
- __version__ = "0.4.9"
3
+ __version__ = "0.4.11"
@@ -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 budget enforcement (issue #220).
1
+ """Tests for the Ollama per-cycle adaptive safety backstop (issue #220, 0.4.11).
2
2
 
3
- TDD: Red Green.
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
- On the Ollama codepath (``effective_model`` is not ``None``) the orchestrator
6
- substitutes non-negotiable #4's overage-abort with two explicit budgets:
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 TestBudgetConstants:
33
- def test_turn_budget_default_is_200(self) -> None:
34
- assert OLLAMA_TURN_BUDGET == 200
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 test_wallclock_budget_default_is_3600_seconds(self) -> None:
37
- assert OLLAMA_WALLCLOCK_BUDGET_SECONDS == 3600
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
- # Turn-count budget
77
+ # Soft-turn warning
73
78
  # ---------------------------------------------------------------------------
74
79
 
75
80
 
76
- class TestTurnBudget:
77
- def test_under_budget_does_not_raise(self) -> None:
78
- """When total num_turns is below the budget, check() is a no-op."""
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
- tracker.check(st, cycle) # must not raise
87
+ with caplog.at_level(logging.WARNING):
88
+ tracker.check(st, cycle)
83
89
 
84
- def test_exactly_at_budget_does_not_raise(self) -> None:
85
- """The budget is strict-greater-than, so equal to the cap passes."""
86
- st, cycle = _make_state_with_usage({}, cycle_turns={"phase3_4": OLLAMA_TURN_BUDGET})
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
- tracker.check(st, cycle) # must not raise at ==
100
+ with caplog.at_level(logging.WARNING):
101
+ tracker.check(st, cycle) # must not raise
90
102
 
91
- def test_over_budget_raises_with_exact_message(self) -> None:
92
- """Exceeding the turn budget raises OllamaBudgetExceeded naming the budget."""
93
- st, cycle = _make_state_with_usage({}, cycle_turns={"phase3_4": OLLAMA_TURN_BUDGET + 1})
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 pytest.raises(OllamaBudgetExceeded) as excinfo:
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
- msg = str(excinfo.value)
100
- assert "turn" in msg.lower()
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 per-cycle budget."""
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": 50,
110
- "phase1": 30,
111
- "phase2": 30,
112
- "phase3_4": 60,
113
- "phase5": 20,
114
- "phase6": 10,
115
- "phase7": 5,
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 = 205 > 200.
138
+ # Sum = 550 > 500.
119
139
  tracker = OllamaBudgetTracker(provider_is_ollama=True)
120
140
 
121
- with pytest.raises(OllamaBudgetExceeded, match="205"):
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
- with pytest.raises(OllamaBudgetExceeded, match="turn"):
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 budget
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
- # No start() call; wall-clock assertion must skip. Turn check still runs.
179
- tracker.check(st, cycle) # must not raise (under turn budget, no start)
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 — budgets do not fire
203
+ # Anthropic Max path — thresholds do not fire
184
204
  # ---------------------------------------------------------------------------
185
205
 
186
206
 
187
207
  class TestAnthropicMaxUntouched:
188
- def test_anthropic_max_mode_skips_turn_budget(self) -> None:
189
- """provider_is_ollama=False → turn budget breach does not raise."""
190
- st, cycle = _make_state_with_usage({}, cycle_turns={"phase3_4": OLLAMA_TURN_BUDGET + 100})
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
- tracker.check(st, cycle) # must not raise
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())