delegate-agent-cli 0.20.0__tar.gz → 0.22.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 (147) hide show
  1. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/CHANGELOG.md +130 -0
  2. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/PKG-INFO +1 -1
  3. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/docs/cli-reference.md +28 -2
  4. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/docs/worktrees.md +22 -0
  5. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/pyproject.toml +1 -1
  6. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/__init__.py +1 -1
  7. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/capability_commands.py +19 -2
  8. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/cli.py +5 -15
  9. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/cli_parser.py +12 -24
  10. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/command_errors.py +2 -0
  11. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/command_help.py +19 -2
  12. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/config.py +3 -5
  13. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/config_commands.py +7 -3
  14. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/constants.py +24 -0
  15. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/describe_payload.py +5 -14
  16. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/harness_discovery.py +272 -33
  17. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/isolation.py +128 -0
  18. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/private_io.py +23 -5
  19. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/profiles.py +1 -6
  20. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/prompt_transport.py +18 -0
  21. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/reasoning.py +2 -31
  22. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/redaction.py +38 -6
  23. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/rendering.py +39 -0
  24. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/request_build.py +276 -125
  25. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/run_registry.py +0 -2
  26. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/run_status.py +6 -10
  27. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/runner.py +42 -111
  28. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/safe_workspace.py +0 -4
  29. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/setup_commands.py +29 -16
  30. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/snapshot_view.py +2 -2
  31. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/structured_output.py +8 -7
  32. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/workflows/commands.py +12 -12
  33. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/workflows/registry.py +3 -4
  34. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/workflows/runtime.py +53 -45
  35. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/workflows/schema.py +5 -4
  36. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/workflows/script.py +8 -4
  37. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/worktree_commands.py +22 -4
  38. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/worktree_execution.py +7 -15
  39. delegate_agent_cli-0.22.0/src/delegate_agent/worktree_gc.py +1129 -0
  40. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/worktree_mgmt.py +8 -0
  41. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/worktree_records.py +1 -3
  42. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent_cli.egg-info/PKG-INFO +1 -1
  43. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_capability_commands.py +70 -2
  44. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_engine_argv.py +461 -1
  45. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_execution_worktree_failure_cleanup.py +0 -3
  46. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_execution_worktree_run.py +21 -2
  47. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_harness_discovery.py +474 -0
  48. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_retention.py +0 -3
  49. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_setup_commands.py +27 -0
  50. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_snapshot_redaction.py +14 -0
  51. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_worktree_prune_gc.py +1032 -1
  52. delegate_agent_cli-0.20.0/src/delegate_agent/worktree_gc.py +0 -577
  53. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/CONTRIBUTING.md +0 -0
  54. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/LICENSE +0 -0
  55. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/MANIFEST.in +0 -0
  56. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/README.md +0 -0
  57. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/SECURITY.md +0 -0
  58. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/bin/delegate-profile-shim +0 -0
  59. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/bin/delegate.py +0 -0
  60. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/config.example.json +0 -0
  61. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/docs/agent-setup.md +0 -0
  62. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/docs/assets/delegate-agent-header.png +0 -0
  63. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/docs/configuration.md +0 -0
  64. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/docs/delegate-workflows.md +0 -0
  65. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/docs/development.md +0 -0
  66. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/docs/live-runtime.md +0 -0
  67. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/docs/publishing-checklist.md +0 -0
  68. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/docs/security-model.md +0 -0
  69. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/docs/troubleshooting.md +0 -0
  70. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/examples/task.claude.json +0 -0
  71. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/examples/task.codex.json +0 -0
  72. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/examples/task.cursor.json +0 -0
  73. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/examples/task.droid.json +0 -0
  74. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/examples/task.grok.json +0 -0
  75. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/examples/task.judge.json +0 -0
  76. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/examples/task.kimi.json +0 -0
  77. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/setup.cfg +0 -0
  78. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/archived_logs.py +0 -0
  79. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/argv_builders.py +0 -0
  80. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/argv_utils.py +0 -0
  81. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/bundled_models.py +0 -0
  82. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/child_failures.py +0 -0
  83. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/errors.py +0 -0
  84. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/git_utils.py +0 -0
  85. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/harness_events.py +0 -0
  86. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/inspection_commands.py +0 -0
  87. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/json_types.py +0 -0
  88. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/log_output.py +0 -0
  89. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/model_discovery.py +0 -0
  90. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/profile_commands.py +0 -0
  91. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/profile_guard.py +0 -0
  92. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/prompt_instructions.py +0 -0
  93. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/request_models.py +0 -0
  94. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/retention.py +0 -0
  95. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/run_metadata.py +0 -0
  96. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/run_output_commands.py +0 -0
  97. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/seatbelt.py +0 -0
  98. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/wait_cancel_commands.py +0 -0
  99. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/workflows/__init__.py +0 -0
  100. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/worktree_remove.py +0 -0
  101. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/worktree_summary.py +0 -0
  102. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent/wsl.py +0 -0
  103. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent_cli.egg-info/SOURCES.txt +0 -0
  104. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent_cli.egg-info/dependency_links.txt +0 -0
  105. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent_cli.egg-info/entry_points.txt +0 -0
  106. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent_cli.egg-info/requires.txt +0 -0
  107. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/src/delegate_agent_cli.egg-info/top_level.txt +0 -0
  108. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_child_failures.py +0 -0
  109. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_codex_pure_sandbox.py +0 -0
  110. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_codex_thread_resilience.py +0 -0
  111. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_command_help.py +0 -0
  112. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_config_commands.py +0 -0
  113. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_delegate_help_cli.py +0 -0
  114. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_delegate_isolation.py +0 -0
  115. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_delegate_parser.py +0 -0
  116. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_delegate_validation.py +0 -0
  117. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_end_to_end_tracking.py +0 -0
  118. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_execution_argv_and_prompt.py +0 -0
  119. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_execution_dry_run.py +0 -0
  120. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_execution_worktree_preflight.py +0 -0
  121. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_harness_events.py +0 -0
  122. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_inspection_commands.py +0 -0
  123. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_kimi_commands.py +0 -0
  124. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_model_discovery.py +0 -0
  125. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_model_selection_wave1a.py +0 -0
  126. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_model_selection_wave1b.py +0 -0
  127. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_omp_read_only_behavior.py +0 -0
  128. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_packaging.py +0 -0
  129. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_profiles.py +0 -0
  130. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_pure_call.py +0 -0
  131. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_reasoning_capabilities.py +0 -0
  132. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_run_registry.py +0 -0
  133. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_runner_capture.py +0 -0
  134. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_safe_workspace_isolation.py +0 -0
  135. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_slash_passthrough.py +0 -0
  136. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_snapshot_rendering.py +0 -0
  137. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_snapshot_run_output.py +0 -0
  138. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_snapshot_view.py +0 -0
  139. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_structured_output.py +0 -0
  140. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_tracked_timeout.py +0 -0
  141. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_utility_modules.py +0 -0
  142. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_wait_cancel_commands.py +0 -0
  143. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_wave4_launch_features.py +0 -0
  144. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_workflow_commands.py +0 -0
  145. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_worktree_list_show.py +0 -0
  146. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_worktree_remove.py +0 -0
  147. {delegate_agent_cli-0.20.0 → delegate_agent_cli-0.22.0}/tests/test_wsl_guardrails.py +0 -0
