pycastle 0.1.3.9.dev0__tar.gz → 0.1.3.11.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 (166) hide show
  1. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/CONTEXT.md +20 -10
  2. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/PKG-INFO +1 -1
  3. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/agent_runner.py +151 -151
  4. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/config/loader.py +1 -0
  5. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/defaults/config.py +1 -0
  6. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/defaults/prompts/implement-prompt.md +0 -10
  7. pycastle-0.1.3.11.dev0/src/pycastle/iteration/__init__.py +130 -0
  8. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/iteration/_deps.py +2 -2
  9. pycastle-0.1.3.11.dev0/src/pycastle/iteration/_phase_row.py +30 -0
  10. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/iteration/_utils.py +4 -3
  11. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/iteration/implement.py +27 -19
  12. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/iteration/merge.py +133 -111
  13. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/iteration/preflight.py +1 -1
  14. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/orchestrator.py +2 -2
  15. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/rich_status_display.py +20 -31
  16. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/services/git_service.py +18 -0
  17. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/status_display.py +42 -52
  18. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/worktree.py +15 -4
  19. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle.egg-info/PKG-INFO +1 -1
  20. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle.egg-info/SOURCES.txt +2 -0
  21. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_agent_runner.py +1 -1
  22. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_config_new.py +15 -0
  23. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_git_service.py +96 -0
  24. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_implement.py +89 -0
  25. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_iteration.py +570 -18
  26. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_merge.py +282 -38
  27. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_orchestrator.py +23 -0
  28. pycastle-0.1.3.11.dev0/tests/test_phase_row.py +50 -0
  29. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_plain_status_display.py +108 -29
  30. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_status_display.py +147 -47
  31. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_worktree.py +47 -0
  32. pycastle-0.1.3.9.dev0/src/pycastle/iteration/__init__.py +0 -120
  33. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/.github/workflows/publish.yml +0 -0
  34. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/.gitignore +0 -0
  35. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/.python-version +0 -0
  36. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/CLAUDE.md +0 -0
  37. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/LICENSE +0 -0
  38. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/README.md +0 -0
  39. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/docs/adr/0001-runtime-dependency-installation.md +0 -0
  40. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/docs/agents/domain.md +0 -0
  41. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/docs/agents/issue-tracker.md +0 -0
  42. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/docs/agents/triage-labels.md +0 -0
  43. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/pyproject.toml +0 -0
  44. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/setup.cfg +0 -0
  45. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__init__.py +0 -0
  46. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/__init__.cpython-311.pyc +0 -0
  47. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/_types.cpython-311.pyc +0 -0
  48. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/agent_output_protocol.cpython-311.pyc +0 -0
  49. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/agent_result.cpython-311.pyc +0 -0
  50. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/agent_runner.cpython-311.pyc +0 -0
  51. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/build_command.cpython-311.pyc +0 -0
  52. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/container_runner.cpython-311.pyc +0 -0
  53. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/errors.cpython-311.pyc +0 -0
  54. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/init_command.cpython-311.pyc +0 -0
  55. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/labels.cpython-311.pyc +0 -0
  56. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/main.cpython-311.pyc +0 -0
  57. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/orchestrator.cpython-311.pyc +0 -0
  58. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/prompt_pipeline.cpython-311.pyc +0 -0
  59. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/prompt_utils.cpython-311.pyc +0 -0
  60. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/rich_status_display.cpython-311.pyc +0 -0
  61. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/stream_parser.cpython-311.pyc +0 -0
  62. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/__pycache__/worktree.cpython-311.pyc +0 -0
  63. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/_types.py +0 -0
  64. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/agent_output_protocol.py +0 -0
  65. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/agent_result.py +0 -0
  66. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/build_command.py +0 -0
  67. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/config/__init__.py +0 -0
  68. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/config/__pycache__/__init__.cpython-311.pyc +0 -0
  69. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/config/__pycache__/loader.cpython-311.pyc +0 -0
  70. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/config/__pycache__/validator.cpython-311.pyc +0 -0
  71. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/config/validator.py +0 -0
  72. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/container_runner.py +0 -0
  73. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/defaults/.gitignore +0 -0
  74. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/defaults/Dockerfile +0 -0
  75. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/defaults/prompts/coding-standards/deep-modules.md +0 -0
  76. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/defaults/prompts/coding-standards/interfaces.md +0 -0
  77. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/defaults/prompts/coding-standards/mocking.md +0 -0
  78. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/defaults/prompts/coding-standards/refactoring.md +0 -0
  79. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/defaults/prompts/coding-standards/tests.md +0 -0
  80. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/defaults/prompts/merge-prompt.md +0 -0
  81. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/defaults/prompts/plan-prompt.md +0 -0
  82. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/defaults/prompts/preflight-issue.md +0 -0
  83. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/defaults/prompts/review-prompt.md +0 -0
  84. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/errors.py +0 -0
  85. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/init_command.py +0 -0
  86. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/iteration/__pycache__/__init__.cpython-311.pyc +0 -0
  87. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/iteration/__pycache__/_deps.cpython-311.pyc +0 -0
  88. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/iteration/__pycache__/_utils.cpython-311.pyc +0 -0
  89. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/iteration/__pycache__/implement.cpython-311.pyc +0 -0
  90. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/iteration/__pycache__/merge.cpython-311.pyc +0 -0
  91. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/iteration/__pycache__/planning.cpython-311.pyc +0 -0
  92. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/iteration/__pycache__/preflight.cpython-311.pyc +0 -0
  93. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/iteration/planning.py +0 -0
  94. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/labels.py +0 -0
  95. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/main.py +0 -0
  96. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/prompt_pipeline.py +0 -0
  97. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/prompt_utils.py +0 -0
  98. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/services/__init__.py +0 -0
  99. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/services/__pycache__/__init__.cpython-311.pyc +0 -0
  100. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/services/__pycache__/_base.cpython-311.pyc +0 -0
  101. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/services/__pycache__/claude_service.cpython-311.pyc +0 -0
  102. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/services/__pycache__/docker_service.cpython-311.pyc +0 -0
  103. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/services/__pycache__/git_service.cpython-311.pyc +0 -0
  104. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/services/__pycache__/github_service.cpython-311.pyc +0 -0
  105. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/services/_base.py +0 -0
  106. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/services/claude_service.py +0 -0
  107. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/services/docker_service.py +0 -0
  108. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle/services/github_service.py +0 -0
  109. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle.egg-info/dependency_links.txt +0 -0
  110. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle.egg-info/entry_points.txt +0 -0
  111. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle.egg-info/requires.txt +0 -0
  112. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/src/pycastle.egg-info/top_level.txt +0 -0
  113. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__init__.py +0 -0
  114. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/__init__.cpython-311.pyc +0 -0
  115. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/conftest.cpython-311-pytest-9.0.3.pyc +0 -0
  116. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_agent_output_protocol.cpython-311-pytest-9.0.3.pyc +0 -0
  117. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_agent_result.cpython-311-pytest-9.0.3.pyc +0 -0
  118. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_agent_runner.cpython-311-pytest-9.0.3.pyc +0 -0
  119. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_build_command.cpython-311-pytest-9.0.3.pyc +0 -0
  120. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_claude_service.cpython-311-pytest-9.0.3.pyc +0 -0
  121. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_config_new.cpython-311-pytest-9.0.3.pyc +0 -0
  122. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_container_runner.cpython-311-pytest-9.0.3.pyc +0 -0
  123. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_default_prompts.cpython-311-pytest-9.0.3.pyc +0 -0
  124. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_deps.cpython-311-pytest-9.0.3.pyc +0 -0
  125. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_docker_service.cpython-311-pytest-9.0.3.pyc +0 -0
  126. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_errors.cpython-311-pytest-9.0.3.pyc +0 -0
  127. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_git_service.cpython-311-pytest-9.0.3.pyc +0 -0
  128. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_github_service.cpython-311-pytest-9.0.3.pyc +0 -0
  129. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_implement.cpython-311-pytest-9.0.3.pyc +0 -0
  130. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_init_command.cpython-311-pytest-9.0.3.pyc +0 -0
  131. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_integration.cpython-311-pytest-9.0.3.pyc +0 -0
  132. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_iteration.cpython-311-pytest-9.0.3.pyc +0 -0
  133. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_labels.cpython-311-pytest-9.0.3.pyc +0 -0
  134. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_main.cpython-311-pytest-9.0.3.pyc +0 -0
  135. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_merge.cpython-311-pytest-9.0.3.pyc +0 -0
  136. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_orchestrator.cpython-311-pytest-9.0.3.pyc +0 -0
  137. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_plan.cpython-311-pytest-9.0.3.pyc +0 -0
  138. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_planning.cpython-311-pytest-9.0.3.pyc +0 -0
  139. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_preflight.cpython-311-pytest-9.0.3.pyc +0 -0
  140. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_prompt_pipeline.cpython-311-pytest-9.0.3.pyc +0 -0
  141. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_prompt_utils.cpython-311-pytest-9.0.3.pyc +0 -0
  142. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_status_display.cpython-311-pytest-9.0.3.pyc +0 -0
  143. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_stream_parser.cpython-311-pytest-9.0.3.pyc +0 -0
  144. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_subprocess_service.cpython-311-pytest-9.0.3.pyc +0 -0
  145. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/__pycache__/test_worktree.cpython-311-pytest-9.0.3.pyc +0 -0
  146. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/conftest.py +0 -0
  147. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_agent_output_protocol.py +0 -0
  148. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_agent_result.py +0 -0
  149. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_build_command.py +0 -0
  150. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_claude_service.py +0 -0
  151. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_container_runner.py +0 -0
  152. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_default_prompts.py +0 -0
  153. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_deps.py +0 -0
  154. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_docker_service.py +0 -0
  155. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_errors.py +0 -0
  156. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_github_service.py +0 -0
  157. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_init_command.py +0 -0
  158. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_integration.py +0 -0
  159. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_labels.py +0 -0
  160. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_main.py +0 -0
  161. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_plan.py +0 -0
  162. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_planning.py +0 -0
  163. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_preflight.py +0 -0
  164. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_prompt_pipeline.py +0 -0
  165. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_prompt_utils.py +0 -0
  166. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.11.dev0}/tests/test_subprocess_service.py +0 -0
