cli-agent-runner 0.1.7__tar.gz → 0.1.8__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.8}/CHANGELOG.md +53 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/PKG-INFO +1 -1
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/api.py +14 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/api_types.py +1 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/cli/common.py +6 -2
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/events.py +6 -1
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/runner.py +3 -3
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/vcs_state.py +60 -1
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/docs/plugins.md +107 -10
- cli_agent_runner-0.1.8/tests/_test_helpers.py +44 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/contract/test_public_api_surface.py +17 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/integration/test_context_enricher_namespacing.py +2 -10
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/integration/test_plugin_detector_loaded.py +2 -10
- cli_agent_runner-0.1.8/tests/integration/test_plugin_owned_paths.py +88 -0
- cli_agent_runner-0.1.8/tests/invariants/test_module_sizes.py +22 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/invariants/test_peek_schema_version.py +2 -2
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_api_observation.py +59 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_api_types.py +33 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_detector_protocol.py +3 -10
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_events.py +3 -10
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_hook_failure_isolation.py +2 -10
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_hooks.py +6 -17
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/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.8}/.codecov.yml +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/.githooks/commit-msg +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/.gitignore +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/.vulture-whitelist.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/LICENSE +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/README.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/README.zh.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/SECURITY.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/agent_runtime.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/cli/serve_cmd.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/config.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/detector_helpers.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/monitor.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/build.sh +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/deploy/example-agent-runner.toml +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/deploy/launchd.plist.tmpl +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/deploy/run-loop.sh +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/deploy/systemd.service.tmpl +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/docs/README.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/docs/architecture.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/docs/commands.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/docs/configuration.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/docs/runbook.md +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/pyproject.toml +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/conftest.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/integration/test_run_loop_backoff.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/invariants/test_architecture.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_detector_helpers.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/unit/test_startup_check.py +0 -0
|
@@ -7,6 +7,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.8] - 2026-05-13
|
|
11
|
+
|
|
12
|
+
### Acknowledgements
|
|
13
|
+
|
|
14
|
+
Thanks to the argus-gateway team for Phase 4 dogfooding feedback that drove
|
|
15
|
+
every item in this release. 3 audit memos (~90KB) silently swept into an
|
|
16
|
+
orphan stash is a real-world failure mode; this release closes that loop.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `agent_runner.vcs_state.register_plugin_owned_paths()` — plugins opt-out
|
|
21
|
+
files/dirs from orphan-stash defense. Matching: trailing-slash prefix or
|
|
22
|
+
`pathlib.PurePath.match` glob (recognizes `**` for recursive segments via
|
|
23
|
+
`fnmatch` fallback on Python 3.11). Call at module import (entry_point
|
|
24
|
+
side-effect).
|
|
25
|
+
- `agent_runner.vcs_state.plugin_owned_paths()` — snapshot accessor for peek.
|
|
26
|
+
- `ProjectState.recent_hook_failures: list[dict]` — last 10 `hook_failed`
|
|
27
|
+
events filtered from `recent_events` for debugging hook integration.
|
|
28
|
+
- peek schema bumped 1.4 → 1.5. `plugins` block now includes
|
|
29
|
+
`pre_round_hooks`, `post_round_hooks`, `owned_paths` lists.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- `docs/plugins.md` register-pattern examples corrected: registration must
|
|
34
|
+
happen as module-top side effect; entry_point loaders only import, they
|
|
35
|
+
do not invoke. Old `_register()` wrapper pattern silently didn't fire.
|
|
36
|
+
- `docs/plugins.md` gained "Declaring plugin-owned paths" and "Plugin tests
|
|
37
|
+
+ consumer pytest collision" sections.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- Plugin outputs in plugin-declared paths (e.g. `proposals/`,
|
|
42
|
+
`logs/plugins/my_plugin/`) no longer silently swept into orphan stashes
|
|
43
|
+
by `process_orphan_wip`. Previously: 90KB Argus audit memos invisible
|
|
44
|
+
after Phase 4 round; required stash archaeology to recover.
|
|
45
|
+
|
|
46
|
+
### Migration
|
|
47
|
+
|
|
48
|
+
No breaking changes. Plugin authors:
|
|
49
|
+
|
|
50
|
+
- If your plugin writes files to `work_dir` and they keep getting stashed
|
|
51
|
+
between rounds, opt them out:
|
|
52
|
+
```python
|
|
53
|
+
from agent_runner.vcs_state import register_plugin_owned_paths
|
|
54
|
+
register_plugin_owned_paths(["your-output-dir/", "logs/your-plugin/**/*"])
|
|
55
|
+
```
|
|
56
|
+
- If you followed the old `_register()` pattern from docs and noticed
|
|
57
|
+
registrations not firing: move the call to module top:
|
|
58
|
+
```python
|
|
59
|
+
# was: def _register(): register_pre_round_hook(MyHook())
|
|
60
|
+
# now: register_pre_round_hook(MyHook()) # module-top side-effect
|
|
61
|
+
```
|
|
62
|
+
|
|
10
63
|
## [0.1.7] - 2026-05-13
|
|
11
64
|
|
|
12
65
|
### Migration for existing 0.1.6 users (DOWNSTREAM CONSUMERS READ THIS)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
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.8'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 8)
|
|
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
|
|
|
@@ -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
|
}
|
|
@@ -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
|
|
|
@@ -95,7 +95,7 @@ def _stitch_enricher_slices(
|
|
|
95
95
|
payload = hooks._summarize_error(exc, tb=tb_mod.format_exc())
|
|
96
96
|
events.emit(
|
|
97
97
|
log_dir,
|
|
98
|
-
|
|
98
|
+
events.HOOK_FAILED,
|
|
99
99
|
hook_name=enricher.name,
|
|
100
100
|
hook_kind="context_enricher",
|
|
101
101
|
**payload,
|
|
@@ -114,7 +114,7 @@ def _run_pre_round_hooks(hook_ctx: hooks.HookContext, log_dir: Path) -> None:
|
|
|
114
114
|
payload = hooks._summarize_error(exc, tb=tb_mod.format_exc())
|
|
115
115
|
events.emit(
|
|
116
116
|
log_dir,
|
|
117
|
-
|
|
117
|
+
events.HOOK_FAILED,
|
|
118
118
|
hook_name=hook.name,
|
|
119
119
|
hook_kind="pre_round",
|
|
120
120
|
**payload,
|
|
@@ -136,7 +136,7 @@ def _run_post_round_hooks(
|
|
|
136
136
|
payload = hooks._summarize_error(exc, tb=tb_mod.format_exc())
|
|
137
137
|
events.emit(
|
|
138
138
|
log_dir,
|
|
139
|
-
|
|
139
|
+
events.HOOK_FAILED,
|
|
140
140
|
hook_name=hook.name,
|
|
141
141
|
hook_kind="post_round",
|
|
142
142
|
**payload,
|
|
@@ -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 (0.1.8+).
|
|
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,10 @@ def detect_dirty_files(repo: Path) -> list[str]:
|
|
|
74
129
|
else:
|
|
75
130
|
out.append(path)
|
|
76
131
|
i += 1
|
|
132
|
+
# Filter out plugin-declared paths (0.1.8+). Early-out preserves zero
|
|
133
|
+
# behavior change when no plugin has registered anything.
|
|
134
|
+
if _PLUGIN_OWNED_PATHS:
|
|
135
|
+
out = [p for p in out if not _matches_owned_path(p)]
|
|
77
136
|
return out
|
|
78
137
|
|
|
79
138
|
|
|
@@ -10,6 +10,12 @@ plugin code is observability/coordination glue, not workflow logic.
|
|
|
10
10
|
|
|
11
11
|
## Entry-points groups (0.1.3)
|
|
12
12
|
|
|
13
|
+
> **Entry-point semantics:** agent-runner imports the target module when it
|
|
14
|
+
> loads a plugin. It does **not** call the target as a function — registration
|
|
15
|
+
> must happen as a module-top side effect (the `register_*` call at module
|
|
16
|
+
> level). A `def _register():` wrapper around the call will NOT fire; the
|
|
17
|
+
> loader only imports.
|
|
18
|
+
|
|
13
19
|
| Group | Purpose | Available in |
|
|
14
20
|
|---|---|---|
|
|
15
21
|
| `agent_runner.event_kinds` | Register custom event kind names | 0.1.3+ |
|
|
@@ -22,7 +28,7 @@ detectors are reserved for 0.1.4 and 0.1.5.
|
|
|
22
28
|
```toml
|
|
23
29
|
# my_plugin/pyproject.toml
|
|
24
30
|
[project.entry-points."agent_runner.event_kinds"]
|
|
25
|
-
my_workflow_stage_advanced = "my_plugin.events
|
|
31
|
+
my_workflow_stage_advanced = "my_plugin.events"
|
|
26
32
|
```
|
|
27
33
|
|
|
28
34
|
```python
|
|
@@ -31,9 +37,9 @@ from agent_runner.events import register_event_kind
|
|
|
31
37
|
|
|
32
38
|
STAGE_ADVANCED = "my_workflow_stage_advanced"
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
# Module-top side effect: entry_point load imports this module, which
|
|
41
|
+
# triggers the registration.
|
|
42
|
+
register_event_kind(STAGE_ADVANCED, source="my-plugin@1.0")
|
|
37
43
|
```
|
|
38
44
|
|
|
39
45
|
After installation, the registered kind:
|
|
@@ -84,7 +90,7 @@ Its field set is stable across 0.1.x (additions only).
|
|
|
84
90
|
```toml
|
|
85
91
|
# my_plugin/pyproject.toml
|
|
86
92
|
[project.entry-points."agent_runner.context_enrichers"]
|
|
87
|
-
current_branch = "my_plugin.enrichers
|
|
93
|
+
current_branch = "my_plugin.enrichers"
|
|
88
94
|
```
|
|
89
95
|
|
|
90
96
|
```python
|
|
@@ -104,8 +110,9 @@ class CurrentBranchEnricher:
|
|
|
104
110
|
return {"branch": out.stdout.strip() or "(detached)"}
|
|
105
111
|
|
|
106
112
|
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
# Module-top side effect: entry_point load imports this module, which
|
|
114
|
+
# triggers the registration.
|
|
115
|
+
register_context_enricher(CurrentBranchEnricher())
|
|
109
116
|
```
|
|
110
117
|
|
|
111
118
|
After installation, each round's `round-context.json` gains a `current_branch` key:
|
|
@@ -164,7 +171,7 @@ detectors that run alongside the 9 builtins on every monitor poll.
|
|
|
164
171
|
|
|
165
172
|
```toml
|
|
166
173
|
[project.entry-points."agent_runner.detectors"]
|
|
167
|
-
my_detector = "my_plugin.detectors
|
|
174
|
+
my_detector = "my_plugin.detectors"
|
|
168
175
|
```
|
|
169
176
|
|
|
170
177
|
```python
|
|
@@ -191,8 +198,9 @@ class MyDetector:
|
|
|
191
198
|
)
|
|
192
199
|
|
|
193
200
|
|
|
194
|
-
|
|
195
|
-
|
|
201
|
+
# Module-top side effect: entry_point load imports this module, which
|
|
202
|
+
# triggers the registration.
|
|
203
|
+
register_detector(MyDetector())
|
|
196
204
|
```
|
|
197
205
|
|
|
198
206
|
`Detector` is a `@runtime_checkable` Protocol — `isinstance(obj, Detector)` returns
|
|
@@ -309,3 +317,92 @@ class NoCommitsDetector:
|
|
|
309
317
|
**Codifies the lesson:** "0 commits in N rounds" detectors mis-fire on
|
|
310
318
|
retrospective/reflection phases that intentionally produce zero commits.
|
|
311
319
|
Pass the set of phase names where the detector should NOT run.
|
|
320
|
+
|
|
321
|
+
## Declaring plugin-owned paths (0.1.8+)
|
|
322
|
+
|
|
323
|
+
If your plugin writes files inside the supervisor's `work_dir`
|
|
324
|
+
(audit memos, generated reports, plugin-local state, etc.), declare them
|
|
325
|
+
so the orphan-stash defense doesn't silently sweep them into a stash
|
|
326
|
+
between rounds.
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
# my_plugin/__init__.py
|
|
330
|
+
from agent_runner.vcs_state import register_plugin_owned_paths
|
|
331
|
+
|
|
332
|
+
# Module-top side effect — must register before the first round runs.
|
|
333
|
+
register_plugin_owned_paths([
|
|
334
|
+
"proposals/", # trailing slash → prefix match
|
|
335
|
+
"logs/plugins/my_plugin/**/*", # recursive glob (fnmatch)
|
|
336
|
+
"reports/*.md", # single-segment glob (PurePath.match)
|
|
337
|
+
])
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Matching semantics
|
|
341
|
+
|
|
342
|
+
| Pattern | Matches | Notes |
|
|
343
|
+
|---|---|---|
|
|
344
|
+
| `"proposals/"` | `proposals`, `proposals/foo.md`, `proposals/sub/bar.md` | Trailing `/` → prefix match. |
|
|
345
|
+
| `"proposals"` (no slash) | `proposals` exactly | Single-segment literal. |
|
|
346
|
+
| `"reports/*.md"` | `reports/dev.md` | `*` does not cross slashes. |
|
|
347
|
+
| `"reports/**/*.md"` | `reports/dev.md`, `reports/sub/qa.md` | `**` matches across directory separators. |
|
|
348
|
+
| `"logs/plugins/**/*"` | `logs/plugins/argus/state.json` | Same — `**` covers intermediate dirs. |
|
|
349
|
+
|
|
350
|
+
### Caveat — this is NOT a "make work_dir messy" license
|
|
351
|
+
|
|
352
|
+
Plugin-owned paths express *"these are the plugin's expected deliverable
|
|
353
|
+
files; do not stash them"*. They are not permission to scatter scratch
|
|
354
|
+
files. Operator owns cleanup of these paths.
|
|
355
|
+
|
|
356
|
+
If your plugin writes ephemeral state that should be cleaned up between
|
|
357
|
+
rounds, do the cleanup yourself in a `PostRoundHook` — don't rely on
|
|
358
|
+
the orphan-stash defense to sweep it.
|
|
359
|
+
|
|
360
|
+
### Visibility
|
|
361
|
+
|
|
362
|
+
`agent-runner peek --select plugins.owned_paths` shows the currently
|
|
363
|
+
registered list (peek schema v1.5+).
|
|
364
|
+
|
|
365
|
+
## Plugin tests + consumer pytest collision
|
|
366
|
+
|
|
367
|
+
Consumer projects often have their own `tests/` directory. If your plugin
|
|
368
|
+
also has tests (e.g. `tools/my_agent_plugin/tests/`), pytest's testpaths
|
|
369
|
+
walk can find both and fail with `ModuleNotFoundError` when the same
|
|
370
|
+
package name lives in two locations.
|
|
371
|
+
|
|
372
|
+
Two recommended patterns:
|
|
373
|
+
|
|
374
|
+
### Pattern A — plugin tests inside the plugin package
|
|
375
|
+
|
|
376
|
+
Plugin author owns this:
|
|
377
|
+
|
|
378
|
+
```
|
|
379
|
+
my_agent_plugin/
|
|
380
|
+
├── __init__.py
|
|
381
|
+
├── core.py
|
|
382
|
+
└── tests/
|
|
383
|
+
├── __init__.py
|
|
384
|
+
└── test_core.py
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
In `my_agent_plugin/pyproject.toml`:
|
|
388
|
+
|
|
389
|
+
```toml
|
|
390
|
+
[tool.pytest.ini_options]
|
|
391
|
+
testpaths = ["my_agent_plugin/tests"]
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
This scopes pytest collection to your plugin's tests when running locally.
|
|
395
|
+
|
|
396
|
+
### Pattern B — consumer ignores your plugin in their pytest config
|
|
397
|
+
|
|
398
|
+
Consumer owns this:
|
|
399
|
+
|
|
400
|
+
```toml
|
|
401
|
+
# In the consumer project's pytest.ini or pyproject.toml:
|
|
402
|
+
[tool.pytest.ini_options]
|
|
403
|
+
addopts = ["--ignore=tools/my_agent_plugin"]
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Both work. Pattern A is preferable for plugin authors (no consumer
|
|
407
|
+
configuration needed); Pattern B is for cases where the consumer integrates
|
|
408
|
+
a plugin they don't own.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Shared test helpers.
|
|
2
|
+
|
|
3
|
+
Centralises the snapshot+clear+restore fixture pattern used by every test
|
|
4
|
+
file that interacts with a plugin-extension registry (hooks, detectors,
|
|
5
|
+
event kinds, owned paths). Before: 8 near-identical autouse fixtures across
|
|
6
|
+
the test suite. After: one factory.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def isolating(*registries: list[Any] | dict[Any, Any]) -> Any:
|
|
17
|
+
"""Return an autouse fixture that snapshots, clears, and restores registries.
|
|
18
|
+
|
|
19
|
+
Usage in a test module:
|
|
20
|
+
|
|
21
|
+
from tests._test_helpers import isolating
|
|
22
|
+
from agent_runner import monitor
|
|
23
|
+
|
|
24
|
+
_reset = isolating(monitor._PLUGIN_DETECTORS)
|
|
25
|
+
|
|
26
|
+
Multiple registries can be passed; all are isolated around each test.
|
|
27
|
+
Supports list and dict registries.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@pytest.fixture(autouse=True)
|
|
31
|
+
def _reset():
|
|
32
|
+
saved: list[Any] = []
|
|
33
|
+
for reg in registries:
|
|
34
|
+
saved.append(reg.copy() if isinstance(reg, dict) else list(reg))
|
|
35
|
+
reg.clear()
|
|
36
|
+
yield
|
|
37
|
+
for reg, snap in zip(registries, saved, strict=True):
|
|
38
|
+
reg.clear()
|
|
39
|
+
if isinstance(reg, dict):
|
|
40
|
+
reg.update(snap)
|
|
41
|
+
else:
|
|
42
|
+
reg.extend(snap)
|
|
43
|
+
|
|
44
|
+
return _reset
|
|
@@ -80,6 +80,11 @@ EXPECTED_DETECTOR_HELPERS_API = {
|
|
|
80
80
|
"phase_filter",
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
EXPECTED_VCS_STATE_API = {
|
|
84
|
+
"register_plugin_owned_paths",
|
|
85
|
+
"plugin_owned_paths",
|
|
86
|
+
}
|
|
87
|
+
|
|
83
88
|
# Doomed symbols (removed in 0.1.7) — verify ABSENCE so a future revert can't
|
|
84
89
|
# silently restore them and re-couple core to Claude.
|
|
85
90
|
FORBIDDEN_AGENT_RUNTIME = {
|
|
@@ -128,3 +133,15 @@ def test_given_agent_runtime_when_imported_then_claude_specific_symbols_absent()
|
|
|
128
133
|
f"agent_runner.agent_runtime: forbidden Claude-specific symbols present: {present}. "
|
|
129
134
|
f"These were intentionally removed in 0.1.7 — env injection lives in AgentConfig.env."
|
|
130
135
|
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_given_vcs_state_module_when_imported_then_plugin_owned_paths_api_present() -> None:
|
|
139
|
+
"""0.1.8: register_plugin_owned_paths + plugin_owned_paths are the new
|
|
140
|
+
plugin-author public surface. Lock them in so a future refactor can't
|
|
141
|
+
silently rename or remove them."""
|
|
142
|
+
actual = _public_names("agent_runner.vcs_state")
|
|
143
|
+
missing = EXPECTED_VCS_STATE_API - actual
|
|
144
|
+
assert not missing, (
|
|
145
|
+
f"agent_runner.vcs_state: missing public names {missing}. "
|
|
146
|
+
f"Plugin authors registered against the 0.1.8 names — do not remove without major bump."
|
|
147
|
+
)
|
|
@@ -4,19 +4,11 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
7
|
from agent_runner import hooks
|
|
10
8
|
from agent_runner.runner import _stitch_enricher_slices
|
|
9
|
+
from tests._test_helpers import isolating
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
@pytest.fixture(autouse=True)
|
|
14
|
-
def _reset_enrichers():
|
|
15
|
-
saved = list(hooks._CONTEXT_ENRICHERS)
|
|
16
|
-
hooks._CONTEXT_ENRICHERS.clear()
|
|
17
|
-
yield
|
|
18
|
-
hooks._CONTEXT_ENRICHERS.clear()
|
|
19
|
-
hooks._CONTEXT_ENRICHERS.extend(saved)
|
|
11
|
+
_reset = isolating(hooks._CONTEXT_ENRICHERS)
|
|
20
12
|
|
|
21
13
|
|
|
22
14
|
class _Branch:
|
{cli_agent_runner-0.1.7 → cli_agent_runner-0.1.8}/tests/integration/test_plugin_detector_loaded.py
RENAMED
|
@@ -4,19 +4,11 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
7
|
from agent_runner import monitor
|
|
10
8
|
from agent_runner.api_types import Alert
|
|
9
|
+
from tests._test_helpers import isolating
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
@pytest.fixture(autouse=True)
|
|
14
|
-
def _reset_plugin_detectors():
|
|
15
|
-
saved = list(monitor._PLUGIN_DETECTORS)
|
|
16
|
-
monitor._PLUGIN_DETECTORS.clear()
|
|
17
|
-
yield
|
|
18
|
-
monitor._PLUGIN_DETECTORS.clear()
|
|
19
|
-
monitor._PLUGIN_DETECTORS.extend(saved)
|
|
11
|
+
_reset = isolating(monitor._PLUGIN_DETECTORS)
|
|
20
12
|
|
|
21
13
|
|
|
22
14
|
class _AlwaysFiresDetector:
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""End-to-end: plugin-owned paths are filtered out of detect_dirty_files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from agent_runner.vcs_state import _PLUGIN_OWNED_PATHS
|
|
9
|
+
from tests._test_helpers import isolating
|
|
10
|
+
|
|
11
|
+
_reset = isolating(_PLUGIN_OWNED_PATHS)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _commit(repo: Path, msg: str) -> None:
|
|
15
|
+
subprocess.run(
|
|
16
|
+
[
|
|
17
|
+
"git",
|
|
18
|
+
"-c",
|
|
19
|
+
"commit.gpgsign=false",
|
|
20
|
+
"commit",
|
|
21
|
+
"-q",
|
|
22
|
+
"-m",
|
|
23
|
+
msg,
|
|
24
|
+
],
|
|
25
|
+
cwd=repo,
|
|
26
|
+
check=True,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _intent_to_add(repo: Path) -> None:
|
|
31
|
+
"""Mark all untracked files with ``git add -N`` so ``status --porcelain``
|
|
32
|
+
lists them individually (rather than collapsing into the parent dir).
|
|
33
|
+
|
|
34
|
+
Real-world plugins write files into already-tracked directories, so
|
|
35
|
+
per-file porcelain entries are the production reality. This helper
|
|
36
|
+
reproduces that shape from a fresh tmp_git_repo.
|
|
37
|
+
"""
|
|
38
|
+
subprocess.run(["git", "add", "-N", "."], cwd=repo, check=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_given_no_registration_when_dirty_files_then_all_returned(
|
|
42
|
+
tmp_git_repo: Path,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Baseline: with no registration, today's behavior is preserved."""
|
|
45
|
+
from agent_runner.vcs_state import detect_dirty_files
|
|
46
|
+
|
|
47
|
+
(tmp_git_repo / "proposals").mkdir()
|
|
48
|
+
(tmp_git_repo / "proposals" / "report.md").write_text("hello\n")
|
|
49
|
+
(tmp_git_repo / "other.txt").write_text("x\n")
|
|
50
|
+
_intent_to_add(tmp_git_repo)
|
|
51
|
+
|
|
52
|
+
dirty = detect_dirty_files(tmp_git_repo)
|
|
53
|
+
assert "other.txt" in dirty
|
|
54
|
+
assert "proposals/report.md" in dirty
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_given_proposals_registered_when_dirty_files_then_proposals_filtered(
|
|
58
|
+
tmp_git_repo: Path,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Plugin-owned 'proposals/' files are excluded from dirty list."""
|
|
61
|
+
from agent_runner.vcs_state import detect_dirty_files, register_plugin_owned_paths
|
|
62
|
+
|
|
63
|
+
register_plugin_owned_paths(["proposals/"])
|
|
64
|
+
(tmp_git_repo / "proposals").mkdir()
|
|
65
|
+
(tmp_git_repo / "proposals" / "report.md").write_text("hello\n")
|
|
66
|
+
(tmp_git_repo / "other.txt").write_text("x\n")
|
|
67
|
+
_intent_to_add(tmp_git_repo)
|
|
68
|
+
|
|
69
|
+
dirty = detect_dirty_files(tmp_git_repo)
|
|
70
|
+
assert "other.txt" in dirty
|
|
71
|
+
assert "proposals/report.md" not in dirty
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_given_recursive_glob_registered_when_dirty_files_then_deep_paths_filtered(
|
|
75
|
+
tmp_git_repo: Path,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Recursive ``**`` glob excludes nested paths too."""
|
|
78
|
+
from agent_runner.vcs_state import detect_dirty_files, register_plugin_owned_paths
|
|
79
|
+
|
|
80
|
+
register_plugin_owned_paths(["logs/plugins/**/*"])
|
|
81
|
+
(tmp_git_repo / "logs" / "plugins" / "argus").mkdir(parents=True)
|
|
82
|
+
(tmp_git_repo / "logs" / "plugins" / "argus" / "state.json").write_text("{}\n")
|
|
83
|
+
(tmp_git_repo / "logs" / "other.log").write_text("x\n")
|
|
84
|
+
_intent_to_add(tmp_git_repo)
|
|
85
|
+
|
|
86
|
+
dirty = detect_dirty_files(tmp_git_repo)
|
|
87
|
+
assert "logs/plugins/argus/state.json" not in dirty
|
|
88
|
+
assert "logs/other.log" in dirty
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Single ceiling on production module sizes.
|
|
2
|
+
|
|
3
|
+
Catches the actual signal worth catching — "this file got long enough that
|
|
4
|
+
a split is overdue" — without the per-release ratchet-bumping bookkeeping
|
|
5
|
+
the old parameterised version generated.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
PKG = Path(__file__).resolve().parent.parent.parent / "agent_runner"
|
|
13
|
+
LIMIT = 1000
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_given_production_module_when_counted_then_under_thousand_lines() -> None:
|
|
17
|
+
offenders: list[tuple[str, int]] = []
|
|
18
|
+
for path in PKG.rglob("*.py"):
|
|
19
|
+
n = sum(1 for _ in path.read_text(encoding="utf-8").splitlines())
|
|
20
|
+
if n > LIMIT:
|
|
21
|
+
offenders.append((str(path.relative_to(PKG)), n))
|
|
22
|
+
assert offenders == [], f"modules exceed {LIMIT} LOC; split overdue: {offenders}"
|