@@ -5,6 +5,134 @@ 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.22.0] - 2026-07-26
9
+
10
+ ### Changed
11
+
12
+ - 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.
13
+
14
+ ### Removed
15
+
16
+ - Removed the write-only `ReasoningCapability.supported_efforts` field and unused `STATUS_FILTER_ACTIVE`/`STATUS_FILTER_RECENT` constants.
17
+ - 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.
18
+
19
+ ## [0.21.0] - 2026-07-26
20
+
21
+ ### Added
22
+
23
+ - Added `delegate worktree gc --all`, which scans the machine-wide worktree
24
+ pool for worktrees whose source repository no longer exists. Such worktrees
25
+ were previously unreachable by any `gc` invocation, because reaping is driven
26
+ by the run registry inside each source repository and that registry dies with
27
+ the repository. The scan adds no catalog or persistent state: it reads each
28
+ worktree's Git backlink directly, and treats a backlink whose target is gone
29
+ as the sole orphan signal. The scan removes nothing at all, at any depth: a
30
+ missing source repository makes a worktree's uncommitted state impossible to
31
+ inspect, so orphans and empty pool directories alike are reported for
32
+ deliberate manual cleanup. Because that output invites manual deletion, every
33
+ uncertainty resolves away from an orphan verdict: a pointer file that cannot
34
+ be read, a path comparison the filesystem cannot answer, an entry too recently
35
+ modified to have settled, and a directory whose name does not match the pool's
36
+ own fingerprint contract are all reported as warnings rather than orphans.
37
+ That last rule is what keeps a scan of an arbitrary root from labelling
38
+ somebody else's directories. `--pool PATH` scans a different pool root, which
39
+ reaches pools stranded by an earlier `worktrees.dataHome`.
40
+ - `capabilities refresh` and `setup` now name each harness on stderr as it is
41
+ probed, so a harness that hangs is identifiable rather than anonymous
42
+ silence. Progress output is suppressed under `--json`.
43
+
44
+ ### Changed
45
+
46
+ - A launch now fails instead of proceeding when the configured binary for a
47
+ harness identifies itself as a *different* known harness. Delegate re-probes
48
+ `--version` when a cached capability record has refused a run, and during
49
+ `capabilities refresh` and `setup` (see Fixed); when that banner positively
50
+ names another harness — a Codex path replaced in place by Grok, say — the run
51
+ stops with `harness_identity_mismatch` and exit code 2, naming the configured
52
+ harness, the harness that answered, and the selector path.
53
+ Previously the launch continued against that same binary with the same Codex
54
+ argv, sandbox flags, and approval policy, and only the cached capability
55
+ metadata was discarded. This fires solely on a banner that NAMES another known
56
+ harness, never on uncertainty: a probe that errors, times out, exceeds its
57
+ output bound, or prints an unbranded banner still fails open and launches on
58
+ the cached record. An unbranded shape — a bare date-and-hash build ID is the
59
+ clearest case — is deliberately not identification, because wrapper prefixes
60
+ are a supported configuration and refusing a legitimate launch costs more than
61
+ carrying a stale record until the next refresh. The selector is scrubbed of
62
+ credentials before it reaches the message or the diagnostics, since a wrapper
63
+ prefix can carry a token inline or after a flag. Dry runs do not probe and are
64
+ unaffected. The refusal also names the discovery cache file for the active
65
+ auth profile, in its `cachePath` diagnostic and in a next action, because
66
+ `capabilities refresh` cannot clear this state: a refresh keeps the
67
+ last-known-good record whenever a probe fails, which is correct for a flaky
68
+ probe and wrong here, where the record itself is the problem. Without the
69
+ path, a wrong identification would refuse every launch with no documented way
70
+ out short of editing configuration.
71
+ - A run whose cached capability record is discarded for version drift now says
72
+ so, naming `delegate capabilities refresh <engine>`. The record is dropped for
73
+ that run only and the superseded copy stays on disk, so every later launch
74
+ repeats the drop and loses the same discovery-sourced models and reasoning
75
+ levels — previously with no indication that a stale cache was the cause.
76
+ Selector drift stays silent, since `capabilities` already reports it under
77
+ `driftedHarnesses`.
78
+
79
+ ### Fixed
80
+
81
+ - Discovery now detects a harness that was upgraded in place. Cached capability
82
+ records were invalidated only by comparing the configured selector, so an
83
+ upgrade behind an unchanged selector left stale capabilities cached
84
+ indefinitely and a newly supported reasoning level was rejected until a manual
85
+ `capabilities refresh`. Delegate now re-probes `--version` for the single
86
+ harness it is about to use and compares it against the version already stored
87
+ in the record, then rebuilds the run against the refreshed picture. The probe
88
+ runs only once the cached record has already cost the run something, never on
89
+ the ordinary path, which therefore spawns no extra process at all — and that
90
+ matters because the probe costs 26-668ms, worst on the Node and Bun harnesses
91
+ that are already slow to start. Two things trigger it: an explicit
92
+ `--reasoning-effort` the record refuses, and a `defaultReasoningEffort` the
93
+ record silently drops. The second is the one most likely to go unnoticed,
94
+ since a configured default degrades to a warning rather than failing the
95
+ launch, and it is set once and then never typed again. A probe that cannot
96
+ repair the default leaves the run exactly as it was: a launch that succeeded
97
+ is never turned into a failure. The check fails open — a probe that errors,
98
+ times out, or prints a banner nothing recognizes keeps the cached record, so a
99
+ failing probe can never prevent a launch. A banner that positively identifies
100
+ a *different* known harness is the exception, and it aborts the launch rather
101
+ than merely invalidating the record — see Changed. Each banner is matched
102
+ against the expected harness's own pattern before anyone else's, so a banner
103
+ mentioning more than one tool is read as the harness that was asked for
104
+ instead of whichever pattern happens to be declared earlier. Banners are read
105
+ with escape sequences stripped, so a colorized version line no longer freezes
106
+ a record permanently. A cached selector whose binary no longer exists is also
107
+ treated as drift.
108
+ - Not covered, deliberately: a harness that *removes* a capability leaves its
109
+ cached record listing more than the harness now supports, which produces no
110
+ refusal and so no probe. The child rejects the effort it was handed, which is
111
+ loud; the one quiet case is Cursor's route table aiming a run at a superseded
112
+ model id. Catching either would cost a probe on every launch, which is the
113
+ trade this path exists to refuse. `capabilities refresh` remains the cure.
114
+ - A dry run never probes the harness for its version. Dry run documents that it
115
+ neither launches a child runtime nor requires the real child binary, and the
116
+ drift probe is a child runtime; it now runs only on a real launch, leaving dry
117
+ run with the two filesystem-local checks.
118
+ - A discovery cache carrying a newer schema than the running build is no longer
119
+ discarded and overwritten. It is left intact while the run degrades to
120
+ probing without persisting, so an older Delegate can no longer destroy a newer
121
+ cache. Losing that race mid-probe degrades the same way rather than failing the
122
+ command: `capabilities refresh` and `setup` both report `cacheWriteSkipped`,
123
+ `setup` additionally leaving `cacheState` at `unchanged`, and each exits
124
+ successfully, since only persistence was lost. The
125
+ schema is re-read immediately before the atomic replacement rather than before
126
+ the snapshot is serialized, which narrows the window in which a newer cache
127
+ could be published and then overwritten down to the `os.replace` call itself.
128
+ POSIX offers no compare-and-swap for a rename, so that last window remains.
129
+ - The test suite no longer writes into the developer's real
130
+ `~/.delegate/worktrees`. Tests exercising persistent-worktree runs created
131
+ real pooled worktrees over temporary source repositories and orphaned them
132
+ permanently, because `worktrees_data_home` falls through to the user's home
133
+ directory with no seam a test could redirect. `HOME` is now redirected at test
134
+ collection time, which reaches every such resolver at once.
135
+
8
136
  ## [0.20.0] - 2026-07-25
