pycastle 0.1.3.7.dev0__tar.gz → 0.1.3.8.dev0__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 (179) hide show
  1. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/CONTEXT.md +7 -6
  2. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/PKG-INFO +1 -1
  3. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/__pycache__/__init__.cpython-311.pyc +0 -0
  4. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/__pycache__/_types.cpython-311.pyc +0 -0
  5. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/__pycache__/agent_output_protocol.cpython-311.pyc +0 -0
  6. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/__pycache__/agent_result.cpython-311.pyc +0 -0
  7. pycastle-0.1.3.8.dev0/src/pycastle/__pycache__/agent_runner.cpython-311.pyc +0 -0
  8. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/__pycache__/build_command.cpython-311.pyc +0 -0
  9. pycastle-0.1.3.8.dev0/src/pycastle/__pycache__/container_runner.cpython-311.pyc +0 -0
  10. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/__pycache__/errors.cpython-311.pyc +0 -0
  11. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/__pycache__/init_command.cpython-311.pyc +0 -0
  12. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/__pycache__/labels.cpython-311.pyc +0 -0
  13. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/__pycache__/main.cpython-311.pyc +0 -0
  14. pycastle-0.1.3.8.dev0/src/pycastle/__pycache__/orchestrator.cpython-311.pyc +0 -0
  15. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/__pycache__/prompt_pipeline.cpython-311.pyc +0 -0
  16. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/__pycache__/prompt_utils.cpython-311.pyc +0 -0
  17. pycastle-0.1.3.8.dev0/src/pycastle/__pycache__/rich_status_display.cpython-311.pyc +0 -0
  18. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/__pycache__/stream_parser.cpython-311.pyc +0 -0
  19. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/__pycache__/worktree.cpython-311.pyc +0 -0
  20. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/agent_runner.py +7 -10
  21. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/config/__pycache__/__init__.cpython-311.pyc +0 -0
  22. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/config/__pycache__/loader.cpython-311.pyc +0 -0
  23. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/config/__pycache__/validator.cpython-311.pyc +0 -0
  24. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/container_runner.py +4 -19
  25. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/iteration/__init__.py +24 -12
  26. pycastle-0.1.3.8.dev0/src/pycastle/iteration/__pycache__/__init__.cpython-311.pyc +0 -0
  27. pycastle-0.1.3.8.dev0/src/pycastle/iteration/__pycache__/_deps.cpython-311.pyc +0 -0
  28. pycastle-0.1.3.8.dev0/src/pycastle/iteration/__pycache__/_utils.cpython-311.pyc +0 -0
  29. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/iteration/__pycache__/implement.cpython-311.pyc +0 -0
  30. pycastle-0.1.3.8.dev0/src/pycastle/iteration/__pycache__/merge.cpython-311.pyc +0 -0
  31. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/iteration/__pycache__/planning.cpython-311.pyc +0 -0
  32. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/iteration/__pycache__/preflight.cpython-311.pyc +0 -0
  33. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/iteration/_deps.py +9 -35
  34. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/iteration/_utils.py +1 -1
  35. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/iteration/implement.py +3 -3
  36. pycastle-0.1.3.8.dev0/src/pycastle/iteration/merge.py +111 -0
  37. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/iteration/planning.py +1 -1
  38. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/iteration/preflight.py +2 -3
  39. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/orchestrator.py +14 -13
  40. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/rich_status_display.py +78 -37
  41. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/services/__pycache__/__init__.cpython-311.pyc +0 -0
  42. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/services/__pycache__/_base.cpython-311.pyc +0 -0
  43. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/services/__pycache__/claude_service.cpython-311.pyc +0 -0
  44. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/services/__pycache__/docker_service.cpython-311.pyc +0 -0
  45. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/services/__pycache__/git_service.cpython-311.pyc +0 -0
  46. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/services/__pycache__/github_service.cpython-311.pyc +0 -0
  47. pycastle-0.1.3.8.dev0/src/pycastle/status_display.py +43 -0
  48. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle.egg-info/PKG-INFO +1 -1
  49. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle.egg-info/SOURCES.txt +2 -0
  50. pycastle-0.1.3.8.dev0/tests/__pycache__/__init__.cpython-311.pyc +0 -0
  51. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/conftest.cpython-311-pytest-9.0.3.pyc +0 -0
  52. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_agent_output_protocol.cpython-311-pytest-9.0.3.pyc +0 -0
  53. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_agent_result.cpython-311-pytest-9.0.3.pyc +0 -0
  54. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_agent_runner.cpython-311-pytest-9.0.3.pyc +0 -0
  55. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_build_command.cpython-311-pytest-9.0.3.pyc +0 -0
  56. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_claude_service.cpython-311-pytest-9.0.3.pyc +0 -0
  57. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_config_new.cpython-311-pytest-9.0.3.pyc +0 -0
  58. pycastle-0.1.3.8.dev0/tests/__pycache__/test_container_runner.cpython-311-pytest-9.0.3.pyc +0 -0
  59. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_default_prompts.cpython-311-pytest-9.0.3.pyc +0 -0
  60. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_deps.cpython-311-pytest-9.0.3.pyc +0 -0
  61. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_docker_service.cpython-311-pytest-9.0.3.pyc +0 -0
  62. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_errors.cpython-311-pytest-9.0.3.pyc +0 -0
  63. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_git_service.cpython-311-pytest-9.0.3.pyc +0 -0
  64. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_github_service.cpython-311-pytest-9.0.3.pyc +0 -0
  65. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_implement.cpython-311-pytest-9.0.3.pyc +0 -0
  66. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_init_command.cpython-311-pytest-9.0.3.pyc +0 -0
  67. pycastle-0.1.3.8.dev0/tests/__pycache__/test_integration.cpython-311-pytest-9.0.3.pyc +0 -0
  68. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_iteration.cpython-311-pytest-9.0.3.pyc +0 -0
  69. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_labels.cpython-311-pytest-9.0.3.pyc +0 -0
  70. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_main.cpython-311-pytest-9.0.3.pyc +0 -0
  71. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_merge.cpython-311-pytest-9.0.3.pyc +0 -0
  72. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_orchestrator.cpython-311-pytest-9.0.3.pyc +0 -0
  73. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_plan.cpython-311-pytest-9.0.3.pyc +0 -0
  74. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_planning.cpython-311-pytest-9.0.3.pyc +0 -0
  75. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_preflight.cpython-311-pytest-9.0.3.pyc +0 -0
  76. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_prompt_pipeline.cpython-311-pytest-9.0.3.pyc +0 -0
  77. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_prompt_utils.cpython-311-pytest-9.0.3.pyc +0 -0
  78. pycastle-0.1.3.8.dev0/tests/__pycache__/test_status_display.cpython-311-pytest-9.0.3.pyc +0 -0
  79. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_stream_parser.cpython-311-pytest-9.0.3.pyc +0 -0
  80. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_subprocess_service.cpython-311-pytest-9.0.3.pyc +0 -0
  81. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__pycache__/test_worktree.cpython-311-pytest-9.0.3.pyc +0 -0
  82. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_agent_runner.py +6 -6
  83. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_container_runner.py +43 -47
  84. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_implement.py +17 -17
  85. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_iteration.py +726 -598
  86. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_merge.py +65 -22
  87. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_orchestrator.py +2105 -2102
  88. pycastle-0.1.3.8.dev0/tests/test_plain_status_display.py +5 -0
  89. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_planning.py +2 -2
  90. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_preflight.py +11 -13
  91. pycastle-0.1.3.8.dev0/tests/test_status_display.py +925 -0
  92. pycastle-0.1.3.7.dev0/src/pycastle/__pycache__/agent_runner.cpython-311.pyc +0 -0
  93. pycastle-0.1.3.7.dev0/src/pycastle/__pycache__/container_runner.cpython-311.pyc +0 -0
  94. pycastle-0.1.3.7.dev0/src/pycastle/__pycache__/orchestrator.cpython-311.pyc +0 -0
  95. pycastle-0.1.3.7.dev0/src/pycastle/__pycache__/rich_status_display.cpython-311.pyc +0 -0
  96. pycastle-0.1.3.7.dev0/src/pycastle/iteration/__pycache__/__init__.cpython-311.pyc +0 -0
  97. pycastle-0.1.3.7.dev0/src/pycastle/iteration/__pycache__/_deps.cpython-311.pyc +0 -0
  98. pycastle-0.1.3.7.dev0/src/pycastle/iteration/__pycache__/_utils.cpython-311.pyc +0 -0
  99. pycastle-0.1.3.7.dev0/src/pycastle/iteration/__pycache__/merge.cpython-311.pyc +0 -0
  100. pycastle-0.1.3.7.dev0/src/pycastle/iteration/merge.py +0 -103
  101. pycastle-0.1.3.7.dev0/tests/__pycache__/__init__.cpython-311.pyc +0 -0
  102. pycastle-0.1.3.7.dev0/tests/__pycache__/test_container_runner.cpython-311-pytest-9.0.3.pyc +0 -0
  103. pycastle-0.1.3.7.dev0/tests/__pycache__/test_integration.cpython-311-pytest-9.0.3.pyc +0 -0
  104. pycastle-0.1.3.7.dev0/tests/__pycache__/test_status_display.cpython-311-pytest-9.0.3.pyc +0 -0
  105. pycastle-0.1.3.7.dev0/tests/test_status_display.py +0 -696
  106. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/.github/workflows/publish.yml +0 -0
  107. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/.gitignore +0 -0
  108. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/.python-version +0 -0
  109. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/CLAUDE.md +0 -0
  110. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/LICENSE +0 -0
  111. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/README.md +0 -0
  112. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/docs/adr/0001-runtime-dependency-installation.md +0 -0
  113. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/docs/agents/domain.md +0 -0
  114. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/docs/agents/issue-tracker.md +0 -0
  115. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/docs/agents/triage-labels.md +0 -0
  116. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/pyproject.toml +0 -0
  117. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/setup.cfg +0 -0
  118. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/__init__.py +0 -0
  119. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/_types.py +0 -0
  120. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/agent_output_protocol.py +0 -0
  121. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/agent_result.py +0 -0
  122. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/build_command.py +0 -0
  123. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/config/__init__.py +0 -0
  124. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/config/loader.py +0 -0
  125. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/config/validator.py +0 -0
  126. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/defaults/.gitignore +0 -0
  127. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/defaults/Dockerfile +0 -0
  128. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/defaults/config.py +0 -0
  129. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/defaults/prompts/coding-standards/deep-modules.md +0 -0
  130. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/defaults/prompts/coding-standards/interfaces.md +0 -0
  131. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/defaults/prompts/coding-standards/mocking.md +0 -0
  132. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/defaults/prompts/coding-standards/refactoring.md +0 -0
  133. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/defaults/prompts/coding-standards/tests.md +0 -0
  134. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/defaults/prompts/implement-prompt.md +0 -0
  135. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/defaults/prompts/merge-prompt.md +0 -0
  136. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/defaults/prompts/plan-prompt.md +0 -0
  137. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/defaults/prompts/preflight-issue.md +0 -0
  138. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/defaults/prompts/review-prompt.md +0 -0
  139. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/errors.py +0 -0
  140. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/init_command.py +0 -0
  141. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/labels.py +0 -0
  142. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/main.py +0 -0
  143. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/prompt_pipeline.py +0 -0
  144. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/prompt_utils.py +0 -0
  145. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/services/__init__.py +0 -0
  146. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/services/_base.py +0 -0
  147. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/services/claude_service.py +0 -0
  148. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/services/docker_service.py +0 -0
  149. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/services/git_service.py +0 -0
  150. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/services/github_service.py +0 -0
  151. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/stream_parser.py +0 -0
  152. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle/worktree.py +0 -0
  153. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle.egg-info/dependency_links.txt +0 -0
  154. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle.egg-info/entry_points.txt +0 -0
  155. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle.egg-info/requires.txt +0 -0
  156. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/src/pycastle.egg-info/top_level.txt +0 -0
  157. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/__init__.py +0 -0
  158. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/conftest.py +0 -0
  159. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_agent_output_protocol.py +0 -0
  160. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_agent_result.py +0 -0
  161. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_build_command.py +0 -0
  162. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_claude_service.py +0 -0
  163. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_config_new.py +0 -0
  164. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_default_prompts.py +0 -0
  165. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_deps.py +0 -0
  166. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_docker_service.py +0 -0
  167. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_errors.py +0 -0
  168. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_git_service.py +0 -0
  169. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_github_service.py +0 -0
  170. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_init_command.py +0 -0
  171. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_integration.py +0 -0
  172. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_labels.py +0 -0
  173. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_main.py +0 -0
  174. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_plan.py +0 -0
  175. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_prompt_pipeline.py +0 -0
  176. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_prompt_utils.py +0 -0
  177. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_stream_parser.py +0 -0
  178. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_subprocess_service.py +0 -0
  179. {pycastle-0.1.3.7.dev0 → pycastle-0.1.3.8.dev0}/tests/test_worktree.py +0 -0
