delegate-agent-cli 0.11.0__py3-none-any.whl

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 (54) hide show
  1. delegate_agent/__init__.py +5 -0
  2. delegate_agent/archived_logs.py +44 -0
  3. delegate_agent/argv_builders.py +423 -0
  4. delegate_agent/argv_utils.py +36 -0
  5. delegate_agent/capability_commands.py +99 -0
  6. delegate_agent/cli.py +987 -0
  7. delegate_agent/cli_parser.py +1627 -0
  8. delegate_agent/command_errors.py +29 -0
  9. delegate_agent/command_help.py +1264 -0
  10. delegate_agent/config.py +1117 -0
  11. delegate_agent/config_commands.py +143 -0
  12. delegate_agent/constants.py +50 -0
  13. delegate_agent/describe_payload.py +1018 -0
  14. delegate_agent/errors.py +32 -0
  15. delegate_agent/git_utils.py +180 -0
  16. delegate_agent/harness_events.py +719 -0
  17. delegate_agent/inspection_commands.py +104 -0
  18. delegate_agent/isolation.py +316 -0
  19. delegate_agent/json_types.py +18 -0
  20. delegate_agent/log_output.py +78 -0
  21. delegate_agent/private_io.py +109 -0
  22. delegate_agent/profile_commands.py +42 -0
  23. delegate_agent/profile_guard.py +116 -0
  24. delegate_agent/profiles.py +389 -0
  25. delegate_agent/prompt_instructions.py +39 -0
  26. delegate_agent/prompt_transport.py +12 -0
  27. delegate_agent/reasoning.py +916 -0
  28. delegate_agent/redaction.py +193 -0
  29. delegate_agent/rendering.py +573 -0
  30. delegate_agent/request_build.py +1681 -0
  31. delegate_agent/request_models.py +191 -0
  32. delegate_agent/retention.py +321 -0
  33. delegate_agent/run_metadata.py +78 -0
  34. delegate_agent/run_output_commands.py +645 -0
  35. delegate_agent/run_registry.py +747 -0
  36. delegate_agent/run_status.py +300 -0
  37. delegate_agent/runner.py +1830 -0
  38. delegate_agent/safe_workspace.py +821 -0
  39. delegate_agent/snapshot_view.py +229 -0
  40. delegate_agent/wait_cancel_commands.py +559 -0
  41. delegate_agent/worktree_commands.py +218 -0
  42. delegate_agent/worktree_execution.py +654 -0
  43. delegate_agent/worktree_gc.py +529 -0
  44. delegate_agent/worktree_mgmt.py +782 -0
  45. delegate_agent/worktree_records.py +232 -0
  46. delegate_agent/worktree_remove.py +547 -0
  47. delegate_agent/worktree_summary.py +242 -0
  48. delegate_agent/wsl.py +65 -0
  49. delegate_agent_cli-0.11.0.dist-info/METADATA +325 -0
  50. delegate_agent_cli-0.11.0.dist-info/RECORD +54 -0
  51. delegate_agent_cli-0.11.0.dist-info/WHEEL +5 -0
  52. delegate_agent_cli-0.11.0.dist-info/entry_points.txt +2 -0
  53. delegate_agent_cli-0.11.0.dist-info/licenses/LICENSE +21 -0
  54. delegate_agent_cli-0.11.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,547 @@