9
137
 
10
138
  ### Added
@@ -631,6 +759,8 @@ Usage-audit fix wave: 82 sessions and 1,241 delegate invocations from one week o
631
759
 
632
760
  - Releases before 0.1.3 predate this changelog.
633
761
 
762
+ [0.22.0]: https://github.com/treygoff24/delegate-agent/compare/v0.21.0...v0.22.0
763
+ [0.21.0]: https://github.com/treygoff24/delegate-agent/compare/v0.20.0...v0.21.0
634
764
  [0.20.0]: https://github.com/treygoff24/delegate-agent/compare/v0.19.0...v0.20.0
635
765
  [0.19.0]: https://github.com/treygoff24/delegate-agent/compare/v0.18.0...v0.19.0
636
766
  [0.18.0]: https://github.com/treygoff24/delegate-agent/compare/v0.17.0...v0.18.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: delegate-agent-cli
3
- Version: 0.20.0
3
+ Version: 0.22.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
@@ -803,7 +803,31 @@ records independently. If one current probe fails, its last-known-good record
803
803
  is retained and the refresh response lists that harness in `staleHarnesses`.
804
804
  If the configured executable selector changes, cached reads mark that harness
805
805
  stale and ordinary launches ignore only that harness's record until refresh.
806
- Cached reads never run `--version` merely to test staleness.
806
+ Cached reads never run `--version` merely to test staleness, and neither does an
807
+ ordinary launch: a cached record can only be short of what the harness now
808
+ supports, never claim more, so one that satisfies the run is used as-is.
809
+
810
+ A launch re-probes `--version` for its own harness only when the cached record
811
+ has already cost the run something: it refused an explicit `--reasoning-effort`,
812
+ or it silently dropped a configured `defaultReasoningEffort` (a configured
813
+ default degrades to a warning instead of failing the launch, so nothing else
814
+ would signal it). An in-place upgrade behind an unchanged selector is the reason
815
+ either can be wrong, so the probe compares the live banner against the version
816
+ stored in the record, and a mismatch drops the record and rebuilds the run
817
+ against config and bundled capabilities. If the refreshed picture cannot satisfy
818
+ a configured default either, the run proceeds as it already had — a probe never
819
+ turns a launch that succeeded into one that fails.
820
+
821
+ The reverse case is not covered: a harness that *removes* a capability leaves
822
+ its record listing more than the harness now supports, which produces no refusal
823
+ and therefore no probe. Run `capabilities refresh` after an upgrade that narrows
824
+ what a harness accepts. The result carries a warning naming
825
+ `capabilities refresh <engine>`, because the superseded record stays on disk
826
+ until a refresh rewrites it. The probe fails open — an error, a timeout, or an
827
+ unrecognized banner leaves the original refusal standing — except when the
828
+ banner positively names a *different* known harness, which fails the run with
829
+ `harness_identity_mismatch` rather than handing one harness's argv and sandbox
830
+ policy to another. Dry runs never probe at all.
807
831
 