@@ -186,10 +186,11 @@
186
186
  | **GithubService** | Service that encapsulates `gh` CLI calls for GitHub issue operations: closing issues, querying parent issues, listing open sub-issues, and reading issue labels | GitHub wrapper, gh provider |
187
187
  | **Logger** | Injectable abstraction that owns all structured log output for one iteration; exposes named channels (`log_error`, `log_agent_output`) each writing to a dedicated file under `logs/`; injected via `Deps` so tests never touch the filesystem | log writer, output handler |
188
188
  | **RecordingLogger** | Test double for `Logger` that records every call in memory; tests assert on recorded calls rather than capturing stderr or reading log files | mock logger, spy logger |
189
- | **StatusDisplay** | Injectable abstraction that owns the live terminal status line; exposes `add_agent(name, phase, work_body="")`, `update_phase`, `reset_idle_timer`, `remove_agent`, and `print` methods; backed by a `rich` `Live` display in production and a `NullStatusDisplay` no-op in tests; injected via `Deps` as a separate concern from `Logger` | terminal display, status bar |
190
- | **work_body** | The caller-constructed string passed as the third argument to `add_agent`; displayed in the body column of the agent status row during the Work phase; empty string for agents that do not reach Work | |
191
- | **NullStatusDisplay** | Test/no-op implementation of `StatusDisplay`; all methods are no-ops; `print` falls back to `builtins.print` | — |
192
- | **agent status row** | One headerless line in the `StatusDisplay` live panel representing one active agent; format: `elapsed \| RoleName \| idle \| body`; elapsed is dim and right-justified so all role names align across rows; role name is bold, with any numeric part (e.g. `5` in `Implementer #5`) styled bold cyan; idle is dim; body column shows the current lifecycle phase name (e.g. `Setup`, `Running ruff Checks`, `Prepare`) for all non-Work states, or the `work_body` string during Work (caller-constructed: `"implementing \"{issue_title}\""` for Implementers, `"reviewing \"{issue_title}\""` for Reviewers, `"Creating Plan from {n} issues"` for the Planner, `"Merging {n} Branches"` for the Merger, `"Checking"` for the preflight-checks agent, `"reporting {check_name} issue"` for the preflight-issue agent); elapsed counts up from agent start and never resets; idle resets on each Docker stream chunk; the live panel is preceded by one blank line to visually separate it from scrollback output; ordered by orchestration phase (plan → implement → review → merge) then by issue number; disappears when the agent finishes | status entry, agent row |
189
+ | **StatusDisplay** | Injectable abstraction that owns the live terminal status panel and all formatted terminal output; exposes `register(caller, startup_message="started", work_body="")`, `update_phase`, `reset_idle_timer`, `remove(caller, shutdown_message="finished", shutdown_style="success")`, and `print(caller, message, style=None)` methods; backed by a `rich` `Live` display in production and a `PlainStatusDisplay` in tests; injected via `Deps` as a separate concern from `Logger`; defined in `status_display` module | terminal display, status bar |
190
+ | **caller** | The identity string passed as the first argument to `StatusDisplay.register`, `remove`, and `print`; rendered as a `[Caller]` prefix on every terminal output line; empty string `""` is the anonymous caller no brackets are printed and the message is output as-is; a blank line is inserted before a `print` call when the caller differs from the previous one; canonical callers orchestration: `"pycastle"`; phase rows: `"Preflight"`, `"Plan"`, `"Implement"`, `"Merge"`; agents: `"Preflight Agent"`, `"Plan Agent"`, `"Implement Agent #N"`, `"Review Agent #N"`, `"Merge Agent"` | source, label |
191
+ | **work_body** | The caller-constructed string passed as the third argument to `register`; displayed in the body column of the status row during the Work phase; empty string for callers that do not reach Work | — |
192
+ | **PlainStatusDisplay** | Plain-terminal adapter for `StatusDisplay` defined in `status_display` module; panel methods (`update_phase`, `reset_idle_timer`) are no-ops; `register` and `remove` print their startup/shutdown messages; `print(caller, message, style=None)` formats output as `[Caller] message` with no ANSI colour codes and style ignored; used in tests so assertions can match the full formatted line | NullStatusDisplay |
193
+ | **status row** | One headerless line in the `StatusDisplay` live panel; created by `register` and removed by `remove`; two kinds: **agent rows** (one per active agent — `"Preflight Agent"`, `"Plan Agent"`, `"Implement Agent #N"`, `"Review Agent #N"`, `"Merge Agent"`) and **phase rows** (one per active phase — `"Preflight"`, `"Plan"`, `"Implement"`, `"Merge"`); phase rows and agent rows within the same phase coexist; format: `elapsed \| Name \| idle \| body`; elapsed is dim and right-justified; name is bold with any numeric part styled bold cyan; idle is dim; body shows the current lifecycle phase name for all non-Work states, or the `work_body` string during Work; elapsed counts up from `register` and never resets; idle resets on each Docker stream chunk; the live panel is preceded by one blank line to visually separate it from scrollback; ordered by orchestration phase (plan → implement → review → merge) then by issue number | agent status row, status entry, agent row |
193
194
  | **IterationOutcome** | Sealed return type of `run_iteration()`; one of four variants: `Continue` (iteration completed, keep looping), `Done` (no issues found, stop cleanly), `AbortedHITL` (HITL verdict — carries `issue_number`; orchestrator exits non-zero), `AbortedUsageLimit` (token ceiling hit — worktrees preserved, safe to retry; orchestrator exits non-zero) | iteration result, loop result |