@@ -37,6 +37,7 @@
37
37
  | **CLI default** | The behavior when no `--model` or `--effort` flag is injected — triggered by an empty string in STAGE_OVERRIDES | default model, unset |
38
38
  | **validate_config** | Public function in the config validator module; takes a `Config` and a `ClaudeService`, resolves model shorthands to full model IDs, validates all stage overrides, and returns a new immutable `Config`; raises `ConfigValidationError` on any invalid entry | config validation, startup check |
39
39
  | **ConfigValidationError** | Error raised by validate_config when a model shorthand or effort level is unrecognised; includes the invalid value, closest valid suggestion, and full list of valid options | validation error, config error |
40
+ | **auto_push** | Boolean config entry (default `True`) that controls whether `merge_phase` pushes local main to the remote after any merges produce commits; set to `False` to disable automatic pushing | push_after_merge, AUTO_PUSH |
40
41
 
41
42
  ## GitHub Integration
42
43
 
@@ -73,6 +74,12 @@
73
74
  | **clean merge** | A `git merge --no-edit` that exits zero and requires no conflict resolution | conflict-free merge, successful merge |
74
75
  | **conflicting branch** | A branch whose `git merge --no-edit` exits non-zero; `git merge --abort` is run immediately and the branch is collected for the Merger | failed merge branch |
