delegate-agent-cli 0.17.0__tar.gz → 0.18.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 (137) hide show
  1. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/CHANGELOG.md +21 -0
  2. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/PKG-INFO +1 -1
  3. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/config.example.json +1 -0
  4. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/cli-reference.md +1 -1
  5. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/configuration.md +2 -0
  6. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/worktrees.md +1 -1
  7. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/pyproject.toml +1 -1
  8. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/__init__.py +1 -1
  9. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/command_help.py +4 -1
  10. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/config.py +7 -0
  11. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/describe_payload.py +1 -0
  12. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/rendering.py +9 -2
  13. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_execution.py +44 -1
  14. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_gc.py +56 -8
  15. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent_cli.egg-info/PKG-INFO +1 -1
  16. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_delegate_validation.py +14 -0
  17. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_execution_worktree_run.py +65 -0
  18. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_snapshot_rendering.py +25 -0
  19. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_worktree_prune_gc.py +80 -0
  20. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/CONTRIBUTING.md +0 -0
  21. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/LICENSE +0 -0
  22. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/MANIFEST.in +0 -0
  23. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/README.md +0 -0
  24. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/SECURITY.md +0 -0
  25. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/bin/delegate-profile-shim +0 -0
  26. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/bin/delegate.py +0 -0
  27. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/agent-setup.md +0 -0
  28. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/assets/delegate-agent-header.png +0 -0
  29. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/delegate-workflows.md +0 -0
  30. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/development.md +0 -0
  31. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/live-runtime.md +0 -0
  32. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/publishing-checklist.md +0 -0
  33. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/security-model.md +0 -0
  34. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/troubleshooting.md +0 -0
  35. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/examples/task.claude.json +0 -0
  36. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/examples/task.codex.json +0 -0
  37. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/examples/task.cursor.json +0 -0
  38. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/examples/task.droid.json +0 -0
  39. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/examples/task.grok.json +0 -0
  40. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/examples/task.judge.json +0 -0
  41. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/examples/task.kimi.json +0 -0
  42. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/setup.cfg +0 -0
  43. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/archived_logs.py +0 -0
  44. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/argv_builders.py +0 -0
  45. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/argv_utils.py +0 -0
  46. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/bundled_models.py +0 -0
  47. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/capability_commands.py +0 -0
  48. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/cli.py +0 -0
  49. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/cli_parser.py +0 -0
  50. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/command_errors.py +0 -0
  51. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/config_commands.py +0 -0
  52. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/constants.py +0 -0
  53. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/errors.py +0 -0
  54. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/git_utils.py +0 -0
  55. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/harness_events.py +0 -0
  56. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/inspection_commands.py +0 -0
  57. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/isolation.py +0 -0
  58. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/json_types.py +0 -0
  59. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/log_output.py +0 -0
  60. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/model_discovery.py +0 -0
  61. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/private_io.py +0 -0
  62. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/profile_commands.py +0 -0
  63. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/profile_guard.py +0 -0
  64. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/profiles.py +0 -0
  65. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/prompt_instructions.py +0 -0
  66. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/prompt_transport.py +0 -0
  67. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/reasoning.py +0 -0
  68. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/redaction.py +0 -0
  69. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/request_build.py +0 -0
  70. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/request_models.py +0 -0
  71. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/retention.py +0 -0
  72. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/run_metadata.py +0 -0
  73. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/run_output_commands.py +0 -0
  74. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/run_registry.py +0 -0
  75. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/run_status.py +0 -0
  76. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/runner.py +0 -0
  77. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/safe_workspace.py +0 -0
  78. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/seatbelt.py +0 -0
  79. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/snapshot_view.py +0 -0
  80. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/wait_cancel_commands.py +0 -0
  81. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/__init__.py +0 -0
  82. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/commands.py +0 -0
  83. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/registry.py +0 -0
  84. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/runtime.py +0 -0
  85. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/schema.py +0 -0
  86. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/script.py +0 -0
  87. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_commands.py +0 -0
  88. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_mgmt.py +0 -0
  89. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_records.py +0 -0
  90. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_remove.py +0 -0
  91. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_summary.py +0 -0
  92. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/wsl.py +0 -0
  93. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent_cli.egg-info/SOURCES.txt +0 -0
  94. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent_cli.egg-info/dependency_links.txt +0 -0
  95. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent_cli.egg-info/entry_points.txt +0 -0
  96. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent_cli.egg-info/requires.txt +0 -0
  97. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent_cli.egg-info/top_level.txt +0 -0
  98. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_capability_commands.py +0 -0
  99. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_codex_pure_sandbox.py +0 -0
  100. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_command_help.py +0 -0
  101. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_config_commands.py +0 -0
  102. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_delegate_help_cli.py +0 -0
  103. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_delegate_isolation.py +0 -0
  104. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_delegate_parser.py +0 -0
  105. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_end_to_end_tracking.py +0 -0
  106. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_engine_argv.py +0 -0
  107. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_execution_argv_and_prompt.py +0 -0
  108. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_execution_dry_run.py +0 -0
  109. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_execution_worktree_failure_cleanup.py +0 -0
  110. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_execution_worktree_preflight.py +0 -0
  111. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_harness_events.py +0 -0
  112. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_inspection_commands.py +0 -0
  113. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_kimi_commands.py +0 -0
  114. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_model_discovery.py +0 -0
  115. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_model_selection_wave1a.py +0 -0
  116. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_model_selection_wave1b.py +0 -0
  117. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_omp_read_only_behavior.py +0 -0
  118. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_packaging.py +0 -0
  119. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_profiles.py +0 -0
  120. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_pure_call.py +0 -0
  121. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_reasoning_capabilities.py +0 -0
  122. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_retention.py +0 -0
  123. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_run_registry.py +0 -0
  124. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_runner_capture.py +0 -0
  125. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_safe_workspace_isolation.py +0 -0
  126. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_slash_passthrough.py +0 -0
  127. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_snapshot_redaction.py +0 -0
  128. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_snapshot_run_output.py +0 -0
  129. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_snapshot_view.py +0 -0
  130. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_tracked_timeout.py +0 -0
  131. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_utility_modules.py +0 -0
  132. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_wait_cancel_commands.py +0 -0
  133. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_wave4_launch_features.py +0 -0
  134. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_workflow_commands.py +0 -0
  135. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_worktree_list_show.py +0 -0
  136. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_worktree_remove.py +0 -0
  137. {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_wsl_guardrails.py +0 -0
@@ -5,6 +5,26 @@ 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.18.0] - 2026-07-19
9
+
10
+ ### Added
11
+
12
+ - `worktree gc --dry-run` now reports `wouldPruneSourceRoots` and classifies
13
+ un-prunable worktrees with structured orphan reasons (`source_root_missing`,
14
+ `worktree_metadata_missing`, `branch_missing`, `detached_backlink`), each
15
+ with a `safeAction`. GC remains non-destructive and never deletes worktree
16
+ directories.
17
+ - New `worktrees.poolWarnCount` config knob (default 20): persistent
18
+ work-mode launches emit a stderr warning when the shared worktree pool
19
+ holds more worktrees than the threshold. Warn-only; nothing is blocked or
20
+ deleted.
21
+
22
+ ### Changed
23
+
24
+ - Text rendering for `worktree gc --dry-run` prints
25
+ `would prune source roots` instead of a constant zero, and orphan lines now
26
+ include the worktree path alongside the reason.
27
+
8
28
  ## [0.17.0] - 2026-07-18