808
832
  All automatic probes run with `shell=False`, stdin closed, a neutral temporary
809
833
  working directory, bounded output, and a timeout. They pass the active profile
@@ -1079,11 +1103,13 @@ delegate worktree show <handle>
1079
1103
  delegate worktree show --latest HARNESS
1080
1104
  delegate worktree remove <handle|--group NAME> [--discard-uncommitted] [--force-branch] [--force] [--keep-branch]
1081
1105
  delegate worktree prune [--merged] [--older-than DAYS] [--harness HARNESS] [--group NAME] [--include-detached] [--dry-run] [--discard-uncommitted] [--force-branch] [--force]
1082
- delegate worktree gc [--dry-run]
1106
+ delegate worktree gc [--dry-run] [--all] [--pool PATH]
1083
1107
  ```
1084
1108
 
1085
1109
  `worktree show --latest HARNESS` selects the latest persistent worktree for the harness, not merely the latest run overall. `worktree list` JSON includes a `summary` with status counts, registry drift counts, warning counts, `autoPruneMode`, and whether the returned operation was read-only; `summary.totalPersistentWorktrees` is always registry-wide, while `allStatusCounts` is scoped to the `--harness` / `--group` filters (pre-status-filter) and `statusCounts` to the visible entries. `worktree remove --group NAME` removes all matching persistent worktrees with the same safety checks as single-handle removal. `worktree prune --group NAME` limits prune candidates to the group. `worktree gc --dry-run` reports `wouldPruneSourceRoots` and structured orphan reasons; `worktree gc` JSON also includes `mode`, `effects`, per-entry `action`, and orphan `safeAction` fields. `gc` never deletes worktree directories.
1086
1110
 
1111
+ `worktree gc --all` additionally scans the machine-wide worktree pool (`worktrees.dataHome`, default `~/.delegate/worktrees`) and adds a `pool` object with `dataHome`, `scannedWorktrees`, `orphans`, `emptyFingerprintDirs`, and `warnings`. A pool orphan is a pooled worktree whose Git admin directory no longer serves it, classified `source_root_missing` or `worktree_metadata_missing` with the same `safeAction` vocabulary as registry orphans, plus `worktreePath`, `fingerprint`, `sourceGitRoot`, and `gitdir` (null when the backlink is absent). This is the only way to see worktrees whose source repository was deleted along with its `.delegate/` registry, so `--all` is accepted outside a Delegate registry. The pool scan removes nothing at all: orphans are reported because uncommitted work in them is undetectable once the source repository is gone, and empty fingerprint directories are reported for the same hand-clearing rather than reclaimed. Only a settled absence produces an orphan — metadata that could not be read (a symlinked, non-regular, oversized, or unreadable `.git` entry, or an admin directory that could not be inspected) is treated as live and reported in `pool.warnings`, and so is any pool entry modified within the last 15 minutes, since worktree creation writes the directory before its metadata and a concurrent scan would otherwise call a worktree being born an orphan (`worktree_unsettled`, `fingerprint_dir_unsettled`). `pool.warnings` also covers first-level directories whose names are not repository fingerprints, fingerprint directories that hold loose files rather than being empty, and unlistable fingerprint directories. `--pool PATH` scans a specific pool root instead of the configured one, which finds pools left behind by an earlier `worktrees.dataHome`; an explicit `--pool` path that is missing, not a directory, or unreadable is an `invalid_pool_root` error.
1112
+
1087
1113
  List/show entry fields include `branchMergedIntoSource` (branch graph only), `mergedIntoSource` (backward-compatible branch graph state), `fullyIntegrated` (branch merged and worktree clean), `hasUncommittedChanges`, `integrationStatus`, and `uncommittedChangesIntegrated`. `workSummary` is included on `worktree show` and run completion payloads when Delegate can inspect the worktree; `worktree list` omits the deep summary for responsiveness. Consumers that need safe retirement should require `fullyIntegrated: true` or inspect `integrationStatus`. When `integrationStatus` is `branch-merged-worktree-dirty`, merge/cherry-pick suggestions are suppressed because commit integration is already complete and only uncommitted edits remain.
1088
1114
 
1089
1115
  Unknown persistent-worktree handles return suggestions scoped to persistent
@@ -216,6 +216,28 @@ delegate worktree gc
216
216
 
217
217
  In dry-run mode, `gc` reports `wouldPruneSourceRoots` and classifies un-prunable worktrees with reasons such as `source_root_missing`, `worktree_metadata_missing`, `branch_missing`, and `detached_backlink`. Without `--dry-run`, it may update Delegate registry status (for example, marking missing paths as `missing` or inconsistent metadata as `unknown`) and may run `git worktree prune` to clean Git administrative metadata for already-missing paths, but it does not delete worktree directories. JSON output includes an `effects` object that makes those mutation boundaries explicit.
218
218
 
219
+ ## Find pooled worktrees whose repository is gone
220
+
221
+ ```bash
222
+ delegate worktree gc --all
223
+ delegate worktree gc --all --dry-run
224
+ delegate worktree gc --pool ~/.delegate/worktrees
225
+ ```
226
+
227
+ Persistent worktrees live in a machine-global pool under `worktrees.dataHome` (default `~/.delegate/worktrees`), but they are tracked in a run registry inside the source repository. Delete the source repository and its registry goes with it, leaving a pooled worktree that no per-repository `gc` can reach. `--all` finds these by walking the pool directly and reading each worktree's `.git` backlink file as text — the ordinary Git-based checks cannot classify them, because every `git` command fails inside a worktree whose repository is gone. Because a live worktree always has a live backlink, the walk is safe to run across every repository on the machine at once. `--all` works outside a Delegate registry for the same reason.
228
+
229
+ The scan adds a `pool` object to the JSON with `dataHome`, `scannedWorktrees`, `orphans`, `emptyFingerprintDirs`, and `warnings`. Each orphan carries `worktreePath`, `fingerprint`, the recovered `sourceGitRoot` (null when the layout does not reveal it), `gitdir` (null when the worktree has no `.git` backlink), a `reason`, and a `safeAction`.
230
+
231
+ A worktree counts as an orphan only when two things are settled. Its `.git` backlink must be definitely gone or definitely unusable — absent, or readable and holding no usable `gitdir:` line. And its Git admin directory must no longer serve it: the admin directory must exist and hold a `gitdir` backfile naming this worktree. Anything that merely *could not be checked* — a `.git` entry that is a symlink, a directory, a FIFO, oversized, or unreadable; an admin directory that could not be inspected — is reported as live and surfaced in `warnings` with reason `worktree_metadata_unverifiable`. A false orphan invites someone to delete real work by hand, while a false live only under-reports, so every unresolved case resolves toward live.
232
+
233
+ Absence also has to have lasted. Creating a persistent worktree is several steps — Delegate makes the fingerprint directory, then `git worktree add` creates the worktree directory, its `.git` pointer, and the admin directory's backfile — and a scan that lands inside that window sees exactly what a deleted repository leaves behind. Since the scan runs in a different process from the one creating the worktree, and often for a different repository, it has nothing to wait on and judges by age instead: a pool entry modified within the last 15 minutes is reported in `warnings` (`worktree_unsettled`, or `fingerprint_dir_unsettled` for an empty directory) and classified on a later run. An entry whose age cannot be read at all is treated the same way. This delays reporting a genuine orphan by one run at most, since a real orphan's metadata never comes back.
234
+
235
+ Only first-level directories named like a repository fingerprint (12 lowercase hex characters, as written by `plan_worktree_path`) are treated as Delegate's. The root is whatever you passed, so anything else is skipped without being descended, and is summarized in `warnings` with reason `not_a_pool_fingerprint_dir` — pointing `--pool` at an ordinary directory yields warnings, never orphans or cleanup candidates.
236
+
237
+ The pool scan removes nothing. With the source repository gone there is no way to tell whether an orphan holds uncommitted work, so removal is a deliberate manual decision — inspect the reported path, rescue anything you need, then delete it yourself. Empty fingerprint directories, left behind when a fingerprint's last worktree was removed, are reported in `emptyFingerprintDirs` for the same hand-clearing; `gc` does not remove those either. "Empty" means the directory holds nothing at all: a fingerprint directory with loose files in it is reported in `warnings` (`fingerprint_dir_not_empty`) instead. Fingerprint directories that could not be listed also appear in `warnings` rather than being reported as empty.
238
+
239
+ Use `--pool PATH` to scan a pool root that is no longer the configured `dataHome`. An explicitly named `--pool` path must exist and be a readable directory; anything else is an `invalid_pool_root` error rather than a successful scan of nothing, including a root that becomes unreadable between validation and the walk. A configured `dataHome` that does not exist yet is not an error — it just means no pool has been created — and is reported in `pool.warnings`.
240
+
219
241
  ## Security boundary
220
242
 
221
243
  Worktree isolation is source-checkout isolation, not a full sandbox. The child process may still use credentials, network access, external tools, and absolute paths according to its runtime and host permissions. See [Security model](security-model.md).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "delegate-agent-cli"
7
- version = "0.20.0"
7
+ version = "0.22.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.20.0"
3
+ VERSION = "0.22.0"
4
4
 
5
5
  __all__ = ["VERSION"]
@@ -138,9 +138,12 @@ def _refresh_payload(
138
138
  *,
139
139
  profile: profiles.ProfileResolution,
140
140
  engines: tuple[str, ...] | None = None,
141
+ progress: harness_discovery.ProgressCallback | None = None,
141
142
  ) -> JsonObject:
142
143
  try:
143
- result = harness_discovery.refresh_discovery(config, profile=profile, engines=engines)
144
+ result = harness_discovery.refresh_discovery(
145
+ config, profile=profile, engines=engines, progress=progress
146
+ )
144
147
  except OSError as exc:
145
148
  raise CapabilitiesError(
146
149
  "capability_refresh_failed",
@@ -215,6 +218,8 @@ def _refresh_payload(
215
218
  }
216
219
  if drifted_harnesses:
217
220
  payload["driftedHarnesses"] = drifted_harnesses
221
+ if result.get("futureSchemaCache") is True:
222
+ payload["cacheWriteSkipped"] = harness_discovery.FUTURE_SCHEMA_CACHE_WARNING
218
223
  _add_legacy_cache_fields(payload, legacy_path, reasoning_payload)
219
224
  return redaction.scrub_public_projection(payload)
220
225
 
@@ -229,6 +234,7 @@ def emit(
229
234
  profile: profiles.ProfileResolution | None = None,
230
235
  discovery: JsonObject | None = None,
231
236
  stdout: TextIO,
237
+ stderr: TextIO | None = None,
232
238
  ) -> int:
233
239
  active_profile = profile or profiles.resolve_active_profile(
234
240
  config,
@@ -236,8 +242,16 @@ def emit(
236
242
  cli_override=auth_profile_override,
237
243
  )
238
244
  if command.refresh:
245
+ # JSON callers get a clean stdout document and a quiet stderr; the
246
+ # per-harness line exists for a human watching a cold refresh.
239
247
  payload = _refresh_payload(
240
- config, workspace, profile=active_profile, engines=command.engines
248
+ config,
249
+ workspace,
250
+ profile=active_profile,
251
+ engines=command.engines,
252
+ progress=(
253
+ None if command.json_mode else harness_discovery.stderr_probe_progress(stderr)
254
+ ),
241
255
  )
242
256
  else:
243
257
  payload = capabilities_payload(
@@ -252,6 +266,9 @@ def emit(
252
266
  delegate_rendering.print_json(payload, stdout)
253
267
  else:
254
268
  print(f"reasoning capabilities: {payload['cachePath']}", file=stdout)
269
+ skipped = payload.get("cacheWriteSkipped")
270
+ if isinstance(skipped, str):
271
+ print(skipped, file=stdout)
255
272
  drifted = payload.get("driftedHarnesses")
256
273
  if isinstance(drifted, list) and drifted:
257
274
  print(
@@ -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",
@@ -1087,6 +1075,7 @@ def main(
1087
1075
  json_mode=global_options.json_mode,
1088
1076
  auth_profile_override=global_options.auth_profile,
1089
1077
  stdout=stdout,
1078
+ stderr=stderr,
1090
1079
  )
1091
1080
 
1092
1081
  # For run --input-json, pre-read the JSON to discover config from the
@@ -1177,6 +1166,7 @@ def main(
1177
1166
  profile=discovery_profile,
1178
1167
  discovery=discovery_snapshot,
1179
1168
  stdout=stdout,
1169
+ stderr=stderr,
1180
1170
  )
1181
1171
 
1182
1172
  if parsed.subcommand in {
@@ -1278,8 +1268,8 @@ def main(
1278
1268
  DelegateError(
1279
1269
  exc.error,
1280
1270
  exc.message,
1281
- diagnostics=getattr(exc, "diagnostics", None),
1282
- next_actions=getattr(exc, "next_actions", None),
1271
+ diagnostics=exc.diagnostics,
1272
+ next_actions=exc.next_actions,
1283
1273
  ),
1284
1274
  json_mode,
1285
1275
  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:
@@ -1982,6 +1962,10 @@ def _require_option_value(rest: list[str], index: int, option: str) -> str:
1982
1962
 
1983
1963
  WorktreeOptionSpec = tuple[str, str]
1984
1964
 
1965
+ # Parsed worktree option values: flags store True, "str"/"group"/"status" kinds
1966
+ # store the validated string, and the int kinds store a parsed int.
1967
+ WorktreeOptionValue = str | int | bool
1968
+
1985
1969
  WORKTREE_OPTION_SPECS: dict[str, dict[str, WorktreeOptionSpec]] = {
1986
1970
  "list": {
1987
1971
  "--harness": ("str", "harness"),
@@ -2013,12 +1997,14 @@ WORKTREE_OPTION_SPECS: dict[str, dict[str, WorktreeOptionSpec]] = {
2013
1997
  },
2014
1998
  "gc": {
2015
1999
  "--dry-run": ("flag", "dry_run"),
2000
+ "--all": ("flag", "all_pools"),
2001
+ "--pool": ("str", "pool"),
2016
2002
  },
2017
2003
  }
2018
2004
 
2019
2005
 
2020
2006
  def _apply_worktree_option(
2021
- options: dict[str, object],
2007
+ options: dict[str, WorktreeOptionValue],
2022
2008
  args: list[str],
2023
2009
  index: int,
2024
2010
  option: str,
@@ -2067,7 +2053,7 @@ def parse_worktree(rest: list[str], json_mode: bool, cwd: str | None) -> ParsedC
2067
2053
  return help_command(json_mode, topic)
2068
2054
  if action not in WORKTREE_OPTION_SPECS:
2069
2055
  raise DelegateError("unknown_worktree_action", f"Unknown worktree action: {action}")
2070
- options: dict[str, object] = {}
2056
+ options: dict[str, WorktreeOptionValue] = {}
2071
2057
  positional: list[str] = []
2072
2058
  i = 0
2073
2059
  action_specs = WORKTREE_OPTION_SPECS[action]
@@ -2090,6 +2076,8 @@ def parse_worktree(rest: list[str], json_mode: bool, cwd: str | None) -> ParsedC
2090
2076
  raise DelegateError(
2091
2077
  "unexpected_argument", f"worktree {action} does not accept positional arguments."
2092
2078
  )
2079
+ if action == "gc" and options.get("pool") == "":
2080
+ raise DelegateError("invalid_option_value", "worktree gc --pool requires a path.")
2093
2081
  if action == "show":
2094
2082
  if options.get("latest_harness") is not None:
2095
2083
  if positional:
@@ -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(
@@ -1408,19 +1408,36 @@ COMMAND_SPECS: dict[str, CommandSpec] = {
1408
1408
  "worktree gc": CommandSpec(
1409
1409
  name="worktree gc",
1410
1410
  summary="Garbage-collect orphaned worktree metadata and stale registry entries.",
1411
- usage=("delegate [--cwd PATH] [--json] worktree gc [--dry-run]",),
1411
+ usage=("delegate [--cwd PATH] [--json] worktree gc [--dry-run] [--all] [--pool PATH]",),
1412
1412
  options=(
1413
1413
  OptionSpec(
1414
1414
  "--dry-run",
1415
1415
  None,
1416
1416
  "Report source roots that would be pruned and un-prunable worktree reasons without changing anything.",
1417
1417
  ),
1418
+ OptionSpec(
1419
+ "--all",
1420
+ None,
1421
+ "Also scan the machine-wide worktree pool for worktrees whose source repository is gone. Reports them; never removes them.",
1422
+ ),
1423
+ OptionSpec(
1424
+ "--pool",
1425
+ "PATH",
1426
+ "Scan this worktree pool root instead of the configured worktrees.dataHome (finds pools left behind by an earlier dataHome). The path must exist and be a readable directory.",
1427
+ ),
1418
1428
  ),
1419
1429
  examples=(
1420
1430
  "delegate worktree gc",
1421
1431
  "delegate worktree gc --dry-run",
1432
+ "delegate worktree gc --all",
1433
+ "delegate worktree gc --all --dry-run",
1434
+ ),
1435
+ notes=(
1436
+ "GC reconciles registry and Git metadata; it never removes worktree paths.",
1437
+ "--all works outside a Delegate registry, since pooled worktrees outlive the repositories that created them.",
1438
+ "Pool orphans are reported, never removed: their uncommitted state cannot be inspected once the source repository is gone.",
1439
+ "The pool scan removes nothing at all, including empty pool directories — it reports them so you can clear them yourself.",
1422
1440
  ),
1423
- notes=("GC reconciles registry and Git metadata; it never removes worktree paths.",),
1424
1441
  see_also=("worktree list", "worktree prune", "worktree remove"),
1425
1442
  unsupported_global_options=("--isolation", "--auth-profile"),
1426
1443
  ),
@@ -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 = {