pycastle 0.1.3.9.dev0__tar.gz → 0.1.3.10.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 (163) hide show
  1. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/CONTEXT.md +8 -0
  2. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/PKG-INFO +1 -1
  3. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/implement-prompt.md +0 -10
  4. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/__init__.py +13 -1
  5. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/implement.py +27 -19
  6. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/merge.py +5 -2
  7. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/git_service.py +11 -0
  8. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle.egg-info/PKG-INFO +1 -1
  9. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_git_service.py +47 -0
  10. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_implement.py +89 -0
  11. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_iteration.py +163 -0
  12. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_merge.py +80 -18
  13. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_orchestrator.py +7 -0
  14. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/.github/workflows/publish.yml +0 -0
  15. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/.gitignore +0 -0
  16. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/.python-version +0 -0
  17. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/CLAUDE.md +0 -0
  18. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/LICENSE +0 -0
  19. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/README.md +0 -0
  20. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/docs/adr/0001-runtime-dependency-installation.md +0 -0
  21. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/docs/agents/domain.md +0 -0
  22. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/docs/agents/issue-tracker.md +0 -0
  23. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/docs/agents/triage-labels.md +0 -0
  24. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/pyproject.toml +0 -0
  25. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/setup.cfg +0 -0
  26. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__init__.py +0 -0
  27. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/__init__.cpython-311.pyc +0 -0
  28. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/_types.cpython-311.pyc +0 -0
  29. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/agent_output_protocol.cpython-311.pyc +0 -0
  30. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/agent_result.cpython-311.pyc +0 -0
  31. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/agent_runner.cpython-311.pyc +0 -0
  32. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/build_command.cpython-311.pyc +0 -0
  33. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/container_runner.cpython-311.pyc +0 -0
  34. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/errors.cpython-311.pyc +0 -0
  35. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/init_command.cpython-311.pyc +0 -0
  36. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/labels.cpython-311.pyc +0 -0
  37. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/main.cpython-311.pyc +0 -0
  38. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/orchestrator.cpython-311.pyc +0 -0
  39. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/prompt_pipeline.cpython-311.pyc +0 -0
  40. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/prompt_utils.cpython-311.pyc +0 -0
  41. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/rich_status_display.cpython-311.pyc +0 -0
  42. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/stream_parser.cpython-311.pyc +0 -0
  43. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/worktree.cpython-311.pyc +0 -0
  44. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/_types.py +0 -0
  45. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/agent_output_protocol.py +0 -0
  46. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/agent_result.py +0 -0
  47. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/agent_runner.py +0 -0
  48. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/build_command.py +0 -0
  49. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/config/__init__.py +0 -0
  50. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/config/__pycache__/__init__.cpython-311.pyc +0 -0
  51. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/config/__pycache__/loader.cpython-311.pyc +0 -0
  52. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/config/__pycache__/validator.cpython-311.pyc +0 -0
  53. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/config/loader.py +0 -0
  54. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/config/validator.py +0 -0
  55. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/container_runner.py +0 -0
  56. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/.gitignore +0 -0
  57. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/Dockerfile +0 -0
  58. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/config.py +0 -0
  59. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/coding-standards/deep-modules.md +0 -0
  60. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/coding-standards/interfaces.md +0 -0
  61. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/coding-standards/mocking.md +0 -0
  62. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/coding-standards/refactoring.md +0 -0
  63. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/coding-standards/tests.md +0 -0
  64. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/merge-prompt.md +0 -0
  65. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/plan-prompt.md +0 -0
  66. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/preflight-issue.md +0 -0
  67. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/review-prompt.md +0 -0
  68. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/errors.py +0 -0
  69. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/init_command.py +0 -0
  70. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/__pycache__/__init__.cpython-311.pyc +0 -0
  71. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/__pycache__/_deps.cpython-311.pyc +0 -0
  72. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/__pycache__/_utils.cpython-311.pyc +0 -0
  73. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/__pycache__/implement.cpython-311.pyc +0 -0
  74. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/__pycache__/merge.cpython-311.pyc +0 -0
  75. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/__pycache__/planning.cpython-311.pyc +0 -0
  76. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/__pycache__/preflight.cpython-311.pyc +0 -0
  77. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/_deps.py +0 -0
  78. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/_utils.py +0 -0
  79. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/planning.py +0 -0
  80. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/preflight.py +0 -0
  81. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/labels.py +0 -0
  82. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/main.py +0 -0
  83. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/orchestrator.py +0 -0
  84. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/prompt_pipeline.py +0 -0
  85. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/prompt_utils.py +0 -0
  86. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/rich_status_display.py +0 -0
  87. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/__init__.py +0 -0
  88. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/__pycache__/__init__.cpython-311.pyc +0 -0
  89. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/__pycache__/_base.cpython-311.pyc +0 -0
  90. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/__pycache__/claude_service.cpython-311.pyc +0 -0
  91. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/__pycache__/docker_service.cpython-311.pyc +0 -0
  92. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/__pycache__/git_service.cpython-311.pyc +0 -0
  93. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/__pycache__/github_service.cpython-311.pyc +0 -0
  94. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/_base.py +0 -0
  95. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/claude_service.py +0 -0
  96. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/docker_service.py +0 -0
  97. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/github_service.py +0 -0
  98. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/status_display.py +0 -0
  99. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/worktree.py +0 -0
  100. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle.egg-info/SOURCES.txt +0 -0
  101. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle.egg-info/dependency_links.txt +0 -0
  102. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle.egg-info/entry_points.txt +0 -0
  103. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle.egg-info/requires.txt +0 -0
  104. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle.egg-info/top_level.txt +0 -0
  105. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__init__.py +0 -0
  106. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/__init__.cpython-311.pyc +0 -0
  107. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/conftest.cpython-311-pytest-9.0.3.pyc +0 -0
  108. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_agent_output_protocol.cpython-311-pytest-9.0.3.pyc +0 -0
  109. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_agent_result.cpython-311-pytest-9.0.3.pyc +0 -0
  110. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_agent_runner.cpython-311-pytest-9.0.3.pyc +0 -0
  111. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_build_command.cpython-311-pytest-9.0.3.pyc +0 -0
  112. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_claude_service.cpython-311-pytest-9.0.3.pyc +0 -0
  113. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_config_new.cpython-311-pytest-9.0.3.pyc +0 -0
  114. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_container_runner.cpython-311-pytest-9.0.3.pyc +0 -0
  115. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_default_prompts.cpython-311-pytest-9.0.3.pyc +0 -0
  116. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_deps.cpython-311-pytest-9.0.3.pyc +0 -0
  117. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_docker_service.cpython-311-pytest-9.0.3.pyc +0 -0
  118. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_errors.cpython-311-pytest-9.0.3.pyc +0 -0
  119. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_git_service.cpython-311-pytest-9.0.3.pyc +0 -0
  120. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_github_service.cpython-311-pytest-9.0.3.pyc +0 -0
  121. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_implement.cpython-311-pytest-9.0.3.pyc +0 -0
  122. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_init_command.cpython-311-pytest-9.0.3.pyc +0 -0
  123. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_integration.cpython-311-pytest-9.0.3.pyc +0 -0
  124. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_iteration.cpython-311-pytest-9.0.3.pyc +0 -0
  125. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_labels.cpython-311-pytest-9.0.3.pyc +0 -0
  126. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_main.cpython-311-pytest-9.0.3.pyc +0 -0
  127. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_merge.cpython-311-pytest-9.0.3.pyc +0 -0
  128. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_orchestrator.cpython-311-pytest-9.0.3.pyc +0 -0
  129. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_plan.cpython-311-pytest-9.0.3.pyc +0 -0
  130. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_planning.cpython-311-pytest-9.0.3.pyc +0 -0
  131. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_preflight.cpython-311-pytest-9.0.3.pyc +0 -0
  132. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_prompt_pipeline.cpython-311-pytest-9.0.3.pyc +0 -0
  133. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_prompt_utils.cpython-311-pytest-9.0.3.pyc +0 -0
  134. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_status_display.cpython-311-pytest-9.0.3.pyc +0 -0
  135. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_stream_parser.cpython-311-pytest-9.0.3.pyc +0 -0
  136. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_subprocess_service.cpython-311-pytest-9.0.3.pyc +0 -0
  137. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/test_worktree.cpython-311-pytest-9.0.3.pyc +0 -0
  138. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/conftest.py +0 -0
  139. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_agent_output_protocol.py +0 -0
  140. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_agent_result.py +0 -0
  141. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_agent_runner.py +0 -0
  142. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_build_command.py +0 -0
  143. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_claude_service.py +0 -0
  144. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_config_new.py +0 -0
  145. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_container_runner.py +0 -0
  146. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_default_prompts.py +0 -0
  147. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_deps.py +0 -0
  148. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_docker_service.py +0 -0
  149. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_errors.py +0 -0
  150. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_github_service.py +0 -0
  151. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_init_command.py +0 -0
  152. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_integration.py +0 -0
  153. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_labels.py +0 -0
  154. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_main.py +0 -0
  155. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_plain_status_display.py +0 -0
  156. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_plan.py +0 -0
  157. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_planning.py +0 -0
  158. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_preflight.py +0 -0
  159. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_prompt_pipeline.py +0 -0
  160. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_prompt_utils.py +0 -0
  161. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_status_display.py +0 -0
  162. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_subprocess_service.py +0 -0
  163. {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_worktree.py +0 -0
@@ -73,6 +73,12 @@
73
73
  | **clean merge** | A `git merge --no-edit` that exits zero and requires no conflict resolution | conflict-free merge, successful merge |
74
74
  | **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
75
  | **RALPH** | The required commit message prefix for all Implementer commits (e.g. `RALPH: fix auth bug`) | — |
76
+ | **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 | — |
77
+ | **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 |
78
+ | **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 |
79
+ | **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 |
80
+ | **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 | — |
81
+ | **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
82
  | **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
83
  | **issue** | A GitHub issue labeled for agent processing, representing one unit of work | ticket, task, card |
78
84
  | **AFK issue** | An issue the Planner assigns to an Implementer because it can be resolved autonomously; labeled `ready-for-agent` | agent issue, auto issue |
@@ -216,6 +222,8 @@
216
222
  - 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
223
  - 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
224
  - 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.
225
+ - 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).
226
+ - 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
227
  - 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
