claude-code-generator 0.4.12__tar.gz → 0.4.14__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 (182) hide show
  1. {claude_code_generator-0.4.12/src/claude_code_generator.egg-info → claude_code_generator-0.4.14}/PKG-INFO +1 -1
  2. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/pyproject.toml +1 -1
  3. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14/src/claude_code_generator.egg-info}/PKG-INFO +1 -1
  4. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/__init__.py +1 -1
  5. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/env.py +30 -10
  6. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/_client_lifecycle.py +15 -4
  7. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/cycle_loop.py +6 -2
  8. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/ollama_budget.py +72 -49
  9. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-phase-6-test.md +13 -6
  10. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/subprocess_runner.py +11 -1
  11. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_ollama_budget.py +77 -28
  12. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/LICENSE +0 -0
  13. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/README.md +0 -0
  14. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/setup.cfg +0 -0
  15. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/claude_code_generator.egg-info/SOURCES.txt +0 -0
  16. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/claude_code_generator.egg-info/dependency_links.txt +0 -0
  17. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/claude_code_generator.egg-info/entry_points.txt +0 -0
  18. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/claude_code_generator.egg-info/requires.txt +0 -0
  19. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/claude_code_generator.egg-info/top_level.txt +0 -0
  20. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/agents.py +0 -0
  21. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/cli.py +0 -0
  22. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/__init__.py +0 -0
  23. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/_bench_io.py +0 -0
  24. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/_crash_recovery.py +0 -0
  25. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/_detect.py +0 -0
  26. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/_dispatch.py +0 -0
  27. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/_resume.py +0 -0
  28. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/_validators.py +0 -0
  29. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/bench.py +0 -0
  30. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/bench_compare.py +0 -0
  31. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/bench_export.py +0 -0
  32. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/generate.py +0 -0
  33. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/init.py +0 -0
  34. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/optimize.py +0 -0
  35. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/review.py +0 -0
  36. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/commands/status.py +0 -0
  37. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/effort.py +0 -0
  38. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/gh/__init__.py +0 -0
  39. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/gh/core.py +0 -0
  40. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/gh/issues.py +0 -0
  41. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/gh/labels.py +0 -0
  42. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/gh/milestones.py +0 -0
  43. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/git_ops.py +0 -0
  44. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/logging_setup.py +0 -0
  45. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/memory.py +0 -0
  46. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/__init__.py +0 -0
  47. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/_comments.py +0 -0
  48. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/_memory_writers.py +0 -0
  49. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/_phase5_precommit.py +0 -0
  50. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/cycle_prompts.py +0 -0
  51. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/phase0_complexity.py +0 -0
  52. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/phase1_plan.py +0 -0
  53. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/phase2_review.py +0 -0
  54. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/phase3_4_implement.py +0 -0
  55. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/phase5_closure.py +0 -0
  56. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/phase6_test.py +0 -0
  57. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/orchestrator/phase7_commit.py +0 -0
  58. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/preflight.py +0 -0
  59. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/__init__.py +0 -0
  60. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/hashes.py +0 -0
  61. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-cycle-specializer.md +0 -0
  62. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-optimize-requirements.md +0 -0
  63. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-phase-0-complexity.md +0 -0
  64. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-phase-1-planning.md +0 -0
  65. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-phase-2-batch-review.md +0 -0
  66. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-phase-2-issue-review.md +0 -0
  67. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-phase-3-implementation.md +0 -0
  68. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-phase-5-final-review.md +0 -0
  69. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-phase-7-commit.md +0 -0
  70. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/prompts/prompt-review.md +0 -0
  71. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/repo_info.py +0 -0
  72. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/repomap.py +0 -0
  73. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/requirements_structure.py +0 -0
  74. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/__init__.py +0 -0
  75. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/_telemetry.py +0 -0
  76. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/batch.py +0 -0
  77. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/fake_runner.py +0 -0
  78. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/mcp.py +0 -0
  79. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/message_parsing.py +0 -0
  80. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/options.py +0 -0
  81. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/protocol.py +0 -0
  82. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/rate_limit.py +0 -0
  83. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/retry.py +0 -0
  84. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/sdk_runner.py +0 -0
  85. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/soft_reset.py +0 -0
  86. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/state_guard.py +0 -0
  87. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/types.py +0 -0
  88. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/runner/utils.py +0 -0
  89. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/state.py +0 -0
  90. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/state_retention.py +0 -0
  91. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/templates/__init__.py +0 -0
  92. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/templates/angular.md +0 -0
  93. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/templates/base.md +0 -0
  94. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/templates/fastapi.md +0 -0
  95. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/templates/finance.md +0 -0
  96. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/templates/fullstack.md +0 -0
  97. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/templates/nestjs.md +0 -0
  98. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/src/code_generator/templates/python-cli.md +0 -0
  99. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_agents.py +0 -0
  100. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_bench.py +0 -0
  101. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_bench_compare.py +0 -0
  102. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_bench_export.py +0 -0
  103. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_bench_fixture.py +0 -0
  104. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_bench_regression.py +0 -0
  105. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_changelog.py +0 -0
  106. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_claude_md.py +0 -0
  107. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_client_lifecycle.py +0 -0
  108. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_comments.py +0 -0
  109. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_commit_message.py +0 -0
  110. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_crash_recovery.py +0 -0
  111. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_cycle_loop.py +0 -0
  112. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_cycle_loop_multicycle.py +0 -0
  113. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_cycle_ollama_model.py +0 -0
  114. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_cycle_prompts.py +0 -0
  115. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_delta_planning.py +0 -0
  116. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_dependencies.py +0 -0
  117. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_detect.py +0 -0
  118. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_docs_no_default_max_turns.py +0 -0
  119. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_docs_ollama_model_guide.py +0 -0
  120. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_docs_ollama_pro.py +0 -0
  121. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_effective_model_routing.py +0 -0
  122. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_effort.py +0 -0
  123. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_env.py +0 -0
  124. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_generate.py +0 -0
  125. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_generate_ollama.py +0 -0
  126. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_generate_resume.py +0 -0
  127. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_gh.py +0 -0
  128. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_gh_labels.py +0 -0
  129. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_gh_milestones.py +0 -0
  130. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_gh_repo_threading.py +0 -0
  131. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_gh_submodules.py +0 -0
  132. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_git_ops.py +0 -0
  133. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_init.py +0 -0
  134. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_logging_setup.py +0 -0
  135. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_max_turns_cli_flag.py +0 -0
  136. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_mcp.py +0 -0
  137. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_memory.py +0 -0
  138. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_memory_writers.py +0 -0
  139. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_message_parsing.py +0 -0
  140. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_no_max_turns_in_call_sites.py +0 -0
  141. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_no_max_turns_literal.py +0 -0
  142. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_non_goals_grep_guard.py +0 -0
  143. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_ollama_rate_limit.py +0 -0
  144. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_optimize.py +0 -0
  145. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_options.py +0 -0
  146. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase0.py +0 -0
  147. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase1.py +0 -0
  148. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase2.py +0 -0
  149. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase2_batch.py +0 -0
  150. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase3_4.py +0 -0
  151. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase5.py +0 -0
  152. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase5_precommit.py +0 -0
  153. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase6.py +0 -0
  154. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase7.py +0 -0
  155. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase_mcp_regression.py +0 -0
  156. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_phase_token_logging.py +0 -0
  157. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_preflight.py +0 -0
  158. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_preflight_ollama.py +0 -0
  159. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_prompt_drift.py +0 -0
  160. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_prompt_prefix_snapshots.py +0 -0
  161. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_prompt_prefix_stability.py +0 -0
  162. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_prompts.py +0 -0
  163. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_rate_limit.py +0 -0
  164. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_repo_info.py +0 -0
  165. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_repomap.py +0 -0
  166. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_requirements_structure.py +0 -0
  167. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_retry.py +0 -0
  168. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_review.py +0 -0
  169. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_runner_protocol.py +0 -0
  170. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_runner_protocol_annotations.py +0 -0
  171. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_runner_types.py +0 -0
  172. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_runner_utils.py +0 -0
  173. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_sdk_runner.py +0 -0
  174. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_sdk_runner_shared.py +0 -0
  175. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_session_mode.py +0 -0
  176. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_state.py +0 -0
  177. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_state_guard.py +0 -0
  178. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_state_retention.py +0 -0
  179. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_status.py +0 -0
  180. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_subprocess_runner.py +0 -0
  181. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/tests/test_telemetry.py +0 -0
  182. {claude_code_generator-0.4.12 → claude_code_generator-0.4.14}/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.12