75
76
  | **RALPH** | The required commit message prefix for all Implementer commits (e.g. `RALPH: fix auth bug`) | — |
77
+ | **RALPH: Review -** | The required commit message prefix for all Reviewer commits (e.g. `RALPH: Review - improve error handling`); distinguished from Implementer commits by the `Review -` infix; each agent produces exactly one commit per branch | — |
78
+ | **in-flight issue** | An open issue that has an existing `pycastle/issue-<n>` branch or worktree from a previous interrupted iteration; signals that implement or review work is already partially or fully complete | mid-flight issue, resumed issue |
79
+ | **merge-time preflight skip** | The behavior when the Merger's Pre-flight phase returns failures: `merge_phase` logs a diagnostic, skips the Merger, and returns normally with conflict issues still pending; the next iteration's pre-planning preflight detects the broken baseline and recovers via the preflight-fix path | merge preflight abort |
80
+ | **planning skip** | The behavior in `run_iteration` when at least one open issue is in-flight: the Planner is not invoked and only the in-flight issues are used as the working set for the current iteration; issues with neither a branch nor a worktree are deferred | plan bypass |
81
+ | **implement skip** | The behavior in `run_issue` when a branch already has a `RALPH:` (non-review) commit: the Implementer is not spawned and the Reviewer runs directly via the existing-branch path | — |
82
+ | **review skip** | The behavior in `run_issue` when a branch already has a `RALPH: Review -` commit: both the Implementer and Reviewer are skipped and the issue is counted as completed immediately | — |
76
83
  | **plan** | The structured JSON output by the Planner listing which issues to work on and the branch name for each; after parsing, `plan_phase()` sorts issues by ascending issue number so the orchestrator always processes older issues first | plan output, plan JSON |
77
84
  | **issue** | A GitHub issue labeled for agent processing, representing one unit of work | ticket, task, card |
78
85
  | **AFK issue** | An issue the Planner assigns to an Implementer because it can be resolved autonomously; labeled `ready-for-agent` | agent issue, auto issue |
@@ -85,7 +92,7 @@
85
92
  | **merge-sandbox worktree** | A temporary named-branch worktree (`pycastle/merge-sandbox`) created by `merge_phase` from HEAD after clean merges complete; the Merger runs inside it to resolve conflicting branches; always removed in a `try/finally` by `merge_phase` regardless of state; on success `merge_phase` fast-forwards `main` from the branch before cleanup; located at `.pycastle/.worktrees/merge-sandbox` | merger worktree, conflict worktree |
86
93
  | **branch** | A git branch name assigned to an issue inside the plan; follows the pattern `pycastle/issue-<n>-<slug>` | feature branch, issue branch |
87
94
  | **orphan worktree** | A worktree directory under `.pycastle/.worktrees/` no longer registered in git, typically left by a crashed agent run | stale worktree, leftover worktree |
88
- | **orphan sweep** | Startup operation that cross-references `.pycastle/.worktrees/` against `git worktree list --porcelain` and deletes unregistered directories | worktree cleanup, stale cleanup |
95
+ | **orphan sweep** | Startup operation that cross-references `.pycastle/.worktrees/` against `git worktree list --porcelain`, deletes unregistered directories, and removes the `.worktrees` parent directory if no active children remain | worktree cleanup, stale cleanup |
89
96
  | **collision detection** | Mechanism that prevents two parallel agents from simultaneously creating worktrees for the same branch, implemented as a per-branch async lock | — |
90
97
 
91
98
  ## Prompts
@@ -156,8 +163,8 @@
156
163
  | **new-branch path** | The `git worktree add -b <branch> <path> <safe-SHA>` form used when the branch does not yet exist; always branched from the pinned safe SHA rather than HEAD | — |
157
164
  | **existing-branch path** | The `git worktree add <path> <branch>` form used when the branch already exists | — |
158
165
  | **worktree contents check** | Guard step run after `git worktree add` that verifies `pyproject.toml` or `requirements.txt` is present; fails with the worktree path and directory listing if absent | checkout guard, file check |
159
- | **`detached_worktree`** | Async context manager in `worktree.py` that creates a detached checkout at a given SHA, yields the path, and guarantees removal in `__aexit__` regardless of outcome; used by `planning_phase` and `preflight_phase` for their sandbox worktrees | managed_worktree |
160
- | **`branch_worktree`** | Async context manager in `worktree.py` that creates a named-branch worktree at a given SHA, yields the path, and on exit removes the worktree and optionally deletes the branch; used by `merge_phase` for the merge-sandbox worktree | managed_worktree |
166
+ | **`detached_worktree`** | Async context manager in `worktree.py` that creates a detached checkout at a given SHA, yields the path, and guarantees removal in `__aexit__` regardless of outcome; also removes the `.worktrees` parent directory if no other worktrees remain after cleanup; used by `planning_phase` and `preflight_phase` for their sandbox worktrees | managed_worktree |
167
+ | **`branch_worktree`** | Async context manager in `worktree.py` that creates a named-branch worktree at a given SHA, yields the path, and on exit removes the worktree, optionally deletes the branch, and removes the `.worktrees` parent directory if no other worktrees remain; used by `merge_phase` for the merge-sandbox worktree | managed_worktree |
161
168
  | **`_agent_worktree`** | Async context manager in `implement.py` that owns the full Implementer and Reviewer worktree lifecycle; accepts a branch name, SHA, `CancellationToken`, and `Deps`; on entry creates the worktree and gitdir overlay; on exit conditionally removes the worktree based on `token.wants_worktree_preserved` and working-tree cleanliness, and always removes the gitdir overlay; used by `run_issue` twice per issue — once for the Implementer (new-branch path) and once for the Reviewer (existing-branch path); defined in `implement.py` not `worktree.py` because its cleanup policy depends on agent-lifecycle state (`CancellationToken`) rather than being unconditional | managed_worktree |