228
  - 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
229
  - 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.
@@ -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.10.dev0
4
4
  Summary: Python orchestrator for autonomous Claude Code agents in Docker
5
5
  License: MIT License
6
6
 
@@ -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}}
@@ -2,6 +2,7 @@ import dataclasses
2
2
  from typing import TypeAlias
3
3
 
4
4
  from ..agent_result import CancellationToken, PreflightFailure
5
+ from ..worktree import worktree_name_for_branch, worktree_path
5
6
  from ._deps import Deps
6
7
  from .implement import branch_for, implement_phase
7
8
  from .merge import merge_phase
@@ -32,6 +33,14 @@ class AbortedUsageLimit:
32
33
  IterationOutcome: TypeAlias = Continue | Done | AbortedHITL | AbortedUsageLimit
33
34
 
34
35
 
36
+ def _is_in_flight(issue: dict, deps: Deps) -> bool:
37
+ branch = branch_for(issue["number"])
38
+ if deps.git_svc.verify_ref_exists(branch, deps.repo_root):
39
+ return True
40
+ name = worktree_name_for_branch(branch)
41
+ return worktree_path(name, deps).exists()
42
+
43
+
35
44
  async def run_iteration(deps: Deps) -> IterationOutcome:
36
45
  deps.status_display.register("Preflight")
