claude-code-generator 0.4.10__tar.gz → 0.4.12__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.10/src/claude_code_generator.egg-info → claude_code_generator-0.4.12}/PKG-INFO +1 -1
  2. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/pyproject.toml +1 -1
  3. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12/src/claude_code_generator.egg-info}/PKG-INFO +1 -1
  4. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/__init__.py +1 -1
  5. claude_code_generator-0.4.12/src/code_generator/orchestrator/ollama_budget.py +215 -0
  6. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/phase1_plan.py +95 -21
  7. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_ollama_budget.py +86 -60
  8. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase1.py +97 -0
  9. claude_code_generator-0.4.10/src/code_generator/orchestrator/ollama_budget.py +0 -116
  10. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/LICENSE +0 -0
  11. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/README.md +0 -0
  12. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/setup.cfg +0 -0
  13. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/claude_code_generator.egg-info/SOURCES.txt +0 -0
  14. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/claude_code_generator.egg-info/dependency_links.txt +0 -0
  15. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/claude_code_generator.egg-info/entry_points.txt +0 -0
  16. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/claude_code_generator.egg-info/requires.txt +0 -0
  17. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/claude_code_generator.egg-info/top_level.txt +0 -0
  18. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/agents.py +0 -0
  19. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/cli.py +0 -0
  20. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/__init__.py +0 -0
  21. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/_bench_io.py +0 -0
  22. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/_crash_recovery.py +0 -0
  23. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/_detect.py +0 -0
  24. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/_dispatch.py +0 -0
  25. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/_resume.py +0 -0
  26. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/_validators.py +0 -0
  27. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/bench.py +0 -0
  28. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/bench_compare.py +0 -0
  29. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/bench_export.py +0 -0
  30. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/generate.py +0 -0
  31. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/init.py +0 -0
  32. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/optimize.py +0 -0
  33. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/review.py +0 -0
  34. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/commands/status.py +0 -0
  35. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/effort.py +0 -0
  36. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/env.py +0 -0
  37. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/gh/__init__.py +0 -0
  38. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/gh/core.py +0 -0
  39. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/gh/issues.py +0 -0
  40. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/gh/labels.py +0 -0
  41. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/gh/milestones.py +0 -0
  42. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/git_ops.py +0 -0
  43. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/logging_setup.py +0 -0
  44. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/memory.py +0 -0
  45. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/__init__.py +0 -0
  46. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/_client_lifecycle.py +0 -0
  47. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/_comments.py +0 -0
  48. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/_memory_writers.py +0 -0
  49. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/_phase5_precommit.py +0 -0
  50. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/cycle_loop.py +0 -0
  51. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/cycle_prompts.py +0 -0
  52. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/phase0_complexity.py +0 -0
  53. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/phase2_review.py +0 -0
  54. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/phase3_4_implement.py +0 -0
  55. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/phase5_closure.py +0 -0
  56. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/phase6_test.py +0 -0
  57. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/orchestrator/phase7_commit.py +0 -0
  58. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/preflight.py +0 -0
  59. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/__init__.py +0 -0
  60. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/hashes.py +0 -0
  61. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-cycle-specializer.md +0 -0
  62. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-optimize-requirements.md +0 -0
  63. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-phase-0-complexity.md +0 -0
  64. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-phase-1-planning.md +0 -0
  65. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-phase-2-batch-review.md +0 -0
  66. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-phase-2-issue-review.md +0 -0
  67. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-phase-3-implementation.md +0 -0
  68. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-phase-5-final-review.md +0 -0
  69. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-phase-6-test.md +0 -0
  70. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-phase-7-commit.md +0 -0
  71. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/prompts/prompt-review.md +0 -0
  72. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/repo_info.py +0 -0
  73. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/repomap.py +0 -0
  74. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/requirements_structure.py +0 -0
  75. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/__init__.py +0 -0
  76. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/_telemetry.py +0 -0
  77. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/batch.py +0 -0
  78. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/fake_runner.py +0 -0
  79. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/mcp.py +0 -0
  80. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/message_parsing.py +0 -0
  81. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/options.py +0 -0
  82. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/protocol.py +0 -0
  83. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/rate_limit.py +0 -0
  84. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/retry.py +0 -0
  85. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/sdk_runner.py +0 -0
  86. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/soft_reset.py +0 -0
  87. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/state_guard.py +0 -0
  88. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/subprocess_runner.py +0 -0
  89. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/types.py +0 -0
  90. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/runner/utils.py +0 -0
  91. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/state.py +0 -0
  92. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/state_retention.py +0 -0
  93. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/templates/__init__.py +0 -0
  94. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/templates/angular.md +0 -0
  95. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/templates/base.md +0 -0
  96. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/templates/fastapi.md +0 -0
  97. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/templates/finance.md +0 -0
  98. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/templates/fullstack.md +0 -0
  99. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/templates/nestjs.md +0 -0
  100. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/src/code_generator/templates/python-cli.md +0 -0
  101. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_agents.py +0 -0
  102. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_bench.py +0 -0
  103. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_bench_compare.py +0 -0
  104. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_bench_export.py +0 -0
  105. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_bench_fixture.py +0 -0
  106. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_bench_regression.py +0 -0
  107. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_changelog.py +0 -0
  108. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_claude_md.py +0 -0
  109. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_client_lifecycle.py +0 -0
  110. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_comments.py +0 -0
  111. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_commit_message.py +0 -0
  112. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_crash_recovery.py +0 -0
  113. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_cycle_loop.py +0 -0
  114. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_cycle_loop_multicycle.py +0 -0
  115. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_cycle_ollama_model.py +0 -0
  116. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_cycle_prompts.py +0 -0
  117. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_delta_planning.py +0 -0
  118. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_dependencies.py +0 -0
  119. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_detect.py +0 -0
  120. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_docs_no_default_max_turns.py +0 -0
  121. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_docs_ollama_model_guide.py +0 -0
  122. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_docs_ollama_pro.py +0 -0
  123. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_effective_model_routing.py +0 -0
  124. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_effort.py +0 -0
  125. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_env.py +0 -0
  126. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_generate.py +0 -0
  127. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_generate_ollama.py +0 -0
  128. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_generate_resume.py +0 -0
  129. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_gh.py +0 -0
  130. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_gh_labels.py +0 -0
  131. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_gh_milestones.py +0 -0
  132. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_gh_repo_threading.py +0 -0
  133. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_gh_submodules.py +0 -0
  134. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_git_ops.py +0 -0
  135. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_init.py +0 -0
  136. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_logging_setup.py +0 -0
  137. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_max_turns_cli_flag.py +0 -0
  138. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_mcp.py +0 -0
  139. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_memory.py +0 -0
  140. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_memory_writers.py +0 -0
  141. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_message_parsing.py +0 -0
  142. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_no_max_turns_in_call_sites.py +0 -0
  143. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_no_max_turns_literal.py +0 -0
  144. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_non_goals_grep_guard.py +0 -0
  145. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_ollama_rate_limit.py +0 -0
  146. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_optimize.py +0 -0
  147. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_options.py +0 -0
  148. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase0.py +0 -0
  149. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase2.py +0 -0
  150. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase2_batch.py +0 -0
  151. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase3_4.py +0 -0
  152. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase5.py +0 -0
  153. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase5_precommit.py +0 -0
  154. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase6.py +0 -0
  155. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase7.py +0 -0
  156. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase_mcp_regression.py +0 -0
  157. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_phase_token_logging.py +0 -0
  158. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_preflight.py +0 -0
  159. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_preflight_ollama.py +0 -0
  160. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_prompt_drift.py +0 -0
  161. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_prompt_prefix_snapshots.py +0 -0
  162. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_prompt_prefix_stability.py +0 -0
  163. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_prompts.py +0 -0
  164. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_rate_limit.py +0 -0
  165. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_repo_info.py +0 -0
  166. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_repomap.py +0 -0
  167. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_requirements_structure.py +0 -0
  168. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_retry.py +0 -0
  169. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_review.py +0 -0
  170. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_runner_protocol.py +0 -0
  171. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_runner_protocol_annotations.py +0 -0
  172. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_runner_types.py +0 -0
  173. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_runner_utils.py +0 -0
  174. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_sdk_runner.py +0 -0
  175. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_sdk_runner_shared.py +0 -0
  176. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_session_mode.py +0 -0
  177. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_state.py +0 -0
  178. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_state_guard.py +0 -0
  179. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_state_retention.py +0 -0
  180. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_status.py +0 -0
  181. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_subprocess_runner.py +0 -0
  182. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/tests/test_telemetry.py +0 -0
  183. {claude_code_generator-0.4.10 → claude_code_generator-0.4.12}/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.10