162
169
  | **`worktree_name_for_branch`** | Function in `worktree.py` that derives a short directory name from a branch string: extracts `issue-N` from `pycastle/issue-N-slug` or falls back to a sanitised slug; single authoritative definition replacing duplicated regex in `agent_runner` and `merge_phase` | — |
163
170
  | **`worktree_path`** | Function in `worktree.py` that constructs the host filesystem path for a named worktree at `<repo_root>/<pycastle_dir>/.worktrees/<name>`; single authoritative path expression replacing duplication across all phase modules | — |
@@ -187,11 +194,12 @@
187
194
  | **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 |
188
195
  | **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 |
189
196
  | **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 |
190
- | **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 |
191
- | **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 any output call (`register`, `remove`, or `print`) when the caller differs from the previous one, or unconditionally when the caller is `""` (anonymous outputs always stand alone); canonical callers — phase rows: `"Preflight"`, `"Plan"`, `"Implement"`, `"Merge"`; agents: `"Preflight Agent"`, `"Plan Agent"`, `"Implement Agent #N"`, `"Review Agent #N"`, `"Merge Agent"` | source, label |
192
- | **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 | — |
193
- | **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, no bold, and style ignored; used in tests so assertions can match the full formatted line | NullStatusDisplay |
194
- | **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 |
197
+ | **StatusDisplay** | Injectable abstraction that owns the live terminal status panel and all formatted terminal output; exposes `register(caller, startup_message="started", work_body="", initial_phase="Setup")`, `update_phase`, `reset_idle_timer`, `remove(caller, shutdown_message="finished", shutdown_style="success")`, and `print(caller, message, style=None)` methods; `shutdown_message` and `message` may contain `\n` — each line is emitted separately with the `[Caller]` prefix and the same style applied to every line; `shutdown_style` accepts `"success"` (green), `"error"` (red), or `"warning"` (yellow); 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 |
198
+ | **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 any output call (`register`, `remove`, or `print`) when the caller differs from the previous one, unconditionally when the caller is `""` (anonymous outputs always stand alone), or before the very first output call (when no previous call has occurred); canonical callers — phase rows: `"Preflight"`, `"Plan"`, `"Implement"`, `"Merge"`; agents: `"Preflight Agent"`, `"Plan Agent"`, `"Implement Agent #N"`, `"Review Agent #N"`, `"Merge Agent"` | source, label |
199
+ | **work_body** | The caller-constructed string passed as the third argument to `register`; applies to agent rows only; displayed in the body column during the Work phase; empty string for agent rows that do not reach Work; unused by phase rows (which use `initial_phase` for their fixed body label) | — |
200
+ | **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, no bold, and style ignored; multi-line messages are split and each line prefixed with `[Caller]`; used in tests so assertions can match the full formatted line | NullStatusDisplay |
201
+ | **phase_row** | Async context manager in `iteration/` that owns the `StatusDisplay` register/remove lifecycle for a single phase row; on entry calls `register(caller)`; yields a `PhaseRow` whose `close(shutdown_message, shutdown_style="success")` method calls `remove()` and marks the row as closed; if `close()` is never called before exit (exception path), automatically calls `remove(caller, "failed", shutdown_style="error")`; the canonical way to manage phase row lifecycle replaces hand-rolled active-flag patterns | |
202
+ | **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 column: for **agent rows**, shows the current agent lifecycle phase name for all non-Work states, or `work_body` during Work; for **phase rows**, shows the fixed `initial_phase` label passed at registration (`"Running"` for Preflight and Implement, `"Planning"` for Plan, `"Merging"` for Merge); 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 |
195
203
  | **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; orchestrator sleeps until 2 minutes past the next local-time full hour, then continues the loop to retry the current issue from scratch; repeats indefinitely on consecutive hits) | iteration result, loop result |
196
204
 
197
205
  ## Test Anti-Patterns (Red Flags)
@@ -216,6 +224,8 @@
216
224
  - The **HITL verdict** is read by the orchestrator from the GitHub issue label after the **preflight-issue agent** completes; `ready-for-agent` triggers the **preflight-fix path**, `ready-for-human` aborts with a non-zero exit code.
217
225
  - On the **preflight-fix path**, the Planner is skipped; one Implementer is spawned for the preflight issue, followed by one Reviewer, then a merge; a new iteration then begins.
218
226
  - The **Planner** and all **Implementer** worktrees are created from the pinned **safe SHA**, never from HEAD directly; this guarantees every agent sees the same verified-clean committed state regardless of external commits that land on main after preflight passes.