37
46
  try:
@@ -51,7 +60,10 @@ async def run_iteration(deps: Deps) -> IterationOutcome:
51
60
  return Done()
52
61
  sha = preflight_result.sha
53
62
  open_issues = preflight_result.issues
54
- if len(open_issues) >= 2:
63
+ in_flight = [i for i in open_issues if _is_in_flight(i, deps)]
64
+ if in_flight:
65
+ issues = in_flight
66
+ elif len(open_issues) >= 2:
55
67
  deps.status_display.register("Plan")
56
68
  try:
57
69
  plan_result = await planning_phase(deps, sha, open_issues)
@@ -98,26 +98,34 @@ async def run_issue(
98
98
  await lock.acquire()
99
99
 
100
100
  try:
101
- async with _agent_worktree(_branch, sha, _token, deps) as impl_mount_path:
102
- result = await _bounded_run_agent(
103
- RunRequest(
104
- name=f"Implement Agent #{issue['number']}",
105
- prompt_file=deps.cfg.prompts_dir / "implement-prompt.md",
106
- mount_path=impl_mount_path,
107
- role=AgentRole.IMPLEMENTER,
108
- prompt_args=prompt_args,
109
- model=deps.cfg.implement_override.model,
110
- effort=deps.cfg.implement_override.effort,
111
- stage="pre-implementation",
112
- skip_preflight=True,
113
- status_display=deps.status_display,
114
- issue_title=issue["title"],
115
- work_body=f'implementing "{issue["title"]}"',
116
- token=_token,
101
+ subjects = deps.git_svc.get_branch_commit_subjects(_branch, deps.repo_root)
102
+ review_done = any(s.startswith("RALPH: Review -") for s in subjects)
103
+ implement_done = any(s.startswith("RALPH:") for s in subjects)
104
+
105
+ if review_done:
106
+ return issue
107
+
108
+ if not implement_done:
109
+ async with _agent_worktree(_branch, sha, _token, deps) as impl_mount_path:
110
+ result = await _bounded_run_agent(
111
+ RunRequest(
112
+ name=f"Implement Agent #{issue['number']}",
113
+ prompt_file=deps.cfg.prompts_dir / "implement-prompt.md",
114
+ mount_path=impl_mount_path,
115
+ role=AgentRole.IMPLEMENTER,
116
+ prompt_args=prompt_args,
117
+ model=deps.cfg.implement_override.model,
118
+ effort=deps.cfg.implement_override.effort,
119
+ stage="pre-implementation",
120
+ skip_preflight=True,
121
+ status_display=deps.status_display,
122
+ issue_title=issue["title"],
123
+ work_body=f'implementing "{issue["title"]}"',
124
+ token=_token,
125
+ )
117
126
  )
118
- )
119
- if isinstance(result, PreflightFailure):
120
- return result
127
+ if isinstance(result, PreflightFailure):
128
+ return result
121
129
 
122
130
  async with _agent_worktree(_branch, None, _token, deps) as review_mount_path:
123
131
  await _bounded_run_agent(
@@ -90,9 +90,12 @@ async def merge_phase(completed: list[dict], deps: Deps) -> MergeResult:
90
90
  )
91
91
  )
92
92
  if isinstance(merger_result, PreflightFailure):
93
- raise RuntimeError(
94
- "Merger preflight checks failed; merge did not complete"
93
+ deps.status_display.print(
94
+ "",
95
+ "Merge-time preflight failed; skipping conflict branch merge. "
96
+ "Conflict issues remain open for recovery in the next iteration.",
95
97
  )
98
+ return MergeResult(clean=clean_issues, conflicts=conflict_issues)
96
99
  deps.git_svc.fast_forward_branch(
97
100
  deps.repo_root, target_branch, MERGE_SANDBOX
98
101
  )
@@ -202,6 +202,17 @@ class GitService(_SubprocessService):
202
202
  cwd=repo_path,
203
203
  )
204
204
 
205
+ def get_branch_commit_subjects(self, branch: str, repo_path: Path) -> list[str]:
206
+ result = self._run(
207
+ ["git", "log", f"main..{branch}", "--format=%s"],
208
+ cwd=repo_path,
209
+ capture_output=True,
210
+ )
211
+ if result.returncode != 0:
212
+ return []
213
+ output = self._decode(result.stdout)
214
+ return [line for line in output.splitlines() if line]
215
+
205
216
  def remove_worktree(self, repo_path: Path, worktree_path: Path) -> None:
206
217
  result = self._run(
207
218
  ["git", "worktree", "remove", "--force", str(worktree_path)],
@@ -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.10.dev0
4
4
  Summary: Python orchestrator for autonomous Claude Code agents in Docker
5
5
  License: MIT License
6
6
 
@@ -98,6 +98,53 @@ def test_get_user_name_strips_trailing_newline():
98
98
  assert svc.get_user_name() == "Bob"
99
99
 
100
100
 
101
+ # ── get_branch_commit_subjects() ──────────────────────────────────────────────
102
+
103
+
104
+ def test_get_branch_commit_subjects_returns_subjects_most_recent_first():
105
+ svc = GitService(_cfg)
106
+ with patch(
107
+ "subprocess.run",
108
+ return_value=MagicMock(
109
+ returncode=0,
110
+ stdout=b"RALPH: Review - fix auth\nRALPH: implement auth\n",
111
+ stderr=b"",
112
+ ),
113
+ ):
114
+ result = svc.get_branch_commit_subjects("pycastle/issue-1", Path("/repo"))
115
+ assert result == ["RALPH: Review - fix auth", "RALPH: implement auth"]
116
+
117
+
118
+ def test_get_branch_commit_subjects_returns_empty_list_when_no_commits_ahead():
119
+ svc = GitService(_cfg)
120
+ with patch(
121
+ "subprocess.run",
122
+ return_value=MagicMock(returncode=0, stdout=b"", stderr=b""),
123
+ ):
124
+ result = svc.get_branch_commit_subjects("pycastle/issue-1", Path("/repo"))
125
+ assert result == []
126
+
127
+
128
+ def test_get_branch_commit_subjects_returns_empty_list_when_branch_missing():
129
+ svc = GitService(_cfg)
130
+ with patch(
131
+ "subprocess.run",
132
+ return_value=MagicMock(returncode=128, stdout=b"", stderr=b"unknown revision"),
133
+ ):
134
+ result = svc.get_branch_commit_subjects("pycastle/issue-99", Path("/repo"))
135
+ assert result == []
136
+
137
+
138
+ def test_get_branch_commit_subjects_raises_git_timeout_error_on_timeout():
139
+ svc = GitService(_cfg)
140
+ with patch(
141
+ "subprocess.run",
142
+ side_effect=subprocess.TimeoutExpired(cmd="git", timeout=30),
143
+ ):
144
+ with pytest.raises(GitTimeoutError):
145
+ svc.get_branch_commit_subjects("pycastle/issue-1", Path("/repo"))
146
+
147
+
101
148
  # ── get_user_email() ───────────────────────────────────────────────────────────
102
149
 
103
150
 
@@ -667,6 +667,95 @@ def test_run_issue_does_not_create_reviewer_worktree_on_preflight_failure(tmp_pa
667
667
  assert deps.git_svc.create_worktree.call_count == 1
668
668
 
669
669
 
670
+ # ── run_issue: RALPH commit prefix skip logic ────────────────────────────────
671
+
672
+
673
+ def test_run_issue_review_skip_returns_issue_without_invoking_any_agent(tmp_path):
674
+ """When branch has a RALPH: Review - commit, run_issue returns the issue without spawning agents."""
675
+ fake = FakeAgentRunner([])
676
+ deps = _make_deps(tmp_path, fake)
677
+ deps.git_svc.get_branch_commit_subjects.return_value = ["RALPH: Review - fix auth"]
678
+
679
+ issue = {"number": 20, "title": "Fix auth"}
680
+ result = asyncio.run(run_issue(issue, deps))
681
+
682
+ assert result == issue
683
+ assert fake.calls == []
684
+
685
+
686
+ def test_run_issue_review_skip_creates_no_worktree(tmp_path):
687
+ """When branch has a RALPH: Review - commit, no worktree is created."""
688
+ fake = FakeAgentRunner([])
689
+ deps = _make_deps(tmp_path, fake)
690
+ deps.git_svc.get_branch_commit_subjects.return_value = ["RALPH: Review - fix auth"]
691
+
692
+ issue = {"number": 21, "title": "Fix auth"}
693
+ asyncio.run(run_issue(issue, deps))
694
+
695
+ deps.git_svc.create_worktree.assert_not_called()
696
+
697
+
698
+ def test_run_issue_implement_skip_invokes_only_reviewer(tmp_path):
699
+ """When branch has a RALPH: (non-review) commit, run_issue skips Implementer and runs only Reviewer."""
700
+ fake = FakeAgentRunner([CompletionOutput()])
701
+ deps = _make_deps(tmp_path, fake)
702
+ deps.git_svc.get_branch_commit_subjects.return_value = ["RALPH: Fix auth"]
703
+
704
+ issue = {"number": 22, "title": "Fix auth"}
705
+ result = asyncio.run(run_issue(issue, deps))
706
+
707
+ assert result == issue
708
+ assert len(fake.calls) == 1
709
+ assert "Review Agent" in fake.calls[0].name
710
+
711
+
712
+ def test_run_issue_implement_skip_creates_no_implementer_worktree(tmp_path):
713
+ """When branch has a RALPH: (non-review) commit, no Implementer worktree is created."""
714
+ fake = FakeAgentRunner([CompletionOutput()])
715
+ deps = _make_deps(tmp_path, fake)
716
+ deps.git_svc.get_branch_commit_subjects.return_value = ["RALPH: Fix auth"]
717
+ deps.git_svc.is_working_tree_clean.return_value = True
718
+
719
+ issue = {"number": 23, "title": "Fix auth"}
720
+ asyncio.run(run_issue(issue, deps))
721
+
722
+ assert deps.git_svc.create_worktree.call_count == 1
723
+ branch_arg = deps.git_svc.create_worktree.call_args[0][2]
724
+ assert branch_arg == "pycastle/issue-23"
725
+
726
+
727
+ def test_run_issue_no_ralph_commit_runs_both_agents(tmp_path):
728
+ """When branch has no RALPH: commit, run_issue runs both Implementer and Reviewer normally."""
729
+ fake = FakeAgentRunner([CompletionOutput()] * 2)
730
+ deps = _make_deps(tmp_path, fake)
731
+ deps.git_svc.get_branch_commit_subjects.return_value = []
732
+
733
+ issue = {"number": 24, "title": "Fix auth"}
734
+ result = asyncio.run(run_issue(issue, deps))
735
+
736
+ assert result == issue
737
+ assert len(fake.calls) == 2
738
+ assert "Implement Agent" in fake.calls[0].name
739
+ assert "Review Agent" in fake.calls[1].name
740
+
741
+
742
+ def test_run_issue_releases_lock_when_get_branch_commit_subjects_raises(tmp_path):
743
+ """If get_branch_commit_subjects raises, run_issue must still release the branch lock."""
744
+ from pycastle.services import GitTimeoutError
745
+
746
+ fake = FakeAgentRunner([])
747
+ deps = _make_deps(tmp_path, fake)
748
+ deps.git_svc.get_branch_commit_subjects.side_effect = GitTimeoutError("timed out")
749
+
750
+ branch_locks: dict[str, asyncio.Lock] = {}
751
+ issue = {"number": 25, "title": "Fix auth"}
752
+
753
+ with pytest.raises(GitTimeoutError):
754
+ asyncio.run(run_issue(issue, deps, branch_locks=branch_locks))
755
+
756
+ assert not branch_locks["pycastle/issue-25"].locked()
757
+
758
+
670
759
  def test_run_issue_reviewer_worktree_uses_no_sha(tmp_path):
671
760
  """run_issue must create the Reviewer worktree without a pinned SHA (existing-branch path)."""
672
761
  fake = FakeAgentRunner([CompletionOutput()] * 2)
@@ -44,6 +44,7 @@ def git_svc():
44
44
  svc.is_working_tree_clean.return_value = True
45
45
  svc.try_merge.return_value = True
46
46
  svc.is_ancestor.return_value = True
47
+ svc.verify_ref_exists.return_value = False
47
48
  return svc
48
49
 
49
50
 
@@ -731,3 +732,165 @@ def test_run_iteration_registers_preflight_row_before_preflight_phase(
731
732
  assert register_idx is not None, "Preflight row must be registered"
732
733
  assert remove_idx is not None, "Preflight row must be removed"
733
734
  assert register_idx < remove_idx
735
+
736
+
737
+ # ── Planning skip when in-flight branches or worktrees exist ─────────────────
738
+
739
+
740
+ def test_run_iteration_skips_planning_when_all_issues_have_existing_branches(
741
+ tmp_path, git_svc, logger
742
+ ):
743
+ """When all open issues have an existing branch, planning_phase is not invoked
744
+ and the iteration proceeds with those issues as the working set."""
745
+ github_svc = MagicMock(spec=GithubService)
746
+ github_svc.get_open_issues.return_value = [
747
+ {"number": 1, "title": "Fix A"},
748
+ {"number": 2, "title": "Fix B"},
749
+ ]
750
+ git_svc.verify_ref_exists.return_value = True
751
+
752
+ agent_names: list[str] = []
753
+
754
+ async def _fake_agent(request: RunRequest):
755
+ agent_names.append(request.name)
756
+ return CompletionOutput()
757
+
758
+ deps = _make_deps(
759
+ tmp_path, _fake_agent, git_svc=git_svc, github_svc=github_svc, logger=logger
760
+ )
761
+ result = asyncio.run(run_iteration(deps))
762
+
763
+ assert isinstance(result, Continue)
764
+ assert "Plan Agent" not in agent_names, (
765
+ "Plan Agent must not be called when all branches exist"
766
+ )
767
+ assert any("Implement Agent" in n for n in agent_names), (
768
+ "Implement Agent must still run"
769
+ )
770
+
771
+
772
+ def test_run_iteration_skips_planning_when_all_issues_have_existing_worktrees(
773
+ tmp_path, git_svc, logger
774
+ ):
775
+ """When all open issues have an existing worktree directory (but no branch),
776
+ planning_phase is not invoked."""
777
+ github_svc = MagicMock(spec=GithubService)
778
+ github_svc.get_open_issues.return_value = [
779
+ {"number": 3, "title": "Fix C"},
780
+ {"number": 4, "title": "Fix D"},
781
+ ]
782
+ git_svc.verify_ref_exists.return_value = False
783
+ for n in [3, 4]:
784
+ (tmp_path / "pycastle" / ".worktrees" / f"issue-{n}").mkdir(parents=True)
785
+
786
+ agent_names: list[str] = []
787
+
788
+ async def _fake_agent(request: RunRequest):
789
+ agent_names.append(request.name)
790
+ return CompletionOutput()
791
+
792
+ deps = _make_deps(
793
+ tmp_path, _fake_agent, git_svc=git_svc, github_svc=github_svc, logger=logger
794
+ )
795
+ result = asyncio.run(run_iteration(deps))
796
+
797
+ assert isinstance(result, Continue)
798
+ assert "Plan Agent" not in agent_names, (
799
+ "Plan Agent must not be called when all worktrees exist"
800
+ )
801
+
802
+
803
+ def test_run_iteration_uses_only_in_flight_issues_when_some_have_existing_branch(
804
+ tmp_path, git_svc, logger
805
+ ):
806
+ """When only some open issues have an existing branch, only those in-flight issues
807
+ are used as the working set and planning_phase is not invoked."""
808
+ github_svc = MagicMock(spec=GithubService)
809
+ github_svc.get_open_issues.return_value = [
810
+ {"number": 5, "title": "In flight"},
811
+ {"number": 6, "title": "Deferred"},
812
+ ]
813
+ git_svc.verify_ref_exists.side_effect = lambda ref, path: ref == "pycastle/issue-5"
814
+
815
+ agent_names: list[str] = []
816
+
817
+ async def _fake_agent(request: RunRequest):
818
+ agent_names.append(request.name)
819
+ return CompletionOutput()
820
+
821
+ deps = _make_deps(
822
+ tmp_path, _fake_agent, git_svc=git_svc, github_svc=github_svc, logger=logger
823
+ )
824
+ result = asyncio.run(run_iteration(deps))
825
+
826
+ assert isinstance(result, Continue)
827
+ assert "Plan Agent" not in agent_names, (
828
+ "Plan Agent must not be called when some branches exist"
829
+ )
830
+ assert "Implement Agent #5" in agent_names, "In-flight issue must be implemented"
831
+ assert not any("Implement Agent #6" in n for n in agent_names), (
832
+ "Deferred issue must not be implemented"
833
+ )
834
+
835
+
836
+ def test_run_iteration_uses_preflight_sha_for_in_flight_issues(
837
+ tmp_path, git_svc, logger
838
+ ):
839
+ """When in-flight issues are used, the implement phase receives the preflight SHA
840
+ unchanged — the in-flight path must not re-pin the SHA from a plan-sandbox."""
841
+ github_svc = MagicMock(spec=GithubService)
842
+ github_svc.get_open_issues.return_value = [{"number": 7, "title": "In flight"}]
843
+ git_svc.verify_ref_exists.return_value = True
844
+ git_svc.get_head_sha.return_value = "preflight-sha-abc"
845
+
846
+ async def _fake_agent(request: RunRequest):
847
+ return CompletionOutput()
848
+
849
+ deps = _make_deps(
850
+ tmp_path, _fake_agent, git_svc=git_svc, github_svc=github_svc, logger=logger
851
+ )
852
+ asyncio.run(run_iteration(deps))
853
+
854
+ implement_shas = {
855
+ c.args[3]
856
+ for c in git_svc.create_worktree.call_args_list
857
+ if c.args[3] is not None
858
+ }
859
+ assert "preflight-sha-abc" in implement_shas, (
860
+ "Implement phase must use the preflight SHA, not a re-pinned SHA"
861
+ )
862
+
863
+
864
+ def test_run_iteration_detects_in_flight_via_both_branch_and_worktree_signals(
865
+ tmp_path, git_svc, logger
866
+ ):
867
+ """Both detection signals (branch and worktree) are checked independently:
868
+ an issue with only a branch, an issue with only a worktree directory, and
869
+ an issue with neither are handled correctly in a single iteration."""
870
+ github_svc = MagicMock(spec=GithubService)
871
+ github_svc.get_open_issues.return_value = [
872
+ {"number": 8, "title": "Branch only"},
873
+ {"number": 9, "title": "Worktree only"},
874
+ {"number": 10, "title": "Deferred"},
875
+ ]
876
+ git_svc.verify_ref_exists.side_effect = lambda ref, path: ref == "pycastle/issue-8"
877
+ (tmp_path / "pycastle" / ".worktrees" / "issue-9").mkdir(parents=True)
878
+
879
+ agent_names: list[str] = []
880
+
881
+ async def _fake_agent(request: RunRequest):
882
+ agent_names.append(request.name)
883
+ return CompletionOutput()
884
+
885
+ deps = _make_deps(
886
+ tmp_path, _fake_agent, git_svc=git_svc, github_svc=github_svc, logger=logger
887
+ )
888
+ result = asyncio.run(run_iteration(deps))
889
+
890
+ assert isinstance(result, Continue)
891
+ assert "Plan Agent" not in agent_names
892
+ assert "Implement Agent #8" in agent_names, "Branch-only in-flight issue must run"
893
+ assert "Implement Agent #9" in agent_names, "Worktree-only in-flight issue must run"
894
+ assert not any("Implement Agent #10" in n for n in agent_names), (
895
+ "Deferred issue must not run"
896
+ )
@@ -16,7 +16,7 @@ from pycastle.iteration._deps import (
16
16
  RecordingStatusDisplay,
17
17
  )
18
18
  from pycastle.status_display import PlainStatusDisplay
19
- from pycastle.iteration.merge import merge_phase
19
+ from pycastle.iteration.merge import MergeResult, merge_phase
20
20
 
21
21
 
22
22
  @pytest.fixture
@@ -247,40 +247,103 @@ def test_incomplete_merger_raises_and_does_not_fast_forward(
247
247
  git_svc.fast_forward_branch.assert_not_called()
248
248
 
249
249
 
250
- def test_preflight_failure_from_merger_raises_and_does_not_fast_forward(
250
+ # ── Graceful merge-time preflight skip ───────────────────────────────────────
251
+
252
+
253
+ def _preflight_failure_deps(tmp_path, git_svc, github_svc):
254
+ from pycastle.agent_result import PreflightFailure
255
+
256
+ failure = PreflightFailure(failures=(("ruff", "ruff check .", "E501"),))
257
+ git_svc.try_merge.return_value = False
258
+ return _make_deps(tmp_path, git_svc, github_svc, FakeAgentRunner([failure]))
259
+
260
+
261
+ def test_preflight_failure_returns_merge_result_without_raising(
262
+ tmp_path, git_svc, github_svc
263
+ ):
264
+ local_deps = _preflight_failure_deps(tmp_path, git_svc, github_svc)
265
+ issues = [{"number": 1, "title": "Conflict"}]
266
+ result = _run(issues, local_deps)
267
+ assert isinstance(result, MergeResult)
268
+
269
+
270
+ def test_preflight_failure_result_separates_clean_and_conflict_issues(
251
271
  tmp_path, git_svc, github_svc
252
272
  ):
253
273
  from pycastle.agent_result import PreflightFailure
254
274
 
255
- git_svc.try_merge.return_value = False
256
275
  failure = PreflightFailure(failures=(("ruff", "ruff check .", "E501"),))
257
- fake = FakeAgentRunner([failure])
258
- local_deps = _make_deps(tmp_path, git_svc, github_svc, fake)
276
+ git_svc.try_merge.side_effect = _conflict_on([2])
277
+ local_deps = _make_deps(tmp_path, git_svc, github_svc, FakeAgentRunner([failure]))
278
+ issues = [{"number": 1, "title": "Clean"}, {"number": 2, "title": "Conflict"}]
279
+ result = _run(issues, local_deps)
280
+ assert result.clean == [{"number": 1, "title": "Clean"}]
281
+ assert result.conflicts == [{"number": 2, "title": "Conflict"}]
282
+
283
+
284
+ def test_preflight_failure_does_not_close_conflict_issue(tmp_path, git_svc, github_svc):
285
+ local_deps = _preflight_failure_deps(tmp_path, git_svc, github_svc)
286
+ issues = [{"number": 5, "title": "Conflict"}]
287
+ _run(issues, local_deps)
288
+ closed = [call.args[0] for call in local_deps.github_svc.close_issue.call_args_list]
289
+ assert 5 not in closed
290
+
291
+
292
+ def test_preflight_failure_does_not_delete_conflict_branch(
293
+ tmp_path, git_svc, github_svc
294
+ ):
295
+ local_deps = _preflight_failure_deps(tmp_path, git_svc, github_svc)
296
+ issues = [{"number": 5, "title": "Conflict"}]
297
+ _run(issues, local_deps)
298
+ deleted = [call.args[0] for call in local_deps.git_svc.delete_branch.call_args_list]
299
+ assert "pycastle/issue-5" not in deleted
300
+
301
+
302
+ def test_preflight_failure_does_not_fast_forward(tmp_path, git_svc, github_svc):
303
+ local_deps = _preflight_failure_deps(tmp_path, git_svc, github_svc)
259
304
  issues = [{"number": 1, "title": "Conflict"}]
260
- with pytest.raises(RuntimeError, match="preflight"):
261
- _run(issues, local_deps)
262
- git_svc.fast_forward_branch.assert_not_called()
305
+ _run(issues, local_deps)
306
+ local_deps.git_svc.fast_forward_branch.assert_not_called()
263
307
 
264
308
 
265
- def test_preflight_failure_from_merger_still_removes_worktree(
309
+ def test_preflight_failure_prints_skip_message(tmp_path, git_svc, github_svc):
310
+ recording = RecordingStatusDisplay()
311
+ local_deps = dataclasses.replace(
312
+ _preflight_failure_deps(tmp_path, git_svc, github_svc),
313
+ status_display=recording,
314
+ )
315
+ issues = [{"number": 1, "title": "Conflict"}]
316
+ _run(issues, local_deps)
317
+ print_messages = [c[2] for c in recording.calls if c[0] == "print"]
318
+ assert any("preflight" in msg.lower() for msg in print_messages)
319
+
320
+
321
+ def test_preflight_failure_closes_parent_issues_for_clean_issues(
266
322
  tmp_path, git_svc, github_svc
267
323
  ):
268
324
  from pycastle.agent_result import PreflightFailure
269
325
 
270
- git_svc.try_merge.return_value = False
271
- failure = PreflightFailure(failures=(("mypy", "mypy .", "error"),))
272
- fake = FakeAgentRunner([failure])
273
- local_deps = _make_deps(tmp_path, git_svc, github_svc, fake)
326
+ failure = PreflightFailure(failures=(("ruff", "ruff check .", "E501"),))
327
+ git_svc.try_merge.side_effect = _conflict_on([2])
328
+ local_deps = _make_deps(tmp_path, git_svc, github_svc, FakeAgentRunner([failure]))
329
+ issues = [{"number": 1, "title": "Clean"}, {"number": 2, "title": "Conflict"}]
330
+ _run(issues, local_deps)
331
+ local_deps.github_svc.close_completed_parent_issues.assert_called_once()
332
+
333
+
334
+ def test_preflight_failure_worktree_still_removed(tmp_path, git_svc, github_svc):
335
+ local_deps = _preflight_failure_deps(tmp_path, git_svc, github_svc)
274
336
  issues = [{"number": 1, "title": "Conflict"}]
275
- with pytest.raises(RuntimeError):
276
- _run(issues, local_deps)
337
+ _run(issues, local_deps)
277
338
  expected_path = (
278
339
  local_deps.repo_root
279
340
  / local_deps.cfg.pycastle_dir
280
341
  / ".worktrees"
281
342
  / "merge-sandbox"
282
343
  )
283
- git_svc.remove_worktree.assert_called_once_with(local_deps.repo_root, expected_path)
344
+ local_deps.git_svc.remove_worktree.assert_called_once_with(
345
+ local_deps.repo_root, expected_path
346
+ )
284
347
 
285
348
 
286
349
  # ── Exception safety ──────────────────────────────────────────────────────────
@@ -632,8 +695,7 @@ def test_merge_row_not_removed_with_failed_style_after_row_already_removed(
632
695
  )
633
696
  git_svc.try_merge.return_value = False
634
697
 
635
- with pytest.raises(RuntimeError, match="preflight"):
636
- asyncio.run(merge_phase([{"number": 1, "title": "Conflict"}], deps))
698
+ asyncio.run(merge_phase([{"number": 1, "title": "Conflict"}], deps))
637
699
 
638
700
  assert ("remove", "Merge", "failed", "error") not in recording.calls
639
701
 
@@ -58,7 +58,14 @@ def _make_git_svc(try_merge_side_effect=None, is_ancestor=True):
58
58
  wt.mkdir(parents=True, exist_ok=True)
59
59
  (wt / "pyproject.toml").write_text("[project]\nname='t'\n")
60
60
 
61
+ def _fake_remove_worktree(repo, wt):
62
+ import shutil
63
+
64
+ if isinstance(wt, Path) and wt.exists():
65
+ shutil.rmtree(wt)
66
+
61
67
  mock_svc.create_worktree.side_effect = _fake_create_worktree
68
+ mock_svc.remove_worktree.side_effect = _fake_remove_worktree
62
69
  return mock_svc
63
70
 
64
71