pycastle 0.1.3.10.dev0__tar.gz → 0.2.0.1.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.10.dev0 → pycastle-0.2.0.1.dev0}/.gitignore +1 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/CONTEXT.md +45 -32
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/PKG-INFO +1 -1
- pycastle-0.2.0.1.dev0/docs/adr/0002-cli-native-model-shorthand-resolution.md +34 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/agent_output_protocol.py +57 -20
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/agent_runner.py +34 -12
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/config/__init__.py +1 -2
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/config/loader.py +31 -11
- pycastle-0.2.0.1.dev0/src/pycastle/container_runner.py +128 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/defaults/config.py +1 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/defaults/prompts/implement-prompt.md +5 -11
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/defaults/prompts/review-prompt.md +4 -4
- pycastle-0.2.0.1.dev0/src/pycastle/docker_session.py +190 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/errors.py +8 -1
- pycastle-0.2.0.1.dev0/src/pycastle/iteration/__init__.py +138 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/iteration/_deps.py +2 -3
- pycastle-0.2.0.1.dev0/src/pycastle/iteration/_phase_row.py +33 -0
- pycastle-0.2.0.1.dev0/src/pycastle/iteration/_utils.py +25 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/iteration/implement.py +80 -15
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/iteration/merge.py +146 -114
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/iteration/planning.py +15 -3
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/iteration/preflight.py +18 -8
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/orchestrator.py +15 -12
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/prompt_pipeline.py +17 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/rich_status_display.py +36 -31
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/services/git_service.py +19 -0
- pycastle-0.2.0.1.dev0/src/pycastle/status_display.py +57 -0
- pycastle-0.2.0.1.dev0/src/pycastle/stream_session.py +67 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/worktree.py +15 -4
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle.egg-info/PKG-INFO +1 -1
- pycastle-0.2.0.1.dev0/src/pycastle.egg-info/SOURCES.txt +102 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_agent_output_protocol.py +99 -84
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_agent_runner.py +900 -853
- pycastle-0.2.0.1.dev0/tests/test_config_new.py +318 -0
- pycastle-0.2.0.1.dev0/tests/test_container_runner.py +336 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_default_prompts.py +1 -2
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_deps.py +10 -1
- pycastle-0.2.0.1.dev0/tests/test_docker_session.py +564 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_errors.py +29 -20
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_git_service.py +115 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_implement.py +237 -18
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_init_command.py +5 -22
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_iteration.py +454 -24
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_merge.py +235 -42
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_orchestrator.py +84 -122
- pycastle-0.2.0.1.dev0/tests/test_phase_row.py +86 -0
- pycastle-0.2.0.1.dev0/tests/test_plain_status_display.py +425 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_planning.py +176 -196
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_preflight.py +94 -144
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_prompt_utils.py +1 -2
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_status_display.py +1317 -1134
- pycastle-0.2.0.1.dev0/tests/test_stream_session.py +190 -0
- pycastle-0.2.0.1.dev0/tests/test_utils.py +54 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_worktree.py +47 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/__init__.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/_types.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/agent_output_protocol.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/agent_result.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/agent_runner.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/build_command.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/container_runner.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/errors.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/init_command.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/labels.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/main.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/orchestrator.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/prompt_pipeline.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/prompt_utils.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/rich_status_display.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/stream_parser.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/__pycache__/worktree.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/config/__pycache__/__init__.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/config/__pycache__/loader.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/config/__pycache__/validator.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/config/validator.py +0 -117
- pycastle-0.1.3.10.dev0/src/pycastle/container_runner.py +0 -341
- pycastle-0.1.3.10.dev0/src/pycastle/iteration/__init__.py +0 -132
- pycastle-0.1.3.10.dev0/src/pycastle/iteration/__pycache__/__init__.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/iteration/__pycache__/_deps.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/iteration/__pycache__/_utils.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/iteration/__pycache__/implement.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/iteration/__pycache__/merge.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/iteration/__pycache__/planning.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/iteration/__pycache__/preflight.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/iteration/_utils.py +0 -16
- pycastle-0.1.3.10.dev0/src/pycastle/prompt_utils.py +0 -18
- pycastle-0.1.3.10.dev0/src/pycastle/services/__pycache__/__init__.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/services/__pycache__/_base.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/services/__pycache__/claude_service.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/services/__pycache__/docker_service.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/services/__pycache__/git_service.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/services/__pycache__/github_service.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/src/pycastle/status_display.py +0 -52
- pycastle-0.1.3.10.dev0/src/pycastle.egg-info/SOURCES.txt +0 -161
- pycastle-0.1.3.10.dev0/tests/__pycache__/__init__.cpython-311.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/conftest.cpython-311-pytest-9.0.3.pyc +0 -0
- 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.10.dev0/tests/__pycache__/test_agent_result.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_agent_runner.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_build_command.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_claude_service.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_config_new.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_container_runner.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_default_prompts.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_deps.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_docker_service.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_errors.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_git_service.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_github_service.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_implement.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_init_command.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_integration.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_iteration.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_labels.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_main.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_merge.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_orchestrator.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_plan.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_planning.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_preflight.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_prompt_pipeline.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_prompt_utils.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_status_display.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_stream_parser.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_subprocess_service.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/__pycache__/test_worktree.cpython-311-pytest-9.0.3.pyc +0 -0
- pycastle-0.1.3.10.dev0/tests/test_config_new.py +0 -633
- pycastle-0.1.3.10.dev0/tests/test_container_runner.py +0 -1263
- pycastle-0.1.3.10.dev0/tests/test_plain_status_display.py +0 -267
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/.github/workflows/publish.yml +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/.python-version +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/CLAUDE.md +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/LICENSE +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/README.md +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/docs/adr/0001-runtime-dependency-installation.md +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/docs/agents/domain.md +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/docs/agents/issue-tracker.md +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/docs/agents/triage-labels.md +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/pyproject.toml +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/setup.cfg +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/__init__.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/_types.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/agent_result.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/build_command.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/defaults/.gitignore +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/defaults/Dockerfile +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/defaults/prompts/coding-standards/deep-modules.md +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/defaults/prompts/coding-standards/interfaces.md +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/defaults/prompts/coding-standards/mocking.md +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/defaults/prompts/coding-standards/refactoring.md +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/defaults/prompts/coding-standards/tests.md +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/defaults/prompts/merge-prompt.md +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/defaults/prompts/plan-prompt.md +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/defaults/prompts/preflight-issue.md +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/init_command.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/labels.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/main.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/services/__init__.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/services/_base.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/services/claude_service.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/services/docker_service.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle/services/github_service.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle.egg-info/dependency_links.txt +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle.egg-info/entry_points.txt +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle.egg-info/requires.txt +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/src/pycastle.egg-info/top_level.txt +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/__init__.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/conftest.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_agent_result.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_build_command.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_claude_service.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_docker_service.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_github_service.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_integration.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_labels.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_main.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_plan.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_prompt_pipeline.py +0 -0
- {pycastle-0.1.3.10.dev0 → pycastle-0.2.0.1.dev0}/tests/test_subprocess_service.py +0 -0
|
@@ -19,8 +19,7 @@
|
|
|
19
19
|
| --- | --- | --- |
|
|
20
20
|
| **config.py** | Python file in the pycastle directory defining behavioral configuration; overrides the defaults module field by field at runtime | settings.py, settings |
|
|
21
21
|
| **defaults module** | `src/pycastle/defaults/config.py` bundled in the package; contains only pure default values, no logic; never touched by users or the config loader directly | defaults config, fallback config |
|
|
22
|
-
| **config loader** | The `loader.py` module inside the `config/` package; reads the defaults, executes the consuming project's config.py via importlib,
|
|
23
|
-
| **config validator** | The `validator.py` module inside the `config/` package; owns `validate_config(cfg, claude_service) -> Config`; resolves model shorthands to full model IDs and validates effort levels; raises `ConfigValidationError` on any invalid entry; returns a new immutable `Config` via `dataclasses.replace` | — |
|
|
22
|
+
| **config loader** | The `loader.py` module inside the `config/` package; reads the defaults, executes the consuming project's config.py via importlib, applies any programmatic overrides, validates effort strings, and returns an immutable `Config`; pure — no subprocess calls, no service dependencies, no default values of its own | — |
|
|
24
23
|
| **.env** | File in the pycastle directory holding secrets and credentials only — never committed to git | environment file, config |
|
|
25
24
|
| **GH_TOKEN** | GitHub personal access token stored in .env, used for GitHub API calls and label management | github token, gh pat |
|
|
26
25
|
| **CLAUDE_CODE_OAUTH_TOKEN** | Long-lived OAuth token for Claude Code authentication, stored in .env | claude token, oauth token |
|
|
@@ -31,12 +30,12 @@
|
|
|
31
30
|
| **field-by-field override** | The config loader strategy: for each non-underscore name in the consuming project's config.py, `setattr` replaces the corresponding name in the config loader module; absent names fall back to the defaults module | full replacement, merge override |
|
|
32
31
|
| **STAGE_OVERRIDES** | Config dict with one entry per orchestration phase (`plan`, `implement`, `review`, `merge`), each holding a model shorthand and an effort level | stage config, model config |
|
|
33
32
|
| **stage override** | The per-phase `model` + `effort` entry inside STAGE_OVERRIDES for one orchestration phase | phase config, agent config |
|
|
34
|
-
| **model shorthand** | A short family alias (`haiku`, `sonnet`, `opus`)
|
|
35
|
-
| **full model ID** | The versioned Claude model identifier (e.g. `claude-sonnet-4-6`)
|
|
36
|
-
| **effort level** | One of
|
|
33
|
+
| **model shorthand** | A short family alias (`haiku`, `sonnet`, `opus`) accepted by the Claude CLI natively; stored as-is in `Config` and passed through to `claude --model` at stage execution time; not resolved at config load time (see ADR 0002) | model alias, model name |
|
|
34
|
+
| **full model ID** | The versioned Claude model identifier (e.g. `claude-sonnet-4-6`); may be stored directly in a stage override instead of a shorthand; passed through to `claude --model` unchanged | model ID, model version |
|
|
35
|
+
| **effort level** | One of five Claude effort values (`low`, `medium`, `high`, `xhigh`, `max`) that controls cost and reasoning depth; validated at config load time against this fixed set | effort, effort flag |
|
|
37
36
|
| **CLI default** | The behavior when no `--model` or `--effort` flag is injected — triggered by an empty string in STAGE_OVERRIDES | default model, unset |
|
|
38
|
-
| **
|
|
39
|
-
| **
|
|
37
|
+
| **ConfigValidationError** | Error raised by the config loader when an effort level is unrecognised; includes the invalid value, closest valid suggestion, and full list of valid options | validation error, config error |
|
|
38
|
+
| **auto_push** | Boolean config entry (default `True`) that controls whether `merge_phase` pushes local main to the remote after any merges produce commits; set to `False` to disable automatic pushing | push_after_merge, AUTO_PUSH |
|
|
40
39
|
|
|
41
40
|
## GitHub Integration
|
|
42
41
|
|
|
@@ -72,8 +71,9 @@
|
|
|
72
71
|
| **programmatic merge path** | Fast-path logic in the merge phase that runs `git merge --no-edit` directly via subprocess without spawning the Merger; used when all branches merge cleanly | fast path, direct merge |
|
|
73
72
|
| **clean merge** | A `git merge --no-edit` that exits zero and requires no conflict resolution | conflict-free merge, successful merge |
|
|
74
73
|
| **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
|
-
| **RALPH
|
|
76
|
-
| **RALPH: Review -** | The
|
|
74
|
+
| **RALPH: Implement -** | The commit message prefix injected by `run_issue()` in code for all Implementer commits (e.g. `RALPH: Implement - fix auth bug`); prepended to the message the Implementer outputs inside `<commit_message>` tags; used by the implement skip to detect whether implement work is complete | — |
|
|
75
|
+
| **RALPH: Review -** | The commit message prefix injected by `run_issue()` in code for all Reviewer commits (e.g. `RALPH: Review - improve error handling`); prepended to the message the Reviewer outputs inside `<commit_message>` tags; used by the review skip to detect whether review work is complete; each agent produces exactly one commit per branch | — |
|
|
76
|
+
| **`<commit_message>` tag** | XML tag emitted by Implementers and Reviewers instead of `<promise>COMPLETE</promise>`; contains the agent's plain description of changes (no prefix); the orchestrator prepends the appropriate RALPH prefix, stages all worktree changes with `git add -A`, and commits; absence of this tag is a failed run — worktree is preserved and the agent restarts to continue | — |
|
|
77
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
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
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 |
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
| **merge-sandbox worktree** | A temporary named-branch worktree (`pycastle/merge-sandbox`) created by `merge_phase` from HEAD after clean merges complete; the Merger runs inside it to resolve conflicting branches; always removed in a `try/finally` by `merge_phase` regardless of state; on success `merge_phase` fast-forwards `main` from the branch before cleanup; located at `.pycastle/.worktrees/merge-sandbox` | merger worktree, conflict worktree |
|
|
92
92
|
| **branch** | A git branch name assigned to an issue inside the plan; follows the pattern `pycastle/issue-<n>-<slug>` | feature branch, issue branch |
|
|
93
93
|
| **orphan worktree** | A worktree directory under `.pycastle/.worktrees/` no longer registered in git, typically left by a crashed agent run | stale worktree, leftover worktree |
|
|
94
|
-
| **orphan sweep** | Startup operation that cross-references `.pycastle/.worktrees/` against `git worktree list --porcelain
|
|
94
|
+
| **orphan sweep** | Startup operation that cross-references `.pycastle/.worktrees/` against `git worktree list --porcelain`, deletes unregistered directories, and removes the `.worktrees` parent directory if no active children remain | worktree cleanup, stale cleanup |
|
|
95
95
|
| **collision detection** | Mechanism that prevents two parallel agents from simultaneously creating worktrees for the same branch, implemented as a per-branch async lock | — |
|
|
96
96
|
|
|
97
97
|
## Prompts
|
|
@@ -102,7 +102,8 @@
|
|
|
102
102
|
| **prompts directory** | The `prompts/` subdirectory inside the pycastle directory holding all prompt files | templates dir |
|
|
103
103
|
| **placeholder** | A `{{VARIABLE}}` token inside a prompt, substituted at render time | template variable, slot |
|
|
104
104
|
| **shell expression** | A `` !`command` `` token inside a prompt, replaced by the command's stdout output at preprocess time | shell expansion |
|
|
105
|
-
| **prompt pipeline** | The
|
|
105
|
+
| **prompt pipeline** | The single module (`prompt_pipeline`) owning all prompt concerns: loading coding-standard files from the prompts directory (`load_standards`), rendering `{{placeholders}}` against an args dict, and preprocessing `` !`shell` `` expressions; exposes `prepare_prompt`, `load_standards`, and `PromptRenderError` | templating, rendering |
|
|
106
|
+
| **`load_standards`** | Function in `prompt_pipeline` that reads the five coding-standard files from the `coding-standards/` subdirectory of the prompts directory and returns a `dict[str, str]` keyed by placeholder name (`TESTING_STANDARDS`, `MOCKING_STANDARDS`, `INTERFACES_STANDARDS`, `DEEP_MODULES_STANDARDS`, `REFACTORING_STANDARDS`); missing files return an empty string | — |
|
|
106
107
|
| **CODING_STANDARDS.md** | A reference document placed in the prompts directory and treated as a prompt for discovery and scaffolding purposes | standards file |
|
|
107
108
|
| **EXPLORATION section** | The section of the implement prompt that instructs the Implementer to read files before coding; scoped to files mentioned in the issue body — not a full repository survey | explore section, discovery section |
|
|
108
109
|
| **FEEDBACK LOOPS section** | The section of the implement prompt that instructs the Implementer to run IMPLEMENT_CHECKS commands before committing | feedback section, pre-commit checks |
|
|
@@ -115,12 +116,14 @@
|
|
|
115
116
|
|
|
116
117
|
| Term | Definition | Aliases to avoid |
|
|
117
118
|
| --- | --- | --- |
|
|
118
|
-
| **agent output protocol** | The contract between prompts and the orchestrator: the set of XML tags agents emit to signal structured output (`<plan>`, `<issue>`, `<promise>`), plus the module that owns the complete NDJSON stream → typed output pipeline | output format, agent tags, agent signals |
|
|
119
|
+
| **agent output protocol** | The contract between prompts and the orchestrator: the set of XML tags agents emit to signal structured output (`<plan>`, `<issue>`, `<commit_message>`, `<promise>`), plus the module that owns the complete NDJSON stream → typed output pipeline | output format, agent tags, agent signals |
|
|
119
120
|
| **`<plan>` tag** | XML tag emitted by the Planner containing a JSON payload listing unblocked issues for the current iteration; extracted by the agent output protocol module | plan output, plan block |
|
|
120
121
|
| **`<issue>` tag** | XML tag emitted by the preflight-issue agent containing the GitHub issue number it filed; extracted by the agent output protocol module | issue output, issue number tag |
|
|
121
|
-
| **`<promise>COMPLETE</promise>`** | XML tag emitted by
|
|
122
|
-
| **`AgentOutputProtocolError`** | Base exception raised by the agent output protocol module when a required tag is missing or malformed; subclassed by `PlanParseError`, `IssueParseError`, and `
|
|
123
|
-
| **`
|
|
122
|
+
| **`<promise>COMPLETE</promise>`** | XML tag emitted by the Merger and the preflight-issue agent to declare that their work phase is complete; Implementers and Reviewers use `<commit_message>` instead | done signal, completion tag |
|
|
123
|
+
| **`AgentOutputProtocolError`** | Base exception raised by the agent output protocol module when a required tag is missing or malformed; subclassed by `PlanParseError`, `IssueParseError`, `PromiseParseError`, and `CommitMessageParseError` | parse error, protocol error |
|
|
124
|
+
| **`CommitMessageParseError`** | Subclass of `AgentOutputProtocolError` raised when an Implementer or Reviewer completes without emitting a `<commit_message>` tag; treated as a failed run — worktree is preserved and the agent is restarted | — |
|
|
125
|
+
| **`CommitMessageOutput`** | Typed output returned by `process_stream` for IMPLEMENTER and REVIEWER roles; carries the agent's plain `message: str`; the orchestrator prepends the RALPH prefix before committing | — |
|
|
126
|
+
| **`process_stream()`** | Single entry point in the agent output protocol module; accepts an iterable of decoded NDJSON lines, an `on_turn` callback, and an `AgentRole`; drives the per-line loop, emits complete assistant turns via the callback, raises `UsageLimitError` immediately on detection of a 429 error response, unwraps the result envelope, and returns a typed `AgentOutput`; the container runner is the only caller — phases never call it directly | protocol entry point, stream processor |
|
|
124
127
|
| **`on_turn` callback** | A `Callable[[str], None]` passed to `process_stream` by the container runner; invoked once per complete assistant turn during the Work phase; constructed by the container runner as a lambda over `StatusDisplay.print` so the agent output protocol module has no dependency on `StatusDisplay` | turn callback, display hook |
|
|
125
128
|
| **Claude streaming envelope** | The NDJSON format Claude Code uses for structured output; lines are JSON objects and the agent's final result is carried in the `{"type": "result", "result": "..."}` line; unwrapped internally by `process_stream` before tag extraction | streaming format, NDJSON output |
|
|
126
129
|
|
|
@@ -128,7 +131,7 @@
|
|
|
128
131
|
|
|
129
132
|
| Term | Definition | Aliases to avoid |
|
|
130
133
|
| --- | --- | --- |
|
|
131
|
-
| **agent lifecycle phase** | One of
|
|
134
|
+
| **agent lifecycle phase** | One of three named stages (Setup, Pre-flight, Work) within a single agent container run; the Prepare phase was retired as a distinct stage — prompt rendering is now an internal step of the Work phase | step, stage |
|
|
132
135
|
| **Setup phase** | First agent lifecycle phase: worktree creation, gitdir overlay creation, parent git dir mount wiring, container start, git identity propagation, and consuming project dependency installation (`pip install -e '.[dev]'` or `pip install -r requirements.txt`); any tool referenced in PREFLIGHT_CHECKS must be declared in the consuming project's dependency file — the image does not provide dev tools as a fallback | container setup, init phase |
|
|
133
136
|
| **Pre-flight phase** | Second agent lifecycle phase: runs quality checks sequentially inside the container and returns a list of failure tuples to the orchestrator; does not spawn agents internally | preflight, pre-flight check phase |
|
|
134
137
|
| **quality check** | One command run during the Pre-flight phase, as defined in PREFLIGHT_CHECKS; each runs independently so all failures are collected in a single pass | quality gate, check |
|
|
@@ -137,8 +140,7 @@
|
|
|
137
140
|
| **pre-existing failure** | A pre-flight failure that existed before the current agent's task began; root cause of scope creep | baseline failure |
|
|
138
141
|
| **scope creep** | The behavior where an agent modifies files outside its assigned task scope, typically caused by inheriting pre-existing failures | overreach |
|
|
139
142
|
| **skip_preflight** | Flag on `run_agent()` that bypasses the Pre-flight phase; always True for the preflight-issue agent; defaults to False for all other agents | — |
|
|
140
|
-
| **
|
|
141
|
-
| **Work phase** | Fourth agent lifecycle phase: Claude Code invocation and streaming output collection | execution phase, run phase |
|
|
143
|
+
| **Work phase** | Third agent lifecycle phase: prompt rendering and injection into the container, followed by Claude Code invocation and streaming output collection; prompt preparation is an internal step of `ContainerRunner.work()` — not a separate phase or method call | execution phase, run phase |
|
|
142
144
|
| **git identity propagation** | Setup phase operation that reads the host `git user.name` and `git user.email` and configures them inside the container | git config injection, user setup |
|
|
143
145
|
| **idle timeout** | Maximum wall-clock seconds an agent may produce no output before being killed and raising AgentTimeoutError; default 300 s | inactivity timeout, silence timeout |
|
|
144
146
|
| **worktree timeout** | Maximum wall-clock seconds a git worktree operation may take before raising WorktreeTimeoutError; default 30 s | git timeout |
|
|
@@ -149,7 +151,9 @@
|
|
|
149
151
|
| Term | Definition | Aliases to avoid |
|
|
150
152
|
| --- | --- | --- |
|
|
151
153
|
| **Dockerfile** | File in the pycastle directory defining the Docker image for agent containers — ships without baked-in credentials and without baked-in dev tools; system utilities (git, gh), Claude Code CLI, and the Python runtime are the only baked-in contents; all dev tools (e.g. ruff, mypy, pytest) must be declared in the consuming project's dependency file and are installed at runtime during the Setup phase | image definition |
|
|
152
|
-
| **
|
|
154
|
+
| **DockerSession** | Module in `docker_session.py` that owns Docker container lifecycle and low-level I/O; constructed from a pre-computed volume spec, filtered container environment, image name, config, and an optional `auto_overlay` path to delete on exit; exposes `exec_simple(command, timeout) → str` and `write_file(content, container_path)`; used by `ContainerRunner` as its Docker substrate — no agent-protocol concepts live here | docker client, container manager |
|
|
155
|
+
| **`build_volume_spec`** | Pure-ish function in `docker_session.py` that computes the complete Docker volume specification for a container run from host paths; owns the necessary file I/O: reads the `.git` file to locate the parent git dir, creates the gitdir overlay on Windows when needed; returns `(volumes_dict, auto_overlay)` where `auto_overlay` is a host path `DockerSession.__exit__` must delete, or `None` if no overlay was created | volume builder, mount spec |
|
|
156
|
+
| **container runner** | Package module that drives the three agent lifecycle phases (Setup, Pre-flight, Work) inside a `DockerSession`; constructed with a name, a `DockerSession` instance, model, effort, `status_display`, and config; delegates all Docker I/O to the session; during the Work phase renders the prompt, writes it to the container, then drives `WorkStream` for byte chunking, log writing, idle timeout detection, and delegates the line stream to `process_stream` | docker wrapper |
|
|
153
157
|
| **host repo** | The git repository on the developer's machine that is mounted into each agent container | project repo, local repo |
|
|
154
158
|
| **volume mount** | A Docker bind mount attaching a host filesystem path to a container-internal path, with an explicit read/write mode | bind mount, volume |
|
|
155
159
|
| **RO mount** | A volume mount with `mode: "ro"` — the container cannot write to it; used for the host repo | read-only mount |
|
|
@@ -162,12 +166,13 @@
|
|
|
162
166
|
| **new-branch path** | The `git worktree add -b <branch> <path> <safe-SHA>` form used when the branch does not yet exist; always branched from the pinned safe SHA rather than HEAD | — |
|
|
163
167
|
| **existing-branch path** | The `git worktree add <path> <branch>` form used when the branch already exists | — |
|
|
164
168
|
| **worktree contents check** | Guard step run after `git worktree add` that verifies `pyproject.toml` or `requirements.txt` is present; fails with the worktree path and directory listing if absent | checkout guard, file check |
|
|
165
|
-
| **`detached_worktree`** | Async context manager in `worktree.py` that creates a detached checkout at a given SHA, yields the path, and guarantees removal in `__aexit__` regardless of outcome; used by `planning_phase` and `preflight_phase` for their sandbox worktrees | managed_worktree |
|
|
166
|
-
| **`branch_worktree`** | Async context manager in `worktree.py` that creates a named-branch worktree at a given SHA, yields the path, and on exit removes the worktree
|
|
169
|
+
| **`detached_worktree`** | Async context manager in `worktree.py` that creates a detached checkout at a given SHA, yields the path, and guarantees removal in `__aexit__` regardless of outcome; also removes the `.worktrees` parent directory if no other worktrees remain after cleanup; used by `planning_phase` and `preflight_phase` for their sandbox worktrees | managed_worktree |
|
|
170
|
+
| **`branch_worktree`** | Async context manager in `worktree.py` that creates a named-branch worktree at a given SHA, yields the path, and on exit removes the worktree, optionally deletes the branch, and removes the `.worktrees` parent directory if no other worktrees remain; used by `merge_phase` for the merge-sandbox worktree | managed_worktree |
|
|
167
171
|
| **`_agent_worktree`** | Async context manager in `implement.py` that owns the full Implementer and Reviewer worktree lifecycle; accepts a branch name, SHA, `CancellationToken`, and `Deps`; on entry creates the worktree and gitdir overlay; on exit conditionally removes the worktree based on `token.wants_worktree_preserved` and working-tree cleanliness, and always removes the gitdir overlay; used by `run_issue` twice per issue — once for the Implementer (new-branch path) and once for the Reviewer (existing-branch path); defined in `implement.py` not `worktree.py` because its cleanup policy depends on agent-lifecycle state (`CancellationToken`) rather than being unconditional | managed_worktree |
|
|
168
172
|
| **`worktree_name_for_branch`** | Function in `worktree.py` that derives a short directory name from a branch string: extracts `issue-N` from `pycastle/issue-N-slug` or falls back to a sanitised slug; single authoritative definition replacing duplicated regex in `agent_runner` and `merge_phase` | — |
|
|
169
173
|
| **`worktree_path`** | Function in `worktree.py` that constructs the host filesystem path for a named worktree at `<repo_root>/<pycastle_dir>/.worktrees/<name>`; single authoritative path expression replacing duplication across all phase modules | — |
|
|
170
174
|
| **runtime injection** | The act of reading `~/.claude.json` from the host and writing it to `/home/agent/.claude.json` inside a container before the agent runs | baking in, build-time config |
|
|
175
|
+
| **WorkStream** | Class in `stream_session.py` that converts a raw Docker byte stream into an `AgentOutput`; constructed with a byte-chunk iterator, a log path, an idle timeout, and an `on_chunk: Callable[[], None]` callback; its `run(role, on_turn) → AgentOutput` method drives a feeder thread, writes each byte chunk to the log file (flushed immediately), calls `on_chunk()` per chunk, detects idle timeouts (raising `AgentTimeoutError`), splits bytes into complete UTF-8 lines, and delegates to `process_stream`; the only caller is `ContainerRunner.run_streaming`, which passes `status_display.reset_idle_timer` as the `on_chunk` callback | stream session, work session |
|
|
171
176
|
| **StreamParser** | Retired — its assistant-turn assembly logic is now a private implementation detail of `process_stream` in the agent output protocol module; `stream_parser.py` no longer exists as a public module | stream processor, message parser |
|
|
172
177
|
| **agent message** | The text content emitted by an agent during a single assistant turn; excludes tool-use and tool-result blocks; during the Work phase, printed to the console prefixed with the agent name and followed by a blank line; not shown in the status panel | assistant message, agent output |
|
|
173
178
|
| **PycastleError** | Base exception class for all pycastle domain errors | — |
|
|
@@ -191,14 +196,19 @@
|
|
|
191
196
|
| **ClaudeService** | Service that encapsulates the `claude list-models` subprocess call with process-lifetime caching | Claude wrapper, model provider |
|
|
192
197
|
| **DockerService** | Service that encapsulates the `docker build` subprocess call with support for build args | Docker wrapper, build provider |
|
|
193
198
|
| **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 |
|
|
199
|
+
| **`Deps`** | Concrete dataclass constructed once per iteration in the orchestrator and passed to `run_iteration`; bundles the full set of iteration-layer dependencies: `repo_root`, `git_svc`, `github_svc`, `agent_runner`, `cfg`, `logger`, and `status_display`; satisfies every per-phase dependency protocol via structural typing so the orchestrator passes it unmodified; `env` is intentionally absent — it is consumed at `AgentRunner` construction time before `Deps` is built and is not threaded through the iteration layer | iteration context, deps container |
|
|
200
|
+
| **per-phase dependency protocol** | A private `Protocol` class declared in each phase module listing only the fields that phase actually accesses; `Deps` satisfies every protocol via structural typing; tests construct minimal inline dataclasses with only the required fields instead of building a full `Deps`; follows the `_WorktreeDeps` pattern established in `worktree.py`; individual protocols: `_PreflightDeps` (in `preflight.py`), `_PlanningDeps` (in `planning.py`), `_ImplementDeps` (in `implement.py`), `_MergeDeps` (in `merge.py`), `_UtilDeps` (in `_utils.py`) | deps narrowing, phase context |
|
|
201
|
+
| **`_WorktreeDeps`** | Private protocol in `worktree.py` listing only the fields that worktree utilities need (`repo_root`, `cfg`, `git_svc`); the original instance of the per-phase dependency protocol pattern; satisfied by `Deps` structurally | — |
|
|
194
202
|
| **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 |
|
|
195
203
|
| **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 |
|
|
196
|
-
| **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 |
|
|
197
|
-
| **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;
|
|
198
|
-
| **
|
|
199
|
-
| **
|
|
200
|
-
| **
|
|
201
|
-
| **
|
|
204
|
+
| **StatusDisplay** | Injectable abstraction that owns the live terminal status panel and all formatted terminal output; exposes `register(caller, kind, startup_message="started", work_body="", initial_phase="Setup")`, `update_phase`, `reset_idle_timer`, `remove(caller, shutdown_message="finished", shutdown_style="success")`, and `print(caller, message, style=None)` methods; `kind` is a required `Literal["phase", "agent"]` discriminator stored per-caller and consulted by the blank-line rule (no default — every call site must classify its caller explicitly); `shutdown_message` and `message` may contain `\n` — each line is emitted separately with the `[Caller]` prefix and the same style applied to every line; `shutdown_style` accepts `"success"` (green), `"error"` (red), or `"warning"` (yellow); backed by a `rich` `Live` display in production and a `PlainStatusDisplay` in tests; injected via `Deps` as a separate concern from `Logger`; defined in `status_display` module | terminal display, status bar |
|
|
205
|
+
| **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; canonical callers — phase rows (`kind="phase"`): `"Preflight"`, `"Plan"`, `"Implement"`, `"Merge"`; agents (`kind="agent"`): `"Preflight Agent"`, `"Plan Agent"`, `"Implement Agent #N"`, `"Review Agent #N"`, `"Merge Agent"` | source, label |
|
|
206
|
+
| **blank-line rule** | The rule the display applies before any output call (`register`, `remove`, or `print`) to decide whether to emit a separating blank line; a blank line is inserted iff (a) the caller is anonymous (`""`, always isolated), or (b) the caller differs from the previous caller AND the (previous-kind, current-kind) pair is *not* `("phase","agent")` or `("agent","phase")`; effect: a phase row and the agent rows it spawns render as one uninterrupted block, while phase→different-phase and agent→different-agent transitions keep their blank line; the very first output call also gets a leading blank line; `update_phase` and `reset_idle_timer` never touch the rule's state; `print` from a registered-but-unknown-kind caller falls through to "blank line yes" as a safe default | separator rule |
|
|
207
|
+
| **work_body** | The caller-constructed string passed as the third argument to `register`; applies to agent rows only; displayed in the body column during the Work phase; empty string for agent rows that do not reach Work; unused by phase rows (which use `initial_phase` for their fixed body label) | — |
|
|
208
|
+
| **PlainStatusDisplay** | Plain-terminal adapter for `StatusDisplay` defined in `status_display` module; panel methods (`update_phase`, `reset_idle_timer`) are no-ops; `register` and `remove` print their startup/shutdown messages; `print(caller, message, style=None)` formats output as `[Caller] message` with no ANSI colour codes, no bold, and style ignored; multi-line messages are split and each line prefixed with `[Caller]`; used in tests so assertions can match the full formatted line | NullStatusDisplay |
|
|
209
|
+
| **phase_row** | Async context manager in `iteration/` that owns the `StatusDisplay` register/remove lifecycle for a single phase row; accepts `startup_message: str = "started"` forwarded to `register`; on entry calls `register(caller, kind="phase", startup_message=startup_message, initial_phase=initial_phase)` — the single source of truth for `kind="phase"`; yields a `PhaseRow` whose `close(shutdown_message, shutdown_style="success")` method calls `remove()` and marks the row as closed; if `close()` is never called before exit (exception path), automatically calls `remove(caller, "failed", shutdown_style="error")`; the canonical way to manage phase row lifecycle — replaces hand-rolled active-flag patterns; all four phase rows (Preflight, Plan, Implement, Merge) register through this wrapper | — |
|
|
210
|
+
| **status row** | One headerless line in the `StatusDisplay` live panel; created by `register` and removed by `remove`; two kinds: **agent rows** (one per active agent — `"Preflight Agent"`, `"Plan Agent"`, `"Implement Agent #N"`, `"Review Agent #N"`, `"Merge Agent"`) and **phase rows** (one per active phase — `"Preflight"`, `"Plan"`, `"Implement"`, `"Merge"`); phase rows and agent rows within the same phase coexist; format: `elapsed \| Name \| idle \| body`; elapsed is dim and right-justified; name is bold with any numeric part styled bold cyan; idle is dim; body column: for **agent rows**, shows the current agent lifecycle phase name for all non-Work states, or `work_body` during Work; for **phase rows**, shows a body derived from the phase: `"Planning"` for Plan, `"Merging"` for Merge, `"Running"` for Preflight; for the **Implement phase row** specifically, the body is dynamic — `"Running: started Agents for X/Y issues"` where Y is the total issue count for the phase and X increments each time an agent acquires the concurrency semaphore (monotonic; either an Implement Agent or Review Agent counts); 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 |
|
|
211
|
+
| **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 — carries `reset_time: datetime | None`; worktrees preserved; orchestrator sleeps until `reset_time + 2 min` when parsed from the Claude message, or until 2 minutes past the next local-time full hour when the reset time cannot be parsed; status message appends `"(estimated)"` on the fallback path; continues the loop to retry the current issue from scratch; repeats indefinitely on consecutive hits) | iteration result, loop result |
|
|
202
212
|
|
|
203
213
|
## Test Anti-Patterns (Red Flags)
|
|
204
214
|
|
|
@@ -213,7 +223,7 @@
|
|
|
213
223
|
## Relationships
|
|
214
224
|
|
|
215
225
|
- **STAGE_OVERRIDES** has exactly four entries, one per orchestration phase (`plan`, `implement`, `review`, `merge`); each entry has independent `model` and `effort` fields — an empty string for either means CLI default (no flag injected).
|
|
216
|
-
-
|
|
226
|
+
- **`load_config()`** is a pure function — no subprocess calls; it validates effort strings against the fixed set and raises `ConfigValidationError` on any invalid entry; model strings (shorthands or full IDs) are stored as-is in `Config` and resolved by the Claude CLI at stage execution time (see ADR 0002).
|
|
217
227
|
- The **Planner** produces one plan per iteration listing only unblocked AFK issues; blockers and HITL issues are excluded via the dependency graph.
|
|
218
228
|
- Each AFK issue in a plan is processed by exactly one **Implementer** followed by one **Reviewer**.
|
|
219
229
|
- The **merge phase** attempts the programmatic merge path for every branch sequentially; the **Merger** is spawned at most once per iteration and only when conflicting branches exist.
|
|
@@ -222,7 +232,7 @@
|
|
|
222
232
|
- 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.
|
|
223
233
|
- 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.
|
|
224
234
|
- 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
|
|
235
|
+
- 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: Implement -` → implement skip only).
|
|
226
236
|
- 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.
|
|
227
237
|
- 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.
|
|
228
238
|
- 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.
|
|
@@ -231,10 +241,13 @@
|
|
|
231
241
|
- 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`.
|
|
232
242
|
- 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.
|
|
233
243
|
- **StatusDisplay** is a separate injectable in `Deps` alongside `Logger`; `Logger` owns file I/O, `StatusDisplay` owns the live terminal UI — they never overlap.
|
|
234
|
-
-
|
|
235
|
-
-
|
|
244
|
+
- **`Deps`** does not carry `env`; credentials are extracted from the environment in `main.py`, passed directly to `AgentRunner` at construction time, and are not accessible to any iteration-layer phase. Phase functions never reference `env` directly.
|
|
245
|
+
- Each phase module declares its own **per-phase dependency protocol** listing only its actual field accesses; `Deps` satisfies all of them structurally so the orchestrator passes it unchanged; tests construct minimal inline dataclasses with only the required fields. `_WorktreeDeps` in `worktree.py` is the established precedent for this pattern.
|
|
246
|
+
- Rich markup (e.g. `[red]...[/red]`) must never be embedded in a `StatusDisplay.print` message string; colouring is expressed exclusively via the `style` parameter (`"error"`, `"success"`, `"warning"`).
|
|
247
|
+
- A **status row** is created by `StatusDisplay.register` and removed by `StatusDisplay.remove`; phase rows are managed via the **`phase_row`** context manager (which sets `kind="phase"`) — registered on entry and removed (with the phase outcome as the shutdown message) via `PhaseRow.close()`; agent rows are registered directly at container Setup with `kind="agent"` 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. The **blank-line rule** consults the registered kind to suppress the separator between a phase row and the agent rows it spawns (in either direction).
|
|
236
248
|
- 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.
|
|
237
|
-
- During the Work phase the container runner owns byte chunking, byte-to-line splitting, log writing, and idle timeout detection
|
|
249
|
+
- During the Work phase the container runner renders and injects the prompt, then owns byte chunking, byte-to-line splitting, log writing, and idle timeout detection via `WorkStream`; it passes the decoded NDJSON line stream and an **`on_turn` callback** to **`process_stream`**, which assembles assistant turns (invoking the callback for each), detects 429 error responses via `_check_usage_limit` and raises `UsageLimitError(reset_time)` immediately (where `reset_time: datetime | None` is parsed from the Claude message and converted to local time), unwraps the result envelope, and returns a typed `AgentOutput`; phases receive `AgentOutput` directly from `AgentRunner.run()` — no phase calls `parse()` or `assert_complete()`. Setup and Pre-flight phases produce no console output — their activity is reflected only in the body column of the agent status row.
|
|
250
|
+
- **`AgentRunner`** constructs a `DockerSession` (calling `build_volume_spec` to resolve volume paths) and a `ContainerRunner` (passing the session), then orchestrates the three lifecycle phases; it is the only caller of `build_volume_spec` and the owner of `CLAUDE_ACCOUNT_JSON` injection into the session.
|
|
238
251
|
|
|
239
252
|
## Example dialogue
|
|
240
253
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# ADR 0002: CLI-native model shorthand resolution over load-time API call
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
**Date:** 2026-05-03
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
`Config.plan_override.model` (and the equivalent fields for implement, review, and merge stages) accepts either a full Claude model ID (`claude-sonnet-4-6`) or a shorthand (`sonnet`). Before this decision, `load_config` resolved shorthands to full model IDs at config load time by calling `claude_service.list_models()` — a subprocess call to the Claude CLI — and selecting the latest matching model.
|
|
9
|
+
|
|
10
|
+
Two approaches were considered:
|
|
11
|
+
|
|
12
|
+
**Option A — Resolve at load time (previous behaviour):**
|
|
13
|
+
`load_config` instantiates `ClaudeService`, calls `list_models()`, and replaces any shorthand in the config with the resolved full model ID before returning. The returned `Config` always contains fully-resolved model IDs.
|
|
14
|
+
|
|
15
|
+
**Option B — Pass through to the CLI:**
|
|
16
|
+
`load_config` is pure (file I/O only). The model string is passed as-is to the Claude CLI at stage execution time. The CLI resolves shorthands natively.
|
|
17
|
+
|
|
18
|
+
## Decision
|
|
19
|
+
|
|
20
|
+
**Option B.** Model shorthand resolution is delegated to the Claude CLI at stage execution time.
|
|
21
|
+
|
|
22
|
+
## Reasons
|
|
23
|
+
|
|
24
|
+
- **Hidden interface cost.** Option A makes `load_config` appear to be a pure file-loading operation but introduces a subprocess call as a hidden side effect. Callers — including tests — must know to mock `ClaudeService` to avoid hitting the CLI.
|
|
25
|
+
- **Verified CLI support.** The Claude CLI accepts shorthands directly (`claude --model sonnet` works). There is no need to pre-resolve them.
|
|
26
|
+
- **Locality of validation.** Invalid model strings surface as CLI errors at the point of use, where the context (which stage, which run) is most relevant.
|
|
27
|
+
- **Testability.** A pure `load_config` can be tested with plain `Config` comparisons and no mocks.
|
|
28
|
+
|
|
29
|
+
## Consequences
|
|
30
|
+
|
|
31
|
+
- `Config.plan_override.model` (and equivalent fields) may hold a shorthand or a full model ID — callers cannot distinguish between them by type alone.
|
|
32
|
+
- Invalid model strings are not caught at startup. A bad model value surfaces as a CLI error when the relevant stage first runs, not when config is loaded.
|
|
33
|
+
- `validator.py` and its `_fetch_models` / `_resolve_shorthand` machinery are removed. Effort validation (a pure set-membership check) moves inline into `load_config`.
|
|
34
|
+
- `load_config` no longer accepts or instantiates a `claude_service` argument.
|
|
@@ -3,10 +3,15 @@ import enum
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
5
|
from collections.abc import Callable, Iterable
|
|
6
|
-
from
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
from typing import Literal, TypeAlias
|
|
7
8
|
|
|
8
9
|
from .errors import UsageLimitError
|
|
9
10
|
|
|
11
|
+
_RESET_TIME_RE = re.compile(
|
|
12
|
+
r"resets\s+(\d{1,2}:\d{2}(?:am|pm))\s+\(UTC\)", re.IGNORECASE
|
|
13
|
+
)
|
|
14
|
+
|
|
10
15
|
|
|
11
16
|
class AgentRole(enum.Enum):
|
|
12
17
|
PLANNER = "planner"
|
|
@@ -32,7 +37,14 @@ class CompletionOutput:
|
|
|
32
37
|
pass
|
|
33
38
|
|
|
34
39
|
|
|
35
|
-
|
|
40
|
+
@dataclasses.dataclass(frozen=True)
|
|
41
|
+
class CommitMessageOutput:
|
|
42
|
+
message: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
AgentOutput: TypeAlias = (
|
|
46
|
+
PlannerOutput | IssueOutput | CompletionOutput | CommitMessageOutput
|
|
47
|
+
)
|
|
36
48
|
|
|
37
49
|
|
|
38
50
|
class AgentOutputProtocolError(Exception):
|
|
@@ -51,6 +63,10 @@ class PromiseParseError(AgentOutputProtocolError):
|
|
|
51
63
|
pass
|
|
52
64
|
|
|
53
65
|
|
|
66
|
+
class CommitMessageParseError(AgentOutputProtocolError):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
54
70
|
def _extract_planner_output(text: str) -> PlannerOutput:
|
|
55
71
|
match = re.search(r"<plan>([\s\S]*?)</plan>", text)
|
|
56
72
|
if not match:
|
|
@@ -99,23 +115,30 @@ def _extract_issue_output(text: str) -> IssueOutput:
|
|
|
99
115
|
return IssueOutput(labels=labels, number=number)
|
|
100
116
|
|
|
101
117
|
|
|
102
|
-
def
|
|
118
|
+
def _check_usage_limit(line: str) -> datetime | None | Literal[False]:
|
|
103
119
|
try:
|
|
104
120
|
obj = json.loads(line)
|
|
105
|
-
if isinstance(obj, dict):
|
|
106
|
-
if obj.get("type") == "result" and obj.get("is_error"):
|
|
107
|
-
if obj.get("api_error_status") == 429:
|
|
108
|
-
return True
|
|
109
|
-
result_text = obj.get("result")
|
|
110
|
-
if isinstance(result_text, str) and any(
|
|
111
|
-
p.lower() in result_text.lower() for p in patterns
|
|
112
|
-
):
|
|
113
|
-
return True
|
|
114
|
-
return False
|
|
115
121
|
except json.JSONDecodeError:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
return False
|
|
123
|
+
if not isinstance(obj, dict) or obj.get("api_error_status") != 429:
|
|
124
|
+
return False
|
|
125
|
+
result_text = obj.get("result")
|
|
126
|
+
if not isinstance(result_text, str):
|
|
127
|
+
return None
|
|
128
|
+
match = _RESET_TIME_RE.search(result_text)
|
|
129
|
+
if not match:
|
|
130
|
+
return None
|
|
131
|
+
try:
|
|
132
|
+
parsed = datetime.strptime(match.group(1).lower(), "%I:%M%p").time()
|
|
133
|
+
except ValueError:
|
|
134
|
+
return None
|
|
135
|
+
today_utc = datetime.now(timezone.utc).date()
|
|
136
|
+
utc_dt = datetime.combine(today_utc, parsed, tzinfo=timezone.utc)
|
|
137
|
+
local_dt = utc_dt.astimezone().replace(tzinfo=None)
|
|
138
|
+
now_local = datetime.now()
|
|
139
|
+
if local_dt < now_local - timedelta(minutes=2):
|
|
140
|
+
local_dt += timedelta(days=1)
|
|
141
|
+
return local_dt
|
|
119
142
|
|
|
120
143
|
|
|
121
144
|
def _extract_turn(line: str) -> str | None:
|
|
@@ -135,22 +158,29 @@ def _extract_turn(line: str) -> str | None:
|
|
|
135
158
|
return "\n\n".join(parts) if parts else None
|
|
136
159
|
|
|
137
160
|
|
|
161
|
+
_COMMIT_MESSAGE_RE = re.compile(r"<commit_message>([\s\S]*?)</commit_message>")
|
|
162
|
+
|
|
163
|
+
|
|
138
164
|
def process_stream(
|
|
139
165
|
lines: Iterable[str],
|
|
140
166
|
on_turn: Callable[[str], None],
|
|
141
167
|
role: AgentRole,
|
|
142
|
-
usage_limit_patterns: tuple[str, ...],
|
|
143
168
|
) -> AgentOutput:
|
|
144
169
|
collected: list[str] = []
|
|
145
170
|
result_text: str | None = None
|
|
146
171
|
for line in lines:
|
|
147
172
|
collected.append(line)
|
|
148
|
-
|
|
149
|
-
|
|
173
|
+
usage_limit = _check_usage_limit(line)
|
|
174
|
+
if usage_limit is not False:
|
|
175
|
+
raise UsageLimitError(reset_time=usage_limit)
|
|
150
176
|
turn = _extract_turn(line)
|
|
151
177
|
if turn is not None:
|
|
152
178
|
on_turn(turn)
|
|
153
|
-
if role in (AgentRole.IMPLEMENTER, AgentRole.REVIEWER
|
|
179
|
+
if role in (AgentRole.IMPLEMENTER, AgentRole.REVIEWER):
|
|
180
|
+
match = _COMMIT_MESSAGE_RE.search(turn)
|
|
181
|
+
if match:
|
|
182
|
+
return CommitMessageOutput(message=match.group(1).strip())
|
|
183
|
+
elif role == AgentRole.MERGER:
|
|
154
184
|
if re.search(r"<promise>COMPLETE</promise>", turn):
|
|
155
185
|
return CompletionOutput()
|
|
156
186
|
elif role == AgentRole.PLANNER:
|
|
@@ -184,6 +214,13 @@ def process_stream(
|
|
|
184
214
|
return _extract_planner_output(text)
|
|
185
215
|
except PlanParseError as exc:
|
|
186
216
|
raise PlanParseError(f"{exc}{tail}") from exc.__cause__
|
|
217
|
+
if role in (AgentRole.IMPLEMENTER, AgentRole.REVIEWER):
|
|
218
|
+
match = _COMMIT_MESSAGE_RE.search(text)
|
|
219
|
+
if not match:
|
|
220
|
+
raise CommitMessageParseError(
|
|
221
|
+
f"Agent produced no <commit_message> tag.{tail}"
|
|
222
|
+
)
|
|
223
|
+
return CommitMessageOutput(message=match.group(1).strip())
|
|
187
224
|
if not re.search(r"<promise>COMPLETE</promise>", text):
|
|
188
225
|
raise PromiseParseError(
|
|
189
226
|
f"Agent produced no <promise>COMPLETE</promise> tag.{tail}"
|
|
@@ -6,6 +6,7 @@ from .agent_output_protocol import AgentOutput, AgentRole
|
|
|
6
6
|
from .agent_result import CancellationToken, PreflightFailure
|
|
7
7
|
from .config import Config
|
|
8
8
|
from .container_runner import ContainerRunner
|
|
9
|
+
from .docker_session import DockerSession, build_volume_spec
|
|
9
10
|
from .errors import AgentTimeoutError, UsageLimitError
|
|
10
11
|
from .services import GitService
|
|
11
12
|
from .status_display import PlainStatusDisplay
|
|
@@ -55,6 +56,25 @@ class AgentRunner:
|
|
|
55
56
|
self._git_service = git_service
|
|
56
57
|
self._docker_client = docker_client
|
|
57
58
|
|
|
59
|
+
def _build_session(self, mount_path: Path) -> DockerSession:
|
|
60
|
+
volumes, auto_overlay = build_volume_spec(mount_path)
|
|
61
|
+
container_env = {
|
|
62
|
+
k: v for k, v in self._env.items() if k != "CLAUDE_ACCOUNT_JSON"
|
|
63
|
+
}
|
|
64
|
+
return DockerSession(
|
|
65
|
+
volumes=volumes,
|
|
66
|
+
container_env=container_env,
|
|
67
|
+
image_name=self._cfg.docker_image_name,
|
|
68
|
+
cfg=self._cfg,
|
|
69
|
+
docker_client=self._docker_client,
|
|
70
|
+
auto_overlay=auto_overlay,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def _inject_claude_credentials(self, session: DockerSession) -> None:
|
|
74
|
+
claude_json = self._env.get("CLAUDE_ACCOUNT_JSON")
|
|
75
|
+
if claude_json:
|
|
76
|
+
session.write_file(claude_json, "/home/agent/.claude.json")
|
|
77
|
+
|
|
58
78
|
async def run(self, request: RunRequest) -> AgentOutput | PreflightFailure:
|
|
59
79
|
name = request.name
|
|
60
80
|
prompt_file = request.prompt_file
|
|
@@ -72,23 +92,23 @@ class AgentRunner:
|
|
|
72
92
|
|
|
73
93
|
_token = token if token is not None else CancellationToken()
|
|
74
94
|
if _token.is_cancelled:
|
|
75
|
-
raise UsageLimitError(
|
|
95
|
+
raise UsageLimitError(reset_time=None)
|
|
76
96
|
|
|
97
|
+
session = self._build_session(mount_path)
|
|
77
98
|
runner = ContainerRunner(
|
|
78
99
|
name,
|
|
79
|
-
|
|
80
|
-
self._env,
|
|
100
|
+
session,
|
|
81
101
|
model=model,
|
|
82
102
|
effort=effort,
|
|
83
|
-
docker_client=self._docker_client,
|
|
84
103
|
status_display=status_display,
|
|
85
104
|
cfg=self._cfg,
|
|
86
105
|
)
|
|
106
|
+
status_display.register(name, "agent", work_body=work_body)
|
|
87
107
|
try:
|
|
88
108
|
git_name = self._git_service.get_user_name()
|
|
89
109
|
git_email = self._git_service.get_user_email()
|
|
90
110
|
await runner.setup(git_name, git_email, work_body)
|
|
91
|
-
|
|
111
|
+
self._inject_claude_credentials(session)
|
|
92
112
|
if not skip_preflight:
|
|
93
113
|
failures = await runner.preflight(list(self._cfg.preflight_checks))
|
|
94
114
|
if failures:
|
|
@@ -96,8 +116,9 @@ class AgentRunner:
|
|
|
96
116
|
retries_left = self._cfg.timeout_retries
|
|
97
117
|
while True:
|
|
98
118
|
try:
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
return await runner.work(
|
|
120
|
+
request.role, prompt_file, prompt_args or {}
|
|
121
|
+
)
|
|
101
122
|
except AgentTimeoutError:
|
|
102
123
|
if retries_left <= 0:
|
|
103
124
|
raise
|
|
@@ -114,7 +135,7 @@ class AgentRunner:
|
|
|
114
135
|
finally:
|
|
115
136
|
status_display.remove(name)
|
|
116
137
|
try:
|
|
117
|
-
|
|
138
|
+
session.__exit__(None, None, None)
|
|
118
139
|
except Exception:
|
|
119
140
|
pass
|
|
120
141
|
|
|
@@ -132,20 +153,21 @@ class AgentRunner:
|
|
|
132
153
|
|
|
133
154
|
git_name = self._git_service.get_user_name()
|
|
134
155
|
git_email = self._git_service.get_user_email()
|
|
156
|
+
session = self._build_session(mount_path)
|
|
135
157
|
runner = ContainerRunner(
|
|
136
158
|
name,
|
|
137
|
-
|
|
138
|
-
self._env,
|
|
139
|
-
docker_client=self._docker_client,
|
|
159
|
+
session,
|
|
140
160
|
status_display=status_display,
|
|
141
161
|
cfg=self._cfg,
|
|
142
162
|
)
|
|
163
|
+
status_display.register(name, "agent", work_body=work_body)
|
|
143
164
|
try:
|
|
144
165
|
await runner.setup(git_name, git_email, work_body)
|
|
166
|
+
self._inject_claude_credentials(session)
|
|
145
167
|
return await runner.preflight(list(self._cfg.preflight_checks))
|
|
146
168
|
finally:
|
|
147
169
|
status_display.remove(name)
|
|
148
170
|
try:
|
|
149
|
-
|
|
171
|
+
session.__exit__(None, None, None)
|
|
150
172
|
except Exception:
|
|
151
173
|
pass
|
|
@@ -2,6 +2,5 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from pycastle._types import StageOverride
|
|
4
4
|
from pycastle.config.loader import Config, load_config
|
|
5
|
-
from pycastle.config.validator import validate_config
|
|
6
5
|
|
|
7
|
-
__all__ = ["Config", "StageOverride", "load_config"
|
|
6
|
+
__all__ = ["Config", "StageOverride", "load_config"]
|