delegate-agent-cli 0.21.0__tar.gz → 0.23.0__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.
Files changed (146) hide show
  1. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/CHANGELOG.md +23 -0
  2. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/PKG-INFO +2 -1
  3. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/README.md +1 -0
  4. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/docs/cli-reference.md +3 -0
  5. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/docs/delegate-workflows.md +2 -0
  6. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/pyproject.toml +1 -1
  7. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/__init__.py +1 -1
  8. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/capability_commands.py +1 -2
  9. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/cli.py +9 -16
  10. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/cli_parser.py +43 -25
  11. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/command_errors.py +2 -0
  12. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/command_help.py +30 -1
  13. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/config.py +3 -5
  14. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/config_commands.py +7 -3
  15. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/constants.py +24 -0
  16. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/describe_payload.py +5 -14
  17. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/harness_discovery.py +8 -4
  18. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/inspection_commands.py +31 -0
  19. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/profiles.py +1 -6
  20. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/prompt_transport.py +18 -0
  21. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/reasoning.py +2 -31
  22. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/redaction.py +5 -5
  23. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/rendering.py +18 -0
  24. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/request_build.py +33 -82
  25. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/run_registry.py +155 -5
  26. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/run_status.py +6 -10
  27. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/runner.py +42 -111
  28. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/safe_workspace.py +0 -4
  29. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/setup_commands.py +3 -12
  30. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/snapshot_view.py +2 -2
  31. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/structured_output.py +8 -7
  32. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/workflows/commands.py +12 -12
  33. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/workflows/registry.py +3 -4
  34. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/workflows/runtime.py +53 -45
  35. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/workflows/schema.py +5 -4
  36. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/workflows/script.py +8 -4
  37. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/worktree_records.py +1 -3
  38. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent_cli.egg-info/PKG-INFO +2 -1
  39. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_delegate_help_cli.py +1 -0
  40. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_delegate_parser.py +24 -0
  41. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_execution_worktree_failure_cleanup.py +0 -3
  42. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_inspection_commands.py +170 -1
  43. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_retention.py +0 -3
  44. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/CONTRIBUTING.md +0 -0
  45. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/LICENSE +0 -0
  46. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/MANIFEST.in +0 -0
  47. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/SECURITY.md +0 -0
  48. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/bin/delegate-profile-shim +0 -0
  49. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/bin/delegate.py +0 -0
  50. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/config.example.json +0 -0
  51. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/docs/agent-setup.md +0 -0
  52. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/docs/assets/delegate-agent-header.png +0 -0
  53. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/docs/configuration.md +0 -0
  54. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/docs/development.md +0 -0
  55. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/docs/live-runtime.md +0 -0
  56. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/docs/publishing-checklist.md +0 -0
  57. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/docs/security-model.md +0 -0
  58. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/docs/troubleshooting.md +0 -0
  59. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/docs/worktrees.md +0 -0
  60. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/examples/task.claude.json +0 -0
  61. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/examples/task.codex.json +0 -0
  62. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/examples/task.cursor.json +0 -0
  63. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/examples/task.droid.json +0 -0
  64. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/examples/task.grok.json +0 -0
  65. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/examples/task.judge.json +0 -0
  66. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/examples/task.kimi.json +0 -0
  67. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/setup.cfg +0 -0
  68. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/archived_logs.py +0 -0
  69. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/argv_builders.py +0 -0
  70. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/argv_utils.py +0 -0
  71. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/bundled_models.py +0 -0
  72. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/child_failures.py +0 -0
  73. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/errors.py +0 -0
  74. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/git_utils.py +0 -0
  75. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/harness_events.py +0 -0
  76. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/isolation.py +0 -0
  77. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/json_types.py +0 -0
  78. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/log_output.py +0 -0
  79. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/model_discovery.py +0 -0
  80. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/private_io.py +0 -0
  81. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/profile_commands.py +0 -0
  82. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/profile_guard.py +0 -0
  83. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/prompt_instructions.py +0 -0
  84. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/request_models.py +0 -0
  85. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/retention.py +0 -0
  86. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/run_metadata.py +0 -0
  87. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/run_output_commands.py +0 -0
  88. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/seatbelt.py +0 -0
  89. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/wait_cancel_commands.py +0 -0
  90. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/workflows/__init__.py +0 -0
  91. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/worktree_commands.py +0 -0
  92. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/worktree_execution.py +0 -0
  93. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/worktree_gc.py +0 -0
  94. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/worktree_mgmt.py +0 -0
  95. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/worktree_remove.py +0 -0
  96. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/worktree_summary.py +0 -0
  97. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent/wsl.py +0 -0
  98. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent_cli.egg-info/SOURCES.txt +0 -0
  99. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent_cli.egg-info/dependency_links.txt +0 -0
  100. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent_cli.egg-info/entry_points.txt +0 -0
  101. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent_cli.egg-info/requires.txt +0 -0
  102. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/src/delegate_agent_cli.egg-info/top_level.txt +0 -0
  103. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_capability_commands.py +0 -0
  104. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_child_failures.py +0 -0
  105. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_codex_pure_sandbox.py +0 -0
  106. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_codex_thread_resilience.py +0 -0
  107. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_command_help.py +0 -0
  108. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_config_commands.py +0 -0
  109. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_delegate_isolation.py +0 -0
  110. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_delegate_validation.py +0 -0
  111. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_end_to_end_tracking.py +0 -0
  112. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_engine_argv.py +0 -0
  113. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_execution_argv_and_prompt.py +0 -0
  114. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_execution_dry_run.py +0 -0
  115. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_execution_worktree_preflight.py +0 -0
  116. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_execution_worktree_run.py +0 -0
  117. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_harness_discovery.py +0 -0
  118. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_harness_events.py +0 -0
  119. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_kimi_commands.py +0 -0
  120. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_model_discovery.py +0 -0
  121. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_model_selection_wave1a.py +0 -0
  122. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_model_selection_wave1b.py +0 -0
  123. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_omp_read_only_behavior.py +0 -0
  124. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_packaging.py +0 -0
  125. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_profiles.py +0 -0
  126. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_pure_call.py +0 -0
  127. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_reasoning_capabilities.py +0 -0
  128. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_run_registry.py +0 -0
  129. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_runner_capture.py +0 -0
  130. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_safe_workspace_isolation.py +0 -0
  131. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_setup_commands.py +0 -0
  132. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_slash_passthrough.py +0 -0
  133. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_snapshot_redaction.py +0 -0
  134. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_snapshot_rendering.py +0 -0
  135. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_snapshot_run_output.py +0 -0
  136. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_snapshot_view.py +0 -0
  137. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_structured_output.py +0 -0
  138. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_tracked_timeout.py +0 -0
  139. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_utility_modules.py +0 -0
  140. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_wait_cancel_commands.py +0 -0
  141. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_wave4_launch_features.py +0 -0
  142. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_workflow_commands.py +0 -0
  143. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_worktree_list_show.py +0 -0
  144. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_worktree_prune_gc.py +0 -0
  145. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_worktree_remove.py +0 -0
  146. {delegate_agent_cli-0.21.0 → delegate_agent_cli-0.23.0}/tests/test_wsl_guardrails.py +0 -0