9
29
 
10
30
  ### Added
@@ -463,6 +483,7 @@ Usage-audit fix wave: 82 sessions and 1,241 delegate invocations from one week o
463
483
 
464
484
  - Releases before 0.1.3 predate this changelog.
465
485
 
486
+ [0.18.0]: https://github.com/treygoff24/delegate-agent/compare/v0.17.0...v0.18.0
466
487
  [0.17.0]: https://github.com/treygoff24/delegate-agent/compare/v0.16.0...v0.17.0
467
488
  [0.16.0]: https://github.com/treygoff24/delegate-agent/compare/v0.15.0...v0.16.0
468
489
  [0.15.0]: https://github.com/treygoff24/delegate-agent/compare/v0.14.0...v0.15.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: delegate-agent-cli
3
- Version: 0.17.0
3
+ Version: 0.18.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
@@ -116,6 +116,7 @@
116
116
  },
117
117
  "worktrees": {
118
118
  "dataHome": null,
119
+ "poolWarnCount": 20,
119
120
  "autoPrune": {
120
121
  "enabled": false,
121
122
  "mergedOlderThanDays": 7
@@ -933,7 +933,7 @@ delegate worktree prune [--merged] [--older-than DAYS] [--harness HARNESS] [--gr
933
933
  delegate worktree gc [--dry-run]
934
934
  ```
935
935
 
936
- `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` JSON includes `mode`, `effects`, per-entry `action`, and orphan `safeAction` fields to distinguish dry-run inspection from registry reconciliation; `gc` never deletes worktree directories.
936
+ `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.
937
937
 
938
938
  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.
939
939
 
@@ -578,6 +578,7 @@ Embedded defaults:
578
578
  {
579
579
  "worktrees": {
580
580
  "dataHome": null,
581
+ "poolWarnCount": 20,
581
582
  "autoPrune": {
582
583
  "enabled": false,
583
584
  "mergedOlderThanDays": 7
@@ -587,6 +588,7 @@ Embedded defaults:
587
588
  ```
588
589
 
589
590
  - `dataHome`: persistent-worktree root. `null` means `~/.delegate/worktrees`.
591
+ - `poolWarnCount`: non-negative worktree-count threshold for a launch-time warning when the shared persistent-worktree pool holds more worktrees. The default is 20. The warning does not block or delete anything.
590
592
  - `autoPrune.enabled`: if true, `delegate worktree list` opportunistically prunes clean, fully merged worktrees older than `mergedOlderThanDays`.
591
593
  - `autoPrune.mergedOlderThanDays`: non-negative integer.
592
594
 
@@ -214,7 +214,7 @@ delegate worktree gc
214
214
 
215
215
  `gc` reconciles registry metadata with the filesystem and Git worktree list. It does not delete paths by itself.
216
216
 
217
- In dry-run mode, `gc` only reports what it would mark. 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.
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
219
  ## Security boundary
220
220
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "delegate-agent-cli"
7
- version = "0.17.0"
7
+ version = "0.18.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.17.0"
3
+ VERSION = "0.18.0"
4
4
 
5
5
  __all__ = ["VERSION"]
@@ -1370,13 +1370,16 @@ COMMAND_SPECS: dict[str, CommandSpec] = {
1370
1370
  usage=("delegate [--cwd PATH] [--json] worktree gc [--dry-run]",),
1371
1371
  options=(
1372
1372
  OptionSpec(
1373
- "--dry-run", None, "Report what would be collected without changing anything."
1373
+ "--dry-run",
1374
+ None,
1375
+ "Report source roots that would be pruned and un-prunable worktree reasons without changing anything.",
1374
1376
  ),
1375
1377
  ),
1376
1378
  examples=(
1377
1379
  "delegate worktree gc",
1378
1380
  "delegate worktree gc --dry-run",
1379
1381
  ),
1382
+ notes=("GC reconciles registry and Git metadata; it never removes worktree paths.",),
1380
1383
  see_also=("worktree list", "worktree prune", "worktree remove"),
1381
1384
  unsupported_global_options=("--isolation", "--auth-profile"),
1382
1385
  ),
@@ -161,6 +161,7 @@ _EMBEDDED_DEFAULT_CONFIG: JsonObject = {
161
161
  },
162
162
  "worktrees": {
163
163
  "dataHome": None,
164
+ "poolWarnCount": 20,
164
165
  "autoPrune": {
165
166
  "enabled": False,
166
167
  "mergedOlderThanDays": 7,
@@ -416,6 +417,12 @@ def _validate_worktrees_section(worktrees: JsonValue) -> None:
416
417
  "invalid_worktrees_config",
417
418
  "worktrees.dataHome must be an absolute path or start with ~/.",
418
419
  )
420
+ if "poolWarnCount" in worktrees:
421
+ _validate_required_non_negative_int(
422
+ worktrees["poolWarnCount"],
423
+ path="worktrees.poolWarnCount",
424
+ error="invalid_worktrees_config",
425
+ )
419
426
  auto_prune = worktrees.get("autoPrune")
420
427
  if auto_prune is not None:
421
428
  if not isinstance(auto_prune, dict):
@@ -889,6 +889,7 @@ def describe_payload(
889
889
  },
890
890
  "worktrees": {
891
891
  "dataHome": config["worktrees"]["dataHome"],
892
+ "poolWarnCount": config["worktrees"]["poolWarnCount"],
892
893
  "autoPrune": config["worktrees"]["autoPrune"],
893
894
  "dirtySource": {
894
895
  "behavior": "Persistent worktree work runs auto-include tracked edits and untracked non-ignored files before child launch.",
@@ -554,7 +554,13 @@ def render_worktree_prune_text(payload: JsonObject, stdout: TextIO) -> None:
554
554
 
555
555
  def render_worktree_gc_text(payload: JsonObject, stdout: TextIO) -> None:
556
556
  print(f"reconciled: {payload.get('reconciled', 0)}", file=stdout)
557
- print(f"pruned source roots: {payload.get('prunedSourceRoots', 0)}", file=stdout)
557
+ if payload.get("dryRun") is True:
558
+ print(
559
+ f"would prune source roots: {payload.get('wouldPruneSourceRoots', 0)}",
560
+ file=stdout,
561
+ )
562
+ else:
563
+ print(f"pruned source roots: {payload.get('prunedSourceRoots', 0)}", file=stdout)
558
564
  warnings = payload.get("warnings")
559
565
  if isinstance(warnings, list) and warnings:
560
566
  print("warnings:", file=stdout)
@@ -569,7 +575,8 @@ def render_worktree_gc_text(payload: JsonObject, stdout: TextIO) -> None:
569
575
  for orphan in orphans:
570
576
  if isinstance(orphan, dict):
571
577
  print(
572
- f" - {orphan.get('alias') or orphan.get('runId')} {orphan.get('reason')}",
578
+ f" - {orphan.get('alias') or orphan.get('runId')} "
579
+ f"{orphan.get('reason')} {orphan.get('executionCwd')}",
573
580
  file=stdout,
574
581
  )
575
582
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
3
4
  import shlex
4
5
  import subprocess # nosec B404 - persistent worktree cleanup intentionally runs fixed git argv with shell=False.
5
6
  from collections.abc import Callable
@@ -30,7 +31,7 @@ from delegate_agent.isolation import (
30
31
  target_contains_source_root,
31
32
  worktrees_data_home,
32
33
  )
33
- from delegate_agent.json_types import JsonObject
34
+ from delegate_agent.json_types import JsonObject, is_non_negative_int
34
35
  from delegate_agent.prompt_transport import (
35
36
  DEVIN_AGENT_CONFIG_ARG_PLACEHOLDER,
36
37
  PROMPT_FILE_ARG_PLACEHOLDER,
@@ -108,11 +109,53 @@ def execute_persistent_worktree(
108
109
  ) -> tuple[int, JsonObject | None]:
109
110
  """Launch a work-mode child in a preserved Delegate-managed git worktree."""
110
111
  preflight = _validate_persistent_worktree_request(execution)
112
+ _warn_if_worktree_pool_large(execution.config, execution.stderr)
111
113
  registration = _register_persistent_worktree_run(execution, preflight)
112
114
  _create_persistent_worktree_or_record_failure(execution, preflight, registration)
113
115
  return _launch_child_in_persistent_worktree(execution, preflight, registration)
114
116
 
115
117
 
118
+ def _worktree_pool_count(data_home: Path) -> int:
119
+ # ponytail: count-based guardrail only — a full-tree byte walk was slowest
120
+ # exactly when the pool was large, the case the warning exists to catch.
121
+ if not data_home.is_dir():
122
+ return 0
123
+ worktree_count = 0
124
+ try:
125
+ with os.scandir(data_home) as repositories:
126
+ for repository in repositories:
127
+ if not repository.is_dir(follow_symlinks=False):
128
+ continue
129
+ with os.scandir(repository.path) as worktrees:
130
+ worktree_count += sum(
131
+ entry.is_dir(follow_symlinks=False) for entry in worktrees
132
+ )
133
+ except OSError:
134
+ pass
135
+ return worktree_count
136
+
137
+
138
+ def _warn_if_worktree_pool_large(config: JsonObject, stderr: TextIO) -> None:
139
+ worktrees = config.get("worktrees")
140
+ if not isinstance(worktrees, dict):
141
+ return
142
+ threshold = worktrees.get("poolWarnCount")
143
+ if not is_non_negative_int(threshold):
144
+ return
145
+ data_home = worktrees_data_home(config)
146
+ worktree_count = _worktree_pool_count(data_home)
147
+ if worktree_count <= threshold:
148
+ return
149
+ noun = "worktree" if worktree_count == 1 else "worktrees"
150
+ print(
151
+ "WARNING: persistent worktree pool "
152
+ f"{data_home} holds {worktree_count} {noun}; "
153
+ f"worktrees.poolWarnCount={threshold}. Reclaim retained work with "
154
+ "`delegate worktree remove` or `delegate worktree prune`.",
155
+ file=stderr,
156
+ )
157
+
158
+
116
159
  def _validate_persistent_worktree_request(
117
160
  execution: PersistentWorktreeExecution,
118
161
  ) -> PersistentWorktreePreflight:
@@ -241,8 +241,10 @@ def _gc_orphan_entry(
241
241
  if message:
242
242
  entry["message"] = message
243
243
  safe_actions = {
244
+ "source_root_missing": "restore_source_root_or_inspect_path_before_manual_cleanup",
244
245
  "worktree_metadata_missing": "inspect_path_before_manual_cleanup",
245
246
  "branch_missing": "inspect_branch_metadata_before_manual_cleanup",
247
+ "detached_backlink": "inspect_path_before_manual_cleanup",
246
248
  "worktree_list_failed": "retry_after_git_worktree_list_succeeds",
247
249
  }
248
250
  if reason in safe_actions:
@@ -261,7 +263,12 @@ def _gc_reconcile_missing_path(
261
263
  ) -> bool:
262
264
  if Path(execution).exists():
263
265
  return False
266
+ source = record.get("sourceGitRoot")
267
+ if isinstance(source, str) and not Path(source).exists():
268
+ return False
264
269
  if dry_run:
270
+ if isinstance(source, str):
271
+ prune_roots.add(source)
265
272
  reconciled.append(_gc_missing_entry(record, execution, dry_run=True))
266
273
  return True
267
274
 
@@ -294,28 +301,57 @@ def _gc_reconcile_list_failure(
294
301
  ) -> bool:
295
302
  if listed_paths is not None:
296
303
  return False
304
+ source = record.get("sourceGitRoot")
305
+ reason = (
306
+ "source_root_missing"
307
+ if isinstance(source, str) and not Path(source).exists()
308
+ else "worktree_list_failed"
309
+ )
297
310
  if dry_run:
298
- orphans.append(_gc_orphan_entry(record, execution, "worktree_list_failed", list_warning))
311
+ orphans.append(_gc_orphan_entry(record, execution, reason, list_warning))
299
312
  return True
300
313
 
301
314
  def mark_unknown(
302
- fresh: PersistentWorktreeRecord, _fresh_source: str, fresh_execution: str
315
+ fresh: PersistentWorktreeRecord, fresh_source: str, fresh_execution: str
303
316
  ) -> None:
304
- if not Path(fresh_execution).exists():
317
+ source_missing = not Path(fresh_source).exists()
318
+ if not source_missing and not Path(fresh_execution).exists():
305
319
  return
320
+ fresh_reason = "source_root_missing" if source_missing else "worktree_list_failed"
306
321
  run_registry.set_worktree_status_locked(
307
322
  registry_root,
308
323
  str(fresh["runId"]),
309
324
  STATUS_UNKNOWN,
310
325
  )
311
- orphans.append(
312
- _gc_orphan_entry(fresh, fresh_execution, "worktree_list_failed", list_warning)
313
- )
326
+ orphans.append(_gc_orphan_entry(fresh, fresh_execution, fresh_reason, list_warning))
314
327
 
315
328
  _with_locked_fresh_gc_candidate(registry_root, record, mark_unknown)
316
329
  return True
317
330
 
318
331
 
332
+ def _resolved_git_common_dir(worktree: str) -> Path | None:
333
+ result = wm._run_git(worktree, ["rev-parse", "--git-common-dir"])
334
+ if result.returncode != 0 or not result.stdout.strip():
335
+ return None
336
+ common_dir = Path(result.stdout.strip())
337
+ if not common_dir.is_absolute():
338
+ common_dir = Path(worktree) / common_dir
339
+ try:
340
+ return common_dir.resolve()
341
+ except (OSError, RuntimeError):
342
+ return None
343
+
344
+
345
+ def _has_detached_backlink(source: str, execution: str) -> bool:
346
+ source_common_dir = _resolved_git_common_dir(source)
347
+ execution_common_dir = _resolved_git_common_dir(execution)
348
+ return (
349
+ source_common_dir is not None
350
+ and execution_common_dir is not None
351
+ and source_common_dir != execution_common_dir
352
+ )
353
+
354
+
319
355
  def _gc_reconcile_missing_metadata(
320
356
  registry_root: Path,
321
357
  record: PersistentWorktreeRecord,
@@ -329,7 +365,13 @@ def _gc_reconcile_missing_metadata(
329
365
  if listed_paths is None or _registered_worktree_path_matches(listed_paths, execution):
330
366
  return False
331
367
  if dry_run:
332
- orphans.append(_gc_orphan_entry(record, execution, "worktree_metadata_missing"))
368
+ source = record.get("sourceGitRoot")
369
+ reason = (
370
+ "detached_backlink"
371
+ if isinstance(source, str) and _has_detached_backlink(source, execution)
372
+ else "worktree_metadata_missing"
373
+ )
374
+ orphans.append(_gc_orphan_entry(record, execution, reason))
333
375
  return True
334
376
 
335
377
  def mark_unknown_if_still_orphaned(
@@ -345,12 +387,17 @@ def _gc_reconcile_missing_metadata(
345
387
  and fresh_paths is not None
346
388
  and not _registered_worktree_path_matches(fresh_paths, fresh_execution)
347
389
  ):
390
+ fresh_reason = (
391
+ "detached_backlink"
392
+ if _has_detached_backlink(fresh_source, fresh_execution)
393
+ else "worktree_metadata_missing"
394
+ )
348
395
  run_registry.set_worktree_status_locked(
349
396
  registry_root,
350
397
  str(fresh["runId"]),
351
398
  STATUS_UNKNOWN,
352
399
  )
353
- orphans.append(_gc_orphan_entry(fresh, fresh_execution, "worktree_metadata_missing"))
400
+ orphans.append(_gc_orphan_entry(fresh, fresh_execution, fresh_reason))
354
401
 
355
402
  _with_locked_fresh_gc_candidate(registry_root, record, mark_unknown_if_still_orphaned)
356
403
  return True
@@ -478,6 +525,7 @@ def gc_worktrees(registry_root: Path, *, dry_run: bool = False) -> JsonObject:
478
525
  "runsGitWorktreePrune": (not dry_run and bool(prune_roots)),
479
526
  },
480
527
  "prunedSourceRoots": 0 if dry_run else len(prune_roots),
528
+ "wouldPruneSourceRoots": len(prune_roots) if dry_run else 0,
481
529
  "reconciled": len(reconciled),
482
530
  "reconciledEntries": reconciled,
483
531
  "orphans": orphans,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: delegate-agent-cli
3
- Version: 0.17.0
3
+ Version: 0.18.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
@@ -745,6 +745,20 @@ class ValidationTests(unittest.TestCase):
745
745
  self.assertEqual(ctx.exception.error, "invalid_worktrees_config")
746
746
  self.assertIn("mergedOlderThanDays", ctx.exception.message)
747
747
 
748
+ def test_worktrees_pool_warn_count_rejects_bad_values(self):
749
+ config_mod = load_config_module()
750
+ for value in (None, -1, True, "1024"):
751
+ with self.subTest(value=value):
752
+ with self.assertRaises(config_mod.ConfigError) as ctx:
753
+ config_mod.validate_config(
754
+ config_mod.deep_merge(
755
+ config_mod.DEFAULT_CONFIG,
756
+ {"worktrees": {"poolWarnCount": value}},
757
+ )
758
+ )
759
+ self.assertEqual(ctx.exception.error, "invalid_worktrees_config")
760
+ self.assertIn("poolWarnCount", ctx.exception.message)
761
+
748
762
  def test_isolation_and_worktrees_valid_does_not_raise(self):
749
763
  config_mod = load_config_module()
750
764
  config_mod.validate_config(
@@ -18,6 +18,71 @@ from delegate_agent import worktree_summary # noqa: E402
18
18
 
19
19
 
20
20
  class ExecutionWorktreeRunTests(ExecutionTestBase):
21
+ def test_persistent_worktree_launch_checks_pool_guardrail(self):
22
+ execution = mock.Mock(config={"worktrees": {}}, stderr=io.StringIO())
23
+ preflight = mock.sentinel.preflight
24
+ with (
25
+ mock.patch.object(
26
+ self.delegate.worktree_execution,
27
+ "_validate_persistent_worktree_request",
28
+ return_value=preflight,
29
+ ),
30
+ mock.patch.object(
31
+ self.delegate.worktree_execution,
32
+ "_warn_if_worktree_pool_large",
33
+ ) as warn,
34
+ mock.patch.object(
35
+ self.delegate.worktree_execution,
36
+ "_register_persistent_worktree_run",
37
+ side_effect=RuntimeError("stop after guardrail"),
38
+ ),
39
+ self.assertRaisesRegex(RuntimeError, "stop after guardrail"),
40
+ ):
41
+ self.delegate.worktree_execution.execute_persistent_worktree(execution)
42
+
43
+ warn.assert_called_once_with(execution.config, execution.stderr)
44
+
45
+ def test_worktree_pool_guardrail_below_threshold_emits_no_warning(self):
46
+ with tempfile.TemporaryDirectory() as fake_home:
47
+ pool = Path(fake_home) / "pool"
48
+ worktree = pool / "fingerprint" / "old-worktree"
49
+ worktree.mkdir(parents=True)
50
+ config = dict(self.delegate.DEFAULT_CONFIG)
51
+ config["worktrees"] = {
52
+ **config["worktrees"],
53
+ "dataHome": str(pool),
54
+ "poolWarnCount": 1,
55
+ }
56
+ stderr = io.StringIO()
57
+
58
+ self.delegate.worktree_execution._warn_if_worktree_pool_large(config, stderr)
59
+
60
+ self.assertEqual(stderr.getvalue(), "")
61
+
62
+ def test_worktree_pool_guardrail_exceeded_warns_with_path_and_threshold(self):
63
+ with tempfile.TemporaryDirectory() as fake_home:
64
+ pool = Path(fake_home) / "pool"
65
+ for name in ("old-worktree", "new-worktree"):
66
+ (pool / "fingerprint" / name).mkdir(parents=True)
67
+ config = dict(self.delegate.DEFAULT_CONFIG)
68
+ config["worktrees"] = {
69
+ **config["worktrees"],
70
+ "dataHome": str(pool),
71
+ "poolWarnCount": 1,
72
+ }
73
+ stderr = io.StringIO()
74
+
75
+ self.delegate.worktree_execution._warn_if_worktree_pool_large(config, stderr)
76
+
77
+ warning = stderr.getvalue()
78
+ self.assertIn("WARNING", warning)
79
+ self.assertIn(str(pool), warning)
80
+ self.assertIn("2 worktrees", warning)
81
+ self.assertIn("poolWarnCount=1", warning)
82
+ self.assertIn("delegate worktree remove", warning)
83
+ self.assertIn("delegate worktree prune", warning)
84
+ self.assertNotIn("gc --all", warning)
85
+
21
86
  # -- Persistent worktree: cursor work runs in isolated worktree -----------
22
87
 
23
88
  def test_cursor_work_persistent_worktree_runs_in_worktree(self):
@@ -130,6 +130,31 @@ class SnapshotRenderingTests(SnapshotCommandTestBase):
130
130
  self.assertIn("warnings:", output)
131
131
  self.assertIn("/repo: fatal: worktree list failed", output)
132
132
 
133
+ def test_render_worktree_gc_dry_run_reports_would_prune_and_reasons(self):
134
+ stdout = io.StringIO()
135
+ self.rendering.render_worktree_gc_text(
136
+ {
137
+ "dryRun": True,
138
+ "reconciled": 1,
139
+ "prunedSourceRoots": 0,
140
+ "wouldPruneSourceRoots": 1,
141
+ "orphans": [
142
+ {
143
+ "alias": "cursor-1",
144
+ "executionCwd": "/pool/cursor-1",
145
+ "reason": "detached_backlink",
146
+ }
147
+ ],
148
+ "warnings": [],
149
+ },
150
+ stdout,
151
+ )
152
+
153
+ output = stdout.getvalue()
154
+ self.assertIn("would prune source roots: 1", output)
155
+ self.assertIn("cursor-1 detached_backlink", output)
156
+ self.assertIn("/pool/cursor-1", output)
157
+
133
158
 
134
159
  if __name__ == "__main__":
135
160
  unittest.main()
@@ -170,6 +170,86 @@ class WorktreePruneGcTests(WorktreeMgmtTestBase):
170
170
  self.assertEqual(state["worktreeStatus"], "missing")
171
171
  self.assertNotIn("worktreeRemovedAt", state)
172
172
 
173
+ def test_worktree_gc_dry_run_reports_would_prune_source_roots(self):
174
+ _repo, path = self._make_repo()
175
+ with tempfile.TemporaryDirectory() as fake_home:
176
+ wt_path = str(Path(fake_home) / "wt" / "missing")
177
+ self._seed_persistent_run(path, execution_cwd=wt_path)
178
+
179
+ code, out, _err = self._run_cli(
180
+ ["--cwd", path, "--json", "worktree", "gc", "--dry-run"],
181
+ home=fake_home,
182
+ )
183
+
184
+ self.assertEqual(code, 0)
185
+ payload = json.loads(out)
186
+ self.assertEqual(payload["wouldPruneSourceRoots"], 1)
187
+ self.assertEqual(payload["prunedSourceRoots"], 0)
188
+ self.assertFalse(payload["effects"]["deletesWorktreePaths"])
189
+
190
+ def test_worktree_gc_classifies_missing_source_root(self):
191
+ _repo, path = self._make_repo()
192
+ with tempfile.TemporaryDirectory() as fake_home:
193
+ execution = Path(fake_home) / "wt" / "orphan"
194
+ run_id, _alias = self._seed_persistent_run(path, execution_cwd=str(execution))
195
+ registry_root = self._registry_root(path)
196
+ manifest = self.delegate.run_registry.load_run_manifest(registry_root, run_id)
197
+ manifest["sourceGitRoot"] = str(Path(fake_home) / "missing-source")
198
+ self.delegate.run_registry.write_json_atomic(
199
+ self.delegate.run_registry.run_directory(registry_root, run_id) / "manifest.json",
200
+ manifest,
201
+ )
202
+
203
+ result = self.delegate.worktree_mgmt.gc_worktrees(registry_root, dry_run=True)
204
+
205
+ self.assertEqual(result["orphans"][0]["reason"], "source_root_missing")
206
+ result = self.delegate.worktree_mgmt.gc_worktrees(registry_root)
207
+ self.assertEqual(result["orphans"][0]["reason"], "source_root_missing")
208
+ state = self.delegate.run_registry.load_run_state(registry_root, run_id)
209
+ self.assertEqual(state["worktreeStatus"], "unknown")
210
+
211
+ def test_worktree_gc_classifies_detached_backlink(self):
212
+ _repo, path = self._make_repo()
213
+ with tempfile.TemporaryDirectory() as fake_home:
214
+ execution = Path(fake_home) / "standalone"
215
+ subprocess.run(
216
+ ["git", "init", str(execution)],
217
+ check=True,
218
+ stdout=subprocess.DEVNULL,
219
+ stderr=subprocess.DEVNULL,
220
+ )
221
+ self._seed_persistent_run(path, execution_cwd=str(execution))
222
+
223
+ result = self.delegate.worktree_mgmt.gc_worktrees(
224
+ self._registry_root(path), dry_run=True
225
+ )
226
+
227
+ self.assertEqual(result["orphans"][0]["reason"], "detached_backlink")
228
+
229
+ def test_worktree_gc_classifies_missing_branch(self):
230
+ _repo, path = self._make_repo()
231
+ with tempfile.TemporaryDirectory() as fake_home:
232
+ execution = Path(fake_home) / "wt" / "missing-branch"
233
+ execution.mkdir(parents=True)
234
+ self._seed_persistent_run(path, execution_cwd=str(execution))
235
+ with (
236
+ mock.patch.object(
237
+ self.delegate.worktree_mgmt,
238
+ "_worktree_list_paths_with_warning",
239
+ return_value=({str(execution)}, None),
240
+ ),
241
+ mock.patch.object(
242
+ self.delegate.worktree_mgmt,
243
+ "_branch_exists",
244
+ return_value=False,
245
+ ),
246
+ ):
247
+ result = self.delegate.worktree_mgmt.gc_worktrees(
248
+ self._registry_root(path), dry_run=True
249
+ )
250
+
251
+ self.assertEqual(result["orphans"][0]["reason"], "branch_missing")
252
+
173
253
  def test_worktree_gc_existing_orphan_path_is_not_deleted_and_marked_unknown(self):
174
254
  _repo, path = self._make_repo()
175
255
  with tempfile.TemporaryDirectory() as fake_home: