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.
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/CONTEXT.md +8 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/PKG-INFO +1 -1
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/implement-prompt.md +0 -10
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/__init__.py +13 -1
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/implement.py +27 -19
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/merge.py +5 -2
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/git_service.py +11 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle.egg-info/PKG-INFO +1 -1
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_git_service.py +47 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_implement.py +89 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_iteration.py +163 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_merge.py +80 -18
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_orchestrator.py +7 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/.github/workflows/publish.yml +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/.gitignore +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/.python-version +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/CLAUDE.md +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/LICENSE +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/README.md +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/docs/adr/0001-runtime-dependency-installation.md +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/docs/agents/domain.md +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/docs/agents/issue-tracker.md +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/docs/agents/triage-labels.md +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/pyproject.toml +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/setup.cfg +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__init__.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/__init__.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/_types.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/agent_output_protocol.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/agent_result.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/agent_runner.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/build_command.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/container_runner.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/errors.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/init_command.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/labels.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/main.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/orchestrator.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/prompt_pipeline.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/prompt_utils.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/rich_status_display.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/stream_parser.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/__pycache__/worktree.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/_types.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/agent_output_protocol.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/agent_result.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/agent_runner.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/build_command.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/config/__init__.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/config/__pycache__/__init__.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/config/__pycache__/loader.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/config/__pycache__/validator.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/config/loader.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/config/validator.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/container_runner.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/.gitignore +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/Dockerfile +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/config.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/coding-standards/deep-modules.md +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/coding-standards/interfaces.md +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/coding-standards/mocking.md +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/coding-standards/refactoring.md +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/coding-standards/tests.md +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/merge-prompt.md +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/plan-prompt.md +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/preflight-issue.md +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/review-prompt.md +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/errors.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/init_command.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/__pycache__/__init__.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/__pycache__/_deps.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/__pycache__/_utils.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/__pycache__/implement.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/__pycache__/merge.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/__pycache__/planning.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/__pycache__/preflight.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/_deps.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/_utils.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/planning.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/iteration/preflight.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/labels.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/main.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/orchestrator.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/prompt_pipeline.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/prompt_utils.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/rich_status_display.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/__init__.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/__pycache__/__init__.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/__pycache__/_base.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/__pycache__/claude_service.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/__pycache__/docker_service.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/__pycache__/git_service.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/__pycache__/github_service.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/_base.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/claude_service.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/docker_service.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/services/github_service.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/status_display.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/worktree.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle.egg-info/SOURCES.txt +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle.egg-info/dependency_links.txt +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle.egg-info/entry_points.txt +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle.egg-info/requires.txt +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle.egg-info/top_level.txt +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__init__.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/__init__.cpython-311.pyc +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/__pycache__/conftest.cpython-311-pytest-9.0.3.pyc +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/conftest.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_agent_output_protocol.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_agent_result.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_agent_runner.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_build_command.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_claude_service.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_config_new.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_container_runner.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_default_prompts.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_deps.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_docker_service.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_errors.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_github_service.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_init_command.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_integration.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_labels.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_main.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_plain_status_display.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_plan.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_planning.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_preflight.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_prompt_pipeline.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_prompt_utils.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_status_display.py +0 -0
- {pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/tests/test_subprocess_service.py +0 -0
- {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.
|
{pycastle-0.1.3.9.dev0 → pycastle-0.1.3.10.dev0}/src/pycastle/defaults/prompts/implement-prompt.md
RENAMED
|
@@ -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
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
"
|
|
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)],
|
|
@@ -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
|
-
|
|
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
|
-
|
|
258
|
-
local_deps = _make_deps(tmp_path, git_svc, github_svc,
|
|
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
|
-
|
|
261
|
-
|
|
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
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
|
File without changes
|