cli-agent-runner 0.1.7__tar.gz → 0.1.9__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.
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/CHANGELOG.md +86 -3
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/PKG-INFO +1 -1
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/api.py +14 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/api_types.py +2 -1
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/cli/common.py +6 -2
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/config.py +48 -5
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/events.py +6 -1
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/runner.py +18 -6
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/service_unit.py +9 -2
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/vcs_state.py +59 -1
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/docs/configuration.md +26 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/docs/plugins.md +125 -17
- cli_agent_runner-0.1.9/tests/_test_helpers.py +44 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/contract/test_public_api_surface.py +17 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/integration/test_context_enricher_namespacing.py +2 -10
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/integration/test_plugin_detector_loaded.py +2 -10
- cli_agent_runner-0.1.9/tests/integration/test_plugin_owned_paths.py +88 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/integration/test_run_one_round_with_fake_agent.py +47 -0
- cli_agent_runner-0.1.9/tests/invariants/test_module_sizes.py +22 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/invariants/test_peek_schema_version.py +2 -2
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_api_observation.py +59 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_api_types.py +33 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_config.py +244 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_detector_protocol.py +3 -10
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_events.py +3 -10
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_hook_failure_isolation.py +2 -10
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_hooks.py +6 -17
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_runner.py +59 -1
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_service_unit.py +34 -3
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_vcs_state.py +79 -0
- cli_agent_runner-0.1.7/tests/invariants/test_module_sizes.py +0 -49
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/.codecov.yml +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/.githooks/commit-msg +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/.gitignore +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/LICENSE +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/README.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/README.zh.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/SECURITY.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/agent_runtime.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/cli/serve_cmd.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/detector_helpers.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/build.sh +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/deploy/example-agent-runner.toml +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/deploy/launchd.plist.tmpl +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/deploy/run-loop.sh +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/deploy/systemd.service.tmpl +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/docs/README.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/docs/architecture.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/docs/commands.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/docs/runbook.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/pyproject.toml +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/conftest.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/integration/test_run_loop_backoff.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/invariants/test_architecture.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_detector_helpers.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.9}/tests/unit/test_startup_check.py +0 -0
|
@@ -7,6 +7,85 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.9] - 2026-05-13
|
|
11
|
+
|
|
12
|
+
### Acknowledgements
|
|
13
|
+
|
|
14
|
+
Thanks to the argus-gateway team for the dev/qa/product wall-time data
|
|
15
|
+
(Phase 4 feedback §3.1) that drove this API shape. Their three-role
|
|
16
|
+
distribution made the case for per-phase overrides concrete.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `[runtime.round_timeout_per_phase]` TOML block — per-phase overrides for
|
|
21
|
+
`round_timeout_s`. Unconfigured phases fall back to global. Keys validated
|
|
22
|
+
against `[phases] list` at config-load (typo catcher); non-positive values
|
|
23
|
+
rejected; bool / float values rejected (would otherwise silently coerce
|
|
24
|
+
to int).
|
|
25
|
+
- `agent_runner.runner._round_timeout_for(cfg, phase)` helper — single
|
|
26
|
+
lookup point for phase-aware timeout resolution.
|
|
27
|
+
|
|
28
|
+
### Migration
|
|
29
|
+
|
|
30
|
+
No breaking changes. Existing configs without the new block keep using a
|
|
31
|
+
single global timeout — identical to 0.1.8 behavior.
|
|
32
|
+
|
|
33
|
+
Plugin authors: no public API change. `RuntimeConfig` is not in the
|
|
34
|
+
documented plugin-author public surface.
|
|
35
|
+
|
|
36
|
+
## [0.1.8] - 2026-05-13
|
|
37
|
+
|
|
38
|
+
### Acknowledgements
|
|
39
|
+
|
|
40
|
+
Thanks to the argus-gateway team for Phase 4 dogfooding feedback that drove
|
|
41
|
+
every item in this release. 3 audit memos (~90KB) silently swept into an
|
|
42
|
+
orphan stash is a real-world failure mode; this release closes that loop.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- `agent_runner.vcs_state.register_plugin_owned_paths()` — plugins opt-out
|
|
47
|
+
files/dirs from orphan-stash defense. Matching: trailing-slash prefix or
|
|
48
|
+
`pathlib.PurePath.match` glob (recognizes `**` for recursive segments via
|
|
49
|
+
`fnmatch` fallback on Python 3.11). Call at module import (entry_point
|
|
50
|
+
side-effect).
|
|
51
|
+
- `agent_runner.vcs_state.plugin_owned_paths()` — snapshot accessor for peek.
|
|
52
|
+
- `ProjectState.recent_hook_failures: list[dict]` — last 10 `hook_failed`
|
|
53
|
+
events filtered from `recent_events` for debugging hook integration.
|
|
54
|
+
- peek schema bumped 1.4 → 1.5. `plugins` block now includes
|
|
55
|
+
`pre_round_hooks`, `post_round_hooks`, `owned_paths` lists.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- `docs/plugins.md` register-pattern examples corrected: registration must
|
|
60
|
+
happen as module-top side effect; entry_point loaders only import, they
|
|
61
|
+
do not invoke. Old `_register()` wrapper pattern silently didn't fire.
|
|
62
|
+
- `docs/plugins.md` gained "Declaring plugin-owned paths" and "Plugin tests
|
|
63
|
+
+ consumer pytest collision" sections.
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
|
|
67
|
+
- Plugin outputs in plugin-declared paths (e.g. `proposals/`,
|
|
68
|
+
`logs/plugins/my_plugin/`) no longer silently swept into orphan stashes
|
|
69
|
+
by `process_orphan_wip`. Previously: 90KB Argus audit memos invisible
|
|
70
|
+
after Phase 4 round; required stash archaeology to recover.
|
|
71
|
+
|
|
72
|
+
### Migration
|
|
73
|
+
|
|
74
|
+
No breaking changes. Plugin authors:
|
|
75
|
+
|
|
76
|
+
- If your plugin writes files to `work_dir` and they keep getting stashed
|
|
77
|
+
between rounds, opt them out:
|
|
78
|
+
```python
|
|
79
|
+
from agent_runner.vcs_state import register_plugin_owned_paths
|
|
80
|
+
register_plugin_owned_paths(["your-output-dir/", "logs/your-plugin/**/*"])
|
|
81
|
+
```
|
|
82
|
+
- If you followed the old `_register()` pattern from docs and noticed
|
|
83
|
+
registrations not firing: move the call to module top:
|
|
84
|
+
```python
|
|
85
|
+
# was: def _register(): register_pre_round_hook(MyHook())
|
|
86
|
+
# now: register_pre_round_hook(MyHook()) # module-top side-effect
|
|
87
|
+
```
|
|
88
|
+
|
|
10
89
|
## [0.1.7] - 2026-05-13
|
|
11
90
|
|
|
12
91
|
### Migration for existing 0.1.6 users (DOWNSTREAM CONSUMERS READ THIS)
|
|
@@ -288,6 +367,10 @@ Initial public release on PyPI as `cli-agent-runner`.
|
|
|
288
367
|
- Tag-triggered release publishing to PyPI via Trusted Publishing OIDC,
|
|
289
368
|
gated by a manual approval on the `pypi` GitHub environment.
|
|
290
369
|
|
|
291
|
-
[Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.1.
|
|
292
|
-
[0.1.
|
|
293
|
-
[0.1.
|
|
370
|
+
[Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.1.9...HEAD
|
|
371
|
+
[0.1.9]: https://github.com/wan9yu/cli-agent-runner/releases/tag/v0.1.9
|
|
372
|
+
[0.1.8]: https://github.com/wan9yu/cli-agent-runner/releases/tag/v0.1.8
|
|
373
|
+
[0.1.7]: https://github.com/wan9yu/cli-agent-runner/releases/tag/v0.1.7
|
|
374
|
+
[0.1.6]: https://github.com/wan9yu/cli-agent-runner/releases/tag/v0.1.6
|
|
375
|
+
[0.1.5]: https://github.com/wan9yu/cli-agent-runner/releases/tag/v0.1.5
|
|
376
|
+
[0.1.4]: https://github.com/wan9yu/cli-agent-runner/releases/tag/v0.1.4
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Restart-on-exit supervisor for autonomous CLI agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/wan9yu/cli-agent-runner
|
|
6
6
|
Project-URL: Documentation, https://github.com/wan9yu/cli-agent-runner#readme
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
21
|
+
__version__ = version = '0.1.9'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 9)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -240,6 +240,9 @@ def _log_dir_for_project(project: str | Path) -> Path:
|
|
|
240
240
|
# for callers that only use lifecycle verbs.
|
|
241
241
|
|
|
242
242
|
from agent_runner import defenses, monitor # noqa: E402
|
|
243
|
+
from agent_runner.events import HOOK_FAILED # noqa: E402
|
|
244
|
+
|
|
245
|
+
_RECENT_HOOK_FAILURES_LIMIT = 10
|
|
243
246
|
|
|
244
247
|
|
|
245
248
|
def peek(
|
|
@@ -266,6 +269,16 @@ def peek(
|
|
|
266
269
|
if current is None:
|
|
267
270
|
raise KeyError(f"round {round_num} not found under {log_dir}/rounds/")
|
|
268
271
|
recent = parsed_events[-events:] if events else []
|
|
272
|
+
# Walk the tail in reverse so we stop as soon as the limit is filled.
|
|
273
|
+
# parsed_events grows unboundedly over a project's lifetime; a full-scan
|
|
274
|
+
# comprehension here would dominate watch-loop peek cost.
|
|
275
|
+
recent_hook_failures: list[dict[str, Any]] = []
|
|
276
|
+
for e in reversed(parsed_events):
|
|
277
|
+
if e.get("event") == HOOK_FAILED:
|
|
278
|
+
recent_hook_failures.append(e)
|
|
279
|
+
if len(recent_hook_failures) == _RECENT_HOOK_FAILURES_LIMIT:
|
|
280
|
+
break
|
|
281
|
+
recent_hook_failures.reverse()
|
|
269
282
|
|
|
270
283
|
state = ProjectState(
|
|
271
284
|
project=base_state.project,
|
|
@@ -286,6 +299,7 @@ def peek(
|
|
|
286
299
|
system=base_state.system,
|
|
287
300
|
service=status(project if project is not None else work_dir),
|
|
288
301
|
recent_events=recent,
|
|
302
|
+
recent_hook_failures=recent_hook_failures,
|
|
289
303
|
)
|
|
290
304
|
return state if select is None else select_path(state, select)
|
|
291
305
|
|
|
@@ -72,6 +72,7 @@ class ProjectState:
|
|
|
72
72
|
system: SystemMetrics
|
|
73
73
|
service: ServiceStatus
|
|
74
74
|
recent_events: list[dict[str, Any]] = field(default_factory=list)
|
|
75
|
+
recent_hook_failures: list[dict[str, Any]] = field(default_factory=list)
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
@dataclass(frozen=True)
|
|
@@ -130,7 +131,7 @@ class InitResult:
|
|
|
130
131
|
work_dir: Path
|
|
131
132
|
files_created: list[Path]
|
|
132
133
|
committed: bool
|
|
133
|
-
preset: str = "claude" #
|
|
134
|
+
preset: str = "claude" # default keeps synthesised InitResults working
|
|
134
135
|
|
|
135
136
|
|
|
136
137
|
@dataclass(frozen=True)
|
|
@@ -12,10 +12,11 @@ from typing import Any
|
|
|
12
12
|
from agent_runner.api_types import ProjectState
|
|
13
13
|
from agent_runner.config import Config, load_config
|
|
14
14
|
from agent_runner.events import plugin_event_kinds
|
|
15
|
-
from agent_runner.hooks import plugin_context_enrichers
|
|
15
|
+
from agent_runner.hooks import plugin_context_enrichers, post_round_hooks, pre_round_hooks
|
|
16
16
|
from agent_runner.monitor import plugin_detectors
|
|
17
|
+
from agent_runner.vcs_state import plugin_owned_paths
|
|
17
18
|
|
|
18
|
-
PEEK_SCHEMA_VERSION = "1.
|
|
19
|
+
PEEK_SCHEMA_VERSION = "1.5"
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
def cfg_from_args(args) -> Config:
|
|
@@ -48,7 +49,10 @@ def emit(value: Any, *, json_mode: bool) -> None:
|
|
|
48
49
|
"plugins": {
|
|
49
50
|
"event_kinds": plugin_event_kinds(),
|
|
50
51
|
"context_enrichers": plugin_context_enrichers(),
|
|
52
|
+
"pre_round_hooks": [h.name for h in pre_round_hooks()],
|
|
53
|
+
"post_round_hooks": [h.name for h in post_round_hooks()],
|
|
51
54
|
"detectors": plugin_detectors(),
|
|
55
|
+
"owned_paths": plugin_owned_paths(),
|
|
52
56
|
},
|
|
53
57
|
**_to_jsonable(value),
|
|
54
58
|
}
|
|
@@ -24,6 +24,7 @@ class RuntimeConfig:
|
|
|
24
24
|
log_dir: Path
|
|
25
25
|
round_timeout_s: int = 1800
|
|
26
26
|
restart_delay_s: int = 3
|
|
27
|
+
round_timeout_per_phase: dict[str, int] = field(default_factory=dict)
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
@dataclass(frozen=True)
|
|
@@ -86,6 +87,32 @@ def _expand_path(s: str, project_name: str) -> Path:
|
|
|
86
87
|
return Path(s.replace("{project}", project_name)).expanduser()
|
|
87
88
|
|
|
88
89
|
|
|
90
|
+
def _require_positive_int(value: Any, *, field: str) -> int:
|
|
91
|
+
"""Validate a TOML value is a positive int. Rejects bool (subclass of int
|
|
92
|
+
in Python, would silently coerce e.g. ``true`` → 1) and any non-int."""
|
|
93
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
94
|
+
raise ValueError(f"{field}: must be an integer, got {type(value).__name__} ({value!r})")
|
|
95
|
+
if value <= 0:
|
|
96
|
+
raise ValueError(f"{field}: must be positive, got {value}")
|
|
97
|
+
return value
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _validate_round_timeout_per_phase_keys(
|
|
101
|
+
per_phase: dict[str, int], phases: list[str] | None
|
|
102
|
+
) -> None:
|
|
103
|
+
"""All keys must appear in [phases] list (typo catcher)."""
|
|
104
|
+
if not per_phase:
|
|
105
|
+
return
|
|
106
|
+
if phases is None:
|
|
107
|
+
raise ValueError("runtime.round_timeout_per_phase requires [phases] list to be defined")
|
|
108
|
+
unknown = set(per_phase) - set(phases)
|
|
109
|
+
if unknown:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"runtime.round_timeout_per_phase keys not in phases list: "
|
|
112
|
+
f"{sorted(unknown)}; available phases: {phases}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
89
116
|
def load_config(toml_path: Path) -> Config:
|
|
90
117
|
if not toml_path.exists():
|
|
91
118
|
raise FileNotFoundError(f"config not found: {toml_path}")
|
|
@@ -103,12 +130,28 @@ def load_config(toml_path: Path) -> Config:
|
|
|
103
130
|
work_dir = _expand_path(raw_work_dir, "").resolve()
|
|
104
131
|
project_name = work_dir.name or "default"
|
|
105
132
|
|
|
133
|
+
# Phases first — needed for per-phase round_timeout validation below.
|
|
134
|
+
phases_d = raw.get("phases", {})
|
|
135
|
+
phases = list(phases_d["list"]) if "list" in phases_d else None
|
|
136
|
+
|
|
106
137
|
runtime_d = raw.get("runtime", {})
|
|
138
|
+
per_phase_raw = runtime_d.get("round_timeout_per_phase", {})
|
|
139
|
+
per_phase: dict[str, int] = {
|
|
140
|
+
str(k): _require_positive_int(v, field=f"runtime.round_timeout_per_phase[{str(k)!r}]")
|
|
141
|
+
for k, v in per_phase_raw.items()
|
|
142
|
+
}
|
|
143
|
+
_validate_round_timeout_per_phase_keys(per_phase, phases)
|
|
144
|
+
|
|
107
145
|
runtime = RuntimeConfig(
|
|
108
146
|
work_dir=work_dir,
|
|
109
147
|
log_dir=_expand_path(str(_require(raw, "runtime", "log_dir")), project_name),
|
|
110
|
-
round_timeout_s=
|
|
111
|
-
|
|
148
|
+
round_timeout_s=_require_positive_int(
|
|
149
|
+
runtime_d.get("round_timeout_s", 1800), field="runtime.round_timeout_s"
|
|
150
|
+
),
|
|
151
|
+
restart_delay_s=_require_positive_int(
|
|
152
|
+
runtime_d.get("restart_delay_s", 3), field="runtime.restart_delay_s"
|
|
153
|
+
),
|
|
154
|
+
round_timeout_per_phase=per_phase,
|
|
112
155
|
)
|
|
113
156
|
prompt_d = raw.get("prompt", {})
|
|
114
157
|
mode = prompt_d.get("context_injection_mode", "prepend")
|
|
@@ -125,7 +168,9 @@ def load_config(toml_path: Path) -> Config:
|
|
|
125
168
|
vcs_d = raw.get("vcs", {})
|
|
126
169
|
vcs = VcsConfig(
|
|
127
170
|
orphan_action=str(vcs_d.get("orphan_action", "stash")),
|
|
128
|
-
stash_idempotency_s=
|
|
171
|
+
stash_idempotency_s=_require_positive_int(
|
|
172
|
+
vcs_d.get("stash_idempotency_s", 5), field="vcs.stash_idempotency_s"
|
|
173
|
+
),
|
|
129
174
|
)
|
|
130
175
|
monitor_d = raw.get("monitor", {})
|
|
131
176
|
monitor = MonitorConfig(
|
|
@@ -133,8 +178,6 @@ def load_config(toml_path: Path) -> Config:
|
|
|
133
178
|
auth_fail_hint=str(monitor_d.get("auth_fail_hint", _DEFAULT_AUTH_HINT)),
|
|
134
179
|
auto_stop_on=list(monitor_d.get("auto_stop_on", _DEFAULT_AUTO_STOP_ON)),
|
|
135
180
|
)
|
|
136
|
-
phases_d = raw.get("phases", {})
|
|
137
|
-
phases = list(phases_d["list"]) if "list" in phases_d else None
|
|
138
181
|
plugins_d = raw.get("plugins")
|
|
139
182
|
|
|
140
183
|
return Config(
|
|
@@ -22,6 +22,11 @@ from datetime import UTC, datetime
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
25
|
+
# Cross-module event-kind constants. Most kinds are emitted in only one place
|
|
26
|
+
# (runner.py), but kinds that are also CONSUMED elsewhere (filtered, surfaced
|
|
27
|
+
# in peek, asserted in tests) earn a constant to keep the spelling honest.
|
|
28
|
+
HOOK_FAILED = "hook_failed"
|
|
29
|
+
|
|
25
30
|
_BUILTIN_KINDS: frozenset[str] = frozenset(
|
|
26
31
|
{
|
|
27
32
|
"round_start",
|
|
@@ -38,7 +43,7 @@ _BUILTIN_KINDS: frozenset[str] = frozenset(
|
|
|
38
43
|
"round_end",
|
|
39
44
|
"monitor_alert_emitted",
|
|
40
45
|
"monitor_auto_stop_triggered",
|
|
41
|
-
|
|
46
|
+
HOOK_FAILED,
|
|
42
47
|
}
|
|
43
48
|
)
|
|
44
49
|
|
|
@@ -49,6 +49,17 @@ def _phase_for(round_num: int, phases: list[str] | None) -> tuple[str | None, in
|
|
|
49
49
|
return phases[idx], idx
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
def _round_timeout_for(cfg: Config, phase: str | None) -> int:
|
|
53
|
+
"""Per-phase override of round_timeout_s; falls back to global default.
|
|
54
|
+
|
|
55
|
+
Phase=None (no phases configured) → global. Phase not in override dict →
|
|
56
|
+
global. Phase in override dict → that phase's configured timeout.
|
|
57
|
+
"""
|
|
58
|
+
if phase is None:
|
|
59
|
+
return cfg.runtime.round_timeout_s
|
|
60
|
+
return cfg.runtime.round_timeout_per_phase.get(phase, cfg.runtime.round_timeout_s)
|
|
61
|
+
|
|
62
|
+
|
|
52
63
|
def _previous_block(prev: context_store.Status | None, dirty_last: bool) -> dict[str, Any] | None:
|
|
53
64
|
if prev is None:
|
|
54
65
|
return None
|
|
@@ -95,7 +106,7 @@ def _stitch_enricher_slices(
|
|
|
95
106
|
payload = hooks._summarize_error(exc, tb=tb_mod.format_exc())
|
|
96
107
|
events.emit(
|
|
97
108
|
log_dir,
|
|
98
|
-
|
|
109
|
+
events.HOOK_FAILED,
|
|
99
110
|
hook_name=enricher.name,
|
|
100
111
|
hook_kind="context_enricher",
|
|
101
112
|
**payload,
|
|
@@ -114,7 +125,7 @@ def _run_pre_round_hooks(hook_ctx: hooks.HookContext, log_dir: Path) -> None:
|
|
|
114
125
|
payload = hooks._summarize_error(exc, tb=tb_mod.format_exc())
|
|
115
126
|
events.emit(
|
|
116
127
|
log_dir,
|
|
117
|
-
|
|
128
|
+
events.HOOK_FAILED,
|
|
118
129
|
hook_name=hook.name,
|
|
119
130
|
hook_kind="pre_round",
|
|
120
131
|
**payload,
|
|
@@ -136,7 +147,7 @@ def _run_post_round_hooks(
|
|
|
136
147
|
payload = hooks._summarize_error(exc, tb=tb_mod.format_exc())
|
|
137
148
|
events.emit(
|
|
138
149
|
log_dir,
|
|
139
|
-
|
|
150
|
+
events.HOOK_FAILED,
|
|
140
151
|
hook_name=hook.name,
|
|
141
152
|
hook_kind="post_round",
|
|
142
153
|
**payload,
|
|
@@ -175,6 +186,7 @@ def _run_one_round_inner(cfg: Config) -> RoundResult:
|
|
|
175
186
|
|
|
176
187
|
round_num = (prev_status.round_num if prev_status else 0) + 1
|
|
177
188
|
phase, phase_idx = _phase_for(round_num, cfg.phases)
|
|
189
|
+
timeout_s = _round_timeout_for(cfg, phase)
|
|
178
190
|
started_at = now_iso_ms()
|
|
179
191
|
|
|
180
192
|
orphan = context_store.read_orphan_state(log_dir)
|
|
@@ -223,12 +235,12 @@ def _run_one_round_inner(cfg: Config) -> RoundResult:
|
|
|
223
235
|
mode=cfg.prompt.context_injection_mode,
|
|
224
236
|
)
|
|
225
237
|
|
|
226
|
-
events.emit(log_dir, "agent_spawn", round_num=round_num, timeout_s=
|
|
238
|
+
events.emit(log_dir, "agent_spawn", round_num=round_num, timeout_s=timeout_s)
|
|
227
239
|
result = agent_runtime.run(
|
|
228
240
|
command=cfg.agent.command,
|
|
229
241
|
prompt_arg_template=cfg.agent.prompt_arg_template,
|
|
230
242
|
prompt=prompt,
|
|
231
|
-
timeout_s=
|
|
243
|
+
timeout_s=timeout_s,
|
|
232
244
|
log_path=log_path,
|
|
233
245
|
env_extra=dict(cfg.agent.env),
|
|
234
246
|
)
|
|
@@ -281,7 +293,7 @@ def _run_one_round_inner(cfg: Config) -> RoundResult:
|
|
|
281
293
|
log_dir,
|
|
282
294
|
"round_timeout_kill",
|
|
283
295
|
round_num=round_num,
|
|
284
|
-
reason=f"exceeded round_timeout_s={
|
|
296
|
+
reason=f"exceeded round_timeout_s={timeout_s}",
|
|
285
297
|
)
|
|
286
298
|
|
|
287
299
|
completed_at = now_iso_ms()
|
|
@@ -5,7 +5,9 @@ Two units per project:
|
|
|
5
5
|
agent-runner-monitor@<project>.service - runs `agent-runner monitor`
|
|
6
6
|
|
|
7
7
|
Install command writes these to ~/.config/systemd/user/. The graceful-stop
|
|
8
|
-
contract relies on KillSignal=SIGTERM + TimeoutStopSec=round_timeout_s
|
|
8
|
+
contract relies on KillSignal=SIGTERM + TimeoutStopSec=max(round_timeout_s,
|
|
9
|
+
round_timeout_per_phase.values())+60 — the LARGEST possible round budget
|
|
10
|
+
plus grace.
|
|
9
11
|
"""
|
|
10
12
|
|
|
11
13
|
from __future__ import annotations
|
|
@@ -32,7 +34,12 @@ def _config_path(cfg: Config) -> Path:
|
|
|
32
34
|
|
|
33
35
|
def render_serve_unit(cfg: Config, *, venv_bin: Path) -> str:
|
|
34
36
|
"""Generate the serve systemd unit body."""
|
|
35
|
-
|
|
37
|
+
# TimeoutStopSec covers the largest possible round so `systemctl stop`
|
|
38
|
+
# doesn't SIGKILL a long per-phase round mid-flight.
|
|
39
|
+
max_round_timeout = max(
|
|
40
|
+
[cfg.runtime.round_timeout_s, *cfg.runtime.round_timeout_per_phase.values()]
|
|
41
|
+
)
|
|
42
|
+
timeout_total = max_round_timeout + _GRACE_S
|
|
36
43
|
return (
|
|
37
44
|
f"[Unit]\n"
|
|
38
45
|
f"Description=Agent Runner Supervisor ({cfg.runtime.work_dir.name})\n"
|
|
@@ -7,14 +7,69 @@ Stash safety rules (R820 + §9 IMMUTABLE):
|
|
|
7
7
|
design means external concurrent ``git stash push`` is not a defended scenario.
|
|
8
8
|
- "Auto-tool change vs human change" detection uses set-based diff vs HEAD,
|
|
9
9
|
not unified-diff +/-line parsing (R2110 lesson).
|
|
10
|
+
- Also hosts the plugin-owned-paths registry consumed by
|
|
11
|
+
``detect_dirty_files()`` so plugins can opt files/dirs out of the
|
|
12
|
+
orphan-stash defense.
|
|
10
13
|
"""
|
|
11
14
|
|
|
12
15
|
from __future__ import annotations
|
|
13
16
|
|
|
17
|
+
import fnmatch
|
|
14
18
|
import subprocess # noqa: TID251 — vcs_state.py is the only sanctioned git CLI caller
|
|
15
19
|
import time
|
|
16
20
|
from dataclasses import dataclass
|
|
17
|
-
from pathlib import Path
|
|
21
|
+
from pathlib import Path, PurePath
|
|
22
|
+
|
|
23
|
+
# Plugin-owned paths registry — set via register_plugin_owned_paths().
|
|
24
|
+
# detect_dirty_files() filters its return through this list, so plugin-declared
|
|
25
|
+
# paths are not flagged as orphan WIP and not stashed by the supervisor.
|
|
26
|
+
_PLUGIN_OWNED_PATHS: list[str] = []
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def register_plugin_owned_paths(paths: list[str]) -> None:
|
|
30
|
+
"""Register paths the plugin considers its own deliverables.
|
|
31
|
+
|
|
32
|
+
Paths are relative to the work_dir. Matching:
|
|
33
|
+
|
|
34
|
+
- Trailing ``/`` → prefix match (e.g. ``"proposals/"`` matches
|
|
35
|
+
``"proposals/dev-round1.md"`` and the bare directory name).
|
|
36
|
+
- Anything else without ``**`` → ``pathlib.PurePath.match`` glob
|
|
37
|
+
(e.g. ``"reports/*.md"``). Single ``*`` does not cross slashes.
|
|
38
|
+
- Patterns containing ``**`` → ``fnmatch.fnmatch`` (e.g.
|
|
39
|
+
``"logs/plugins/**/*"``). ``**`` matches recursive directory
|
|
40
|
+
segments. (``PurePath.full_match`` would handle this natively
|
|
41
|
+
but requires Python 3.13+; this project's minimum is 3.11.)
|
|
42
|
+
|
|
43
|
+
Plugins call this at module import time (entry_point side-effect) so the
|
|
44
|
+
paths are known before the first round runs.
|
|
45
|
+
|
|
46
|
+
Raises ValueError on non-string entries.
|
|
47
|
+
"""
|
|
48
|
+
for p in paths:
|
|
49
|
+
if not isinstance(p, str):
|
|
50
|
+
raise ValueError(f"register_plugin_owned_paths: non-string entry {p!r}")
|
|
51
|
+
_PLUGIN_OWNED_PATHS.extend(paths)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def plugin_owned_paths() -> list[str]:
|
|
55
|
+
"""Snapshot of registered plugin-owned paths (for peek visibility)."""
|
|
56
|
+
return list(_PLUGIN_OWNED_PATHS)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _matches_owned_path(path: str) -> bool:
|
|
60
|
+
"""True if `path` matches any registered plugin-owned pattern."""
|
|
61
|
+
for pattern in _PLUGIN_OWNED_PATHS:
|
|
62
|
+
if pattern.endswith("/"):
|
|
63
|
+
stripped = pattern.rstrip("/")
|
|
64
|
+
if path == stripped or path.startswith(pattern):
|
|
65
|
+
return True
|
|
66
|
+
elif "**" in pattern:
|
|
67
|
+
# fnmatch handles ** recursively; PurePath.match (3.11) does not.
|
|
68
|
+
if fnmatch.fnmatch(path, pattern):
|
|
69
|
+
return True
|
|
70
|
+
elif PurePath(path).match(pattern):
|
|
71
|
+
return True
|
|
72
|
+
return False
|
|
18
73
|
|
|
19
74
|
|
|
20
75
|
@dataclass(frozen=True)
|
|
@@ -74,6 +129,9 @@ def detect_dirty_files(repo: Path) -> list[str]:
|
|
|
74
129
|
else:
|
|
75
130
|
out.append(path)
|
|
76
131
|
i += 1
|
|
132
|
+
# Early-out preserves zero behavior change when no plugin has registered.
|
|
133
|
+
if _PLUGIN_OWNED_PATHS:
|
|
134
|
+
out = [p for p in out if not _matches_owned_path(p)]
|
|
77
135
|
return out
|
|
78
136
|
|
|
79
137
|
|
|
@@ -23,6 +23,7 @@ writes a templated copy you can edit.
|
|
|
23
23
|
| `log_dir` | `Path` | — |
|
|
24
24
|
| `round_timeout_s` | `int` | 1800 |
|
|
25
25
|
| `restart_delay_s` | `int` | 3 |
|
|
26
|
+
| `round_timeout_per_phase` | `dict[str, int]` | {} |
|
|
26
27
|
|
|
27
28
|
### `[prompt]`
|
|
28
29
|
|
|
@@ -76,6 +77,31 @@ Override in your `agent-runner.toml` if you ship a custom CLI.
|
|
|
76
77
|
|---|---|---|---|
|
|
77
78
|
| `list` | list[str] | (none → no phase rotation) | round N gets `phases[(N-1) % len(phases)]` |
|
|
78
79
|
|
|
80
|
+
## Per-phase timeouts (0.1.9+)
|
|
81
|
+
|
|
82
|
+
If your `[phases]` rotation has phases with different wall-clock budgets,
|
|
83
|
+
override the global timeout per phase:
|
|
84
|
+
|
|
85
|
+
```toml
|
|
86
|
+
[runtime]
|
|
87
|
+
round_timeout_s = 1800 # fallback for unconfigured phases
|
|
88
|
+
|
|
89
|
+
[runtime.round_timeout_per_phase]
|
|
90
|
+
dev = 3600 # implementation work, longer budget
|
|
91
|
+
qa = 1200 # test review, tighter budget
|
|
92
|
+
product = 1200 # docs writing, tighter budget
|
|
93
|
+
|
|
94
|
+
[phases]
|
|
95
|
+
list = ["dev", "qa", "product"]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Validation: typos in phase names (keys not in `[phases] list`) and
|
|
99
|
+
non-positive / non-integer values are caught at config-load time with
|
|
100
|
+
`ValueError`.
|
|
101
|
+
|
|
102
|
+
Unconfigured phases (and configs without `[phases]`) keep using the
|
|
103
|
+
global `round_timeout_s`.
|
|
104
|
+
|
|
79
105
|
## `[monitor]` (optional, defaults shown)
|
|
80
106
|
|
|
81
107
|
```toml
|