@@ -5,6 +5,27 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.23.0] - 2026-07-30
9
+
10
+ ### Added
11
+
12
+ - Added `delegate runs prune` with a 30-day default age threshold, an explicit `--older-than` override, and `--dry-run` preview output. It prunes terminal Run records and their associated Registry artifacts (per-Run directory, logs, Completion Report, retained raw-log archive) without touching genuinely running Runs. Persistent-worktree Runs are skipped unless their worktree is recorded as removed or missing — unknown or unrecorded worktree status is treated as possibly present, mirroring `worktree gc` — so live worktrees never lose their Registry records. The ambient retention pass is bypassed on the prune command path so a dry run never mutates state.
13
+
14
+ ### Fixed
15
+
16
+ - Focused unittest invocations (`python3 -m unittest tests.<module>`) no longer fail on the src layout import: `tests/__init__.py` now puts `src` on `sys.path` before any test module import runs.
17
+
18
+ ## [0.22.0] - 2026-07-26
19
+
20
+ ### Changed
21
+
22
+ - Consolidated pure-call validation in `constants.validate_pure_call`, collapsed repeated `attempt`/`call` invocations in `runner.py` into local closures, eliminated `typing.Any` from `src`, and removed defensive `getattr`/`isinstance` hedges where the contract guarantees the type.
23
+
24
+ ### Removed
25
+
26
+ - Removed the write-only `ReasoningCapability.supported_efforts` field and unused `STATUS_FILTER_ACTIVE`/`STATUS_FILTER_RECENT` constants.
27
+ - Removed dead back-compat re-exports from `delegate_agent.cli`: `build_kimi_argv`, `build_opencode_argv`, `build_pi_argv`, `prefix_droid_safe_prompt`, `redacted_prompt_argv`, `KIMI_PROMPT_REDACTION`, `_grok_harness_bypass_enabled`, `_resolve_default_model`, and `block_external_symlinks`. These had no known consumers; the supported import surface is the CLI and `delegate --json` contracts.
28
+
8
29
  ## [0.21.0] - 2026-07-26
9
30
 
10
31
  ### Added
@@ -748,6 +769,8 @@ Usage-audit fix wave: 82 sessions and 1,241 delegate invocations from one week o
748
769
 
749
770
  - Releases before 0.1.3 predate this changelog.
750
771
 
772
+ [0.23.0]: https://github.com/treygoff24/delegate-agent/compare/v0.22.0...v0.23.0
773
+ [0.22.0]: https://github.com/treygoff24/delegate-agent/compare/v0.21.0...v0.22.0
751
774
  [0.21.0]: https://github.com/treygoff24/delegate-agent/compare/v0.20.0...v0.21.0
752
775
  [0.20.0]: https://github.com/treygoff24/delegate-agent/compare/v0.19.0...v0.20.0
753
776
  [0.19.0]: https://github.com/treygoff24/delegate-agent/compare/v0.18.0...v0.19.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: delegate-agent-cli
3
- Version: 0.21.0
3
+ Version: 0.23.0
4
4
  Summary: A tiny CLI for delegating bounded agent tasks across coding-agent runtimes.
5
5
  Author: Trey Goff
6
6
  License-Expression: MIT