194
195
 
195
196
  ## Test Anti-Patterns (Red Flags)
@@ -221,9 +222,9 @@
221
222
  - Host mounts per container: host repo → RO at `/home/agent/repo`; worktree → RW at `/home/agent/workspace`; `<host-repo>/.git` → RW at `/.pycastle-parent-git`; on Windows, gitdir overlay → RO over `/home/agent/workspace/.git`.
222
223
  - A **Service** defines a Custom exception hierarchy so callers never handle raw subprocess exceptions; tests inject Default implementations from a test fixture and override per-test for error paths.
223
224
  - **StatusDisplay** is a separate injectable in `Deps` alongside `Logger`; `Logger` owns file I/O, `StatusDisplay` owns the live terminal UI — they never overlap.
224
- - The **agent status row** is created when the Setup phase begins; the row is removed when the agent finishes or errors; the `rich` `Live` display is started on the first `add_agent` call and stopped after the last `remove_agent` call.
225
+ - A **status row** is created by `StatusDisplay.register` and removed by `StatusDisplay.remove`; phase rows are registered at the start of each orchestration phase and removed at its end; agent rows are registered at container Setup and removed when the agent finishes or errors; the `rich` `Live` display is started on the first `register` call and stopped after the last `remove` call.
225
226
  - All orchestrator-level terminal output (e.g. "Planning complete…") is routed through `StatusDisplay.print()` so `rich` can coordinate it with the live panel; bare `print()` calls are not used while a `StatusDisplay` is active.