3
+ Version: 0.4.12
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "claude-code-generator"
7
- version = "0.4.10"
7
+ version = "0.4.12"
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" }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-generator
3
- Version: 0.4.10
3
+ Version: 0.4.12
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
@@ -1,3 +1,3 @@
1
1
  """code-generator: orchestrator CLI for end-to-end project generation."""
2
2
 
3
- __version__ = "0.4.10"
3
+ __version__ = "0.4.12"
@@ -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())
@@ -116,6 +116,47 @@ _PHASE1_DEFAULT_MODEL = "claude-opus-4-7"
116
116
  # CLAUDE.md invariant #8; overridden via ``effective_model`` on Ollama — #219.
117
117
 
118
118
 
119
+ _PHASE1_MAX_ATTEMPTS = 3
120
+ """Max planning attempts before surfacing ``Phase1NoIssuesError`` (0.4.12).
121
+
122
+ Weak open models on the Ollama codepath often respond with prose describing
123
+ the plan instead of invoking ``gh issue create`` via the Bash tool. A single
124
+ stricter re-prompt is usually enough to unblock them without operator
125
+ intervention.
126
+ """
127
+
128
+
129
+ _PHASE1_RETRY_NUDGE = (
130
+ "Your previous attempt finished without creating any GitHub issues. "
131
+ "This is a CRITICAL failure. You MUST use the Bash tool to invoke "
132
+ "`gh issue create` for each issue right now. Do NOT output markdown "
133
+ "or prose describing the plan — every planned issue must become a "
134
+ "real GitHub issue via `gh issue create ... --milestone "
135
+ '"{MILESTONE}" --assignee @me --label "..."`. Once you have called '
136
+ "`gh issue create` for every planned issue, print a one-line summary "
137
+ "per issue and stop.\n\n"
138
+ "The original instructions follow.\n\n"
139
+ )
140
+
141
+
142
+ def _accumulate_usage(
143
+ total: _state.TokenUsage | None,
144
+ delta: _state.TokenUsage,
145
+ ) -> _state.TokenUsage:
146
+ """Sum two TokenUsage records field-by-field across Phase 1 attempts."""
147
+ from code_generator.runner.types import TokenUsage
148
+
149
+ if total is None:
150
+ return delta
151
+ return TokenUsage(
152
+ input=total.input + delta.input,
153
+ output=total.output + delta.output,
154
+ cache_read=total.cache_read + delta.cache_read,
155
+ cache_write=total.cache_write + delta.cache_write,
156
+ num_turns=total.num_turns + delta.num_turns,
157
+ )
158
+
159
+
119
160
  def _load_specialized_prompt(
120
161
  project_dir: Path,
121
162
  state: State,
@@ -249,28 +290,56 @@ async def run(
249
290
  **max_turns_kwargs(max_turns),
250
291
  )
251
292
 
252
- result = await rate_limit.main_loop(
253
- runner_module,
254
- prompt,
255
- options,
256
- state_path=state_path,
257
- logger=logger,
258
- )
293
+ # Up to _PHASE1_MAX_ATTEMPTS attempts: the first with the normal
294
+ # prompt, subsequent attempts prefixed with a stricter nudge that
295
+ # tells the model to use the Bash tool to call ``gh issue create``
296
+ # right now. Weak open models on the Ollama codepath commonly
297
+ # respond with prose on the first turn; the nudge recovers most
298
+ # of those cases without operator intervention (0.4.12).
299
+ issue_states: list[_state.IssueState] = []
300
+ total_usage = result = None # type: ignore[assignment]
301
+ attempt_prompt = prompt
302
+ effective_model_name = effective_model or _PHASE1_DEFAULT_MODEL
303
+ for attempt in range(1, _PHASE1_MAX_ATTEMPTS + 1):
304
+ result = await rate_limit.main_loop(
305
+ runner_module,
306
+ attempt_prompt,
307
+ options,
308
+ state_path=state_path,
309
+ logger=logger,
310
+ )
311
+ total_usage = _accumulate_usage(total_usage, result.usage)
259
312
 
260
- # Fetch all issues in the milestone (open + closed) to support deduplication.
261
- raw_issues = gh.list_issues(
262
- repo,
263
- milestone=milestone_title,
264
- state="all",
265
- )
266
- issue_states = _build_issue_states(raw_issues)
313
+ raw_issues = gh.list_issues(
314
+ repo,
315
+ milestone=milestone_title,
316
+ state="all",
317
+ )
318
+ issue_states = _build_issue_states(raw_issues)
319
+ if issue_states:
320
+ break
321
+
322
+ if attempt >= _PHASE1_MAX_ATTEMPTS:
323
+ break
324
+
325
+ logger.warning(
326
+ "Phase 1 attempt %d/%d: model returned without creating any "
327
+ "GitHub issues. Re-prompting with a stricter nudge.",
328
+ attempt,
329
+ _PHASE1_MAX_ATTEMPTS,
330
+ )
331
+ nudge = _PHASE1_RETRY_NUDGE.replace("{MILESTONE}", milestone_title or "")
332
+ attempt_prompt = nudge + prompt
267
333
 
268
334
  if not issue_states:
269
335
  raise Phase1NoIssuesError(
270
- "Phase 1 finished without creating any GitHub issues. "
271
- "Opus likely misread the task (e.g. audited existing code instead "
272
- "of planning new work). Inspect .code-generator/logs/phase1.log, "
273
- "adjust prompt-phase-1-planning.md if needed, and re-run."
336
+ f"Phase 1 finished without creating any GitHub issues after "
337
+ f"{_PHASE1_MAX_ATTEMPTS} attempts (model={effective_model_name!r}). "
338
+ "The model likely responded with prose instead of invoking "
339
+ "`gh issue create` via the Bash tool. Inspect "
340
+ ".code-generator/logs/phase1.log for the tool-call trace, "
341
+ "try a stronger model, or simplify the cycle scope in "
342
+ "requirements.md and re-run."
274
343
  )
275
344
 
276
345
  target = cycle if cycle is not None else state
@@ -278,12 +347,17 @@ async def run(
278
347
  cycle.issues = issue_states
279
348
  else:
280
349
  state.issues = issue_states
281
- target.token_usage["phase1"] = result.usage
350
+ # Persist the accumulated usage across all attempts (not just the last).
351
+ target.token_usage["phase1"] = total_usage if total_usage is not None else result.usage
282
352
  if hasattr(target, "cache_telemetry"):
283
- accumulate_telemetry(target.cache_telemetry, result.usage, result.wall_seconds)
353
+ accumulate_telemetry(
354
+ target.cache_telemetry,
355
+ target.token_usage["phase1"],
356
+ result.wall_seconds,
357
+ )
284
358
 
285
359
  _state.save_state(state_path, state)
286
- log_phase_usage(logger, 1, result.usage)
360
+ log_phase_usage(logger, 1, target.token_usage["phase1"])
287
361
  logger.info("Phase 1: %d issues created.", len(issue_states))
288
362
 
289
363
  except Exception:
@@ -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."""