god-code 1.0.0__tar.gz → 1.0.1__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 (260) hide show
  1. {god_code-1.0.0 → god_code-1.0.1}/CHANGELOG.md +71 -0
  2. {god_code-1.0.0 → god_code-1.0.1}/PKG-INFO +1 -1
  3. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/agents/dispatcher.py +47 -0
  4. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/cli/commands.py +87 -14
  5. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/cli/engine_wiring.py +5 -0
  6. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/config.py +4 -0
  7. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/context_manager.py +18 -3
  8. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/engine.py +260 -11
  9. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/base.py +38 -0
  10. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/godot_cli.py +13 -1
  11. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/image_gen.py +11 -2
  12. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/screenshot.py +15 -3
  13. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tui/display.py +30 -0
  14. {god_code-1.0.0 → god_code-1.0.1}/pyproject.toml +1 -1
  15. {god_code-1.0.0 → god_code-1.0.1}/tests/agents/test_dispatcher.py +177 -0
  16. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_config.py +18 -0
  17. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_context_manager.py +72 -0
  18. god_code-1.0.1/tests/runtime/test_engine.py +1173 -0
  19. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_multi_agent_flow.py +5 -0
  20. {god_code-1.0.0 → god_code-1.0.1}/tests/test_cli_interactive_flows.py +13 -1
  21. {god_code-1.0.0 → god_code-1.0.1}/tests/tui/test_display.py +55 -0
  22. god_code-1.0.0/tests/runtime/test_engine.py +0 -495
  23. {god_code-1.0.0 → god_code-1.0.1}/.codetape/component-map.json +0 -0
  24. {god_code-1.0.0 → god_code-1.0.1}/.codetape/config.json +0 -0
  25. {god_code-1.0.0 → god_code-1.0.1}/.github/FUNDING.yml +0 -0
  26. {god_code-1.0.0 → god_code-1.0.1}/.github/workflows/docs.yml +0 -0
  27. {god_code-1.0.0 → god_code-1.0.1}/.github/workflows/publish.yml +0 -0
  28. {god_code-1.0.0 → god_code-1.0.1}/.gitignore +0 -0
  29. {god_code-1.0.0 → god_code-1.0.1}/.gitleaks.toml +0 -0
  30. {god_code-1.0.0 → god_code-1.0.1}/AGENTS.md +0 -0
  31. {god_code-1.0.0 → god_code-1.0.1}/CLAUDE.md +0 -0
  32. {god_code-1.0.0 → god_code-1.0.1}/CONTRIBUTING.md +0 -0
  33. {god_code-1.0.0 → god_code-1.0.1}/LICENSE +0 -0
  34. {god_code-1.0.0 → god_code-1.0.1}/PRIVACY.md +0 -0
  35. {god_code-1.0.0 → god_code-1.0.1}/README.md +0 -0
  36. {god_code-1.0.0 → god_code-1.0.1}/SECURITY.md +0 -0
  37. {god_code-1.0.0 → god_code-1.0.1}/docs/cli/ask.md +0 -0
  38. {god_code-1.0.0 → god_code-1.0.1}/docs/cli/chat.md +0 -0
  39. {god_code-1.0.0 → god_code-1.0.1}/docs/empty-project-to-first-verified-change.md +0 -0
  40. {god_code-1.0.0 → god_code-1.0.1}/docs/gameplay-intent-and-enemy-ai-roadmap.md +0 -0
  41. {god_code-1.0.0 → god_code-1.0.1}/docs/getting-started/first-run.md +0 -0
  42. {god_code-1.0.0 → god_code-1.0.1}/docs/getting-started/install.md +0 -0
  43. {god_code-1.0.0 → god_code-1.0.1}/docs/index.md +0 -0
  44. {god_code-1.0.0 → god_code-1.0.1}/docs/mcp/overview.md +0 -0
  45. {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-04-api-backend-design.md +0 -0
  46. {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-04-api-backend-impl.md +0 -0
  47. {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-04-bullet-hell-sprite-qa-demo-polish-backlog.md +0 -0
  48. {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-04-demo-ready-upgrade.md +0 -0
  49. {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-05-dogfooding-inventory.md +0 -0
  50. {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-05-v08-design.md +0 -0
  51. {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-05-v08-impl.md +0 -0
  52. {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-05-work-status.md +0 -0
  53. {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-06-auto-flow-impl.md +0 -0
  54. {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-06-interactive-ux-redesign.md +0 -0
  55. {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-07-god-code-prelaunch-security-audit-design.md +0 -0
  56. {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-07-god-code-prelaunch-security-audit-report.md +0 -0
  57. {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-07-god-code-prelaunch-security-audit.md +0 -0
  58. {god_code-1.0.0 → god_code-1.0.1}/docs/plans/2026-04-07-v1.0.0-ux-upgrade-design.md +0 -0
  59. {god_code-1.0.0 → god_code-1.0.1}/docs/providers/byok-and-oauth.md +0 -0
  60. {god_code-1.0.0 → god_code-1.0.1}/docs/tui/menu-and-commands.md +0 -0
  61. {god_code-1.0.0 → god_code-1.0.1}/docs/tui/settings-and-byok.md +0 -0
  62. {god_code-1.0.0 → god_code-1.0.1}/docs/validation/quality-gate.md +0 -0
  63. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/__init__.py +0 -0
  64. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/addons/god_code_bridge/god_code_bridge.gd +0 -0
  65. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/addons/god_code_bridge/plugin.cfg +0 -0
  66. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/agents/__init__.py +0 -0
  67. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/agents/configs.py +0 -0
  68. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/agents/results.py +0 -0
  69. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/cli/__init__.py +0 -0
  70. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/cli/__main__.py +0 -0
  71. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/cli/helpers.py +0 -0
  72. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/cli/menus.py +0 -0
  73. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/entrypoint.py +0 -0
  74. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/__init__.py +0 -0
  75. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/audio_scaffolder.py +0 -0
  76. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/collision_planner.py +0 -0
  77. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/consistency_checker.py +0 -0
  78. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/dependency_graph.py +0 -0
  79. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/gdscript_linter.py +0 -0
  80. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/impact_analysis.py +0 -0
  81. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/pattern_advisor.py +0 -0
  82. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/project.py +0 -0
  83. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/resource_validator.py +0 -0
  84. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/scene_parser.py +0 -0
  85. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/scene_writer.py +0 -0
  86. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/tscn_validator.py +0 -0
  87. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/ui_layout_advisor.py +0 -0
  88. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/godot/variant_codec.py +0 -0
  89. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/__init__.py +0 -0
  90. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/adapters/__init__.py +0 -0
  91. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/adapters/anthropic.py +0 -0
  92. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/adapters/base.py +0 -0
  93. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/adapters/openai.py +0 -0
  94. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/client.py +0 -0
  95. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/redact.py +0 -0
  96. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/streaming.py +0 -0
  97. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/types.py +0 -0
  98. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/llm/vision.py +0 -0
  99. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/mcp_server.py +0 -0
  100. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/__init__.py +0 -0
  101. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/assembler.py +0 -0
  102. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/build_discipline.py +0 -0
  103. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/genre_templates.py +0 -0
  104. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/godot_playbook.py +0 -0
  105. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/image_templates.py +0 -0
  106. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/knowledge_selector.py +0 -0
  107. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/skill_library.py +0 -0
  108. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/skill_selector.py +0 -0
  109. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/system.py +0 -0
  110. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/prompts/vision_templates.py +0 -0
  111. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/py.typed +0 -0
  112. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/__init__.py +0 -0
  113. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/auth.py +0 -0
  114. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/context_health.py +0 -0
  115. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/design_memory.py +0 -0
  116. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/error_loop.py +0 -0
  117. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/events.py +0 -0
  118. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/execution_plan.py +0 -0
  119. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/gameplay_reviewer.py +0 -0
  120. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/intent_resolver.py +0 -0
  121. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/live_client.py +0 -0
  122. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/modes.py +0 -0
  123. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/oauth.py +0 -0
  124. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/playtest_harness.py +0 -0
  125. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/polish_rubric.py +0 -0
  126. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/providers.py +0 -0
  127. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/quality_gate.py +0 -0
  128. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/reviewer.py +0 -0
  129. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/runtime_bridge.py +0 -0
  130. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/bullet_hell_pattern_readability.json +0 -0
  131. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/bullet_hell_phase_transition.json +0 -0
  132. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/bullet_hell_wave_progression.json +0 -0
  133. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/demo_boss_transition.json +0 -0
  134. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/demo_combat_feedback.json +0 -0
  135. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/demo_ui_readability.json +0 -0
  136. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/demo_wave_pacing.json +0 -0
  137. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/hud_feedback.json +0 -0
  138. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/platformer_enemy_gap_jump.json +0 -0
  139. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/platformer_enemy_patrol_response.json +0 -0
  140. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/player_movement.json +0 -0
  141. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/scene_transition.json +0 -0
  142. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/topdown_shooter_flank_resolution.json +0 -0
  143. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/scenario_specs/topdown_shooter_pressure.json +0 -0
  144. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/session.py +0 -0
  145. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/validation_checks.py +0 -0
  146. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/runtime/visual_regression.py +0 -0
  147. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/security/__init__.py +0 -0
  148. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/security/classifier.py +0 -0
  149. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/security/hooks.py +0 -0
  150. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/security/policies.py +0 -0
  151. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/security/protected_paths.py +0 -0
  152. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/security/tool_pipeline.py +0 -0
  153. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/testing/__init__.py +0 -0
  154. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/testing/scenario_runner.py +0 -0
  155. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/__init__.py +0 -0
  156. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/analysis_tools.py +0 -0
  157. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/editor_bridge.py +0 -0
  158. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/file_ops.py +0 -0
  159. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/git.py +0 -0
  160. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/list_dir.py +0 -0
  161. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/memory_tool.py +0 -0
  162. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/registry.py +0 -0
  163. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/runtime_harness.py +0 -0
  164. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/scene_tools.py +0 -0
  165. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/script_tools.py +0 -0
  166. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/search.py +0 -0
  167. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/shell.py +0 -0
  168. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/sprite_pipeline.py +0 -0
  169. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/sprite_qa.py +0 -0
  170. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/vision_analysis.py +0 -0
  171. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/vision_scoring.py +0 -0
  172. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tools/web_search.py +0 -0
  173. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tui/__init__.py +0 -0
  174. {god_code-1.0.0 → god_code-1.0.1}/godot_agent/tui/input_handler.py +0 -0
  175. {god_code-1.0.0 → god_code-1.0.1}/mkdocs.yml +0 -0
  176. {god_code-1.0.0 → god_code-1.0.1}/scripts/install-hooks.sh +0 -0
  177. {god_code-1.0.0 → god_code-1.0.1}/scripts/vision_model_comparison.py +0 -0
  178. {god_code-1.0.0 → god_code-1.0.1}/skills/god-code-setup/SKILL.md +0 -0
  179. {god_code-1.0.0 → god_code-1.0.1}/tests/__init__.py +0 -0
  180. {god_code-1.0.0 → god_code-1.0.1}/tests/agents/test_playtest_analyst.py +0 -0
  181. {god_code-1.0.0 → god_code-1.0.1}/tests/cli/__init__.py +0 -0
  182. {god_code-1.0.0 → god_code-1.0.1}/tests/cli/test_setup_bridge.py +0 -0
  183. {god_code-1.0.0 → god_code-1.0.1}/tests/cli_test_utils.py +0 -0
  184. {god_code-1.0.0 → god_code-1.0.1}/tests/e2e/test_planner_worker_reviewer_flow.py +0 -0
  185. {god_code-1.0.0 → god_code-1.0.1}/tests/e2e/test_policy_enforcement.py +0 -0
  186. {god_code-1.0.0 → god_code-1.0.1}/tests/e2e/test_scenario_runner.py +0 -0
  187. {god_code-1.0.0 → god_code-1.0.1}/tests/fuzz/test_input_sequences.py +0 -0
  188. {god_code-1.0.0 → god_code-1.0.1}/tests/godot/__init__.py +0 -0
  189. {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_audio_scaffolder.py +0 -0
  190. {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_collision_planner.py +0 -0
  191. {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_consistency.py +0 -0
  192. {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_dependency_graph.py +0 -0
  193. {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_impact_analysis.py +0 -0
  194. {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_linter.py +0 -0
  195. {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_pattern_advisor.py +0 -0
  196. {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_project.py +0 -0
  197. {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_resource_validator.py +0 -0
  198. {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_scene_parser.py +0 -0
  199. {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_scene_writer.py +0 -0
  200. {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_tscn_validator.py +0 -0
  201. {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_ui_layout_advisor.py +0 -0
  202. {god_code-1.0.0 → god_code-1.0.1}/tests/godot/test_variant_codec.py +0 -0
  203. {god_code-1.0.0 → god_code-1.0.1}/tests/llm/__init__.py +0 -0
  204. {god_code-1.0.0 → god_code-1.0.1}/tests/llm/test_adapters.py +0 -0
  205. {god_code-1.0.0 → god_code-1.0.1}/tests/llm/test_client.py +0 -0
  206. {god_code-1.0.0 → god_code-1.0.1}/tests/llm/test_streaming.py +0 -0
  207. {god_code-1.0.0 → god_code-1.0.1}/tests/llm/test_vision.py +0 -0
  208. {god_code-1.0.0 → god_code-1.0.1}/tests/prompts/__init__.py +0 -0
  209. {god_code-1.0.0 → god_code-1.0.1}/tests/prompts/test_genre_templates.py +0 -0
  210. {god_code-1.0.0 → god_code-1.0.1}/tests/prompts/test_knowledge_selector.py +0 -0
  211. {god_code-1.0.0 → god_code-1.0.1}/tests/prompts/test_prompt_assembler.py +0 -0
  212. {god_code-1.0.0 → god_code-1.0.1}/tests/prompts/test_skill_selector.py +0 -0
  213. {god_code-1.0.0 → god_code-1.0.1}/tests/prompts/test_system_prompt.py +0 -0
  214. {god_code-1.0.0 → god_code-1.0.1}/tests/prompts/test_vision_templates.py +0 -0
  215. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/__init__.py +0 -0
  216. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_context_health.py +0 -0
  217. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_design_memory.py +0 -0
  218. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_error_loop.py +0 -0
  219. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_execution_plan.py +0 -0
  220. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_gameplay_reviewer.py +0 -0
  221. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_intent_resolver.py +0 -0
  222. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_live_client.py +0 -0
  223. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_mode_restrictions.py +0 -0
  224. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_playtest_harness.py +0 -0
  225. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_quality_gate.py +0 -0
  226. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_reviewer.py +0 -0
  227. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_runtime_bridge.py +0 -0
  228. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_session.py +0 -0
  229. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_validation_checks.py +0 -0
  230. {god_code-1.0.0 → god_code-1.0.1}/tests/runtime/test_visual_regression.py +0 -0
  231. {god_code-1.0.0 → god_code-1.0.1}/tests/security/test_classifier.py +0 -0
  232. {god_code-1.0.0 → god_code-1.0.1}/tests/security/test_hooks.py +0 -0
  233. {god_code-1.0.0 → god_code-1.0.1}/tests/security/test_permissions.py +0 -0
  234. {god_code-1.0.0 → god_code-1.0.1}/tests/security/test_tool_pipeline.py +0 -0
  235. {god_code-1.0.0 → god_code-1.0.1}/tests/test_auto_flow.py +0 -0
  236. {god_code-1.0.0 → god_code-1.0.1}/tests/test_cli_config_flow.py +0 -0
  237. {god_code-1.0.0 → god_code-1.0.1}/tests/test_e2e.py +0 -0
  238. {god_code-1.0.0 → god_code-1.0.1}/tests/test_package_compatibility.py +0 -0
  239. {god_code-1.0.0 → god_code-1.0.1}/tests/test_runtime_switch_commands.py +0 -0
  240. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/__init__.py +0 -0
  241. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_analysis_tools.py +0 -0
  242. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_editor_bridge.py +0 -0
  243. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_file_ops.py +0 -0
  244. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_git.py +0 -0
  245. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_godot_cli.py +0 -0
  246. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_list_dir.py +0 -0
  247. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_memory_tool.py +0 -0
  248. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_registry.py +0 -0
  249. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_runtime_harness.py +0 -0
  250. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_scene_tools.py +0 -0
  251. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_screenshot_tool.py +0 -0
  252. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_script_tools.py +0 -0
  253. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_search.py +0 -0
  254. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_shell.py +0 -0
  255. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_sprite_qa.py +0 -0
  256. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_vision_analysis.py +0 -0
  257. {god_code-1.0.0 → god_code-1.0.1}/tests/tools/test_vision_scoring.py +0 -0
  258. {god_code-1.0.0 → god_code-1.0.1}/tests/tui/__init__.py +0 -0
  259. {god_code-1.0.0 → god_code-1.0.1}/tests/tui/test_input_handler.py +0 -0
  260. {god_code-1.0.0 → god_code-1.0.1}/tests/tui/test_plan_display.py +0 -0
@@ -2,6 +2,77 @@
2
2
 
3
3
  All notable changes to God Code will be documented in this file.
4
4
 
5
+ ## [1.0.1] — 2026-04-07
6
+
7
+ **Token-efficiency + cancellation closeout.** This release attacks the dominant token cost driver on long dev sessions (planner blocks accumulating in history and rebilling on every subsequent LLM call) and completes the v1.0.0 deferred items (full asyncio.Task cancellation, subprocess termination, per-tool progress events).
8
+
9
+ **No breaking API changes.** Config additions are opt-out (defaults preserve the new behavior). Event taxonomy gains three additive kinds (`tool_progress`, `plan_pruned`, `planner_skipped`). All 681 v1.0.0 tests continue to pass; 58 new regression tests added (739 total).
10
+
11
+ ### Token efficiency (the main motivator)
12
+
13
+ - **T1: Planner blocks pruned from history** — `_maybe_run_planner` now calls `prune_system_reports` (a helper that existed and was tested but never wired into production code) to cap plan history at 2 blocks (configurable via `plan_history_keep`). On a 40-turn dev session, this cuts planner-attributable rebill from ~410K tokens to ~2K tokens — roughly $1.2/session saved at gpt-5.4 input rates. Helper extended with an additive `prefix_filter` parameter so pruning is scoped to `"[SYSTEM] Planner pass"` and cannot accidentally drop quality-gate or reviewer reports.
14
+
15
+ - **T2: Lazy planner triggering** — `_maybe_run_planner` now calls a new `should_run_planner(user_input)` heuristic first. Trivial read-only inputs (`explain X`, `show Y`, `what is in Z`, `如何`, `解釋`, `顯示`, `為什麼`, question marks with `什麼`) skip the full planner cost. Action verbs (EN: implement/add/fix/refactor/... | 繁中: 實作/新增/修/改/重寫) or multi-file references still trigger a plan. Config opt-out: `planner_lazy: false` restores the v1.0.0 unconditional behavior. Emits a new `planner_skipped` event with the reason for dogfood observability.
16
+
17
+ - **T3: Plan content shaping** — Added `extract_worker_plan()` in `agents/dispatcher.py`. The `[SYSTEM] Planner pass` block injected into main history now contains only the actionable subset (Goal + Steps), not the full 5-section markdown. Scope + Risks + Validation are user-facing context already streamed to the TUI via the planner sub-engine, so they don't need to be re-injected. Typical saving: ~30% of per-plan injected tokens. Defensive fallback: returns the full text unchanged if the LLM produces malformed markdown.
18
+
19
+ - **T4: `prune_system_reports` wired up** — the helper shipped in v0.x with tests but was dead code. Now in use per T1 above.
20
+
21
+ ### Cancellation closeout (v1.0.0 deferred)
22
+
23
+ - **D1: Full asyncio.Task cancellation in chat loop** — Completes the v1.0.0/C2 partial implementation. New `_submit_cancellable` helper in `cli/commands.py` wraps `engine.submit()` in `asyncio.create_task`, catches `CancelledError`/`KeyboardInterrupt`, cancels the task, runs cleanup, and raises a new `TurnCancelled` exception (used instead of re-raising `KeyboardInterrupt` because `KeyboardInterrupt` inside an asyncio task trips pytest-asyncio's BaseException handling). Shared cleanup in `_cleanup_after_cancel(engine)` guarded with `hasattr` for FakeEngine compat.
24
+
25
+ - **D2: Subprocess registry + termination** — New contextvar-based subprocess registry on `ConversationEngine`. `register_subprocess(proc)` / `unregister_subprocess(proc)` / `async terminate_active_subprocesses(timeout=2.0)`. `terminate_active_subprocesses` sends SIGTERM, waits up to 2s, then escalates to SIGKILL for stubborn processes. Activated during `submit()` / `submit_with_images()` via contextvar. Tools lookup the active registry via `get_current_subprocess_registry()`. `tools/godot_cli.run_godot_command` and `tools/screenshot.CaptureScreenshotTool` register their subprocesses on spawn. Ctrl+C during a Godot validate or screenshot capture now kills the subprocess within 2s instead of letting it run to natural completion (30-120s wasted).
26
+
27
+ - **D3: Per-tool progress events** — Completes v1.0.0/A2 Layer 2. New `tool_progress` event kind + `tools/base.py::emit_tool_progress(context, ...)` helper. `generate_sprite` emits 5 phases (API call → post-process → save → QA → reimport) so the spinner shows `generate_sprite: post-processing (2/5)` instead of a static label. TUI display handler updates the active status spinner's label dynamically.
28
+
29
+ ### Measured impact (design-doc projection)
30
+
31
+ | Metric | v1.0.0 | v1.0.1 | Delta |
32
+ |---|---|---|---|
33
+ | Planner-attributable rebill (40-turn session) | ~430K tokens | ~18K tokens | **-96%** |
34
+ | Plan history carried (40-turn session) | ~20K tokens | ~700 tokens | **-96%** |
35
+ | Whole-session input tokens (conservative) | baseline | baseline × 0.70-0.92 | **-8% to -30%** |
36
+ | Ctrl+C latency (worst case subprocess) | 30-120s | < 2s | **-95%** |
37
+
38
+ Actual impact will be measured over the first dogfood session on v1.0.1.
39
+
40
+ ### Added
41
+
42
+ - `prune_system_reports` `prefix_filter` parameter (scoped pruning)
43
+ - `should_run_planner(user_input)` heuristic
44
+ - `extract_worker_plan(plan_text)` function in dispatcher
45
+ - `plan_history_keep: int = 2` config field
46
+ - `planner_lazy: bool = True` config field
47
+ - `ConversationEngine._active_subprocesses` set
48
+ - `register_subprocess` / `unregister_subprocess` / `terminate_active_subprocesses` engine methods
49
+ - `_activate_subprocess_registry` / `_deactivate_subprocess_registry` engine methods
50
+ - `get_current_subprocess_registry()` module-level accessor
51
+ - `SubprocessRegistryProtocol` typing protocol
52
+ - `TurnCancelled(Exception)` in `cli/commands.py`
53
+ - `_cleanup_after_cancel(engine)` async helper in `cli/commands.py`
54
+ - `_submit_cancellable(engine, user_input, cfg, display)` async helper in `cli/commands.py`
55
+ - `emit_tool_progress(context, ...)` helper in `tools/base.py`
56
+ - New event kinds: `plan_pruned`, `planner_skipped`, `tool_progress`
57
+
58
+ ### Changed
59
+
60
+ - `_maybe_run_planner`: now checks `should_run_planner` when `planner_lazy=True`, calls `extract_worker_plan` on the planner output before injection, and calls `prune_system_reports` after appending
61
+ - `submit()` / `submit_with_images()`: wrap work in `_activate_subprocess_registry` / `_deactivate_subprocess_registry` context
62
+ - `tools/godot_cli.run_godot_command`: registers subprocess with active registry
63
+ - `tools/screenshot.CaptureScreenshotTool.execute`: registers subprocess with active registry
64
+ - Chat loop (`cli/commands.py`): uses `_submit_cancellable` instead of bare `await engine.submit`
65
+
66
+ ### Test count
67
+
68
+ 681 → 739 (+58 regression tests)
69
+
70
+ ### Known scope notes
71
+
72
+ - `image_gen` single-candidate API call — the design doc mentioned "4 candidates" but the current `GenerateSpriteTool` requests `n=1` from the image API. Per-tool progress is instead emitted as 5 execution phases (API call, post-process, save, QA, reimport) which covers the full wall-clock.
73
+ - `_run_visual_iteration` engine method doesn't exist as a standalone — the vision loop is LLM-driven via tool calls, not a hardcoded counter. `tool_progress` instrumentation for vision iteration was dropped from this release; the user-visible stage updates come from the individual tool-level progress (screenshot, analyze, score) rather than a synthetic "iteration N/3" counter.
74
+ - Windows cancellation: `asyncio.CancelledError` handling path works cross-platform. The subprocess SIGTERM/SIGKILL path is POSIX-only but macOS/Linux is the current supported platform set. Windows gets best-effort behavior via normal Python subprocess termination.
75
+
5
76
  ## [1.0.0] — 2026-04-07
6
77
 
7
78
  **Promoted from 1.0.0rc1 with no code changes** after Starfall workflow dogfood verification. See the `[1.0.0rc1]` section below for the full list of fixes and design rationale.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: god-code
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: AI coding agent specialized for Godot game development
5
5
  Project-URL: Homepage, https://github.com/888wing/god-code
6
6
  Project-URL: Repository, https://github.com/888wing/god-code
@@ -2,10 +2,57 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import re
5
6
  from dataclasses import replace
6
7
  from pathlib import Path
7
8
  from typing import Callable
8
9
 
10
+
11
+ def extract_worker_plan(plan_text: str) -> str:
12
+ """Reduce a full planner output to just the actionable subset.
13
+
14
+ The planner prompt (v1.0.0/F2) enforces a 5-section markdown format:
15
+ Goal / Scope / Steps / Risks / Validation. The worker sub-agent and
16
+ the main engine only need Goal + Steps to execute correctly — Scope,
17
+ Risks, and Validation are user-facing context that the TUI already
18
+ shows separately. Injecting them into main history wastes tokens on
19
+ every subsequent LLM call (v1.0.1/T3).
20
+
21
+ Returns the reduced plan. Defensive fallback: if the markdown can't
22
+ be parsed (malformed LLM output), returns the input unchanged — we
23
+ never want the worker to lose its plan entirely because of a parser
24
+ mismatch.
25
+
26
+ Empty input returns empty string.
27
+ """
28
+ if not plan_text:
29
+ return ""
30
+
31
+ # Try to find the **Goal**: and **Steps**: sections. If either is
32
+ # missing, we're dealing with malformed output — fall back to the
33
+ # full text.
34
+ goal_match = re.search(
35
+ r"\*\*Goal\*\*\s*:\s*(.+?)(?=\n\s*\*\*|$)",
36
+ plan_text,
37
+ re.DOTALL,
38
+ )
39
+ steps_match = re.search(
40
+ r"\*\*Steps\*\*\s*:\s*\n(.+?)(?=\n\s*\*\*(?:Risks|Validation)\*\*|$)",
41
+ plan_text,
42
+ re.DOTALL,
43
+ )
44
+ if not goal_match or not steps_match:
45
+ return plan_text # defensive fallback
46
+
47
+ goal = goal_match.group(1).strip()
48
+ steps = steps_match.group(1).strip()
49
+
50
+ return (
51
+ "## Plan\n\n"
52
+ f"**Goal**: {goal}\n\n"
53
+ f"**Steps**:\n{steps}\n"
54
+ )
55
+
9
56
  from godot_agent.godot.impact_analysis import format_impact_report, infer_request_impact
10
57
  from godot_agent.agents.configs import AGENT_CONFIGS, AgentConfig
11
58
  from godot_agent.agents.results import AgentTaskResult
@@ -537,6 +537,76 @@ def ask(prompt: str, project: str, config: str | None, image: tuple[str, ...], p
537
537
  click.echo(result)
538
538
 
539
539
 
540
+ class TurnCancelled(Exception):
541
+ """Raised by :func:`_submit_cancellable` when the user cancels mid-turn.
542
+
543
+ Used as an internal signal between the helper and the chat loop so
544
+ pytest-asyncio can catch cancellation via ``pytest.raises`` without
545
+ tripping over KeyboardInterrupt / CancelledError's special handling
546
+ inside an event loop. The chat loop translates this back into a
547
+ "Cancelled" message and continues the session.
548
+ """
549
+
550
+
551
+ async def _cleanup_after_cancel(engine) -> None:
552
+ """Shared cleanup for the cancellation path.
553
+
554
+ Calls ``engine.terminate_active_subprocesses()`` (v1.0.1/D2) to
555
+ SIGTERM + SIGKILL any in-flight subprocesses, then
556
+ ``engine.rollback_current_turn()`` (v1.0.0/C2) to drop any messages
557
+ appended during the cancelled turn. Both are guarded with hasattr
558
+ for FakeEngine compatibility.
559
+ """
560
+ if hasattr(engine, "terminate_active_subprocesses"):
561
+ try:
562
+ await engine.terminate_active_subprocesses()
563
+ except Exception:
564
+ pass
565
+ if hasattr(engine, "rollback_current_turn"):
566
+ try:
567
+ engine.rollback_current_turn()
568
+ except Exception:
569
+ pass
570
+
571
+
572
+ async def _submit_cancellable(engine, user_input: str, cfg, display) -> str:
573
+ """Submit a user input as a cancellable asyncio task.
574
+
575
+ Wraps ``engine.submit()`` in ``asyncio.create_task`` so that a
576
+ KeyboardInterrupt (Ctrl+C) during the await can cancel the task,
577
+ terminate any tool-spawned subprocesses, and roll back the turn's
578
+ message state — completing the v1.0.0/C2 partial fix with the full
579
+ cancellation architecture planned in v1.0.1/D1.
580
+
581
+ On success: returns the assistant response string.
582
+
583
+ On KeyboardInterrupt / CancelledError: cancels the task, awaits it
584
+ to drain, runs cleanup via ``_cleanup_after_cancel``, then raises
585
+ ``TurnCancelled`` so the chat loop can catch it and continue the
586
+ session. Uses a custom exception type (not KeyboardInterrupt) to
587
+ avoid pytest-asyncio / event loop special handling of BaseException.
588
+ """
589
+ submit_task = asyncio.create_task(engine.submit(user_input))
590
+ try:
591
+ if cfg.streaming and engine.on_stream_chunk:
592
+ return await submit_task
593
+ else:
594
+ with display.thinking():
595
+ result = await submit_task
596
+ display.agent_response(result)
597
+ return result
598
+ except (KeyboardInterrupt, asyncio.CancelledError):
599
+ submit_task.cancel()
600
+ try:
601
+ await submit_task
602
+ except (asyncio.CancelledError, KeyboardInterrupt):
603
+ pass
604
+ except Exception:
605
+ pass
606
+ await _cleanup_after_cancel(engine)
607
+ raise TurnCancelled()
608
+
609
+
540
610
  @main.command()
541
611
  @click.option("--project", "-p", default=".", help="Path to Godot project root")
542
612
  @click.option("--config", "-c", default=None, help="Path to config file")
@@ -1775,21 +1845,24 @@ def chat(project: str = ".", config: str | None = None):
1775
1845
  await _edit_intent_profile(checkpoint=True)
1776
1846
  engine.refresh_intent_profile(user_input)
1777
1847
  _refresh_workspace()
1778
- # v1.0.0/C2: submit returns naturally for both streaming
1779
- # and non-streaming paths. KeyboardInterrupt is propagated
1780
- # at the next await point and caught below — combined with
1781
- # rollback_current_turn(), this ensures cancelled turns
1782
- # don't pollute the message history of the next turn.
1783
- if cfg.streaming and engine.on_stream_chunk:
1784
- response = await engine.submit(user_input)
1785
- else:
1786
- with display.thinking():
1787
- response = await engine.submit(user_input)
1788
- display.agent_response(response)
1789
- except KeyboardInterrupt:
1848
+ # v1.0.1/D1: fully-cancellable submit. Wraps
1849
+ # engine.submit() in asyncio.create_task so Ctrl+C
1850
+ # can cancel the task, terminate any spawned
1851
+ # subprocesses, and roll back the turn's message
1852
+ # state (completes v1.0.0/C2). See
1853
+ # _submit_cancellable docstring for details.
1854
+ response = await _submit_cancellable(
1855
+ engine, user_input, cfg, display
1856
+ )
1857
+ except (KeyboardInterrupt, TurnCancelled):
1790
1858
  display.info("Cancelled")
1791
- if hasattr(engine, "rollback_current_turn"):
1792
- engine.rollback_current_turn()
1859
+ # _submit_cancellable already ran cleanup via
1860
+ # _cleanup_after_cancel, but if the exception was a
1861
+ # raw KeyboardInterrupt (e.g. during the
1862
+ # refresh_intent_profile or _edit_intent_profile
1863
+ # phase before submit started), run cleanup
1864
+ # defensively to ensure no partial state leaks.
1865
+ await _cleanup_after_cancel(engine)
1793
1866
  continue
1794
1867
  except httpx.HTTPStatusError as e:
1795
1868
  status_code = e.response.status_code if e.response is not None else "?"
@@ -334,6 +334,11 @@ def _wire_engine_callbacks(
334
334
  engine.on_event = display.handle_event
335
335
  engine.auto_commit = cfg.auto_commit
336
336
  engine.use_streaming = cfg.streaming
337
+ # v1.0.1/T1+T2: propagate token-efficiency settings from config.
338
+ # getattr defensively so CLI flow tests that construct configs
339
+ # without these fields continue to work.
340
+ engine.planner_lazy = getattr(cfg, "planner_lazy", True)
341
+ engine.plan_history_keep = getattr(cfg, "plan_history_keep", 2)
337
342
  if cfg.streaming:
338
343
  engine.on_stream_start = display.agent_streaming_start
339
344
  engine.on_stream_chunk = display.agent_streaming_chunk
@@ -46,6 +46,10 @@ class AgentConfig(BaseModel):
46
46
  enabled_skills: list[str] = Field(default_factory=list)
47
47
  disabled_skills: list[str] = Field(default_factory=list)
48
48
 
49
+ # Token efficiency (v1.0.1)
50
+ planner_lazy: bool = True # Skip planner on trivial read-only turns (T2)
51
+ plan_history_keep: int = 2 # How many planner blocks to keep in history (T1)
52
+
49
53
  # Backend orchestration
50
54
  backend_url: str = "" # Empty = direct provider (current behavior)
51
55
  backend_cost_preference: str = "balanced" # economy | balanced | quality
@@ -241,11 +241,26 @@ def truncate_tool_result(content: str, max_chars: int = 2000) -> str:
241
241
  return f"{content[:head_size]}\n[...truncated {omitted} chars...]\n{content[-tail_size:]}"
242
242
 
243
243
 
244
- def prune_system_reports(messages: list[Message], max_reports: int = 2) -> list[Message]:
245
- """Remove old [SYSTEM] quality/reviewer/playtest reports, keeping latest N."""
244
+ def prune_system_reports(
245
+ messages: list[Message],
246
+ max_reports: int = 2,
247
+ prefix_filter: str | None = None,
248
+ ) -> list[Message]:
249
+ """Remove old [SYSTEM] reports from history, keeping the latest N.
250
+
251
+ If ``prefix_filter`` is set, only messages whose content starts with the
252
+ given prefix are considered for pruning. Other [SYSTEM]-prefixed messages
253
+ pass through untouched. This lets callers scope the prune to a specific
254
+ report type (e.g. ``"[SYSTEM] Planner"``) without collateral damage to
255
+ quality-gate or reviewer reports.
256
+
257
+ Backward compat: when ``prefix_filter`` is None, every [SYSTEM]-prefixed
258
+ message is eligible for pruning (original v1.0.0 behavior).
259
+ """
260
+ match_prefix = prefix_filter if prefix_filter is not None else "[SYSTEM]"
246
261
  report_indices: list[int] = []
247
262
  for i, m in enumerate(messages):
248
- if m.role == "user" and isinstance(m.content, str) and m.content.startswith("[SYSTEM]"):
263
+ if m.role == "user" and isinstance(m.content, str) and m.content.startswith(match_prefix):
249
264
  report_indices.append(i)
250
265
  if len(report_indices) <= max_reports:
251
266
  return messages
@@ -2,18 +2,20 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
6
+ import contextvars
5
7
  import enum
6
8
  import json
7
9
  import logging
8
10
  from dataclasses import dataclass, field
9
11
  from pathlib import Path
10
- from typing import TYPE_CHECKING, Any, Callable
12
+ from typing import TYPE_CHECKING, Any, Callable, Protocol
11
13
 
12
14
  from godot_agent.godot.impact_analysis import ImpactAnalysisReport, analyze_change_impact
13
15
  from godot_agent.llm.client import LLMClient, Message, TokenUsage
14
16
  from godot_agent.prompts.assembler import PromptAssembler
15
17
  from godot_agent.prompts.skill_selector import narrow_tools_for_skills, resolve_skills
16
- from godot_agent.runtime.context_manager import smart_compact, estimate_message_tokens, truncate_tool_result, compress_step_messages
18
+ from godot_agent.runtime.context_manager import smart_compact, estimate_message_tokens, truncate_tool_result, compress_step_messages, prune_system_reports
17
19
  from godot_agent.runtime.execution_plan import ExecutionPlan, PlanStep
18
20
  from godot_agent.runtime.design_memory import DesignMemory, GameplayIntentProfile, load_design_memory
19
21
  from godot_agent.runtime.events import EngineEvent
@@ -38,6 +40,125 @@ log = logging.getLogger(__name__)
38
40
 
39
41
  # Compact at 75% of 1.05M context to leave room for current turn
40
42
  _COMPACT_THRESHOLD = 787500 # 75% of 1.05M
43
+
44
+
45
+ # v1.0.1/D2: subprocess registry context variable. Tools that spawn
46
+ # subprocesses can look up the current engine's registry via
47
+ # get_current_subprocess_registry() and register their process so the
48
+ # engine can terminate it on Ctrl+C. Scoped per-asyncio-task so
49
+ # concurrent engines (e.g. in tests) don't collide.
50
+ _current_subprocess_registry: contextvars.ContextVar[
51
+ "SubprocessRegistryProtocol | None"
52
+ ] = contextvars.ContextVar("_current_subprocess_registry", default=None)
53
+
54
+
55
+ class SubprocessRegistryProtocol(Protocol):
56
+ """Minimal interface a subprocess registry must expose. In practice
57
+ this is ConversationEngine but tests and alternative runtimes can
58
+ provide their own."""
59
+ def register_subprocess(self, proc: Any) -> None: ...
60
+ def unregister_subprocess(self, proc: Any) -> None: ...
61
+
62
+
63
+ def get_current_subprocess_registry() -> "SubprocessRegistryProtocol | None":
64
+ """Return the currently-active subprocess registry, or None if
65
+ called outside an engine's submit() scope. Tools use this to opt
66
+ into cancellable subprocess execution without having the engine
67
+ passed through every call."""
68
+ return _current_subprocess_registry.get()
69
+
70
+ # v1.0.1/T2: lazy planner heuristic — action verbs trigger a plan pass,
71
+ # read-only question words skip it. Kept as module-level frozen sets so
72
+ # should_run_planner can't mutate them across calls.
73
+ _ACTION_VERBS_EN = frozenset({
74
+ "implement", "add", "create", "build", "fix", "refactor", "rewrite",
75
+ "change", "update", "delete", "remove", "generate", "migrate",
76
+ "rename", "extract", "split", "merge", "introduce", "replace",
77
+ })
78
+ _ACTION_VERBS_ZH = frozenset({
79
+ "實作", "新增", "建立", "修", "重寫", "改", "刪除", "產生", "遷移",
80
+ "重構", "替換", "抽出", "合併", "引入", "更新", "加入", "加上",
81
+ })
82
+ _READONLY_INTENTS_EN = frozenset({
83
+ "what", "why", "how", "where", "which", "who", "when",
84
+ "show", "explain", "read", "list", "find", "describe", "display",
85
+ "print", "tell", "inspect", "view",
86
+ })
87
+ _READONLY_INTENTS_ZH = frozenset({
88
+ "為什麼", "如何", "顯示", "解釋", "讀", "列", "描述", "查看",
89
+ "印出", "看一下", "說明", "介紹",
90
+ })
91
+ # ZH interrogative patterns: if these appear anywhere, the sentence is a
92
+ # question. Separate from _READONLY_INTENTS_ZH because these are positional-
93
+ # independent (e.g. "player.gd 裡面有什麼?" is a question without starting
94
+ # with an interrogative keyword).
95
+ _ZH_INTERROGATIVE_PATTERNS = (
96
+ "什麼", "什么", "嗎?", "嗎?", "何時", "哪個", "哪裡", "哪里",
97
+ )
98
+
99
+
100
+ def should_run_planner(user_input: str) -> tuple[bool, str]:
101
+ """Heuristic: should the planner run for this user input?
102
+
103
+ Returns (run, reason). When run=False the caller should skip the
104
+ planner pass and emit a planner_skipped event with the reason.
105
+
106
+ Rules (in priority order):
107
+ 1. Empty / whitespace-only → skip (caller already guards, this is defence)
108
+ 2. Multiple file paths (res:// or .gd, >= 2) → run (multi-file scope)
109
+ 3. > 5 lines → run (complex requests probably need planning)
110
+ 4. Read-only intent keyword (first-token EN or 繁中 anywhere) → skip
111
+ — checked BEFORE action verbs so "如何實作 X" (how to implement X)
112
+ stays a question even though it contains an action verb.
113
+ 5. Explicit action verb (EN or ZH) → run
114
+ 6. Default → run (safe direction, preserves v1.0.0 behavior on ambiguity)
115
+ """
116
+ text = user_input.strip()
117
+ if not text:
118
+ return False, "empty_input"
119
+
120
+ # Rule 2: multiple file paths — strong signal for multi-file change
121
+ file_refs = text.count("res://") + sum(1 for token in text.split() if token.endswith(".gd"))
122
+ if file_refs >= 2:
123
+ return True, f"multi_file:{file_refs}"
124
+
125
+ # Rule 3: complex / long requests
126
+ line_count = text.count("\n") + 1
127
+ if line_count > 5:
128
+ return True, f"long_request:{line_count}_lines"
129
+
130
+ # Rule 4: read-only intents — checked before action verbs so that
131
+ # question-framed inputs ("how do I X", "如何 X") are correctly
132
+ # classified as questions even when they contain action verbs.
133
+ lower = text.lower()
134
+ first_token = lower.split()[0] if lower.split() else ""
135
+ if first_token in _READONLY_INTENTS_EN:
136
+ return False, f"readonly_intent:{first_token}"
137
+ # ZH uses prefix matching (not substring) to avoid false positives
138
+ # like "改 enemy 的血量顯示" matching "顯示" as readonly.
139
+ for intent in _READONLY_INTENTS_ZH:
140
+ if text.startswith(intent):
141
+ return False, f"readonly_intent:{intent}"
142
+ # Positional-independent ZH interrogative patterns catch cases like
143
+ # "player.gd 裡面有什麼?" that don't start with a keyword.
144
+ for pattern in _ZH_INTERROGATIVE_PATTERNS:
145
+ if pattern in text:
146
+ return False, f"interrogative:{pattern}"
147
+
148
+ # Rule 5: explicit action verbs
149
+ words = set(lower.split())
150
+ for verb in _ACTION_VERBS_EN:
151
+ if verb in words:
152
+ return True, f"action_verb:{verb}"
153
+ # ZH action verbs use prefix matching for the same reason as readonly.
154
+ for verb in _ACTION_VERBS_ZH:
155
+ if text.startswith(verb):
156
+ return True, f"action_verb:{verb}"
157
+
158
+ # Rule 6: default — run. Preserves v1.0.0 behavior on ambiguous input.
159
+ return True, "default_run"
160
+
161
+
41
162
  _FILE_MUTATING_TOOLS = {
42
163
  "write_file",
43
164
  "edit_file",
@@ -155,6 +276,21 @@ class ConversationEngine:
155
276
  self.use_streaming = False
156
277
  self.base_allowed_tools: set[str] | None = None
157
278
  self.allowed_tools: set[str] | None = None
279
+ # v1.0.1/T1: how many planner blocks to keep in history. Default 2
280
+ # keeps the current plan plus one previous for "what did I just do"
281
+ # context without carrying N>2 worth of stale plans. Override via
282
+ # config (runtime/config.py `plan_history_keep`) or by setting on
283
+ # the engine instance directly.
284
+ self.plan_history_keep: int = 2
285
+ # v1.0.1/T2: skip the planner pass on trivial read-only turns.
286
+ # Set to False to restore v1.0.0's unconditional planner behavior.
287
+ self.planner_lazy: bool = True
288
+ # v1.0.1/D2: subprocess registry. Tools that spawn subprocesses
289
+ # (run_godot, screenshot_scene) register them here so Ctrl+C can
290
+ # terminate the entire subprocess tree, not just the Python
291
+ # coroutine. Populated via register_subprocess / cleared on
292
+ # rollback_and_terminate or after tool completion.
293
+ self._active_subprocesses: set[Any] = set()
158
294
  self.active_skills: list[str] = []
159
295
  self.skill_mode: str = "auto"
160
296
  self.enabled_skills: list[str] = []
@@ -881,17 +1017,61 @@ class ConversationEngine:
881
1017
  if self.dispatcher is None or self.mode not in {"apply", "fix"} or not _has_meaningful_text(user_input):
882
1018
  return
883
1019
 
1020
+ # v1.0.1/T2: lazy planner — skip trivial read-only turns so we don't
1021
+ # pay the full planner cost (LLM call + impact report + plan
1022
+ # injection + rebill) on inputs that gain nothing from a plan pass.
1023
+ if self.planner_lazy:
1024
+ should_plan, reason = should_run_planner(user_input)
1025
+ if not should_plan:
1026
+ self._emit_event(
1027
+ "planner_skipped",
1028
+ f"Planner skipped: {reason}",
1029
+ reason=reason,
1030
+ )
1031
+ return
1032
+
884
1033
  self._emit_event("planner_started", "Running planner pass")
885
1034
  if self.project_path:
886
1035
  self.last_impact_report = analyze_change_impact(Path(self.project_path), set(self.changeset.read_files) or {str(Path(self.project_path) / "project.godot")})
887
1036
  planner_result = await self.dispatcher.run_planner(user_input)
888
1037
  self.last_plan = planner_result.content
1038
+ # v1.0.1/T3: inject only the actionable subset (Goal + Steps) into
1039
+ # main history. The full plan with Risks/Validation is already
1040
+ # streamed to the user via the planner sub-engine's TUI output;
1041
+ # injecting it again here just wastes tokens on every rebill.
1042
+ from godot_agent.agents.dispatcher import extract_worker_plan
1043
+ reduced_plan = extract_worker_plan(planner_result.content)
889
1044
  self.messages.append(
890
1045
  Message.user(
891
- f"[SYSTEM] Planner pass before implementation:\n{planner_result.content}\n"
1046
+ f"[SYSTEM] Planner pass before implementation:\n{reduced_plan}\n"
892
1047
  "Follow this plan unless direct inspection or validation proves it wrong."
893
1048
  )
894
1049
  )
1050
+ # v1.0.1/T1+T4: prune old planner blocks from history so long sessions
1051
+ # don't accumulate N × 500-token plans that rebill on every subsequent
1052
+ # LLM call. Scoped by prefix so quality-gate / reviewer reports (also
1053
+ # [SYSTEM]-prefixed) are never touched as collateral damage.
1054
+ before_count = sum(
1055
+ 1 for m in self.messages
1056
+ if isinstance(m.content, str) and m.content.startswith("[SYSTEM] Planner pass")
1057
+ )
1058
+ self.messages = prune_system_reports(
1059
+ self.messages,
1060
+ max_reports=self.plan_history_keep,
1061
+ prefix_filter="[SYSTEM] Planner pass",
1062
+ )
1063
+ after_count = sum(
1064
+ 1 for m in self.messages
1065
+ if isinstance(m.content, str) and m.content.startswith("[SYSTEM] Planner pass")
1066
+ )
1067
+ dropped = before_count - after_count
1068
+ if dropped > 0:
1069
+ self._emit_event(
1070
+ "plan_pruned",
1071
+ f"Pruned {dropped} stale planner block(s) from history",
1072
+ dropped=dropped,
1073
+ kept=after_count,
1074
+ )
895
1075
  self._emit_event("planner_finished", "Planner pass complete", used_tools=planner_result.used_tools)
896
1076
 
897
1077
  async def submit(self, user_input: str) -> str:
@@ -903,10 +1083,18 @@ class ConversationEngine:
903
1083
  self.last_user_input = user_input
904
1084
  self.refresh_intent_profile(user_input)
905
1085
  self._sync_registry_context()
906
- await self._maybe_run_planner(user_input)
907
- self.messages.append(Message.user(user_input))
908
- self._emit_event("turn_started", user_input.splitlines()[0][:120], user_input=user_input)
909
- return await self._run_loop(None, use_streaming=self.use_streaming)
1086
+ # v1.0.1/D2: activate this engine as the subprocess registry so
1087
+ # tools spawned during this turn can register their subprocesses
1088
+ # for cancellation. The contextvar is reset in finally so
1089
+ # subsequent engines/tests get a clean state.
1090
+ registry_token = self._activate_subprocess_registry()
1091
+ try:
1092
+ await self._maybe_run_planner(user_input)
1093
+ self.messages.append(Message.user(user_input))
1094
+ self._emit_event("turn_started", user_input.splitlines()[0][:120], user_input=user_input)
1095
+ return await self._run_loop(None, use_streaming=self.use_streaming)
1096
+ finally:
1097
+ self._deactivate_subprocess_registry(registry_token)
910
1098
 
911
1099
  def rollback_current_turn(self) -> int:
912
1100
  """Drop any messages appended since the last submit() began.
@@ -928,16 +1116,77 @@ class ConversationEngine:
928
1116
  self._emit_event("turn_cancelled", f"turn cancelled, dropped {removed} messages")
929
1117
  return removed
930
1118
 
1119
+ # ──────────────────────────────────────────────────────────────────
1120
+ # v1.0.1/D2: subprocess registry API
1121
+ # ──────────────────────────────────────────────────────────────────
1122
+ def register_subprocess(self, proc: Any) -> None:
1123
+ """Register a subprocess so the engine can terminate it on
1124
+ cancellation. Tools call this after ``create_subprocess_exec``
1125
+ and ``unregister_subprocess`` after ``communicate`` completes.
1126
+ """
1127
+ self._active_subprocesses.add(proc)
1128
+
1129
+ def unregister_subprocess(self, proc: Any) -> None:
1130
+ """Remove a subprocess from the active set. Idempotent —
1131
+ unregistering an already-removed process is a no-op."""
1132
+ self._active_subprocesses.discard(proc)
1133
+
1134
+ async def terminate_active_subprocesses(self, timeout: float = 2.0) -> int:
1135
+ """Terminate every registered subprocess that's still running.
1136
+
1137
+ Sends SIGTERM (via ``proc.terminate()``), waits up to ``timeout``
1138
+ for graceful exit, then escalates to SIGKILL for any survivor.
1139
+ Clears the registry after. Returns the number of processes that
1140
+ were still running when this was called (i.e. the count that
1141
+ required termination).
1142
+ """
1143
+ if not self._active_subprocesses:
1144
+ return 0
1145
+ running = [p for p in self._active_subprocesses if p.returncode is None]
1146
+ for proc in running:
1147
+ try:
1148
+ proc.terminate()
1149
+ except ProcessLookupError:
1150
+ pass # already gone
1151
+ for proc in running:
1152
+ if proc.returncode is not None:
1153
+ continue
1154
+ try:
1155
+ await asyncio.wait_for(proc.wait(), timeout=timeout)
1156
+ except asyncio.TimeoutError:
1157
+ try:
1158
+ proc.kill()
1159
+ await proc.wait()
1160
+ except ProcessLookupError:
1161
+ pass
1162
+ self._active_subprocesses.clear()
1163
+ return len(running)
1164
+
1165
+ def _activate_subprocess_registry(self):
1166
+ """Activate this engine as the current subprocess registry for
1167
+ the duration of a submit() call. Returns a token that must be
1168
+ passed to _deactivate_subprocess_registry to restore the prior
1169
+ state (matching contextvars' set/reset API)."""
1170
+ return _current_subprocess_registry.set(self)
1171
+
1172
+ def _deactivate_subprocess_registry(self, token) -> None:
1173
+ _current_subprocess_registry.reset(token)
1174
+
931
1175
  async def submit_with_images(self, text: str, images_b64: list[str]) -> str:
932
1176
  if not images_b64 and not _has_meaningful_text(text):
933
1177
  return ""
934
1178
  self.last_user_input = text
935
1179
  self.refresh_intent_profile(text)
936
1180
  self._sync_registry_context()
937
- await self._maybe_run_planner(text)
938
- self.messages.append(Message.user_with_images(text, images_b64))
939
- self._emit_event("turn_started", text.splitlines()[0][:120], user_input=text, images=len(images_b64))
940
- return await self._run_loop(None, use_streaming=self.use_streaming)
1181
+ # v1.0.1/D2: activate subprocess registry for this turn (see submit)
1182
+ registry_token = self._activate_subprocess_registry()
1183
+ try:
1184
+ await self._maybe_run_planner(text)
1185
+ self.messages.append(Message.user_with_images(text, images_b64))
1186
+ self._emit_event("turn_started", text.splitlines()[0][:120], user_input=text, images=len(images_b64))
1187
+ return await self._run_loop(None, use_streaming=self.use_streaming)
1188
+ finally:
1189
+ self._deactivate_subprocess_registry(registry_token)
941
1190
 
942
1191
  def _check_auto_health(self) -> "ContextHealth":
943
1192
  from godot_agent.runtime.context_health import ContextHealth