226
- - The streaming loop calls `reset_idle_timer` on every Docker chunk (matching the kill-agent timeout behavior); each complete line is fed to `StreamParser.feed()` — during the Work phase, if a complete assistant turn is returned, it is printed to the console via `StatusDisplay.print()` with the agent-name prefix and a trailing blank line; tool-use blocks are silently dropped; Setup, Pre-flight, and Prepare phases produce no console output — their activity is reflected only in the body column of the agent status row.
227
+ - The streaming loop calls `reset_idle_timer` on every Docker chunk (matching the kill-agent timeout behavior); each complete line is fed to `StreamParser.feed()` — during the Work phase, if a complete assistant turn is returned, it is printed to the console via `StatusDisplay.print()` with the agent-name prefix; tool-use blocks are silently dropped; Setup, Pre-flight, and Prepare phases produce no console output — their activity is reflected only in the body column of the agent status row.
227
228
 
228
229
  ## Example dialogue
229
230
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycastle
3
- Version: 0.1.3.7.dev0
3
+ Version: 0.1.3.8.dev0
4
4
  Summary: Python orchestrator for autonomous Claude Code agents in Docker
5
5
  License: MIT License
6
6
 
@@ -9,6 +9,7 @@ from .config import Config
9
9
  from .container_runner import ContainerRunner
10
10
  from .errors import AgentTimeoutError, BranchCollisionError, UsageLimitError
11
11
  from .services import GitService
12
+ from .status_display import PlainStatusDisplay
12
13
  from .worktree import patch_gitdir_for_container, worktree_name_for_branch
13
14
 
14
15
 
@@ -73,9 +74,7 @@ class AgentRunner:
73
74
  work_body = request.work_body
74
75
 
75
76
  if status_display is None:
76
- from .iteration._deps import NullStatusDisplay
77
-
78
- status_display = NullStatusDisplay()
77
+ status_display = PlainStatusDisplay()
79
78
 
80
79
  _token = token if token is not None else CancellationToken()
81
80
  if _token.is_cancelled:
@@ -169,9 +168,9 @@ class AgentRunner:
169
168
  raise
170
169
  restart_num = self._cfg.timeout_retries - retries_left + 1
171
170
  status_display.print(
172
- f"[{name}] Timeout — restarting"
171
+ name,
172
+ f"Timeout — restarting"
173
173
  f" (attempt {restart_num}/{self._cfg.timeout_retries})",
174
- source="agent-timeout",
175
174
  )
176
175
  retries_left -= 1
177
176
  except UsageLimitError:
@@ -179,7 +178,7 @@ class AgentRunner:
179
178
  raise
180
179
  return output
181
180
  finally:
182
- status_display.remove_agent(name)
181
+ status_display.remove(name)
183
182
  if lock is not None and lock.locked():
184
183
  lock.release()
185
184
 
@@ -193,9 +192,7 @@ class AgentRunner:
193
192
  work_body: str = "",
194
193
  ) -> list[tuple[str, str, str]]:
195
194
  if status_display is None:
196
- from .iteration._deps import NullStatusDisplay
197
-
198
- status_display = NullStatusDisplay()
195
+ status_display = PlainStatusDisplay()
199
196
 
200
197
  git_name = self._git_service.get_user_name()
201
198
  git_email = self._git_service.get_user_email()
@@ -211,7 +208,7 @@ class AgentRunner:
211
208
  await runner.setup(git_name, git_email, work_body)
212
209
  return await runner.preflight(list(self._cfg.preflight_checks))
213
210
  finally:
214
- status_display.remove_agent(name)
211
+ status_display.remove(name)
215
212
  try:
216
213
  runner.__exit__(None, None, None)
217
214
  except Exception:
@@ -11,7 +11,6 @@ import threading
11
11
  from pathlib import Path
12
12
 
13
13
  import docker
14
- from rich.text import Text
15
14
  from docker.models.containers import Container as DockerContainer
16
15
 
17
16
  from .config import Config
@@ -21,6 +20,7 @@ from .errors import (
21
20
  DockerTimeoutError,
22
21
  UsageLimitError,
23
22
  )
23
+ from .status_display import PlainStatusDisplay
24
24
  from .stream_parser import StreamParser