@@ -332,6 +332,7 @@ delegate runs --recent
332
332
  delegate ps # active and stale tracked runs
333
333
  delegate snapshot <alias-or-runId>
334
334
  delegate run-output <alias-or-runId>
335
+ delegate runs prune --dry-run # preview GC of terminal run records older than 30 days
335
336
  ```
336
337
 
337
338
  Aliases are always numbered (`codex-1`, `cursor-2`); a **bare harness name
@@ -299,6 +299,7 @@ delegate runs --recent
299
299
  delegate ps # active and stale tracked runs
300
300
  delegate snapshot <alias-or-runId>
301
301
  delegate run-output <alias-or-runId>
302
+ delegate runs prune --dry-run # preview GC of terminal run records older than 30 days
302
303
  ```
303
304
 
304
305
  Aliases are always numbered (`codex-1`, `cursor-2`); a **bare harness name
@@ -931,6 +931,7 @@ Tracked runs return bounded parent-facing output and store local metadata under
931
931
 
932
932
  ```bash
933
933
  delegate runs [--active|--running|--stale|--recent] [--harness HARNESS] [--group NAME] [--limit N]
934
+ delegate runs prune [--older-than DAYS] [--dry-run]
934
935
  delegate ps [--harness HARNESS] [--group NAME] [--limit N]
935
936
  delegate snapshot [--latest HARNESS] [--no-redact] <handle>
936
937
  delegate run-output [--latest HARNESS] <handle> [--completion-report] [--stdout] [--stderr] [--tail N] [--max-chars N] [--raw] [--no-redact]
@@ -942,6 +943,8 @@ delegate cancel <handle>...
942
943
  `delegate ps` is the shorter first-class form of `delegate runs --active` and
943
944
  accepts the same harness, group, and limit selectors.
944
945
 
946
+ `delegate runs prune` removes old terminal Run records from the workspace Registry so they stop accumulating forever. Only runs whose effective status is terminal (`succeeded`, `failed`, `cancelled`, or `stale` from a dead child) and whose last Registry activity is older than the threshold (default 30 days; override with `--older-than DAYS`) are eligible. Effectively running runs are always skipped, and persistent-worktree runs are skipped unless their worktree is recorded as removed or missing — pruning the record of a live worktree would orphan it from `worktree list`/`show`/`remove`. Pruning deletes the per-Run directory (Snapshot, Manifest, logs, events, Completion Report) and the retained raw-log archive; worktree paths on disk are never touched. `--dry-run` reports what would be removed without changing anything. JSON output uses schema `delegate.runs-prune.v1` with `planned`, `removed`, `skipped` (each entry carries a `reason`), and `errors` sections.
947
+
945
948
  Run-scoped handles (`snapshot`, `run-output`, `wait`, and `cancel`)
946
949
  resolve exact run IDs and numbered aliases first. A bare harness name such as
947
950
  `codex` resolves to that harness's latest run, and `harness:modelAlias` (for
@@ -175,6 +175,7 @@ engines = args.get("engines", ["codex", "claude", "cursor"])
175
175
  models = args.get("models", {})
176
176
  prompt = args["prompt"]
177
177
 
178
+
178
179
  def reviewer(engine):
179
180
  return lambda: agent(
180
181
  prompt,
@@ -184,6 +185,7 @@ def reviewer(engine):
184
185
  phase=f"Review ({engine})",
185
186
  )
186
187
 
188
+
187
189
  reviews = parallel([reviewer(engine) for engine in engines])
188
190
  return {"reviews": dict(zip(engines, reviews))}
189
191
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "delegate-agent-cli"
7
- version = "0.21.0"
7
+ version = "0.23.0"
8
8
  description = "A tiny CLI for delegating bounded agent tasks across coding-agent runtimes."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,5 +1,5 @@
1
1
  """Delegate Agent CLI package."""
2
2
 
3
- VERSION = "0.21.0"
3
+ VERSION = "0.23.0"
4
4
 
5
5
  __all__ = ["VERSION"]
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Callable
4
3
  from dataclasses import dataclass
5
4
  from pathlib import Path
6
5
  from typing import TextIO
@@ -139,7 +138,7 @@ def _refresh_payload(
139
138
  *,
140
139
  profile: profiles.ProfileResolution,
141
140
  engines: tuple[str, ...] | None = None,
142
- progress: Callable[[str], None] | None = None,
141
+ progress: harness_discovery.ProgressCallback | None = None,
143
142
  ) -> JsonObject:
144
143
  try:
145
144
  result = harness_discovery.refresh_discovery(
@@ -40,20 +40,14 @@ from delegate_agent import runner as delegate_runner
40
40
  from delegate_agent.argv_builders import ( # noqa: F401 # re-exported for tests / back-compat
41
41
  SAFE_REVIEW_PREFIX_BY_ENGINE,
42
42
  _claude_harness_bypass_enabled,
43
- _grok_harness_bypass_enabled,
44
43
  build_claude_argv,
45
44
  build_codex_argv,
46
45
  build_cursor_argv,
47
46
  build_devin_argv,
48
47
  build_droid_argv,
49
48
  build_grok_argv,
50
- build_kimi_argv,
51
49
  build_omp_argv,
52
- build_opencode_argv,
53
- build_pi_argv,
54
50
  prefix_cursor_safe_prompt,
55
- prefix_droid_safe_prompt,
56
- redacted_prompt_argv,
57
51
  )
58
52
  from delegate_agent.argv_utils import public_argv
59
53
  from delegate_agent.cli_parser import ( # noqa: F401 # re-exported for tests / back-compat
@@ -98,7 +92,6 @@ from delegate_agent.prompt_transport import ( # noqa: F401 # CURSOR_PROMPT_RED
98
92
  DEVIN_AGENT_CONFIG_DISPLAY,
99
93
  DROID_PROMPT_FILE_ARG_PLACEHOLDER,
100
94
  DROID_PROMPT_FILE_DISPLAY,
101
- KIMI_PROMPT_REDACTION,
102
95
  PROMPT_FILE_ARG_PLACEHOLDER,
103
96
  PROMPT_FILE_DISPLAY,
104
97
  PROMPT_TRANSPORT_ARGV,
@@ -109,7 +102,6 @@ from delegate_agent.request_build import ( # noqa: F401 # re-exported for test
109
102
  CALL_TEMP_CWD_PLACEHOLDER,
110
103
  RUN_INPUT_KEYS,
111
104
  _load_input_json_object,
112
- _resolve_default_model,
113
105
  build_request,
114
106
  effective_prompt,
115
107
  load_config,
@@ -136,7 +128,6 @@ from delegate_agent.safe_workspace import ( # noqa: F401 # re-exported for tes
136
128
  SAFE_BLOCKED_SYMLINK_PLACEHOLDER,
137
129
  SAFE_EXTERNAL_SYMLINK_WARNING_PREFIX,
138
130
  SAFE_UNBORN_GIT_WARNING,
139
- block_external_symlinks,
140
131
  cleanup_safe_isolated_workspace,
141
132
  create_directory_safe_workspace,
142
133
  create_git_safe_workspace,
@@ -332,7 +323,6 @@ def dry_run_payload(request: Request) -> JsonObject:
332
323
  if request.profile_resolution.name is not None:
333
324
  payload["profileEnv"] = redaction.redact_env_map(request.profile_resolution.env)
334
325
 
335
- # Structured isolation fields from the isolation context.
336
326
  if request.isolation_context is not None:
337
327
  ctx = request.isolation_context
338
328
 
@@ -515,7 +505,6 @@ def make_run_context(
515
505
  if request.isolation_context is not None
516
506
  else False
517
507
  )
518
- # Extract isolation metadata from the isolation context.
519
508
  iso_ctx = request.isolation_context
520
509
  if iso_ctx is not None:
521
510
  isolation_mode = iso_ctx.isolation_mode
@@ -860,7 +849,7 @@ def execute_request(
860
849
  profiles.preflight_codex_request(request, config.get("codex", {}))
861
850
  ctx = request.isolation_context
862
851
 
863
- # --- Persistent worktree path (work + worktree) ---
852
+ # Persistent worktree runs (work + worktree) hand off to worktree_execution.
864
853
  if ctx is not None and ctx.isolation_lifecycle == "persistent":
865
854
  try:
866
855
  return worktree_execution.execute_persistent_worktree(
@@ -1022,7 +1011,6 @@ def pre_read_run_json_for_config(
1022
1011
  if json_cwd is not None and not isinstance(json_cwd, str):
1023
1012
  raise DelegateError("invalid_cwd", "cwd must be a string.")
1024
1013
 
1025
- # Reject explicit null isolation in the JSON pre-read.
1026
1014
  if "isolation" in raw and raw["isolation"] is None:
1027
1015
  raise DelegateError(
1028
1016
  "invalid_isolation",
@@ -1192,7 +1180,12 @@ def main(
1192
1180
  "workflow",
1193
1181
  }:
1194
1182
  existing_registry = run_registry.registry_root_if_exists(Path(workspace.path))
1195
- if existing_registry is not None:
1183
+ pruning_runs = (
1184
+ parsed.subcommand == "runs"
1185
+ and parsed.runs is not None
1186
+ and parsed.runs.action == "prune"
1187
+ )
1188
+ if existing_registry is not None and not pruning_runs:
1196
1189
  maybe_run_retention_pass(existing_registry, config)
1197
1190
  if parsed.subcommand == "snapshot":
1198
1191
  return emit_snapshot(parsed, workspace, stdout)
@@ -1280,8 +1273,8 @@ def main(
1280
1273
  DelegateError(
1281
1274
  exc.error,
1282
1275
  exc.message,
1283
- diagnostics=getattr(exc, "diagnostics", None),
1284
- next_actions=getattr(exc, "next_actions", None),
1276
+ diagnostics=exc.diagnostics,
1277
+ next_actions=exc.next_actions,
1285
1278
  ),
1286
1279
  json_mode,
1287
1280
  stdout,
@@ -32,10 +32,9 @@ from delegate_agent.constants import (
32
32
  ENGINES_PROSE,
33
33
  KNOWN_ENGINES,
34
34
  MODELESS_ENGINES,
35
- PURE_CALL_ENGINES,
36
35
  VALID_MODES,
37
- pure_call_supported,
38
36
  validate_mode,
37
+ validate_pure_call,
39
38
  )
40
39
  from delegate_agent.errors import DelegateError
41
40
  from delegate_agent.request_models import (
@@ -647,10 +646,6 @@ def corrected_global_argv(argv: list[str]) -> list[str]:
647
646
  return [*globals_out, *rest]
648
647
 
649
648
 
650
- def corrected_global_command(argv: list[str]) -> str:
651
- return _shell_command(corrected_global_argv(argv))
652
-
653
-
654
649
  def raise_misplaced_global_option(message: str, argv: list[str] | None = None) -> NoReturn:
655
650
  guidance = (
656
651
  "Move global options before the subcommand "
@@ -1320,22 +1315,7 @@ def _validate_pure_options(
1320
1315
  return
1321
1316
  if mode != "call":
1322
1317
  raise DelegateError("unsupported_pure_call", "--pure only applies to call mode.")
1323
- if group is not None:
1324
- raise DelegateError(
1325
- "pure_conflicts_group",
1326
- "--pure cannot be combined with --group; pure call is a stateless one-hop completion.",
1327
- )
1328
- if read_only:
1329
- raise DelegateError(
1330
- "pure_conflicts_read_only", "--pure cannot be combined with --read-only."
1331
- )
1332
- if not pure_call_supported(engine):
1333
- supported = ", ".join(PURE_CALL_ENGINES)
1334
- raise DelegateError(
1335
- "unsupported_pure_call",
1336
- f"{engine} does not support pure call mode.",
1337
- next_actions=[f"Use --pure with one of: {supported}."],
1338
- )
1318
+ validate_pure_call(engine, pure=pure, read_only=read_only, group=group)
1339
1319
 
1340
1320
 
1341
1321
  def parse_snapshot(rest: list[str], json_mode: bool, cwd: str | None) -> ParsedCommand:
@@ -1400,7 +1380,10 @@ def parse_runs(
1400
1380
  # front-ends (ps) do not report errors against flags nobody entered.
1401
1381
  rest, json_mode = consume_json_option(rest, json_mode)
1402
1382
  if any(command_help.is_help_token(token) for token in rest):
1403
- return help_command(json_mode, "runs")
1383
+ topic = "runs prune" if rest and rest[0] == "prune" else "runs"
1384
+ return help_command(json_mode, topic)
1385
+ if rest and rest[0] == "prune":
1386
+ return parse_runs_prune(rest[1:], json_mode, cwd)
1404
1387
  active = False
1405
1388
  recent = False
1406
1389
  running = False
@@ -1488,6 +1471,37 @@ def parse_runs(
1488
1471
  )
1489
1472
 
1490
1473
 
1474
+ def parse_runs_prune(rest: list[str], json_mode: bool, cwd: str | None) -> ParsedCommand:
1475
+ rest, json_mode = consume_json_option(rest, json_mode)
1476
+ if any(command_help.is_help_token(token) for token in rest):
1477
+ return help_command(json_mode, "runs prune")
1478
+ older_than_days = None
1479
+ dry_run = False
1480
+ i = 0
1481
+ while i < len(rest):
1482
+ token = rest[i]
1483
+ if token == "--older-than":
1484
+ value = _require_option_value(rest, i, token)
1485
+ older_than_days = parse_non_negative_int(value, option="runs prune --older-than")
1486
+ i += 2
1487
+ continue
1488
+ if token == "--dry-run":
1489
+ dry_run = True
1490
+ i += 1
1491
+ continue
1492
+ raise DelegateError("unknown_option", f"runs prune does not support option: {token}")
1493
+ return ParsedCommand(
1494
+ "runs",
1495
+ global_options=GlobalOptions(json_mode=json_mode, cwd=cwd),
1496
+ runs=inspection_commands.RunsCommand(
1497
+ action="prune",
1498
+ older_than_days=older_than_days,
1499
+ dry_run=dry_run,
1500
+ json_mode=json_mode,
1501
+ ),
1502
+ )
1503
+
1504
+
1491
1505
  def parse_ps(rest: list[str], json_mode: bool, cwd: str | None) -> ParsedCommand:
1492
1506
  rest, json_mode = consume_json_option(rest, json_mode)
1493
1507
  if any(command_help.is_help_token(token) for token in rest):
@@ -1982,6 +1996,10 @@ def _require_option_value(rest: list[str], index: int, option: str) -> str:
1982
1996
 
1983
1997
  WorktreeOptionSpec = tuple[str, str]
1984
1998
 
1999
+ # Parsed worktree option values: flags store True, "str"/"group"/"status" kinds
2000
+ # store the validated string, and the int kinds store a parsed int.
2001
+ WorktreeOptionValue = str | int | bool
2002
+
1985
2003
  WORKTREE_OPTION_SPECS: dict[str, dict[str, WorktreeOptionSpec]] = {
1986
2004
  "list": {
1987
2005
  "--harness": ("str", "harness"),
@@ -2020,7 +2038,7 @@ WORKTREE_OPTION_SPECS: dict[str, dict[str, WorktreeOptionSpec]] = {
2020
2038
 
2021
2039
 
2022
2040
  def _apply_worktree_option(
2023
- options: dict[str, object],
2041
+ options: dict[str, WorktreeOptionValue],
2024
2042
  args: list[str],
2025
2043
  index: int,
2026
2044
  option: str,
@@ -2069,7 +2087,7 @@ def parse_worktree(rest: list[str], json_mode: bool, cwd: str | None) -> ParsedC
2069
2087
  return help_command(json_mode, topic)
2070
2088
  if action not in WORKTREE_OPTION_SPECS:
2071
2089
  raise DelegateError("unknown_worktree_action", f"Unknown worktree action: {action}")
2072
- options: dict[str, object] = {}
2090
+ options: dict[str, WorktreeOptionValue] = {}
2073
2091
  positional: list[str] = []
2074
2092
  i = 0
2075
2093
  action_specs = WORKTREE_OPTION_SPECS[action]
@@ -10,6 +10,8 @@ class CommandError(Exception):
10
10
  super().__init__(message)
11
11
  self.error = error
12
12
  self.message = message
13
+ self.diagnostics: dict | None = None
14
+ self.next_actions: list[str] | None = None
13
15
 
14
16
 
15
17
  def resolve_run_target(
@@ -16,6 +16,7 @@ from dataclasses import dataclass, field
16
16
  from delegate_agent import VERSION
17
17
  from delegate_agent.constants import ENGINES_PROSE
18
18
  from delegate_agent.json_types import JsonObject
19
+ from delegate_agent.run_registry import DEFAULT_RUN_PRUNE_DAYS
19
20
 
20
21
 
21
22
  @dataclass(frozen=True)
@@ -818,7 +819,34 @@ COMMAND_SPECS: dict[str, CommandSpec] = {
818
819
  "delegate runs --group wave4",
819
820
  ),
820
821
  notes=("--active, --running, --stale, and --recent are mutually exclusive.",),
821
- see_also=("ps", "snapshot", "run-output"),
822
+ see_also=("ps", "runs prune", "snapshot", "run-output"),
823
+ unsupported_global_options=("--auth-profile",),
824
+ ),
825
+ "runs prune": CommandSpec(
826
+ name="runs prune",
827
+ summary="Prune old terminal Run records from the workspace Registry.",
828
+ usage=("delegate [--cwd PATH] [--json] runs prune [--older-than DAYS] [--dry-run]",),
829
+ options=(
830
+ OptionSpec(
831
+ "--older-than",
832
+ "DAYS",
833
+ f"Prune Runs older than DAYS by Registry activity time (defaults to {DEFAULT_RUN_PRUNE_DAYS}; non-negative integer).",
834
+ ),
835
+ OptionSpec(
836
+ "--dry-run",
837
+ None,
838
+ "Report planned removals without changing the Registry, logs, or artifacts.",
839
+ ),
840
+ ),
841
+ examples=(
842
+ "delegate runs prune --dry-run",
843
+ "delegate runs prune --older-than 14",
844
+ ),
845
+ notes=(
846
+ "Only terminal effective statuses are eligible: succeeded, failed, cancelled, and stale from a dead child; effective running Runs are always skipped.",
847
+ "Runs with a registered present persistent worktree are skipped, so their worktree remains manageable. Pruning removes the per-Run directory, including its Snapshot, Manifest, logs, events, and Completion Report, plus the retained raw-log archive.",
848
+ ),
849
+ see_also=("runs", "snapshot", "run-output"),
822
850
  unsupported_global_options=("--auth-profile",),
823
851
  ),
824
852
  "ps": CommandSpec(
@@ -1745,6 +1773,7 @@ def render_overview_text() -> str:
1745
1773
  "delegate [--cwd PATH] [--json] snapshot [--latest HARNESS] [--no-redact] <handle>",
1746
1774
  "delegate [--cwd PATH] [--json] runs "
1747
1775
  "[--active|--running|--stale|--recent] [--harness HARNESS] [--limit N]",
1776
+ "delegate [--cwd PATH] [--json] runs prune [--older-than DAYS] [--dry-run]",
1748
1777
  "delegate [--cwd PATH] [--json] ps [--harness HARNESS] [--group NAME] [--limit N]",
1749
1778
  "delegate [--cwd PATH] [--json] run-output <handle> "
1750
1779
  "[--completion-report] [--stdout] [--stderr] [--tail N] [--max-chars N] "
@@ -821,11 +821,9 @@ def _reject_opencode_flag_like_value(
821
821
  def _reject_pi_family_flag_like_value(value: JsonValue, *, path: str, error: str) -> None:
822
822
  if value is None:
823
823
  return
824
- if not isinstance(value, str) or not value.strip() or value.startswith("-"):
825
- raise ConfigError(
826
- error,
827
- f"{path} must be a non-empty string that does not start with '-'.",
828
- )
824
+ _reject_opencode_flag_like_value(value, path=path, error=error)
825
+ if not isinstance(value, str):
826
+ return
829
827
  if ":" in value:
830
828
  raise ConfigError(
831
829
  error,
@@ -23,10 +23,14 @@ class ConfigCommand:
23
23
  PROFILE_CONFIG_NAMES = delegate_config.PROFILE_CONFIG_NAMES
24
24
 
25
25
 
26
- def _target_path() -> Path:
26
+ def target_config_path() -> Path:
27
+ """Resolve the config path, rejecting Windows-style paths under WSL."""
27
28
  raw = delegate_config.config_path()
28
29
  if wsl.should_reject_windows_path(str(raw)):
29
- raise DelegateError("windows_path", wsl.windows_path_message("DELEGATE_CONFIG", str(raw)))
30
+ raise DelegateError(
31
+ "windows_path",
32
+ wsl.windows_path_message(delegate_config.CONFIG_ENV, str(raw)),
33
+ )
30
34
  return raw
31
35
 
32
36
 
@@ -94,7 +98,7 @@ def _validated_effective_config(config: JsonObject) -> JsonObject:
94
98
  def emit(command: ConfigCommand, stdout: TextIO) -> int:
95
99
  if command.action not in {"init", "sync-profiles"}:
96
100
  raise DelegateError("invalid_config_command", f"Unknown config action: {command.action}")
97
- path = _target_path()
101
+ path = target_config_path()
98
102
  if command.action == "sync-profiles":
99
103
  if not path.exists():
100
104
  raise DelegateError(
@@ -53,6 +53,30 @@ def pure_call_supported(engine: str) -> bool:
53
53
  return engine == "claude"
54
54
 
55
55
 
56
+ def validate_pure_call(
57
+ engine: str, *, pure: bool, read_only: bool, group: str | None = None
58
+ ) -> None:
59
+ """Reject --pure combinations that conflict with its stateless one-hop contract."""
60
+ if not pure:
61
+ return
62
+ if group is not None:
63
+ raise DelegateError(
64
+ "pure_conflicts_group",
65
+ "--pure cannot be combined with --group; pure call is a stateless one-hop completion.",
66
+ )
67
+ if read_only:
68
+ raise DelegateError(
69
+ "pure_conflicts_read_only", "--pure cannot be combined with --read-only."
70
+ )
71
+ if not pure_call_supported(engine):
72
+ supported = ", ".join(PURE_CALL_ENGINES)
73
+ raise DelegateError(
74
+ "unsupported_pure_call",
75
+ f"{engine} does not support pure call mode.",
76
+ next_actions=[f"Use --pure with one of: {supported}."],
77
+ )
78
+
79
+
56
80
  # Public harness capability contract. Request validation and describe output both
57
81
  # derive from this map so enabling a capability cannot drift between the two.
58
82
  ENGINE_CAPABILITIES = {
@@ -48,14 +48,12 @@ from delegate_agent.constants import (
48
48
  from delegate_agent.errors import EXIT_OK, DelegateError
49
49
  from delegate_agent.json_types import JsonObject
50
50
  from delegate_agent.prompt_transport import (
51
- DEVIN_AGENT_CONFIG_ARG_PLACEHOLDER,
52
- DEVIN_AGENT_CONFIG_DISPLAY,
53
51
  DROID_PROMPT_FILE_DISPLAY,
54
- PROMPT_FILE_ARG_PLACEHOLDER,
55
- PROMPT_FILE_DISPLAY,
56
52
  PROMPT_TRANSPORT_ARGV,
57
53
  PROMPT_TRANSPORT_FILE,
58
54
  PROMPT_TRANSPORT_STDIN,
55
+ devin_display_argv,
56
+ prompt_file_display_argv,
59
57
  )
60
58
  from delegate_agent.request_build import OPENCODE_SAFE_AGENT, _resolve_default_model
61
59
 
@@ -718,7 +716,7 @@ def _grok_describe_argv(
718
716
  policy,
719
717
  allow_bypass_permissions=_grok_harness_bypass_enabled(config, mode),
720
718
  )
721
- return [PROMPT_FILE_DISPLAY if item == PROMPT_FILE_ARG_PLACEHOLDER else item for item in argv]
719
+ return prompt_file_display_argv(argv)
722
720
 
723
721
 
724
722
  def _devin_describe_argv(
@@ -730,14 +728,7 @@ def _devin_describe_argv(
730
728
  _resolve_default_model(devin),
731
729
  call_read_only=call_read_only,
732
730
  )
733
- return [
734
- DEVIN_AGENT_CONFIG_DISPLAY
735
- if item == DEVIN_AGENT_CONFIG_ARG_PLACEHOLDER
736
- else PROMPT_FILE_DISPLAY
737
- if item == PROMPT_FILE_ARG_PLACEHOLDER
738
- else item
739
- for item in argv
740
- ]
731
+ return devin_display_argv(argv)
741
732
 
742
733
 
743
734
  def _kimi_describe_argv(kimi: JsonObject, *, mode: str) -> list[str]:
@@ -774,7 +765,7 @@ def _opencode_describe_argv(
774
765
  if isinstance(opencode.get("defaultReasoningEffort"), str)
775
766
  else None,
776
767
  )
777
- return [PROMPT_FILE_DISPLAY if item == PROMPT_FILE_ARG_PLACEHOLDER else item for item in argv]
768
+ return prompt_file_display_argv(argv)
778
769
 
779
770
 
780
771
  def _pi_family_describe_argv(section: JsonObject, engine: str, *, mode: str) -> list[str]:
@@ -19,13 +19,17 @@ from collections.abc import Callable, Collection, Mapping
19
19
  from dataclasses import dataclass
20
20
  from datetime import UTC, datetime
21
21
  from pathlib import Path
22
- from typing import BinaryIO, TextIO
22
+ from typing import BinaryIO, TextIO, TypeAlias
23
23
 
24
24
  from delegate_agent import config as delegate_config
25
25
  from delegate_agent import private_io, profiles, redaction
26
26
  from delegate_agent.constants import KNOWN_ENGINES
27
27
  from delegate_agent.json_types import JsonObject
28
28
 
29
+ # A refresh-progress callback: called with each harness name before its probe
30
+ # spawns. Shared by every caller that threads a progress hook into discovery.
31
+ ProgressCallback: TypeAlias = Callable[[str], None]
32
+
29
33
  DISCOVERY_SCHEMA = 1
30
34
  PROBE_STATUSES = frozenset({"ok", "partial", "missing", "error"})
31
35
  MODEL_SCOPES = frozenset({"account", "configured", "catalog", "unknown"})
@@ -1364,7 +1368,7 @@ def probe_harness(
1364
1368
  )
1365
1369
 
1366
1370
 
1367
- def stderr_probe_progress(stream: TextIO | None = None) -> Callable[[str], None]:
1371
+ def stderr_probe_progress(stream: TextIO | None = None) -> ProgressCallback:
1368
1372
  """Return a progress callback that names each harness as probing starts.
1369
1373
 
1370
1374
  Every probe is bounded by ``METADATA_PROBE_TIMEOUT_SEC``, so a wedged
@@ -1384,7 +1388,7 @@ def probe_all_harnesses(
1384
1388
  *,
1385
1389
  env: Mapping[str, str],
1386
1390
  factory_settings_path: Path | None = None,
1387
- progress: Callable[[str], None] | None = None,
1391
+ progress: ProgressCallback | None = None,
1388
1392
  ) -> JsonObject:
1389
1393
  records: JsonObject = {}
1390
1394
  for harness in KNOWN_ENGINES:
@@ -1603,7 +1607,7 @@ def refresh_discovery(
1603
1607
  home: Path | None = None,
1604
1608
  factory_settings_path: Path | None = None,
1605
1609
  persist: bool = True,
1606
- progress: Callable[[str], None] | None = None,
1610
+ progress: ProgressCallback | None = None,
1607
1611
  ) -> JsonObject:
1608
1612
  """Probe harnesses, optionally persisting successful last-known-good records."""
1609
1613
  if isinstance(engines, str):
@@ -19,12 +19,15 @@ class SnapshotCommand:
19
19
 
20
20
  @dataclass(frozen=True)
21
21
  class RunsCommand:
22
+ action: str | None = None
22
23
  active: bool = False
23
24
  running: bool = False
24
25
  stale: bool = False
25
26
  harness: str | None = None
26
27
  group: str | None = None
27
28
  limit: int | None = None
29
+ older_than_days: int | None = None
30
+ dry_run: bool = False
28
31
  json_mode: bool = False
29
32
 
30
33
 
@@ -64,6 +67,34 @@ def emit_snapshot(command: SnapshotCommand, *, workspace_path: str, stdout: Text
64
67
 
65
68
  def emit_runs(command: RunsCommand, *, workspace_path: str, stdout: TextIO) -> int:
66
69
  registry_root = run_registry.registry_root_if_exists(Path(workspace_path))
70
+ if command.action == "prune":
71
+ older_than_days = (
72
+ command.older_than_days
73
+ if command.older_than_days is not None
74
+ else run_registry.DEFAULT_RUN_PRUNE_DAYS
75
+ )
76
+ payload = (
77
+ run_registry.prune_runs(
78
+ registry_root,
79
+ older_than_days=older_than_days,
80
+ dry_run=command.dry_run,
81
+ )
82
+ if registry_root is not None
83
+ else run_registry.empty_run_prune_payload(
84
+ older_than_days=older_than_days,
85
+ dry_run=command.dry_run,
86
+ )
87
+ )
88
+ if command.json_mode:
89
+ delegate_rendering.print_json(payload, stdout)
90
+ else:
91
+ delegate_rendering.render_runs_prune_text(payload, stdout)
92
+ if payload.get("ok") is False:
93
+ exit_code = payload.get("exitCode")
94
+ return exit_code if isinstance(exit_code, int) else 1
95
+ return 0
96
+ if command.action is not None:
97
+ raise InspectionError("unknown_runs_action", f"Unknown runs action: {command.action}")
67
98
  limit = command.limit or run_registry.DEFAULT_RUNS_LIMIT
68
99
  if command.running:
69
100
  mode = "running"
@@ -275,13 +275,8 @@ def preflight_codex_home(codex_home: str, *, codex: JsonObject) -> None:
275
275
  )
276
276
 
277
277
 
278
- def _request_resolution(request: object) -> ProfileResolution:
279
- resolution = getattr(request, "profile_resolution", None)
280
- return resolution if resolution is not None else empty_profile_resolution()
281
-
282
-
283
278
  def preflight_codex_request(request: object, codex_config: JsonObject) -> None:
284
- resolution = _request_resolution(request)
279
+ resolution: ProfileResolution = request.profile_resolution # type: ignore[attr-defined]
285
280
  if resolution.name is not None and resolution.codex_home is None:
286
281
  raise DelegateError(
287
282
  "profile_missing_codex_home",