1
+ """Worktree removal pipeline.
2
+
3
+ Implements ``delegate worktree remove`` end to end: option normalization, dirty
4
+ and merged safety gates, the ``git worktree remove`` + branch-delete sequence,
5
+ and registry status updates. ``worktree_mgmt`` re-exports this surface so callers
6
+ and tests keep importing from ``worktree_mgmt``.
7
+
8
+ Cross-cutting seams that tests monkeypatch on the ``worktree_mgmt`` module
9
+ (``_run_git``, ``_remove_branch``, ``merged_into_source``, ``detect_worktree_status``,
10
+ ``dirty_info``, ``resolve_record``, ``_error_payload``) are read back through the
11
+ ``worktree_mgmt`` facade (the ``wm`` alias) at call time so those patches still
12
+ take effect.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+
20
+ from delegate_agent import run_registry
21
+ from delegate_agent.git_utils import GIT_TIMEOUT_RETURN_CODE
22
+ from delegate_agent.json_types import JsonObject
23
+ from delegate_agent.worktree_records import (
24
+ SCHEMA_REMOVE,
25
+ STATUS_MISSING,
26
+ STATUS_PRESENT,
27
+ STATUS_REMOVED,
28
+ STATUS_UNKNOWN,
29
+ WORKTREE_ERROR_EXIT_CODE,
30
+ PersistentWorktreeRecord,
31
+ _utc_now_iso,
32
+ )
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class BranchRemovalResult:
37
+ removed: bool
38
+ kept_reason: str | None = None
39
+ error: str | None = None
40
+ error_code: str | None = None
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class RemoveWorktreeOptions:
45
+ discard_uncommitted: bool
46
+ force_branch: bool
47
+ keep_branch: bool
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class RemoveWorktreePlan:
52
+ record: PersistentWorktreeRecord
53
+ alias: str
54
+ status: str
55
+ source_git_root: str
56
+ execution_cwd: str
57
+ branch: str | None
58
+ discarded_paths: list[str] | None
59
+ warnings: list[str]
60
+
61
+
62
+ def _remove_branch(source_git_root: str, branch: str, *, force: bool) -> BranchRemovalResult:
63
+ flag = "-D" if force else "-d"
64
+ result = wm._run_git(source_git_root, ["branch", flag, branch])
65
+ if result.returncode == 0:
66
+ return BranchRemovalResult(removed=True)
67
+ stderr = result.stderr.strip() or "delete_failed"
68
+ if result.returncode == GIT_TIMEOUT_RETURN_CODE:
69
+ return BranchRemovalResult(removed=False, error=stderr, error_code="git_timeout")
70
+ lowered = stderr.lower()
71
+ if not force and ("not fully merged" in lowered or "not merged" in lowered):
72
+ return BranchRemovalResult(removed=False, kept_reason="unmerged")
73
+ return BranchRemovalResult(removed=False, error=stderr)
74
+
75
+
76
+ def _apply_branch_removal_result(
77
+ payload: JsonObject,
78
+ result: BranchRemovalResult,
79
+ *,
80
+ alias: str,
81
+ ) -> None:
82
+ payload["branchRemoved"] = result.removed
83
+ if result.kept_reason:
84
+ payload["branchKept"] = result.kept_reason
85
+ if result.kept_reason == "unmerged":
86
+ payload["nextActions"] = [f"delegate worktree remove {alias} --force-branch"]
87
+ if result.error:
88
+ payload["ok"] = False
89
+ code = result.error_code or "branch_remove_failed"
90
+ payload["code"] = code
91
+ payload["error"] = code
92
+ payload["exitCode"] = WORKTREE_ERROR_EXIT_CODE
93
+ payload["branchRemovalError"] = result.error
94
+ if payload.get("pathRemoved") is True:
95
+ payload["partialSuccess"] = True
96
+ payload["nextActions"] = [f"delegate worktree remove {alias} --force-branch"]
97
+
98
+
99
+ def _normalize_remove_options(
100
+ *,
101
+ discard_uncommitted: bool,
102
+ force_branch: bool,
103
+ keep_branch: bool,
104
+ force: bool,
105
+ handle: str,
106
+ ) -> tuple[bool, bool, bool]:
107
+ """Normalize raw CLI flags into canonical removal booleans.
108
+
109
+ Returns (discard_uncommitted, force_branch, keep_branch) after applying
110
+ the ``--force`` shorthand and validating mutual exclusions.
111
+ """
112
+ if force:
113
+ discard_uncommitted = True
114
+ force_branch = True
115
+ if keep_branch and force_branch:
116
+ raise wm.WorktreeManagementError(
117
+ wm._error_payload(
118
+ "invalid_option_combination",
119
+ "--keep-branch is mutually exclusive with --force-branch.",
120
+ next_actions=[f"delegate worktree remove {handle} --keep-branch"],
121
+ )
122
+ )
123
+ return discard_uncommitted, force_branch, keep_branch
124
+
125
+
126
+ def _raise_if_dirty_without_discard(
127
+ *,
128
+ dirty: bool | None,
129
+ dirty_paths: list[str],
130
+ dirty_warnings: list[str] | None = None,
131
+ discard_uncommitted: bool,
132
+ record: PersistentWorktreeRecord,
133
+ alias: str,
134
+ ) -> None:
135
+ """Fail closed when dirty state is unsafe to discard implicitly."""
136
+ if dirty is None and not discard_uncommitted:
137
+ raise wm.WorktreeManagementError(
138
+ wm._error_payload(
139
+ "dirty_check_failed",
140
+ "Could not determine whether the worktree has uncommitted changes; inspect it or pass --discard-uncommitted to remove anyway.",
141
+ record=record,
142
+ warnings=dirty_warnings or None,
143
+ next_actions=[
144
+ f"delegate worktree show {alias}",
145
+ f"delegate worktree remove {alias} --discard-uncommitted",
146
+ ],
147
+ )
148
+ )
149
+ if dirty is True and not discard_uncommitted:
150
+ raise wm.WorktreeManagementError(
151
+ wm._error_payload(
152
+ "dirty_worktree",
153
+ f"Worktree has {len(dirty_paths)} uncommitted changes; pass --discard-uncommitted to remove anyway.",
154
+ record=record,
155
+ dirty_paths=dirty_paths,
156
+ next_actions=[
157
+ f"delegate worktree show {alias}",
158
+ f"delegate worktree remove {alias} --discard-uncommitted",
159
+ ],
160
+ )
161
+ )
162
+
163
+
164
+ def _raise_if_unmerged_without_override(
165
+ *,
166
+ record: PersistentWorktreeRecord,
167
+ status: str,
168
+ branch: str | None,
169
+ keep_branch: bool,
170
+ force_branch: bool,
171
+ alias: str,
172
+ ) -> None:
173
+ """Raise ``unmerged_branch`` when the branch is not merged into source
174
+ and the caller did not request ``--keep-branch`` or ``--force-branch``."""
175
+ if status not in (STATUS_PRESENT, STATUS_UNKNOWN):
176
+ return
177
+ if not isinstance(branch, str) or not branch:
178
+ return
179
+ if keep_branch or force_branch:
180
+ return
181
+ merged, merge_warnings = wm.merged_into_source(record, status)
182
+ if merged is None:
183
+ raise wm.WorktreeManagementError(
184
+ wm._error_payload(
185
+ "merge_check_failed",
186
+ "Could not determine whether the worktree branch is merged into current source HEAD; inspect it, merge it, or pass --keep-branch/--force-branch explicitly.",
187
+ record=record,
188
+ next_actions=[
189
+ f"delegate worktree show {alias}",
190
+ f"delegate worktree remove {alias} --keep-branch",
191
+ f"delegate worktree remove {alias} --force-branch",
192
+ ],
193
+ warnings=merge_warnings or None,
194
+ )
195
+ )
196
+ if merged is False:
197
+ raise wm.WorktreeManagementError(
198
+ wm._error_payload(
199
+ "unmerged_branch",
200
+ "Worktree branch is not merged into current source HEAD; inspect it, merge it, or pass --keep-branch/--force-branch explicitly.",
201
+ record=record,
202
+ next_actions=[
203
+ f"delegate worktree show {alias}",
204
+ f"delegate worktree remove {alias} --keep-branch",
205
+ f"delegate worktree remove {alias} --force-branch",
206
+ ],
207
+ warnings=merge_warnings or None,
208
+ )
209
+ )
210
+
211
+
212
+ def _require_removal_metadata(record: PersistentWorktreeRecord) -> tuple[str, str]:
213
+ """Validate that ``sourceGitRoot`` and ``executionCwd`` are present.
214
+
215
+ Returns ``(source_git_root, execution_cwd)`` for direct use by the caller.
216
+ """
217
+ source_git_root = record.get("sourceGitRoot")
218
+ execution_cwd = record.get("executionCwd")
219
+ if not isinstance(source_git_root, str) or not isinstance(execution_cwd, str):
220
+ raise wm.WorktreeManagementError(
221
+ wm._error_payload(
222
+ "worktree_remove_failed",
223
+ "Run is missing sourceGitRoot or executionCwd metadata.",
224
+ record=record,
225
+ )
226
+ )
227
+ return source_git_root, execution_cwd
228
+
229
+
230
+ def _remove_worktree_path(
231
+ *,
232
+ source_git_root: str,
233
+ execution_cwd: str,
234
+ discard_uncommitted: bool,
235
+ record: PersistentWorktreeRecord,
236
+ alias: str,
237
+ ) -> None:
238
+ """Execute ``git worktree remove`` and raise on failure."""
239
+ remove_args = ["worktree", "remove"]
240
+ if discard_uncommitted:
241
+ remove_args.append("--force")
242
+ remove_args.append(execution_cwd)
243
+ result = wm._run_git(source_git_root, remove_args)
244
+ if result.returncode != 0:
245
+ if result.returncode == GIT_TIMEOUT_RETURN_CODE:
246
+ raise wm.WorktreeManagementError(
247
+ wm._error_payload(
248
+ "git_timeout",
249
+ f"git worktree remove timed out: {result.stderr.strip()}",
250
+ record=record,
251
+ next_actions=[f"delegate worktree show {alias}"],
252
+ retry_safe=True,
253
+ )
254
+ )
255
+ raise wm.WorktreeManagementError(
256
+ wm._error_payload(
257
+ "worktree_remove_failed",
258
+ f"git worktree remove failed: {result.stderr.strip()}",
259
+ record=record,
260
+ next_actions=[f"delegate worktree show {alias}"],
261
+ retry_safe=True,
262
+ )
263
+ )
264
+
265
+
266
+ def _remove_branch_if_requested(
267
+ *,
268
+ source_git_root: str | None,
269
+ branch: str | None,
270
+ keep_branch: bool,
271
+ force_branch: bool,
272
+ status: str,
273
+ ) -> BranchRemovalResult:
274
+ """Decide and execute branch removal after the worktree path is gone.
275
+
276
+ Returns the ``BranchRemovalResult`` describing what happened to the branch.
277
+ For the normal-success path (not missing, not already-removed), the caller
278
+ should post-process the result to handle prune-originated ``keep_branch``
279
+ with an unmerged branch (spec L673).
280
+ """
281
+ if keep_branch:
282
+ return BranchRemovalResult(removed=False, kept_reason="requested")
283
+ if status == STATUS_REMOVED and not force_branch:
284
+ return BranchRemovalResult(removed=False)
285
+ if force_branch and isinstance(source_git_root, str) and isinstance(branch, str) and branch:
286
+ return wm._remove_branch(source_git_root, branch, force=True)
287
+ if status == STATUS_MISSING:
288
+ return BranchRemovalResult(removed=False, kept_reason="path_missing")
289
+ if isinstance(source_git_root, str) and isinstance(branch, str) and branch:
290
+ return wm._remove_branch(source_git_root, branch, force=False)
291
+ return BranchRemovalResult(removed=False)
292
+
293
+
294
+ def _mark_worktree_removed(
295
+ *,
296
+ registry_root: Path,
297
+ run_id: str,
298
+ discarded_paths: list[str] | None,
299
+ ) -> None:
300
+ """Update registry state to mark the run as removed."""
301
+ run_registry.set_worktree_status_locked(
302
+ registry_root,
303
+ run_id,
304
+ "removed",
305
+ removed_at=_utc_now_iso(),
306
+ discarded_dirty_paths=discarded_paths,
307
+ )
308
+
309
+
310
+ def _remove_payload(
311
+ *,
312
+ record: PersistentWorktreeRecord,
313
+ branch: object,
314
+ execution_cwd: object,
315
+ source_git_root: object,
316
+ path_removed: bool,
317
+ noop: bool,
318
+ branch_result: BranchRemovalResult,
319
+ alias: str,
320
+ discarded_paths: list[str] | None = None,
321
+ warnings: list[str] | None = None,
322
+ ) -> JsonObject:
323
+ payload: JsonObject = {
324
+ "schema": SCHEMA_REMOVE,
325
+ "ok": not bool(branch_result.error),
326
+ "alias": record.get("alias"),
327
+ "runId": record.get("runId"),
328
+ "branch": branch,
329
+ "executionCwd": execution_cwd,
330
+ "sourceGitRoot": source_git_root,
331
+ "removed": True,
332
+ "pathRemoved": path_removed,
333
+ "worktreeStatus": STATUS_REMOVED,
334
+ "noop": noop,
335
+ }
336
+ _apply_branch_removal_result(payload, branch_result, alias=alias)
337
+ if discarded_paths is not None:
338
+ payload["discardedDirtyPaths"] = discarded_paths
339
+ if warnings:
340
+ payload["warnings"] = warnings
341
+ return payload
342
+
343
+
344
+ def _build_remove_worktree_plan(
345
+ record: PersistentWorktreeRecord,
346
+ *,
347
+ alias: str,
348
+ status: str,
349
+ status_warnings: list[str],
350
+ options: RemoveWorktreeOptions,
351
+ merged_check_already_passed: bool,
352
+ ) -> RemoveWorktreePlan:
353
+ dirty, dirty_paths, _dirty_total, dirty_warnings = wm.dirty_info(record, status)
354
+ all_warnings = [*status_warnings, *dirty_warnings]
355
+ if status in (STATUS_PRESENT, STATUS_UNKNOWN):
356
+ _raise_if_dirty_without_discard(
357
+ dirty=dirty,
358
+ dirty_paths=dirty_paths,
359
+ dirty_warnings=dirty_warnings,
360
+ discard_uncommitted=options.discard_uncommitted,
361
+ record=record,
362
+ alias=alias,
363
+ )
364
+
365
+ source_git_root, execution_cwd = _require_removal_metadata(record)
366
+ branch = record.get("branch")
367
+ if not merged_check_already_passed:
368
+ _raise_if_unmerged_without_override(
369
+ record=record,
370
+ status=status,
371
+ branch=branch,
372
+ keep_branch=options.keep_branch,
373
+ force_branch=options.force_branch,
374
+ alias=alias,
375
+ )
376
+
377
+ return RemoveWorktreePlan(
378
+ record=record,
379
+ alias=alias,
380
+ status=status,
381
+ source_git_root=source_git_root,
382
+ execution_cwd=execution_cwd,
383
+ branch=branch,
384
+ discarded_paths=dirty_paths if options.discard_uncommitted and dirty_paths else None,
385
+ warnings=all_warnings,
386
+ )
387
+
388
+
389
+ def _remove_already_removed(
390
+ record: PersistentWorktreeRecord,
391
+ *,
392
+ alias: str,
393
+ options: RemoveWorktreeOptions,
394
+ ) -> JsonObject:
395
+ source_git_root = record.get("sourceGitRoot")
396
+ branch = record.get("branch")
397
+ branch_result = _remove_branch_if_requested(
398
+ source_git_root=source_git_root,
399
+ branch=branch,
400
+ keep_branch=options.keep_branch,
401
+ force_branch=options.force_branch,
402
+ status=STATUS_REMOVED,
403
+ )
404
+ return _remove_payload(
405
+ record=record,
406
+ branch=branch,
407
+ execution_cwd=record.get("executionCwd"),
408
+ source_git_root=source_git_root,
409
+ path_removed=False,
410
+ noop=not branch_result.removed,
411
+ branch_result=branch_result,
412
+ alias=alias,
413
+ )
414
+
415
+
416
+ def _remove_missing_worktree_path(
417
+ registry_root: Path,
418
+ plan: RemoveWorktreePlan,
419
+ *,
420
+ options: RemoveWorktreeOptions,
421
+ ) -> JsonObject:
422
+ branch_result = _remove_branch_if_requested(
423
+ source_git_root=plan.source_git_root,
424
+ branch=plan.branch,
425
+ keep_branch=options.keep_branch,
426
+ force_branch=options.force_branch,
427
+ status=plan.status,
428
+ )
429
+ _mark_worktree_removed(
430
+ registry_root=registry_root,
431
+ run_id=str(plan.record["runId"]),
432
+ discarded_paths=plan.discarded_paths,
433
+ )
434
+ return _remove_payload(
435
+ record=plan.record,
436
+ branch=plan.branch,
437
+ execution_cwd=plan.execution_cwd,
438
+ source_git_root=plan.source_git_root,
439
+ path_removed=False,
440
+ noop=False,
441
+ branch_result=branch_result,
442
+ alias=plan.alias,
443
+ discarded_paths=plan.discarded_paths,
444
+ )
445
+
446
+
447
+ def _remove_present_worktree_path(
448
+ registry_root: Path,
449
+ plan: RemoveWorktreePlan,
450
+ *,
451
+ options: RemoveWorktreeOptions,
452
+ ) -> JsonObject:
453
+ _remove_worktree_path(
454
+ source_git_root=plan.source_git_root,
455
+ execution_cwd=plan.execution_cwd,
456
+ discard_uncommitted=options.discard_uncommitted,
457
+ record=plan.record,
458
+ alias=plan.alias,
459
+ )
460
+ branch_result = _remove_branch_if_requested(
461
+ source_git_root=plan.source_git_root,
462
+ branch=plan.branch,
463
+ keep_branch=options.keep_branch,
464
+ force_branch=options.force_branch,
465
+ status=plan.status,
466
+ )
467
+ # Override branchKept when keep_branch came from prune on a clean worktree
468
+ # whose branch is not merged into source (spec L673).
469
+ if (
470
+ options.keep_branch
471
+ and branch_result.kept_reason == "requested"
472
+ and isinstance(plan.branch, str)
473
+ ):
474
+ merged_val, _ = wm.merged_into_source(plan.record, plan.status)
475
+ if merged_val is False:
476
+ branch_result = BranchRemovalResult(removed=False, kept_reason="unmerged")
477
+
478
+ _mark_worktree_removed(
479
+ registry_root=registry_root,
480
+ run_id=str(plan.record["runId"]),
481
+ discarded_paths=plan.discarded_paths,
482
+ )
483
+ return _remove_payload(
484
+ record=plan.record,
485
+ branch=plan.branch,
486
+ execution_cwd=plan.execution_cwd,
487
+ source_git_root=plan.source_git_root,
488
+ path_removed=True,
489
+ noop=False,
490
+ branch_result=branch_result,
491
+ alias=plan.alias,
492
+ discarded_paths=plan.discarded_paths,
493
+ warnings=plan.warnings,
494
+ )
495
+
496
+
497
+ def remove_worktree(
498
+ registry_root: Path,
499
+ *,
500
+ handle: str,
501
+ discard_uncommitted: bool = False,
502
+ force_branch: bool = False,
503
+ keep_branch: bool = False,
504
+ force: bool = False,
505
+ _merged_check_already_passed: bool = False,
506
+ ) -> JsonObject:
507
+ discard_uncommitted, force_branch, keep_branch = _normalize_remove_options(
508
+ discard_uncommitted=discard_uncommitted,
509
+ force_branch=force_branch,
510
+ keep_branch=keep_branch,
511
+ force=force,
512
+ handle=handle,
513
+ )
514
+ options = RemoveWorktreeOptions(
515
+ discard_uncommitted=discard_uncommitted,
516
+ force_branch=force_branch,
517
+ keep_branch=keep_branch,
518
+ )
519
+ with run_registry.registry_lock(registry_root):
520
+ record = wm.resolve_record(registry_root, handle=handle)
521
+ status, warnings = wm.detect_worktree_status(record)
522
+ alias = str(record.get("alias") or handle)
523
+
524
+ if status == STATUS_REMOVED:
525
+ return _remove_already_removed(record, alias=alias, options=options)
526
+
527
+ plan = _build_remove_worktree_plan(
528
+ record,
529
+ alias=alias,
530
+ status=status,
531
+ status_warnings=warnings,
532
+ options=options,
533
+ merged_check_already_passed=_merged_check_already_passed,
534
+ )
535
+
536
+ if status == STATUS_MISSING:
537
+ return _remove_missing_worktree_path(registry_root, plan, options=options)
538
+
539
+ return _remove_present_worktree_path(registry_root, plan, options=options)
540
+
541
+
542
+ # Deferred to the bottom to break the worktree_mgmt<->worktree_remove facade
543
+ # cycle: worktree_mgmt re-exports this module's surface (a top-level import here
544
+ # would fail when worktree_remove is imported first). All `wm.<seam>` access
545
+ # above is call-time, so binding the alias after our own definitions is
546
+ # sufficient and keeps mock.patch.object(worktree_mgmt, ...) seams working.
547
+ from delegate_agent import worktree_mgmt as wm # noqa: E402