25
25
  from .worktree import (
26
26
  CONTAINER_PARENT_GIT,
@@ -57,16 +57,6 @@ def _build_claude_command(model: str = "", effort: str = "") -> str:
57
57
  return f"claude {flags} < /tmp/.pycastle_prompt"
58
58
 
59
59
 
60
- def _build_agent_prefix(name: str) -> Text:
61
- """Return a styled Text object for the agent output prefix, e.g. ``[Implementer #1] ``."""
62
- msg = Text()
63
- msg.append("[", style="bold")
64
- for segment in re.split(r"(\d+)", name):
65
- if segment:
66
- msg.append(segment, style="bold cyan" if segment.isdigit() else "bold")
67
- msg.append("] ", style="bold")
68
- return msg
69
-
70
60
 
71
61
  class ContainerRunner:
72
62
  def __init__(
@@ -94,9 +84,7 @@ class ContainerRunner:
94
84
  self.effort = effort
95
85
  self._cfg = cfg
96
86
  if status_display is None:
97
- from .iteration._deps import NullStatusDisplay
98
-
99
- status_display = NullStatusDisplay()
87
+ status_display = PlainStatusDisplay()
100
88
  self._status_display = status_display
101
89
  self._owns_client = docker_client is None
102
90
  self._client = docker_client if docker_client is not None else docker.from_env()
@@ -268,7 +256,7 @@ class ContainerRunner:
268
256
  async def setup(self, git_name: str, git_email: str, work_body: str = "") -> None:
269
257
  loop = asyncio.get_running_loop()
270
258
  await loop.run_in_executor(None, self.__enter__)
271
- self._status_display.add_agent(self.name, "Setup", work_body)
259
+ self._status_display.register(self.name, work_body=work_body)
272
260
  await loop.run_in_executor(
273
261
  None,
274
262
  self.exec_simple,
@@ -362,9 +350,7 @@ class ContainerRunner:
362
350
  raise UsageLimitError(line)
363
351
  turn = parser.feed(line)
364
352
  if print_output and turn is not None:
365
- msg = _build_agent_prefix(self.name)
366
- msg.append(f"{turn}\n")
367
- self._status_display.print(msg, source=self.name)
353
+ self._status_display.print(self.name, turn)
368
354
  finally:
369
355
  try:
370
356
  self._active_container.exec_run(
@@ -374,4 +360,3 @@ class ContainerRunner:
374
360
  except Exception:
375
361
  pass
376
362
  return "".join(parts)
377
-
@@ -33,12 +33,16 @@ IterationOutcome: TypeAlias = Continue | Done | AbortedHITL | AbortedUsageLimit
33
33
 
34
34
 
35
35
  async def run_iteration(deps: Deps) -> IterationOutcome:
36
- preflight_result = await preflight_phase(deps)
36
+ deps.status_display.register("Preflight")
37
+ try:
38
+ preflight_result = await preflight_phase(deps)
39
+ finally:
40
+ deps.status_display.remove("Preflight")
37
41
 
38
42
  if isinstance(preflight_result, PreflightHITL):
39
43
  deps.status_display.print(
44
+ "pycastle",
40
45
  f"Preflight issue #{preflight_result.issue_number} requires human intervention. Exiting.",
41
- source="preflight-request-human-error",
42
46
  )
43
47
  return AbortedHITL(issue_number=preflight_result.issue_number)
44
48
 
@@ -48,7 +52,11 @@ async def run_iteration(deps: Deps) -> IterationOutcome:
48
52
  sha = preflight_result.sha
49
53
  open_issues = preflight_result.issues
50
54
  if len(open_issues) >= 2:
51
- plan_result = await planning_phase(deps, sha, open_issues)
55
+ deps.status_display.register("Plan")
56
+ try:
57
+ plan_result = await planning_phase(deps, sha, open_issues)
58
+ finally:
59
+ deps.status_display.remove("Plan")
52
60
  sha = plan_result.worktree_sha
53
61
  issues = plan_result.issues
54
62
  else:
@@ -59,15 +67,19 @@ async def run_iteration(deps: Deps) -> IterationOutcome:
59
67
 
60
68
  issues = issues[: deps.cfg.max_parallel]
61
69
 
62
- deps.status_display.print(f"Planning complete. {len(issues)} issue(s):", source="planning")
70
+ deps.status_display.print("pycastle", f"Planning complete. {len(issues)} issue(s):")
63
71
  for issue in issues:
64
72
  deps.status_display.print(
73
+ "pycastle",
65
74
  f" #{issue['number']}: {issue['title']} → {branch_for(issue['number'])}",
66
- source="planning",
67
75
  )
68
76
 
69
77
  token = CancellationToken()
70
- impl_result = await implement_phase(issues, sha, deps, token=token)
78
+ deps.status_display.register("Implement")
79
+ try:
80
+ impl_result = await implement_phase(issues, sha, deps, token=token)
81
+ finally:
82
+ deps.status_display.remove("Implement")
71
83
 
72
84
  if impl_result.usage_limit_hit:
73
85
  return AbortedUsageLimit()
@@ -76,32 +88,32 @@ async def run_iteration(deps: Deps) -> IterationOutcome:
76
88
  match error:
77
89
  case PreflightFailure(failures=fs):
78
90
  deps.status_display.print(
91
+ "pycastle",
79
92
  f" ✗ #{issue['number']} ({branch_for(issue['number'])}) pre-flight failed:",
80
- source="execution-errors",
81
93
  )
82
94
  for check_name, command, output in fs:
83
95
  deps.status_display.print(
96
+ "pycastle",
84
97
  f" ✗ {check_name} ({command}): {output}",
85
- source="execution-errors",
86
98
  )
87
99
  case _:
88
100
  deps.status_display.print(
101
+ "pycastle",
89
102
  f" ✗ #{issue['number']} ({branch_for(issue['number'])}) failed: {error}",
90
- source="execution-errors",
91
103
  )
92
104
 
93
105
  completed = impl_result.completed
94
106
 
95
107
  if not completed:
96
- deps.status_display.print("No commits produced. Nothing to merge.", source="execution-complete")
108
+ deps.status_display.print("pycastle", "No commits produced. Nothing to merge.")
97
109
  return Continue()
98
110
 
99
111
  deps.status_display.print(
112
+ "pycastle",
100
113
  f"Execution complete. {len(completed)} branch(es) with commits:",
101
- source="execution-complete",
102
114
  )
103
115
  for i in completed:
104
- deps.status_display.print(f" {branch_for(i['number'])}", source="execution-complete")
116
+ deps.status_display.print("pycastle", f" {branch_for(i['number'])}")
105
117
 
106
118
  await merge_phase(completed, deps)
107
119
 
@@ -1,15 +1,15 @@
1
1
  import asyncio
2
- import builtins
3
2
  import dataclasses
4
3
  from collections.abc import Callable
5
4
  from pathlib import Path
6
- from typing import Any, Protocol, runtime_checkable
5
+ from typing import Any, Protocol
7
6
 
8
7
  from ..agent_result import PreflightFailure
9
8
  from ..agent_runner import AgentRunnerProtocol, RunRequest
10
9
  from ..config import Config
11
10
  from ..services import GitService
12
11
  from ..services import GithubService
12
+ from ..status_display import StatusDisplay
13
13
 
14
14
 
15
15
  class Logger(Protocol):
@@ -29,50 +29,24 @@ class RecordingLogger:
29
29
  self.agent_outputs.append((agent_name, output))
30
30
 
31
31
 
32
- @runtime_checkable
33
- class StatusDisplay(Protocol):
34
- def add_agent(self, name: str, phase: str, work_body: str = "") -> None: ...
35
- def update_phase(self, name: str, phase: str) -> None: ...
36
- def remove_agent(self, name: str) -> None: ...
37
- def reset_idle_timer(self, name: str) -> None: ...
38
- def print(self, message: object, *, source: str = "") -> None: ...
39
-
40
-
41
- class NullStatusDisplay:
42
- def add_agent(self, name: str, phase: str, work_body: str = "") -> None:
43
- pass
44
-
45
- def update_phase(self, name: str, phase: str) -> None:
46
- pass
47
-
48
- def remove_agent(self, name: str) -> None:
49
- pass
50
-
51
- def reset_idle_timer(self, name: str) -> None:
52
- pass
53
-
54
- def print(self, message: object, *, source: str = "") -> None:
55
- builtins.print(message)
56
-
57
-
58
32
  class RecordingStatusDisplay:
59
33
  def __init__(self) -> None:
60
34
  self.calls: list[tuple] = []
61
35
 
62
- def add_agent(self, name: str, phase: str, work_body: str = "") -> None:
63
- self.calls.append(("add_agent", name, phase, work_body))
36
+ def register(self, caller: str, startup_message: str = "started", work_body: str = "") -> None:
37
+ self.calls.append(("register", caller, startup_message, work_body))
64
38
 
65
39
  def update_phase(self, name: str, phase: str) -> None:
66
40
  self.calls.append(("update_phase", name, phase))
67
41
 
68
- def remove_agent(self, name: str) -> None:
69
- self.calls.append(("remove_agent", name))
70
-
71
42
  def reset_idle_timer(self, name: str) -> None:
72
43
  self.calls.append(("reset_idle_timer", name))
73
44
 
74
- def print(self, message: object, *, source: str = "") -> None:
75
- self.calls.append(("print", message, source))
45
+ def remove(self, caller: str, shutdown_message: str = "finished", shutdown_style: str = "success") -> None:
46
+ self.calls.append(("remove", caller, shutdown_message, shutdown_style))
47
+
48
+ def print(self, caller: str, message: object, style: str | None = None) -> None:
49
+ self.calls.append(("print", caller, message, style))
76
50
 
77
51
 
78
52
  class FakeAgentRunner:
@@ -7,9 +7,9 @@ async def _wait_for_clean_working_tree(deps: Deps, phase: str = "merge") -> None
7
7
  if deps.git_svc.is_working_tree_clean(deps.repo_root):
8
8
  return
9
9
  deps.status_display.print(
10
+ "pycastle",
10
11
  "[red]Working tree has uncommitted changes. "
11
12
  f"Please commit or revert all local changes before the {phase} phase can proceed.[/red]",
12
- source="working-tree-dirty",
13
13
  )
14
14
  while not deps.git_svc.is_working_tree_clean(deps.repo_root):
15
15
  await asyncio.sleep(10)
@@ -54,7 +54,7 @@ async def run_issue(
54
54
 
55
55
  result = await _bounded_run_agent(
56
56
  RunRequest(
57
- name=f"Implementer #{issue['number']}",
57
+ name=f"Implement Agent #{issue['number']}",
58
58
  prompt_file=deps.cfg.prompts_dir / "implement-prompt.md",
59
59
  mount_path=deps.repo_root,
60
60
  prompt_args=prompt_args,
@@ -74,11 +74,11 @@ async def run_issue(
74
74
  return result
75
75
 
76
76
  assert_complete(result)
77
- deps.logger.log_agent_output(f"Implementer #{issue['number']}", result)
77
+ deps.logger.log_agent_output(f"Implement Agent #{issue['number']}", result)
78
78
 
79
79
  review_result = await _bounded_run_agent(
80
80
  RunRequest(
81
- name=f"Reviewer #{issue['number']}",
81
+ name=f"Review Agent #{issue['number']}",
82
82
  prompt_file=deps.cfg.prompts_dir / "review-prompt.md",
83
83
  mount_path=deps.repo_root,
84
84
  prompt_args=prompt_args,
@@ -0,0 +1,111 @@
1
+ import dataclasses
2
+ import sys
3
+
4
+ from ..agent_output_protocol import assert_complete
5
+ from ..agent_result import PreflightFailure
6
+ from ..agent_runner import RunRequest
7
+ from ..services import GitCommandError
8
+ from ..worktree import branch_worktree, worktree_name_for_branch, worktree_path
9
+ from ._deps import Deps
10
+ from ._utils import _wait_for_clean_working_tree
11
+ from .implement import branch_for
12
+
13
+ MERGE_SANDBOX = "pycastle/merge-sandbox"
14
+
15
+
16
+ @dataclasses.dataclass
17
+ class MergeResult:
18
+ clean: list[dict]
19
+ conflicts: list[dict]
20
+
21
+
22
+ def _delete_merged_branches(branches: list[str], deps: Deps) -> None:
23
+ registered_worktrees = deps.git_svc.list_worktrees(deps.repo_root)
24
+ for branch in branches:
25
+ if not deps.git_svc.is_ancestor(branch, deps.repo_root):
26
+ continue
27
+ worktree_path_ = worktree_path(worktree_name_for_branch(branch), deps)
28
+ if worktree_path_ in registered_worktrees:
29
+ try:
30
+ deps.git_svc.remove_worktree(deps.repo_root, worktree_path_)
31
+ except Exception as e:
32
+ print(
33
+ f"Warning: could not remove worktree for {branch!r}: {e}",
34
+ file=sys.stderr,
35
+ )
36
+
37
+ try:
38
+ deps.git_svc.delete_branch(branch, deps.repo_root)
39
+ deps.status_display.print("pycastle", f"Deleted merged branch: {branch}")
40
+ except GitCommandError as e:
41
+ print(f"Warning: could not delete branch {branch!r}: {e}", file=sys.stderr)
42
+
43
+
44
+ async def merge_phase(completed: list[dict], deps: Deps) -> MergeResult:
45
+ deps.status_display.register("Merge", work_body="Merging")
46
+ _merge_row_active = True
47
+ try:
48
+ await _wait_for_clean_working_tree(deps)
49
+
50
+ conflict_issues: list[dict] = []
51
+ for issue in completed:
52
+ if deps.git_svc.try_merge(deps.repo_root, branch_for(issue["number"])):
53
+ deps.github_svc.close_issue(issue["number"])
54
+ else:
55
+ conflict_issues.append(issue)
56
+
57
+ clean_issues = [i for i in completed if i not in conflict_issues]
58
+
59
+ if clean_issues:
60
+ deps.github_svc.close_completed_parent_issues()
61
+
62
+ _delete_merged_branches([branch_for(i["number"]) for i in clean_issues], deps)
63
+
64
+ if not conflict_issues:
65
+ deps.status_display.remove("Merge")
66
+ _merge_row_active = False
67
+ else:
68
+ target_branch = deps.git_svc.get_current_branch(deps.repo_root)
69
+ sha = deps.git_svc.get_head_sha(deps.repo_root)
70
+ async with branch_worktree("merge-sandbox", MERGE_SANDBOX, sha, deps) as sandbox_path:
71
+ deps.status_display.remove("Merge")
72
+ _merge_row_active = False
73
+ merger_result = await deps.agent_runner.run(
74
+ RunRequest(
75
+ name="Merge Agent",
76
+ prompt_file=deps.cfg.prompts_dir / "merge-prompt.md",
77
+ mount_path=sandbox_path,
78
+ prompt_args={
79
+ "BRANCHES": "\n".join(
80
+ f"- {branch_for(i['number'])}" for i in conflict_issues
81
+ ),
82
+ "CHECKS": " && ".join(cmd for _, cmd in deps.cfg.preflight_checks),
83
+ },
84
+ model=deps.cfg.merge_override.model,
85
+ status_display=deps.status_display,
86
+ effort=deps.cfg.merge_override.effort,
87
+ stage="pre-merge",
88
+ work_body=f"Merging {len(conflict_issues)} Branches",
89
+ )
90
+ )
91
+ if isinstance(merger_result, PreflightFailure):
92
+ raise RuntimeError(
93
+ "Merger preflight checks failed; merge did not complete"
94
+ )
95
+ assert_complete(merger_result)
96
+ deps.git_svc.fast_forward_branch(
97
+ deps.repo_root, target_branch, MERGE_SANDBOX
98
+ )
99
+ deps.status_display.print("pycastle", "Branches merged.")
100
+ _delete_merged_branches(
101
+ [branch_for(i["number"]) for i in conflict_issues], deps
102
+ )
103
+ for issue in conflict_issues:
104
+ deps.github_svc.close_issue(issue["number"])
105
+ deps.github_svc.close_completed_parent_issues()
106
+
107
+ return MergeResult(clean=clean_issues, conflicts=conflict_issues)
108
+ except BaseException:
109
+ if _merge_row_active:
110
+ deps.status_display.remove("Merge", shutdown_message="failed", shutdown_style="error")
111
+ raise
@@ -18,7 +18,7 @@ async def planning_phase(deps: Deps, sha: str, open_issues: list[dict]) -> PlanR
18
18
  async with detached_worktree("plan-sandbox", sha, deps) as wt:
19
19
  raw = await deps.agent_runner.run(
20
20
  RunRequest(
21
- name="Planner",
21
+ name="Plan Agent",
22
22
  prompt_file=deps.cfg.prompts_dir / "plan-prompt.md",
23
23
  mount_path=wt,
24
24
  prompt_args={"OPEN_ISSUES_JSON": json.dumps(open_issues)},
@@ -94,9 +94,9 @@ async def preflight_phase(deps: Deps) -> PreflightResult:
94
94
  deps.git_svc.pull(deps.repo_root)
95
95
  except GitCommandError:
96
96
  deps.status_display.print(
97
+ "pycastle",
97
98
  "[red]git pull --ff-only failed — remote branch has diverged or is unreachable. "
98
99
  "Resolve manually and retry.[/red]",
99
- source="preflight-phase",
100
100
  )
101
101
  raise
102
102
  sha = deps.git_svc.get_head_sha(deps.repo_root)
@@ -108,7 +108,7 @@ async def preflight_phase(deps: Deps) -> PreflightResult:
108
108
 
109
109
  async with detached_worktree("pre-flight-sandbox", sha, deps) as wt:
110
110
  failures = await deps.agent_runner.run_preflight(
111
- name="Pre-Flight",
111
+ name="Preflight Agent",
112
112
  mount_path=wt,
113
113
  stage="PREFLIGHT",
114
114
  status_display=deps.status_display,
@@ -129,5 +129,4 @@ async def preflight_phase(deps: Deps) -> PreflightResult:
129
129
  worktree_sha=sha, issues=[{"number": pf_num, "title": pf_title}]
130
130
  )
131
131
 
132
- deps.status_display.print("Preflight checks passed.", source="preflight-phase")
133
132
  return PreflightReady(sha=sha, issues=open_issues)