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