227
+ - The **planning skip** is checked before every Planner invocation; it takes priority over normal planning when any open issue is **in-flight**. The **implement skip** and **review skip** are checked inside `run_issue` before any worktree is created; they are mutually exclusive with normal agent spawning for that phase. Both skips are triggered by commit prefix detection (`RALPH: Review -` → review skip; `RALPH:` without `Review -` → implement skip only).
228
+ - A **merge-time preflight skip** leaves conflict issues open; they become **in-flight issues** on the next iteration, triggering the **planning skip** and then the **implement skip** or **review skip** as appropriate once the baseline is fixed.
219
229
  - In **sequential mode** (`max_parallel = 1`), the iteration processes issues one by one: after each issue's merge the safe SHA is re-pinned to the new HEAD, and the next Implementer starts from that SHA; a failed issue is skipped (remains `ready-for-agent`) and the queue continues; the Merger remains available as a fallback for unexpected conflicts; no additional pre-flight checks run between issues.
220
230
  - The **Pre-flight phase** (agent lifecycle) runs quality checks inside the container and returns a list of failure tuples to the orchestrator; it never spawns agents internally.
221
231
  - An **orphan sweep** runs once at orchestrator startup; **collision detection** uses a per-branch `asyncio.Lock` held in `implement_phase` for the full duration of each `run_issue` call — from first worktree creation to final worktree teardown.