3
+ Version: 0.4.14
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.12"
7
+ version = "0.4.14"
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.12
3
+ Version: 0.4.14
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.12"
3
+ __version__ = "0.4.13"
@@ -48,7 +48,17 @@ def strip_dangerous_env() -> None:
48
48
  (``sdk_runner.run``, ``run_with_shared_client``).
49
49
  """
50
50
  ollama_mode = _is_localhost_base_url(os.environ.get("ANTHROPIC_BASE_URL"))
51
- protected = {"ANTHROPIC_AUTH_TOKEN"} if ollama_mode else set()
51
+ # On the Ollama codepath, ANTHROPIC_AUTH_TOKEN carries the daemon token
52
+ # and must survive stripping. ANTHROPIC_API_KEY is also protected when it
53
+ # has been set to "" by assert_safe_environment_ollama() — the empty-string
54
+ # sentinel tells the SDK to accept non-Claude model tags. A non-empty
55
+ # ANTHROPIC_API_KEY (e.g. "sk-evil") is still stripped because it would
56
+ # route traffic to Anthropic's real API, defeating the localhost redirect.
57
+ protected: set[str] = set()
58
+ if ollama_mode:
59
+ protected.add("ANTHROPIC_AUTH_TOKEN")
60
+ if os.environ.get("ANTHROPIC_API_KEY") == "":
61
+ protected.add("ANTHROPIC_API_KEY")
52
62
  for var in DANGEROUS_VARS:
53
63
  if var in protected:
54
64
  continue
@@ -116,7 +126,7 @@ def _require_ollama_preconditions() -> str:
116
126
 
117
127
  Refuses the bypass when:
118
128
  * ``OLLAMA_API_KEY`` is unset or empty.
119
- * ``ANTHROPIC_BASE_URL`` is pre-set to anything other than the pinned URL.
129
+ * ``ANTHROPIC_BASE_URL`` is pre-set to a non-localhost value.
120
130
 
121
131
  A non-empty ``ANTHROPIC_API_KEY`` in the parent env is **not** a refusal:
122
132
  ``_build_ollama_env`` strips every ``ANTHROPIC_*`` var from the returned
@@ -132,10 +142,10 @@ def _require_ollama_preconditions() -> str:
132
142
  "before running with provider='ollama'."
133
143
  )
134
144
  preset_base = os.environ.get("ANTHROPIC_BASE_URL")
135
- if preset_base and preset_base != OLLAMA_BASE_URL:
145
+ if preset_base and not _is_localhost_base_url(preset_base):
136
146
  raise RuntimeError(
137
- f"ANTHROPIC_BASE_URL={preset_base!r} does not match the pinned "
138
- f"{OLLAMA_BASE_URL!r}. Unset it or point it at the local daemon."
147
+ f"ANTHROPIC_BASE_URL={preset_base!r} does not point at a local "
148
+ f"daemon. Unset it or point it at localhost/127.0.0.1."
139
149
  )
140
150
  return token
141
151
 
@@ -222,14 +232,24 @@ def assert_single_workspace(
222
232
  )
223
233
 
224
234
 
235
+ _LOCALHOST_PREFIXES: tuple[str, ...] = (
236
+ "http://localhost:",
237
+ "http://127.0.0.1:",
238
+ "http://[::1]:",
239
+ )
240
+
241
+
225
242
  def _is_localhost_base_url(base_url: str | None) -> bool:
226
243
  """Return True when the ANTHROPIC_BASE_URL points at any localhost port.
227
244
 
228
- Deliberately lenient — any ``http://localhost:*`` prefix matches — because
229
- the workspace invariant is \"local endpoint\", not the strict :11434 the
230
- Ollama preflight refusal gate enforces.
245
+ Deliberately lenient — ``http://localhost:*``, ``http://127.0.0.1:*``,
246
+ and ``http://[::1]:*`` all match because the workspace invariant is
247
+ \"local endpoint\", not the strict :11434 the Ollama preflight refusal
248
+ gate enforces.
231
249
  """
232
- return base_url is not None and base_url.startswith("http://localhost:")
250
+ if base_url is None:
251
+ return False
252
+ return any(base_url.startswith(prefix) for prefix in _LOCALHOST_PREFIXES)
233
253
 
234
254
 
235
255
  def _log_localhost_short_circuit_once(state: dict[str, object]) -> None:
@@ -302,7 +322,7 @@ def assert_safe_environment_ollama() -> None:
302
322
  preset_base = os.environ.get("ANTHROPIC_BASE_URL")
303
323
  if not token:
304
324
  return
305
- if preset_base and preset_base != OLLAMA_BASE_URL:
325
+ if preset_base and not _is_localhost_base_url(preset_base):
306
326
  return
307
327
 
308
328
  os.environ["ANTHROPIC_AUTH_TOKEN"] = token
@@ -92,12 +92,15 @@ def _open_cycle_client(options_template: Any) -> Any:
92
92
  return ClaudeSDKClient(options=options_template)
93
93
 
94
94
 
95
- def _build_cycle_options(project_dir: Path) -> Any:
95
+ def _build_cycle_options(project_dir: Path, effective_model: str | None = None) -> Any:
96
96
  """Return cycle-scoped options for the shared ``ClaudeSDKClient``.
97
97
 
98
98
  ``ClaudeSDKClient`` locks options at open-time — per-query changes do not
99
99
  apply. The shared client therefore opens with:
100
100
 
101
+ - ``model``: the effective Ollama model tag (when on the Ollama codepath),
102
+ or ``None`` to let the SDK default (Anthropic Max path). Must be set at
103
+ open-time because per-query overrides are silently ignored.
101
104
  - ``system_prompt``: canonical ``claude_code`` preset with
102
105
  ``exclude_dynamic_sections=True`` (§2 cache-safe prefix).
103
106
  - ``extra_headers``: default betas (extended-cache-ttl + token-efficient-tools).
@@ -114,6 +117,9 @@ def _build_cycle_options(project_dir: Path) -> Any:
114
117
 
115
118
  Args:
116
119
  project_dir: Project root; used to resolve MCP servers.
120
+ effective_model: Ollama model tag to lock into the shared client
121
+ (threaded from cycle_loop). When ``None``, the SDK default
122
+ (Anthropic Max path) is used.
117
123
 
118
124
  Returns:
119
125
  A ``ClaudeAgentOptions`` instance (or duck-typed fallback) suitable
@@ -121,15 +127,18 @@ def _build_cycle_options(project_dir: Path) -> Any:
121
127
  """
122
128
  mcp_servers = build_mcp_servers(project_dir)
123
129
  allowed_tools: list[str] = list(_SHARED_CLIENT_ALLOWED_TOOLS) + mcp_tool_wildcards(mcp_servers)
124
- return make_agent_options(
130
+ kwargs: dict[str, Any] = dict(
125
131
  allowed_tools=allowed_tools,
126
132
  permission_mode="bypassPermissions",
127
133
  mcp_servers=mcp_servers,
128
134
  )
135
+ if effective_model is not None:
136
+ kwargs["model"] = effective_model
137
+ return make_agent_options(**kwargs)
129
138
 
130
139
 
131
140
  @contextlib.asynccontextmanager
132
- async def managed_shared_client(state: State, state_path: Path): # type: ignore[return]
141
+ async def managed_shared_client(state: State, state_path: Path, effective_model: str | None = None): # type: ignore[return]
133
142
  """Async context manager owning the full shared-client lifecycle.
134
143
 
135
144
  On entry: strips dangerous env vars (non-negotiable #1), opens one
@@ -151,6 +160,8 @@ async def managed_shared_client(state: State, state_path: Path): # type: ignore
151
160
  Args:
152
161
  state: Root state (mutated by mark/clear helpers).
153
162
  state_path: Path to ``state.json`` for atomic persistence.
163
+ effective_model: Ollama model tag to lock into the shared client.
164
+ When ``None``, the SDK default (Anthropic Max path) is used.
154
165
 
155
166
  Yields:
156
167
  The open ``ClaudeSDKClient`` instance.
@@ -158,7 +169,7 @@ async def managed_shared_client(state: State, state_path: Path): # type: ignore
158
169
  _env.strip_dangerous_env()
159
170
  # state.json lives at {project_dir}/.code-generator/state.json.
160
171
  project_dir = state_path.parent.parent
161
- options = _build_cycle_options(project_dir)
172
+ options = _build_cycle_options(project_dir, effective_model=effective_model)
162
173
  async with _open_cycle_client(options) as client:
163
174
  _state.mark_client_alive(state, state_path)
164
175
  try:
@@ -435,7 +435,9 @@ async def run_single_mode(
435
435
  state_path = project_dir / ".code-generator" / "state.json"
436
436
 
437
437
  if state.session_mode in ("shared", "batch"):
438
- async with managed_shared_client(state, state_path) as client:
438
+ async with managed_shared_client(
439
+ state, state_path, effective_model=effective_model
440
+ ) as client:
439
441
  await _run_phases(
440
442
  state,
441
443
  None,
@@ -665,7 +667,9 @@ async def _run_cycle_phases(
665
667
  log_prefix = f"cycle{cycle.id}_"
666
668
 
667
669
  if state.session_mode in ("shared", "batch"):
668
- async with managed_shared_client(state, state_path) as client:
670
+ async with managed_shared_client(
671
+ state, state_path, effective_model=effective_model
672
+ ) as client:
669
673
  await _run_phases(
670
674
  state,
671
675
  cycle,
@@ -1,10 +1,8 @@
1
1
  """Per-cycle safety backstop for the Ollama codepath.
2
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:
3
+ The pre-0.4.11 design aborted cycles on two arbitrary counters — 200 turns
4
+ and 1 h wall-clock. That's the wrong layer: fault-detection already lives
5
+ in two places that trigger on real malfunctions, not on counters:
8
6
 
9
7
  * :class:`~code_generator.runner.retry.CircuitBreaker` — trips after ``N``
10
8
  consecutive failures on a single phase call. Already wrapped around
@@ -13,29 +11,27 @@ arbitrary counter:
13
11
  * :func:`~code_generator.runner.rate_limit.handle_ollama_429` —
14
12
  wait-and-resume on 429s returned by the Ollama daemon.
15
13
 
16
- This module therefore degrades to a **non-blocking adaptive** backstop:
14
+ Starting with 0.4.13 both thresholds are **soft warnings** — the module
15
+ never aborts the pipeline. Weak open models are slow AND chatty; letting
16
+ them run is the right call. The operator can always Ctrl-C a runaway.
17
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).
18
+ * ``OLLAMA_SOFT_TURN_WARN`` (int, positive; default 500). Emitted once
19
+ per cycle when cumulative ``num_turns`` crosses the threshold.
20
+ * ``OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS`` (int, positive; default 14400).
21
+ Emitted once per cycle when elapsed wall-clock crosses the threshold.
26
22
 
27
- Both thresholds are env-overridable:
23
+ Backwards-compatible shims: the legacy env names ``OLLAMA_TURN_BUDGET`` and
24
+ ``OLLAMA_WALLCLOCK_BUDGET_SECONDS`` remain honoured and map onto the new
25
+ soft-warn thresholds. Scripts that previously relied on the abort now see
26
+ a WARNING instead; the 0.4.11 changelog entry called this out for the turn
27
+ budget, and 0.4.13 extends the same semantics to wall-clock.
28
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.
29
+ :class:`OllamaBudgetExceeded` is retained only as a concrete exception
30
+ type for backwards compatibility with callers that ``except`` it it is
31
+ no longer raised by this module.
36
32
 
37
33
  Nothing is persisted in ``state.json`` — the tracker is per-run and
38
- discarded on abort or clean completion.
34
+ discarded on clean completion.
39
35
  """
40
36
 
41
37
  from __future__ import annotations
@@ -57,7 +53,7 @@ _logger = logging.getLogger(__name__)
57
53
  # ---------------------------------------------------------------------------
58
54
 
59
55
  _DEFAULT_SOFT_TURN_WARN = 500
60
- _DEFAULT_WALLCLOCK_BUDGET_SECONDS = 14400 # 4 h
56
+ _DEFAULT_WALLCLOCK_SOFT_WARN_SECONDS = 14400 # 4 h
61
57
 
62
58
 
63
59
  def _read_int_env(name: str, default: int) -> int:
@@ -100,17 +96,31 @@ aborts.
100
96
  OLLAMA_TURN_BUDGET = OLLAMA_SOFT_TURN_WARN
101
97
  """Backwards-compatible alias for :data:`OLLAMA_SOFT_TURN_WARN`."""
102
98
 
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.
99
+ def _resolve_wallclock_soft_warn() -> int:
100
+ """Honour legacy ``OLLAMA_WALLCLOCK_BUDGET_SECONDS`` env var for backwards compat."""
101
+ legacy = _read_int_env("OLLAMA_WALLCLOCK_BUDGET_SECONDS", 0)
102
+ if legacy:
103
+ return legacy
104
+ return _read_int_env(
105
+ "OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS", _DEFAULT_WALLCLOCK_SOFT_WARN_SECONDS
106
+ )
107
+
108
+
109
+ OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS = _resolve_wallclock_soft_warn()
110
+ """Soft warning threshold on per-cycle wall-clock elapsed (seconds).
107
111
 
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
+ Defaults to 14400 (4 h). Override via ``OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS``
113
+ (new name) or the legacy ``OLLAMA_WALLCLOCK_BUDGET_SECONDS`` (preserved for
114
+ backwards compatibility). The value is **non-blocking**: the pipeline only
115
+ logs a WARNING once per cycle when elapsed first crosses this threshold.
116
+ It never aborts.
112
117
  """
113
118
 
119
+ # Kept as a module-level alias so existing importers (tests, scripts) keep
120
+ # working. The semantics are now "soft warning threshold", not "abort".
121
+ OLLAMA_WALLCLOCK_BUDGET_SECONDS = OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS
122
+ """Backwards-compatible alias for :data:`OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS`."""
123
+
114
124
 
115
125
  # ---------------------------------------------------------------------------
116
126
  # Exception
@@ -118,11 +128,12 @@ stuck daemon or a pathological loop the per-phase
118
128
 
119
129
 
120
130
  class OllamaBudgetExceeded(RuntimeError):
121
- """Raised when the Ollama per-cycle wall-clock backstop is exceeded.
131
+ """Retained only for backwards compatibility with ``except`` clauses.
122
132
 
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`.
133
+ As of 0.4.13 this module never raises ``OllamaBudgetExceeded``. Both the
134
+ turn counter and the wall-clock are non-blocking soft WARNINGs. Real
135
+ per-call failures are handled by the ``CircuitBreaker`` in
136
+ :mod:`code_generator.runner.retry`.
126
137
 
127
138
  Subclasses ``RuntimeError`` to match the existing safety-abort hierarchy
128
139
  (e.g. :class:`~code_generator.runner.types.OverageAbort`).
@@ -137,9 +148,8 @@ class OllamaBudgetExceeded(RuntimeError):
137
148
  class OllamaBudgetTracker:
138
149
  """Adaptive per-cycle safety backstop; a no-op on the Anthropic Max path.
139
150
 
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
151
+ Emits at most one WARNING per threshold (turn count, wall-clock). **Never
152
+ aborts**. Real failures are the responsibility of
143
153
  :class:`~code_generator.runner.retry.CircuitBreaker` and the rate-limit
144
154
  handlers in :mod:`code_generator.runner.rate_limit`.
145
155
  """
@@ -162,6 +172,7 @@ class OllamaBudgetTracker:
162
172
  self._clock = clock or time.monotonic
163
173
  self._start_time: float | None = None
164
174
  self._turn_warning_emitted = False
175
+ self._wallclock_warning_emitted = False
165
176
 
166
177
  def start(self) -> None:
167
178
  """Record the cycle start time. Idempotent; only the first call matters."""
@@ -169,11 +180,11 @@ class OllamaBudgetTracker:
169
180
  self._start_time = self._clock()
170
181
 
171
182
  def check(self, state: State, cycle: CycleState | None) -> None:
172
- """Warn on soft-turn threshold; raise only on wall-clock overflow."""
183
+ """Warn on either threshold; never raise."""
173
184
  if not self._active:
174
185
  return
175
186
  self._check_turn_soft_warn(state, cycle)
176
- self._check_wallclock_budget()
187
+ self._check_wallclock_soft_warn()
177
188
 
178
189
  def _check_turn_soft_warn(self, state: State, cycle: CycleState | None) -> None:
179
190
  """Emit one WARNING the first time the cycle crosses the soft threshold.
@@ -196,17 +207,29 @@ class OllamaBudgetTracker:
196
207
  )
197
208
  self._turn_warning_emitted = True
198
209
 
199
- def _check_wallclock_budget(self) -> None:
200
- if self._start_time is None:
210
+ def _check_wallclock_soft_warn(self) -> None:
211
+ """Emit one WARNING the first time the cycle crosses the wall-clock threshold.
212
+
213
+ Never raises. As of 0.4.13 the wall-clock ceiling is advisory: a
214
+ genuinely stuck daemon will be caught by the per-phase
215
+ CircuitBreaker or by the operator's Ctrl-C; aborting a live session
216
+ on a counter wastes hours of work.
217
+ """
218
+ if self._wallclock_warning_emitted or self._start_time is None:
201
219
  return
202
220
  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."
221
+ if elapsed > OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS:
222
+ _logger.warning(
223
+ "Ollama cycle has been running for %.0fs (soft threshold: "
224
+ "%ds). Letting it run — real stalls are caught by the "
225
+ "per-phase CircuitBreaker in runner/retry.py. Raise the "
226
+ "threshold via OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS or the "
227
+ "legacy OLLAMA_WALLCLOCK_BUDGET_SECONDS env var to silence "
228
+ "this warning. Use Ctrl-C to stop a truly runaway cycle.",
229
+ elapsed,
230
+ OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS,
209
231
  )
232
+ self._wallclock_warning_emitted = True
210
233
 
211
234
 
212
235
  def _sum_num_turns(state: State, cycle: CycleState | None) -> int:
@@ -18,16 +18,22 @@ You are a senior engineer specialized in testing. Your task is to run the projec
18
18
  - `Cargo.toml` → Rust/cargo test
19
19
  - `go.mod` → Go test
20
20
 
21
- 2. **Run the full test suite:**
21
+ 2. **Run the full test suite with concise output** (see Constraints):
22
22
  ```bash
23
- # Adapt to the detected framework
24
- pytest -xvs # Python
25
- npm test -- --run # Vitest
23
+ # Adapt to the detected framework.
24
+ pytest -q --tb=line # Python — quiet, one-line tracebacks
25
+ npm test -- --run --reporter=default # Vitest
26
26
  npm test # Jest/Angular
27
- cargo test --all # Rust
28
- go test ./... # Go
27
+ cargo test --all --quiet # Rust
28
+ go test ./... 2>&1 | tail -200 # Go — only the last 200 lines
29
29
  ```
30
30
 
31
+ **Do not use `-v`, `-vv`, `-s`, or any other verbose/stream flag.** A
32
+ tool result returned to the model cannot exceed roughly 1 MB — verbose
33
+ pytest output on a medium-sized project blows past that limit and
34
+ crashes the SDK stream reader mid-cycle. Start quiet; escalate only the
35
+ individual failing test with `-v` after you have the list of failures.
36
+
31
37
  3. **If all tests pass on the first attempt:**
32
38
  - Also run any available linters/type checkers (`mypy`, `ruff`, `eslint`, `tsc --noEmit`)
33
39
  - Collect test coverage if the framework supports it
@@ -98,6 +104,7 @@ You are a senior engineer specialized in testing. Your task is to run the projec
98
104
  - **Do not ask the user for confirmation**: act autonomously in YOLO mode.
99
105
  - **If you find flakiness** (a test that passes/fails non-deterministically), do not ignore it: document the flaky behavior in the bug issue.
100
106
  - **Environment variables already available globally**: `GITHUB_TOKEN`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY`, `OLLAMA_API_KEY`, `OLLAMA_BASE_URL`. If a test fails because "an API key is missing" among these, the cause is **not** the missing key — check the variable name, the `.env` loading, or an explicit override in the test. Do not add dummy keys as a fix. For tests that make real calls and are slow/expensive, use mocking/VCR cassettes instead of disabling them.
107
+ - **Tool-result size ceiling.** Bash tool-results larger than ~1 MB crash the SDK stream reader. Always prefer `-q`/`--tb=line`/`--quiet` over `-v`/`-vv`/`-s`. If a command genuinely produces more than 1 MB of output, tee it to a file (`cmd > /tmp/out.log 2>&1 || true`) and then `Read` or `Grep` the file — never let the full output flow back through a single tool result.
101
108
 
102
109
  ---
103
110
 
@@ -13,6 +13,7 @@ from __future__ import annotations
13
13
  import asyncio
14
14
  import contextlib
15
15
  import json
16
+ import os
16
17
  import subprocess
17
18
  import time
18
19
  from typing import TYPE_CHECKING, Any
@@ -313,7 +314,16 @@ async def run(
313
314
  "CLI session starting (model=%s effort=%s max_turns=%s).", model, effort_display, max_turns
314
315
  )
315
316
 
316
- safe_env = _env.build_agent_env()
317
+ # Detect Ollama mode so build_agent_env() preserves the scoped localhost
318
+ # routing (ANTHROPIC_API_KEY="" + pinned BASE_URL). Without provider="ollama"
319
+ # the default "anthropic-max" path omits ANTHROPIC_API_KEY, which causes
320
+ # the CLI to reject non-Claude model tags or fall back to real credentials.
321
+ _provider: _env.Provider = (
322
+ "ollama"
323
+ if _env._is_localhost_base_url(os.environ.get("ANTHROPIC_BASE_URL"))
324
+ else "anthropic-max"
325
+ )
326
+ safe_env = _env.build_agent_env(provider=_provider)
317
327
 
318
328
  text_parts: list[str] = []
319
329
  result_holder: dict[str, Any] = {
@@ -1,30 +1,35 @@
1
- """Tests for the Ollama per-cycle adaptive safety backstop (issue #220, 0.4.11).
1
+ """Tests for the Ollama per-cycle adaptive safety backstop (0.4.13).
2
2
 
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).
3
+ Both the turn counter and the wall-clock are **non-blocking soft warnings**
4
+ — the pipeline is never aborted on either threshold alone. Real per-call
5
+ failures are the job of :class:`~code_generator.runner.retry.CircuitBreaker`
6
+ and the rate-limit handlers.
7
7
 
8
- Backwards compatibility: ``OLLAMA_TURN_BUDGET`` is preserved as an alias
9
- for :data:`OLLAMA_SOFT_TURN_WARN` so existing scripts keep importing.
8
+ Backwards compatibility: ``OLLAMA_TURN_BUDGET`` and
9
+ ``OLLAMA_WALLCLOCK_BUDGET_SECONDS`` are preserved as aliases for
10
+ :data:`OLLAMA_SOFT_TURN_WARN` and :data:`OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS`
11
+ respectively, so existing scripts keep importing.
10
12
  """
11
13
 
12
14
  from __future__ import annotations
13
15
 
14
16
  import logging
15
-
16
- import pytest
17
+ from typing import TYPE_CHECKING
17
18
 
18
19
  from code_generator import state as _state
19
20
  from code_generator.orchestrator.ollama_budget import (
20
21
  OLLAMA_SOFT_TURN_WARN,
21
22
  OLLAMA_TURN_BUDGET,
22
23
  OLLAMA_WALLCLOCK_BUDGET_SECONDS,
24
+ OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS,
23
25
  OllamaBudgetExceeded,
24
26
  OllamaBudgetTracker,
25
27
  )
26
28
  from code_generator.runner.types import TokenUsage
27
29
 
30
+ if TYPE_CHECKING:
31
+ import pytest
32
+
28
33
  # ---------------------------------------------------------------------------
29
34
  # Constants
30
35
  # ---------------------------------------------------------------------------
@@ -38,8 +43,12 @@ class TestThresholdConstants:
38
43
  """Legacy ``OLLAMA_TURN_BUDGET`` must alias the new soft-warn constant."""
39
44
  assert OLLAMA_TURN_BUDGET == OLLAMA_SOFT_TURN_WARN
40
45
 
41
- def test_wallclock_budget_default_is_4_hours(self) -> None:
42
- assert OLLAMA_WALLCLOCK_BUDGET_SECONDS == 14400
46
+ def test_wallclock_soft_warn_default_is_4_hours(self) -> None:
47
+ assert OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS == 14400
48
+
49
+ def test_wallclock_budget_alias_matches_soft_warn(self) -> None:
50
+ """Legacy ``OLLAMA_WALLCLOCK_BUDGET_SECONDS`` must alias the soft-warn constant."""
51
+ assert OLLAMA_WALLCLOCK_BUDGET_SECONDS == OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS
43
52
 
44
53
 
45
54
  # ---------------------------------------------------------------------------
@@ -149,9 +158,9 @@ class TestTurnSoftWarning:
149
158
  # ---------------------------------------------------------------------------
150
159
 
151
160
 
152
- class TestWallclockBudget:
153
- def test_under_budget_does_not_raise(self) -> None:
154
- """Elapsed < budget → no raise."""
161
+ class TestWallclockSoftWarning:
162
+ def test_under_threshold_does_not_warn(self, caplog: pytest.LogCaptureFixture) -> None:
163
+ """Elapsed < threshold → no WARNING emitted, no raise."""
155
164
  st, cycle = _make_state_with_usage({}, cycle_turns={"phase0": 1})
156
165
  now = 1_000_000.0
157
166
  tracker = OllamaBudgetTracker(
@@ -159,12 +168,18 @@ class TestWallclockBudget:
159
168
  clock=lambda: now,
160
169
  )
161
170
  tracker.start()
162
- now += OLLAMA_WALLCLOCK_BUDGET_SECONDS - 10
171
+ now += OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS - 10
163
172
 
164
- tracker.check(st, cycle) # must not raise
173
+ with caplog.at_level(logging.WARNING):
174
+ tracker.check(st, cycle)
175
+
176
+ wallclock_warnings = [r for r in caplog.records if "running for" in r.message.lower()]
177
+ assert wallclock_warnings == []
165
178
 
166
- def test_over_budget_raises_with_exact_message(self) -> None:
167
- """Elapsed > budget → OllamaBudgetExceeded naming wall-clock."""
179
+ def test_over_threshold_warns_without_raising(
180
+ self, caplog: pytest.LogCaptureFixture
181
+ ) -> None:
182
+ """Crossing the threshold logs a WARNING; the pipeline continues."""
168
183
  st, cycle = _make_state_with_usage({}, cycle_turns={"phase0": 1})
169
184
  t = [1_000_000.0]
170
185
 
@@ -173,14 +188,32 @@ class TestWallclockBudget:
173
188
 
174
189
  tracker = OllamaBudgetTracker(provider_is_ollama=True, clock=_clock)
175
190
  tracker.start()
176
- t[0] += OLLAMA_WALLCLOCK_BUDGET_SECONDS + 1
191
+ t[0] += OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS + 1
192
+
193
+ with caplog.at_level(logging.WARNING):
194
+ tracker.check(st, cycle) # must not raise
195
+
196
+ wallclock_warnings = [r for r in caplog.records if "running for" in r.message.lower()]
197
+ assert len(wallclock_warnings) == 1
198
+ assert str(OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS) in wallclock_warnings[0].message
199
+
200
+ def test_warning_is_emitted_only_once_per_cycle(
201
+ self, caplog: pytest.LogCaptureFixture
202
+ ) -> None:
203
+ """Subsequent checks after the first wall-clock warning must stay silent."""
204
+ st, cycle = _make_state_with_usage({}, cycle_turns={"phase0": 1})
205
+ t = [1_000_000.0]
206
+ tracker = OllamaBudgetTracker(provider_is_ollama=True, clock=lambda: t[0])
207
+ tracker.start()
208
+ t[0] += OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS + 1
177
209
 
178
- with pytest.raises(OllamaBudgetExceeded) as excinfo:
210
+ with caplog.at_level(logging.WARNING):
211
+ tracker.check(st, cycle)
212
+ tracker.check(st, cycle)
179
213
  tracker.check(st, cycle)
180
214
 
181
- msg = str(excinfo.value)
182
- assert "wall" in msg.lower() or "clock" in msg.lower()
183
- assert str(OLLAMA_WALLCLOCK_BUDGET_SECONDS) in msg
215
+ wallclock_warnings = [r for r in caplog.records if "running for" in r.message.lower()]
216
+ assert len(wallclock_warnings) == 1
184
217
 
185
218
  def test_start_is_required_before_check(self) -> None:
186
219
  """check() without start() returns immediately on wall-clock (None start)."""
@@ -190,7 +223,7 @@ class TestWallclockBudget:
190
223
  tracker.check(st, cycle) # must not raise
191
224
 
192
225
  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."""
226
+ """Neither counter should raise on its own — both are soft warnings now."""
194
227
  st, cycle = _make_state_with_usage(
195
228
  {}, cycle_turns={"phase3_4": OLLAMA_SOFT_TURN_WARN * 100}
196
229
  )
@@ -198,6 +231,16 @@ class TestWallclockBudget:
198
231
 
199
232
  tracker.check(st, cycle) # must not raise
200
233
 
234
+ def test_extreme_wallclock_does_not_raise(self) -> None:
235
+ """Wall-clock far past threshold must not raise either (0.4.13)."""
236
+ st, cycle = _make_state_with_usage({}, cycle_turns={"phase0": 1})
237
+ t = [1_000_000.0]
238
+ tracker = OllamaBudgetTracker(provider_is_ollama=True, clock=lambda: t[0])
239
+ tracker.start()
240
+ t[0] += OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS * 10 # 40 h
241
+
242
+ tracker.check(st, cycle) # must not raise
243
+
201
244
 
202
245
  # ---------------------------------------------------------------------------
203
246
  # Anthropic Max path — thresholds do not fire
@@ -218,15 +261,21 @@ class TestAnthropicMaxUntouched:
218
261
  turn_warnings = [r for r in caplog.records if "consumed" in r.message.lower()]
219
262
  assert turn_warnings == []
220
263
 
221
- def test_anthropic_max_mode_skips_wallclock_budget(self) -> None:
222
- """provider_is_ollama=False wall-clock breach does not raise."""
264
+ def test_anthropic_max_mode_skips_wallclock_warning(
265
+ self, caplog: pytest.LogCaptureFixture
266
+ ) -> None:
267
+ """provider_is_ollama=False → no WARNING, no raise."""
223
268
  st, cycle = _make_state_with_usage({}, cycle_turns={"phase0": 1})
224
269
  t = [1_000_000.0]
225
270
  tracker = OllamaBudgetTracker(provider_is_ollama=False, clock=lambda: t[0])
226
271
  tracker.start()
227
- t[0] += OLLAMA_WALLCLOCK_BUDGET_SECONDS + 100
272
+ t[0] += OLLAMA_WALLCLOCK_SOFT_WARN_SECONDS + 100
228
273
 
229
- tracker.check(st, cycle) # must not raise
274
+ with caplog.at_level(logging.WARNING):
275
+ tracker.check(st, cycle)
276
+
277
+ wallclock_warnings = [r for r in caplog.records if "running for" in r.message.lower()]
278
+ assert wallclock_warnings == []
230
279
 
231
280
 
232
281
  # ---------------------------------------------------------------------------