claude-code-generator 0.5.3__tar.gz → 0.5.5__tar.gz

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