@@ -223,8 +233,8 @@
223
233
  - 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`.
224
234
  - 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.
225
235
  - **StatusDisplay** is a separate injectable in `Deps` alongside `Logger`; `Logger` owns file I/O, `StatusDisplay` owns the live terminal UI — they never overlap.
226
- - Rich markup (e.g. `[red]...[/red]`) must never be embedded in a `StatusDisplay.print` message string; colouring is expressed exclusively via the `style` parameter (`"error"`, `"success"`).
227
- - 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.
236
+ - Rich markup (e.g. `[red]...[/red]`) must never be embedded in a `StatusDisplay.print` message string; colouring is expressed exclusively via the `style` parameter (`"error"`, `"success"`, `"warning"`).
237
+ - A **status row** is created by `StatusDisplay.register` and removed by `StatusDisplay.remove`; phase rows are managed via the **`phase_row`** context manager registered on entry and removed (with the phase outcome as the shutdown message) via `PhaseRow.close()`; 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.
228
238
  - 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.
229
239
  - During the Work phase the container runner owns byte chunking, byte-to-line splitting, log writing, and idle timeout detection; it passes the decoded NDJSON line stream and an **`on_turn` callback** to **`process_stream`**, which assembles assistant turns (invoking the callback for each), detects usage limit lines and raises `UsageLimitError` immediately, unwraps the result envelope, and returns a typed `AgentOutput`; phases receive `AgentOutput` directly from `AgentRunner.run()` — no phase calls `parse()` or `assert_complete()`. Setup, Pre-flight, and Prepare phases produce no console output — their activity is reflected only in the body column of the agent status row.
230
240
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycastle
3
- Version: 0.1.3.9.dev0
3
+ Version: 0.1.3.11.dev0
4
4
  Summary: Python orchestrator for autonomous Claude Code agents in Docker
5
5
  License: MIT License
6
6
 
@@ -1,151 +1,151 @@
1
- import dataclasses
2
- from pathlib import Path
3
- from typing import Any, Protocol
4
-
5
- from .agent_output_protocol import AgentOutput, AgentRole
6
- from .agent_result import CancellationToken, PreflightFailure
7
- from .config import Config
8
- from .container_runner import ContainerRunner
9
- from .errors import AgentTimeoutError, UsageLimitError
10
- from .services import GitService
11
- from .status_display import PlainStatusDisplay
12
-
13
-
14
- @dataclasses.dataclass
15
- class RunRequest:
16
- name: str
17
- prompt_file: Path
18
- mount_path: Path
19
- role: AgentRole = AgentRole.IMPLEMENTER
20
- prompt_args: dict[str, str] | None = None
21
- skip_preflight: bool = False
22
- model: str = ""
23
- effort: str = ""
24
- stage: str = ""
25
- token: CancellationToken | None = None
26
- status_display: Any = None
27
- issue_title: str = ""
28
- work_body: str = ""
29
-
30
-
31
- class AgentRunnerProtocol(Protocol):
32
- async def run(self, request: RunRequest) -> AgentOutput | PreflightFailure: ...
33
-
34
- async def run_preflight(
35
- self,
36
- *,
37
- name: str,
38
- mount_path: Path,
39
- stage: str = "",
40
- status_display=None,
41
- work_body: str = "",
42
- ) -> list[tuple[str, str, str]]: ...
43
-
44
-
45
- class AgentRunner:
46
- def __init__(
47
- self,
48
- env: dict[str, str],
49
- cfg: Config,
50
- git_service: GitService,
51
- docker_client=None,
52
- ) -> None:
53
- self._env = env
54
- self._cfg = cfg
55
- self._git_service = git_service
56
- self._docker_client = docker_client
57
-
58
- async def run(self, request: RunRequest) -> AgentOutput | PreflightFailure:
59
- name = request.name
60
- prompt_file = request.prompt_file
61
- mount_path = request.mount_path
62
- prompt_args = request.prompt_args
63
- skip_preflight = request.skip_preflight
64
- model = request.model
65
- effort = request.effort
66
- token = request.token
67
- status_display = request.status_display
68
- work_body = request.work_body
69
-
70
- if status_display is None:
71
- status_display = PlainStatusDisplay()
72
-
73
- _token = token if token is not None else CancellationToken()
74
- if _token.is_cancelled:
75
- raise UsageLimitError("Agent cancelled due to usage limit")
76
-
77
- runner = ContainerRunner(
78
- name,
79
- mount_path,
80
- self._env,
81
- model=model,
82
- effort=effort,
83
- docker_client=self._docker_client,
84
- status_display=status_display,
85
- cfg=self._cfg,
86
- )
87
- try:
88
- git_name = self._git_service.get_user_name()
89
- git_email = self._git_service.get_user_email()
90
- await runner.setup(git_name, git_email, work_body)
91
- await runner.prepare(prompt_file, prompt_args or {})
92
- if not skip_preflight:
93
- failures = await runner.preflight(list(self._cfg.preflight_checks))
94
- if failures:
95
- return PreflightFailure(failures=tuple(failures))
96
- retries_left = self._cfg.timeout_retries
97
- while True:
98
- try:
99
- output = await runner.work(request.role)
100
- return output
101
- except AgentTimeoutError:
102
- if retries_left <= 0:
103
- raise
104
- restart_num = self._cfg.timeout_retries - retries_left + 1
105
- status_display.print(
106
- name,
107
- f"Timeout — restarting"
108
- f" (attempt {restart_num}/{self._cfg.timeout_retries})",
109
- )
110
- retries_left -= 1
111
- except UsageLimitError:
112
- _token.cancel(preserve_worktree=True)
113
- raise
114
- finally:
115
- status_display.remove(name)
116
- try:
117
- runner.__exit__(None, None, None)
118
- except Exception:
119
- pass
120
-
121
- async def run_preflight(
122
- self,
123
- *,
124
- name: str,
125
- mount_path: Path,
126
- stage: str = "",
127
- status_display=None,
128
- work_body: str = "",
129
- ) -> list[tuple[str, str, str]]:
130
- if status_display is None:
131
- status_display = PlainStatusDisplay()
132
-
133
- git_name = self._git_service.get_user_name()
134
- git_email = self._git_service.get_user_email()
135
- runner = ContainerRunner(
136
- name,
137
- mount_path,
138
- self._env,
139
- docker_client=self._docker_client,
140
- status_display=status_display,
141
- cfg=self._cfg,
142
- )
143
- try:
144
- await runner.setup(git_name, git_email, work_body)
145
- return await runner.preflight(list(self._cfg.preflight_checks))
146
- finally:
147
- status_display.remove(name)
148
- try:
149
- runner.__exit__(None, None, None)
150
- except Exception:
151
- pass
1
+ import dataclasses
2
+ from pathlib import Path
3
+ from typing import Any, Protocol
4
+
5
+ from .agent_output_protocol import AgentOutput, AgentRole
6
+ from .agent_result import CancellationToken, PreflightFailure
7
+ from .config import Config
8
+ from .container_runner import ContainerRunner
9
+ from .errors import AgentTimeoutError, UsageLimitError
10
+ from .services import GitService
11
+ from .status_display import PlainStatusDisplay
12
+
13
+
14
+ @dataclasses.dataclass
15
+ class RunRequest:
16
+ name: str
17
+ prompt_file: Path
18
+ mount_path: Path
19
+ role: AgentRole = AgentRole.IMPLEMENTER
20
+ prompt_args: dict[str, str] | None = None
21
+ skip_preflight: bool = False
22
+ model: str = ""
23
+ effort: str = ""
24
+ stage: str = ""
25
+ token: CancellationToken | None = None
26
+ status_display: Any = None
27
+ issue_title: str = ""
28
+ work_body: str = ""
29
+
30
+
31
+ class AgentRunnerProtocol(Protocol):
32
+ async def run(self, request: RunRequest) -> AgentOutput | PreflightFailure: ...
33
+
34
+ async def run_preflight(
35
+ self,
36
+ *,
37
+ name: str,
38
+ mount_path: Path,
39
+ stage: str = "",
40
+ status_display=None,
41
+ work_body: str = "",
42
+ ) -> list[tuple[str, str, str]]: ...
43
+
44
+
45
+ class AgentRunner:
46
+ def __init__(
47
+ self,
48
+ env: dict[str, str],
49
+ cfg: Config,
50
+ git_service: GitService,
51
+ docker_client=None,
52
+ ) -> None:
53
+ self._env = env
54
+ self._cfg = cfg
55
+ self._git_service = git_service
56
+ self._docker_client = docker_client
57
+
58
+ async def run(self, request: RunRequest) -> AgentOutput | PreflightFailure:
59
+ name = request.name
60
+ prompt_file = request.prompt_file
61
+ mount_path = request.mount_path
62
+ prompt_args = request.prompt_args
63
+ skip_preflight = request.skip_preflight
64
+ model = request.model
65
+ effort = request.effort
66
+ token = request.token
67
+ status_display = request.status_display
68
+ work_body = request.work_body
69
+
70
+ if status_display is None:
71
+ status_display = PlainStatusDisplay()
72
+
73
+ _token = token if token is not None else CancellationToken()
74
+ if _token.is_cancelled:
75
+ raise UsageLimitError("Agent cancelled due to usage limit")
76
+
77
+ runner = ContainerRunner(
78
+ name,
79
+ mount_path,
80
+ self._env,
81
+ model=model,
82
+ effort=effort,
83
+ docker_client=self._docker_client,
84
+ status_display=status_display,
85
+ cfg=self._cfg,
86
+ )
87
+ try:
88
+ git_name = self._git_service.get_user_name()
89
+ git_email = self._git_service.get_user_email()
90
+ await runner.setup(git_name, git_email, work_body)
91
+ await runner.prepare(prompt_file, prompt_args or {})
92
+ if not skip_preflight:
93
+ failures = await runner.preflight(list(self._cfg.preflight_checks))
94
+ if failures:
95
+ return PreflightFailure(failures=tuple(failures))
96
+ retries_left = self._cfg.timeout_retries
97
+ while True:
98
+ try:
99
+ output = await runner.work(request.role)
100
+ return output
101
+ except AgentTimeoutError:
102
+ if retries_left <= 0:
103
+ raise
104
+ restart_num = self._cfg.timeout_retries - retries_left + 1
105
+ status_display.print(
106
+ name,
107
+ f"Timeout — restarting"
108
+ f" (attempt {restart_num}/{self._cfg.timeout_retries})",
109
+ )
110
+ retries_left -= 1
111
+ except UsageLimitError:
112
+ _token.cancel(preserve_worktree=True)
113
+ raise
114
+ finally:
115
+ status_display.remove(name)
116
+ try:
117
+ runner.__exit__(None, None, None)
118
+ except Exception:
119
+ pass
120
+
121
+ async def run_preflight(
122
+ self,
123
+ *,
124
+ name: str,
125
+ mount_path: Path,
126
+ stage: str = "",
127
+ status_display=None,
128
+ work_body: str = "",
129
+ ) -> list[tuple[str, str, str]]:
130
+ if status_display is None:
131
+ status_display = PlainStatusDisplay()
132
+
133
+ git_name = self._git_service.get_user_name()
134
+ git_email = self._git_service.get_user_email()
135
+ runner = ContainerRunner(
136
+ name,
137
+ mount_path,
138
+ self._env,
139
+ docker_client=self._docker_client,
140
+ status_display=status_display,
141
+ cfg=self._cfg,
142
+ )
143
+ try:
144
+ await runner.setup(git_name, git_email, work_body)
145
+ return await runner.preflight(list(self._cfg.preflight_checks))
146
+ finally:
147
+ status_display.remove(name)
148
+ try:
149
+ runner.__exit__(None, None, None)
150
+ except Exception:
151
+ pass
@@ -46,6 +46,7 @@ class Config:
46
46
  "pytest",
47
47
  )
48
48
  )
49
+ auto_push: bool = True
49
50
  timeout_retries: int = 1
50
51
  usage_limit_patterns: tuple[str, ...] = dataclasses.field(
51
52
  default_factory=lambda: ("You've hit your", "Credit balance is too low")
@@ -5,6 +5,7 @@ from pycastle import StageOverride
5
5
  # max_parallel = 1
6
6
  # worktree_timeout = 30
7
7
  # idle_timeout = 300
8
+ # auto_push = True
8
9
 
9
10
  # --- Docker ---
10
11
  # Name for the Docker image built by `pycastle build`.
@@ -1,15 +1,5 @@
1
1
  # Workflow
2
2
 
3
- ### 0. Prior Run Detection
4
-
5
- Before starting, check whether prior RALPH work exists on this branch.
6
-
7
- Run `git log main..HEAD --oneline`. If any commits are present, prior RALPH work is already done — emit `<promise>COMPLETE</promise>` and stop.
8
-
9
- Otherwise, run `git status`. If the working tree is dirty, review the existing uncommitted changes and continue from the current state rather than starting over.
10
-
11
- If both checks show a clean, empty branch, fall through to step 1 and proceed normally.
12
-
13
3
  ### 1. Task
14
4
 
15
5
  Fix issue #{{ISSUE_NUMBER}}: {{ISSUE_TITLE}}
@@ -0,0 +1,130 @@
1
+ import dataclasses
2
+ from typing import TypeAlias
3
+
4
+ from ..agent_result import CancellationToken, PreflightFailure
5
+ from ..worktree import worktree_name_for_branch, worktree_path
6
+ from ._deps import Deps
7
+ from ._phase_row import PhaseRow, phase_row
8
+ from .implement import branch_for, implement_phase
9
+ from .merge import merge_phase
10
+ from .preflight import PreflightAFK, PreflightHITL, PreflightReady, preflight_phase
11
+ from .planning import PlanReady, planning_phase
12
+
13
+
14
+ @dataclasses.dataclass(frozen=True)
15
+ class Continue:
16
+ pass
17
+
18
+
19
+ @dataclasses.dataclass(frozen=True)
20
+ class Done:
21
+ pass
22
+
23
+
24
+ @dataclasses.dataclass(frozen=True)
25
+ class AbortedHITL:
26
+ issue_number: int
27
+
28
+
29
+ @dataclasses.dataclass(frozen=True)
30
+ class AbortedUsageLimit:
31
+ pass
32
+
33
+
34
+ IterationOutcome: TypeAlias = Continue | Done | AbortedHITL | AbortedUsageLimit
35
+
36
+
37
+ def _is_in_flight(issue: dict, deps: Deps) -> bool:
38
+ branch = branch_for(issue["number"])
39
+ if deps.git_svc.verify_ref_exists(branch, deps.repo_root):
40
+ return True
41
+ name = worktree_name_for_branch(branch)
42
+ return worktree_path(name, deps).exists()
43
+
44
+
45
+ async def run_iteration(deps: Deps) -> IterationOutcome:
46
+ deps.status_display.register("Preflight", initial_phase="Running")
47
+ try:
48
+ preflight_result = await preflight_phase(deps)
49
+ finally:
50
+ deps.status_display.remove("Preflight")
51
+
52
+ if isinstance(preflight_result, PreflightHITL):
53
+ deps.status_display.print(
54
+ "Preflight",
55
+ f"Preflight issue #{preflight_result.issue_number} requires human intervention. Exiting.",
56
+ )
57
+ return AbortedHITL(issue_number=preflight_result.issue_number)
58
+
59
+ if isinstance(preflight_result, PreflightReady):
60
+ if not preflight_result.issues:
61
+ return Done()
62
+ sha = preflight_result.sha
63
+ open_issues = preflight_result.issues
64
+ in_flight = [i for i in open_issues if _is_in_flight(i, deps)]
65
+ if in_flight:
66
+ issues = in_flight
67
+ elif len(open_issues) >= 2:
68
+ async with phase_row(deps.status_display, "Plan", initial_phase="Planning") as row:
69
+ plan_result = await planning_phase(deps, sha, open_issues)
70
+ issue_lines = [
71
+ f" #{i['number']}: {i['title']} → {branch_for(i['number'])}"
72
+ for i in plan_result.issues
73
+ ]
74
+ row.close(
75
+ "\n".join(
76
+ [f"Planning complete, {len(plan_result.issues)} issue(s):"] + issue_lines
77
+ )
78
+ )
79
+ sha = plan_result.worktree_sha
80
+ issues = plan_result.issues
81
+ else:
82
+ issues = open_issues
83
+ elif isinstance(preflight_result, PreflightAFK):
84
+ sha = preflight_result.worktree_sha
85
+ issues = preflight_result.issues
86
+
87
+ issues = issues[: deps.cfg.max_parallel]
88
+
89
+ token = CancellationToken()
90
+ async with phase_row(deps.status_display, "Implement", initial_phase="Running") as row:
91
+ impl_result = await implement_phase(issues, sha, deps, token=token)
92
+
93
+ if impl_result.usage_limit_hit:
94
+ row.close("finished")
95
+ return AbortedUsageLimit()
96
+
97
+ for issue, error in impl_result.errors:
98
+ match error:
99
+ case PreflightFailure(failures=fs):
100
+ deps.status_display.print(
101
+ "Implement",
102
+ f" ✗ #{issue['number']} ({branch_for(issue['number'])}) pre-flight failed:",
103
+ )
104
+ for check_name, command, output in fs:
105
+ deps.status_display.print(
106
+ "Implement",
107
+ f" ✗ {check_name} ({command}): {output}",
108
+ )
109
+ case _:
110
+ deps.status_display.print(
111
+ "Implement",
112
+ f" ✗ #{issue['number']} ({branch_for(issue['number'])}) failed: {error}",
113
+ )
114
+
115
+ completed = impl_result.completed
116
+
117
+ if not completed:
118
+ row.close("No commits produced. Nothing to merge.", shutdown_style="warning")
119
+ return Continue()
120
+
121
+ branch_lines = [f" {branch_for(i['number'])}" for i in completed]
122
+ row.close(
123
+ "\n".join(
124
+ [f"Execution complete, {len(completed)} branch(es) with commits:"] + branch_lines
125
+ )
126
+ )
127
+
128
+ await merge_phase(completed, deps)
129
+
130
+ return Continue()
@@ -34,8 +34,8 @@ class RecordingStatusDisplay:
34
34
  def __init__(self) -> None:
35
35
  self.calls: list[tuple] = []
36
36
 
37
- def register(self, caller: str, startup_message: str = "started", work_body: str = "") -> None:
38
- self.calls.append(("register", caller, startup_message, work_body))
37
+ def register(self, caller: str, startup_message: str = "started", work_body: str = "", initial_phase: str = "Setup") -> None:
38
+ self.calls.append(("register", caller, startup_message, initial_phase))
39
39
 
40
40
  def update_phase(self, name: str, phase: str) -> None:
41
41
  self.calls.append(("update_phase", name, phase))
@@ -0,0 +1,30 @@
1
+ from collections.abc import AsyncGenerator
2
+ from contextlib import asynccontextmanager
3
+
4
+ from ..status_display import StatusDisplay
5
+
6
+
7
+ class PhaseRow:
8
+ def __init__(self, status_display: StatusDisplay, caller: str) -> None:
9
+ self._status_display = status_display
10
+ self._caller = caller
11
+ self._closed = False
12
+
13
+ def close(self, shutdown_message: str, shutdown_style: str = "success") -> None:
14
+ if self._closed:
15
+ return
16
+ self._status_display.remove(self._caller, shutdown_message, shutdown_style)
17
+ self._closed = True
18
+
19
+
20
+ @asynccontextmanager
21
+ async def phase_row(
22
+ status_display: StatusDisplay, caller: str, initial_phase: str = "Setup"
23
+ ) -> AsyncGenerator[PhaseRow, None]:
24
+ status_display.register(caller, initial_phase=initial_phase)
25
+ row = PhaseRow(status_display, caller)
26
+ try:
27
+ yield row
28
+ finally:
29
+ if not row._closed:
30
+ status_display.remove(caller, "failed", shutdown_style="error")
@@ -3,13 +3,14 @@ import asyncio
3
3
  from ._deps import Deps
4
4
 
5
5
 
6
- async def _wait_for_clean_working_tree(deps: Deps, phase: str = "merge") -> None:
6
+ async def _wait_for_clean_working_tree(deps: Deps, caller: str, phase: str = "") -> None:
7
7
  if deps.git_svc.is_working_tree_clean(deps.repo_root):
8
8
  return
9
+ phase_name = phase or caller.lower()
9
10
  deps.status_display.print(
10
- "",
11
+ caller,
11
12
  "Working tree has uncommitted changes. "
12
- f"Please commit or revert all local changes before the {phase} phase can proceed.",
13
+ f"Please commit or revert all local changes before the {phase_name} phase can proceed.",
13
14
  style="error",
14
15
  )
15
16
  while not deps.git_svc.is_working_tree_clean(deps.repo_root):