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.
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/CHANGELOG.md +21 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/PKG-INFO +1 -1
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/config.example.json +1 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/cli-reference.md +1 -1
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/configuration.md +2 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/worktrees.md +1 -1
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/pyproject.toml +1 -1
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/__init__.py +1 -1
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/command_help.py +4 -1
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/config.py +7 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/describe_payload.py +1 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/rendering.py +9 -2
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_execution.py +44 -1
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_gc.py +56 -8
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent_cli.egg-info/PKG-INFO +1 -1
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_delegate_validation.py +14 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_execution_worktree_run.py +65 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_snapshot_rendering.py +25 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_worktree_prune_gc.py +80 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/CONTRIBUTING.md +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/LICENSE +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/MANIFEST.in +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/README.md +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/SECURITY.md +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/bin/delegate-profile-shim +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/bin/delegate.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/agent-setup.md +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/assets/delegate-agent-header.png +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/delegate-workflows.md +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/development.md +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/live-runtime.md +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/publishing-checklist.md +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/security-model.md +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/troubleshooting.md +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/examples/task.claude.json +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/examples/task.codex.json +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/examples/task.cursor.json +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/examples/task.droid.json +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/examples/task.grok.json +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/examples/task.judge.json +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/examples/task.kimi.json +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/setup.cfg +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/archived_logs.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/argv_builders.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/argv_utils.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/bundled_models.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/capability_commands.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/cli.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/cli_parser.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/command_errors.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/config_commands.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/constants.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/errors.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/git_utils.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/harness_events.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/inspection_commands.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/isolation.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/json_types.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/log_output.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/model_discovery.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/private_io.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/profile_commands.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/profile_guard.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/profiles.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/prompt_instructions.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/prompt_transport.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/reasoning.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/redaction.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/request_build.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/request_models.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/retention.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/run_metadata.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/run_output_commands.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/run_registry.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/run_status.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/runner.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/safe_workspace.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/seatbelt.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/snapshot_view.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/wait_cancel_commands.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/__init__.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/commands.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/registry.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/runtime.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/schema.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/script.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_commands.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_mgmt.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_records.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_remove.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_summary.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/wsl.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent_cli.egg-info/SOURCES.txt +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent_cli.egg-info/dependency_links.txt +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent_cli.egg-info/entry_points.txt +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent_cli.egg-info/requires.txt +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent_cli.egg-info/top_level.txt +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_capability_commands.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_codex_pure_sandbox.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_command_help.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_config_commands.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_delegate_help_cli.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_delegate_isolation.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_delegate_parser.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_end_to_end_tracking.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_engine_argv.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_execution_argv_and_prompt.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_execution_dry_run.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_execution_worktree_failure_cleanup.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_execution_worktree_preflight.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_harness_events.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_inspection_commands.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_kimi_commands.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_model_discovery.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_model_selection_wave1a.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_model_selection_wave1b.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_omp_read_only_behavior.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_packaging.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_profiles.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_pure_call.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_reasoning_capabilities.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_retention.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_run_registry.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_runner_capture.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_safe_workspace_isolation.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_slash_passthrough.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_snapshot_redaction.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_snapshot_run_output.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_snapshot_view.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_tracked_timeout.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_utility_modules.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_wait_cancel_commands.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_wave4_launch_features.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_workflow_commands.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_worktree_list_show.py +0 -0
- {delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_worktree_remove.py +0 -0
- {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
|
|
@@ -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
|
|
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`
|
|
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.
|
|
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"
|
|
@@ -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",
|
|
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):
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/describe_payload.py
RENAMED
|
@@ -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
|
-
|
|
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')}
|
|
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
|
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_execution.py
RENAMED
|
@@ -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,
|
|
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,
|
|
315
|
+
fresh: PersistentWorktreeRecord, fresh_source: str, fresh_execution: str
|
|
303
316
|
) -> None:
|
|
304
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
@@ -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(
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_execution_worktree_run.py
RENAMED
|
@@ -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:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/docs/assets/delegate-agent-header.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/bundled_models.py
RENAMED
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/capability_commands.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/command_errors.py
RENAMED
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/config_commands.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/harness_events.py
RENAMED
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/inspection_commands.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/model_discovery.py
RENAMED
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/profile_commands.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/prompt_instructions.py
RENAMED
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/prompt_transport.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/request_models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/run_output_commands.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/safe_workspace.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/wait_cancel_commands.py
RENAMED
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/__init__.py
RENAMED
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/commands.py
RENAMED
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/registry.py
RENAMED
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/runtime.py
RENAMED
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/schema.py
RENAMED
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/workflows/script.py
RENAMED
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_commands.py
RENAMED
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_records.py
RENAMED
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_remove.py
RENAMED
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent/worktree_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent_cli.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/src/delegate_agent_cli.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_execution_argv_and_prompt.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_execution_worktree_preflight.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_model_selection_wave1a.py
RENAMED
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_model_selection_wave1b.py
RENAMED
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_omp_read_only_behavior.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_reasoning_capabilities.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{delegate_agent_cli-0.17.0 → delegate_agent_cli-0.18.0}/tests/test_safe_workspace_isolation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|