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,747 @@
1
+ from __future__ import annotations
2
+
3
+ import fcntl
4
+ import json
5
+ import os
6
+ import re
7
+ import secrets
8
+ import shlex
9
+ import subprocess
10
+ import time
11
+ from collections.abc import Iterator
12
+ from contextlib import contextmanager
13
+ from dataclasses import dataclass
14
+ from datetime import UTC, datetime
15
+ from pathlib import Path
16
+
17
+ from delegate_agent import private_io
18
+ from delegate_agent.json_types import JsonObject
19
+ from delegate_agent.private_io import ( # noqa: F401 # re-exported
20
+ RegistryJsonError,
21
+ ensure_private_dir,
22
+ ensure_private_file,
23
+ read_json_object,
24
+ read_json_object_or_none,
25
+ supports_private_modes,
26
+ write_json_atomic,
27
+ write_private_bytes,
28
+ write_private_text,
29
+ write_text_atomic,
30
+ )
31
+
32
+ DELEGATE_DIR_NAME = ".delegate"
33
+ GIT_EXCLUDE_ENTRY = ".delegate/"
34
+ RUN_ID_RE = re.compile(r"^del_\d{8}T\d{6}Z_[0-9a-f]{6}$")
35
+
36
+ STDOUT_LOG = "stdout.log"
37
+ STDERR_LOG = "stderr.log"
38
+ EVENTS_JSONL = "events.jsonl"
39
+ MANIFEST_FILE = "manifest.json"
40
+ STATE_FILE = "state.json"
41
+ SNAPSHOT_FILE = "snapshot.json"
42
+ COMPLETION_REPORT_FILE = "completion-report.md"
43
+ INDEX_VERSION = 1
44
+ MANIFEST_SCHEMA = "delegate.manifest.v1"
45
+ STATE_SCHEMA = "delegate.state.v1"
46
+ SNAPSHOT_SCHEMA = "delegate.snapshot.v1"
47
+ RUNS_SCHEMA = "delegate.runs.v1"
48
+ RUN_OUTPUT_SCHEMA = "delegate.run-output.v1"
49
+ REGISTRY_LOCK_NAME = ".registry.lock"
50
+ REGISTRY_LOCK_TIMEOUT_SECONDS = 30.0
51
+ REGISTRY_LOCK_POLL_SECONDS = 0.05
52
+ PRIVATE_DIR_MODE = private_io.PRIVATE_DIR_MODE
53
+ PRIVATE_FILE_MODE = private_io.PRIVATE_FILE_MODE
54
+ GIT_INFO_EXCLUDE_TIMEOUT_SECONDS = 5.0
55
+ BYTES_PER_KIB = 1 << 10
56
+ BYTES_PER_MIB = BYTES_PER_KIB * BYTES_PER_KIB
57
+ HARNESS_NAMES = frozenset({"codex", "cursor", "grok", "kimi", "claude", "droid"})
58
+
59
+
60
+ def generate_run_id(now: datetime | None = None) -> str:
61
+ moment = now or datetime.now(UTC)
62
+ stamp = moment.strftime("%Y%m%dT%H%M%SZ")
63
+ suffix = secrets.token_hex(3)
64
+ run_id = f"del_{stamp}_{suffix}"
65
+ if not RUN_ID_RE.match(run_id):
66
+ raise ValueError(f"generated run id does not match expected format: {run_id}")
67
+ return run_id
68
+
69
+
70
+ def delegate_root(workspace: Path) -> Path:
71
+ return workspace / DELEGATE_DIR_NAME
72
+
73
+
74
+ def registry_root(workspace: Path) -> Path:
75
+ return delegate_root(workspace)
76
+
77
+
78
+ def registry_root_if_exists(workspace: Path) -> Path | None:
79
+ root = registry_root(workspace)
80
+ if not index_path(root).exists():
81
+ return None
82
+ return root
83
+
84
+
85
+ def git_info_exclude_path(git_root: Path) -> Path | None:
86
+ try:
87
+ completed = subprocess.run( # nosec B603 B607 - fixed git argv is executed with shell=False.
88
+ ["git", "-C", str(git_root), "rev-parse", "--git-path", "info/exclude"],
89
+ capture_output=True,
90
+ text=True,
91
+ check=True,
92
+ timeout=GIT_INFO_EXCLUDE_TIMEOUT_SECONDS,
93
+ )
94
+ except (FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
95
+ return None
96
+ relative = completed.stdout.strip()
97
+ if not relative:
98
+ return None
99
+ exclude_file = Path(relative)
100
+ if not exclude_file.is_absolute():
101
+ exclude_file = (git_root / exclude_file).resolve()
102
+ return exclude_file
103
+
104
+
105
+ def aliases_dir(registry_root: Path) -> Path:
106
+ return registry_root / "aliases"
107
+
108
+
109
+ def runs_dir(registry_root: Path) -> Path:
110
+ return registry_root / "runs"
111
+
112
+
113
+ def index_path(registry_root: Path) -> Path:
114
+ return registry_root / "index.json"
115
+
116
+
117
+ def empty_index() -> JsonObject:
118
+ return {"version": INDEX_VERSION, "aliases": {}, "runs": {}}
119
+
120
+
121
+ def load_index(registry_root: Path) -> JsonObject:
122
+ path = index_path(registry_root)
123
+ data = read_json_object(path)
124
+ if data is None:
125
+ return empty_index()
126
+ aliases = data.get("aliases")
127
+ runs = data.get("runs")
128
+ if not isinstance(aliases, dict) or not isinstance(runs, dict):
129
+ raise RegistryJsonError(f"{path} must contain aliases and runs objects")
130
+ return {
131
+ "version": data.get("version", INDEX_VERSION),
132
+ "aliases": aliases,
133
+ "runs": runs,
134
+ }
135
+
136
+
137
+ def save_index(registry_root: Path, index: JsonObject) -> None:
138
+ write_json_atomic(index_path(registry_root), index)
139
+
140
+
141
+ def ensure_git_delegate_exclude(git_root: Path) -> None:
142
+ exclude_file = git_info_exclude_path(git_root)
143
+ if exclude_file is None or not exclude_file.parent.exists():
144
+ return
145
+ existing = exclude_file.read_text(encoding="utf-8") if exclude_file.exists() else ""
146
+ lines = existing.splitlines()
147
+ if any(
148
+ line.strip() == GIT_EXCLUDE_ENTRY.rstrip("/") or line.strip() == GIT_EXCLUDE_ENTRY
149
+ for line in lines
150
+ ):
151
+ return
152
+ if existing and not existing.endswith("\n"):
153
+ existing += "\n"
154
+ write_text_atomic(exclude_file, existing + GIT_EXCLUDE_ENTRY + "\n")
155
+
156
+
157
+ def ensure_registry(workspace: Path, *, workspace_kind: str) -> Path:
158
+ root = delegate_root(workspace)
159
+ with registry_lock(root):
160
+ ensure_private_dir(aliases_dir(root))
161
+ ensure_private_dir(runs_dir(root))
162
+ if workspace_kind == "git":
163
+ ensure_git_delegate_exclude(workspace)
164
+ if not index_path(root).exists():
165
+ save_index(root, empty_index())
166
+ else:
167
+ ensure_private_file(index_path(root))
168
+ return root
169
+
170
+
171
+ def allocate_alias(registry_root: Path, harness: str) -> str:
172
+ claims = aliases_dir(registry_root)
173
+ ensure_private_dir(claims)
174
+ counter = 1
175
+ while True:
176
+ alias = f"{harness}-{counter}"
177
+ claim_path = claims / alias
178
+ try:
179
+ fd = os.open(claim_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, PRIVATE_FILE_MODE)
180
+ except FileExistsError:
181
+ counter += 1
182
+ continue
183
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
184
+ handle.write("")
185
+ return alias
186
+
187
+
188
+ def bind_alias_claim(registry_root: Path, alias: str, run_id: str) -> None:
189
+ claim_path = aliases_dir(registry_root) / alias
190
+ write_private_text(claim_path, run_id + "\n")
191
+
192
+
193
+ def registry_lock_path(registry_root: Path) -> Path:
194
+ return registry_root / REGISTRY_LOCK_NAME
195
+
196
+
197
+ @contextmanager
198
+ def registry_lock(
199
+ registry_root: Path,
200
+ *,
201
+ timeout_seconds: float = REGISTRY_LOCK_TIMEOUT_SECONDS,
202
+ ) -> Iterator[None]:
203
+ """Serialize registry mutations. flock releases on process exit (no stale locks)."""
204
+ ensure_private_dir(registry_root)
205
+ lock_path = registry_lock_path(registry_root)
206
+ fd = os.open(lock_path, os.O_CREAT | os.O_RDWR, PRIVATE_FILE_MODE)
207
+ ensure_private_file(lock_path)
208
+ try:
209
+ deadline = time.monotonic() + timeout_seconds
210
+ while True:
211
+ try:
212
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
213
+ break
214
+ except BlockingIOError:
215
+ if time.monotonic() >= deadline:
216
+ raise TimeoutError(
217
+ f"timed out waiting for registry lock at {lock_path} after {timeout_seconds}s"
218
+ ) from None
219
+ time.sleep(REGISTRY_LOCK_POLL_SECONDS)
220
+ yield
221
+ finally:
222
+ try:
223
+ fcntl.flock(fd, fcntl.LOCK_UN)
224
+ finally:
225
+ os.close(fd)
226
+
227
+
228
+ def register_run(
229
+ registry_root: Path,
230
+ *,
231
+ harness: str,
232
+ run_id: str | None = None,
233
+ metadata: JsonObject | None = None,
234
+ ) -> tuple[str, str]:
235
+ run_id = run_id or generate_run_id()
236
+ if not RUN_ID_RE.match(run_id):
237
+ raise ValueError(f"run id does not match expected format: {run_id}")
238
+ with registry_lock(registry_root):
239
+ alias = allocate_alias(registry_root, harness)
240
+ bind_alias_claim(registry_root, alias, run_id)
241
+ run_dir = runs_dir(registry_root) / run_id
242
+ ensure_private_dir(run_dir)
243
+ index = load_index(registry_root)
244
+ # Stamp an explicit registration ordinal so the latest-run tiebreaker
245
+ # survives the persisted round trip. save_index writes with
246
+ # sort_keys=True, which reorders the ``runs`` mapping lexicographically
247
+ # by run_id on disk; an in-memory-only insertion index would silently
248
+ # degrade to random-suffix order after reload. The ordinal is
249
+ # 1 + max existing ordinal (0 when the index is empty or fully legacy),
250
+ # so newly registered runs always outrank legacy entries that lack an
251
+ # ordinal -- which is chronologically correct because new runs ARE
252
+ # later than any legacy run already in the index.
253
+ existing_ordinals = (
254
+ entry.get("registrationOrdinal")
255
+ for entry in index.get("runs", {}).values()
256
+ if isinstance(entry, dict)
257
+ )
258
+ next_ordinal = 1 + max(
259
+ (value for value in existing_ordinals if isinstance(value, int)),
260
+ default=0,
261
+ )
262
+ entry = {
263
+ "alias": alias,
264
+ "harness": harness,
265
+ **(metadata or {}),
266
+ "registrationOrdinal": next_ordinal,
267
+ }
268
+ index["aliases"][alias] = run_id
269
+ index["runs"][run_id] = entry
270
+ save_index(registry_root, index)
271
+ return run_id, alias
272
+
273
+
274
+ def lookup_run_id(index: JsonObject, handle: str) -> str | None:
275
+ if handle in index.get("runs", {}):
276
+ return handle
277
+ aliases = index.get("aliases", {})
278
+ if handle in aliases:
279
+ return aliases[handle]
280
+ return None
281
+
282
+
283
+ UTC_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
284
+
285
+
286
+ def utc_now_iso() -> str:
287
+ return datetime.now(UTC).strftime(UTC_TIMESTAMP_FORMAT)
288
+
289
+
290
+ def parse_utc_timestamp(value: str | None) -> datetime | None:
291
+ if not value or not isinstance(value, str):
292
+ return None
293
+ try:
294
+ if value.endswith("Z"):
295
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
296
+ return datetime.fromisoformat(value)
297
+ except ValueError:
298
+ return None
299
+
300
+
301
+ def _format_age(timestamp: str, *, now: datetime | None = None) -> str:
302
+ parsed = parse_utc_timestamp(timestamp)
303
+ if parsed is None:
304
+ return "unknown age"
305
+ delta = (now or datetime.now(UTC)) - parsed
306
+ total_seconds = max(int(delta.total_seconds()), 0)
307
+ minutes, seconds = divmod(total_seconds, 60)
308
+ hours, minutes = divmod(minutes, 60)
309
+ days, hours = divmod(hours, 24)
310
+ if days:
311
+ return f"{days}d ago"
312
+ if hours:
313
+ return f"{hours}h ago"
314
+ if minutes:
315
+ return f"{minutes}m ago"
316
+ return f"{seconds}s ago"
317
+
318
+
319
+ def latest_handle_suggestions(
320
+ index: JsonObject, registry_root: Path, *, limit: int = 8
321
+ ) -> list[str]:
322
+ suggestions: list[str] = []
323
+ seen: set[str] = set()
324
+ harnesses = sorted(
325
+ {
326
+ entry.get("harness")
327
+ for entry in index.get("runs", {}).values()
328
+ if isinstance(entry, dict) and isinstance(entry.get("harness"), str)
329
+ }
330
+ )
331
+ for harness in harnesses:
332
+ run_id = latest_run_id_for_harness(registry_root, index, str(harness))
333
+ if run_id is None:
334
+ continue
335
+ alias = alias_for_run(index, run_id) or run_id
336
+ if alias in seen:
337
+ continue
338
+ state = load_run_state_or_none(registry_root, run_id)
339
+ manifest = load_run_manifest_or_none(registry_root, run_id)
340
+ suggestions.append(f"{alias} ({_format_age(activity_timestamp(state, manifest, run_id))})")
341
+ seen.add(alias)
342
+ if len(suggestions) >= limit:
343
+ break
344
+ return suggestions
345
+
346
+
347
+ def snapshot_command(alias: str, *, cwd: str | None = None) -> str:
348
+ if cwd is None:
349
+ return f"delegate snapshot {alias}"
350
+ return shlex.join(["delegate", "--cwd", cwd, "snapshot", alias])
351
+
352
+
353
+ def run_output_command(
354
+ handle: str,
355
+ *,
356
+ completion_report: bool = False,
357
+ cwd: str | None = None,
358
+ ) -> str:
359
+ if cwd is not None:
360
+ argv = ["delegate", "--cwd", cwd, "run-output", handle]
361
+ if completion_report:
362
+ argv.append("--completion-report")
363
+ return shlex.join(argv)
364
+ base = f"delegate run-output {handle}"
365
+ if completion_report:
366
+ return f"{base} --completion-report"
367
+ return base
368
+
369
+
370
+ def alias_sequence_for_harness(alias: object, harness: str) -> int:
371
+ """Return the monotonic alias generation for a harness alias.
372
+
373
+ Alias claims are durable and allocated with exclusive creates, so the
374
+ numeric suffix is a better same-timestamp tiebreaker than the random run-id
375
+ suffix. ``cursor-1`` is generation 1; pre-v0.10 literal ``cursor`` also
376
+ ranks as generation 1 so old registries sort sanely.
377
+ """
378
+ if not isinstance(alias, str):
379
+ return 0
380
+ if alias == harness:
381
+ return 1
382
+ prefix = f"{harness}-"
383
+ if not alias.startswith(prefix):
384
+ return 0
385
+ suffix = alias.removeprefix(prefix)
386
+ if not suffix.isdecimal():
387
+ return 0
388
+ return int(suffix)
389
+
390
+
391
+ @dataclass(frozen=True)
392
+ class ResolveResult:
393
+ run_id: str | None
394
+ alias: str | None
395
+ suggestions: tuple[str, ...]
396
+ requested_handle: str | None = None
397
+ resolved_handle: str | None = None
398
+ resolution_kind: str = "literal"
399
+
400
+
401
+ @dataclass(frozen=True)
402
+ class RunTarget:
403
+ run_id: str
404
+ alias: str | None
405
+ requested_handle: str | None = None
406
+ resolved_handle: str | None = None
407
+ resolution_kind: str = "literal"
408
+
409
+
410
+ @dataclass(frozen=True)
411
+ class RunTargetLookupError:
412
+ error: str
413
+ message: str
414
+
415
+
416
+ def run_directory(registry_root: Path, run_id: str) -> Path:
417
+ return runs_dir(registry_root) / run_id
418
+
419
+
420
+ def suggest_handles(index: JsonObject, handle: str, *, limit: int = 8) -> list[str]:
421
+ aliases = sorted(index.get("aliases", {}).keys())
422
+ if not aliases:
423
+ return []
424
+ exact_ci = [alias for alias in aliases if alias.lower() == handle.lower()]
425
+ if exact_ci:
426
+ return exact_ci[:limit]
427
+ prefix = [alias for alias in aliases if alias.startswith(handle) or handle.startswith(alias)]
428
+ substring = [alias for alias in aliases if handle in alias or alias in handle]
429
+ ordered: list[str] = []
430
+ seen: set[str] = set()
431
+ for candidate in prefix + substring + aliases:
432
+ if candidate in seen:
433
+ continue
434
+ seen.add(candidate)
435
+ ordered.append(candidate)
436
+ if len(ordered) >= limit:
437
+ break
438
+ return ordered
439
+
440
+
441
+ def _alias_for_resolved(index: JsonObject, run_id: str) -> str | None:
442
+ return alias_for_run(index, run_id)
443
+
444
+
445
+ def resolve_handle(
446
+ index: JsonObject,
447
+ handle: str,
448
+ *,
449
+ registry_root: Path | None = None,
450
+ ) -> ResolveResult:
451
+ if registry_root is not None and handle in HARNESS_NAMES:
452
+ run_id = latest_run_id_for_harness(registry_root, index, handle)
453
+ if run_id is None:
454
+ return ResolveResult(None, None, tuple(suggest_handles(index, handle)))
455
+ alias = _alias_for_resolved(index, run_id)
456
+ return ResolveResult(run_id, alias, (), handle, alias or run_id, "latest")
457
+ if registry_root is not None and ":" in handle:
458
+ harness, model_alias = handle.split(":", 1)
459
+ if harness in HARNESS_NAMES and model_alias:
460
+ run_id = latest_run_id_for_harness_model(registry_root, index, harness, model_alias)
461
+ if run_id is None:
462
+ return ResolveResult(None, None, tuple(suggest_handles(index, handle)))
463
+ alias = _alias_for_resolved(index, run_id)
464
+ return ResolveResult(run_id, alias, (), handle, alias or run_id, "latest_model")
465
+ run_id = lookup_run_id(index, handle)
466
+ if run_id is None:
467
+ return ResolveResult(None, None, tuple(suggest_handles(index, handle)))
468
+ alias = index.get("runs", {}).get(run_id, {}).get("alias")
469
+ if not isinstance(alias, str):
470
+ alias = None
471
+ for candidate, candidate_id in index.get("aliases", {}).items():
472
+ if candidate_id == run_id:
473
+ alias = candidate
474
+ break
475
+ return ResolveResult(run_id, alias, (), handle, alias or run_id, "literal")
476
+
477
+
478
+ def resolve_run_target(
479
+ registry_root: Path,
480
+ *,
481
+ handle: str | None,
482
+ latest_harness: str | None,
483
+ ) -> RunTarget | RunTargetLookupError:
484
+ index = load_index(registry_root)
485
+ if latest_harness is not None:
486
+ if ":" in latest_harness:
487
+ harness, model_alias = latest_harness.split(":", 1)
488
+ run_id = latest_run_id_for_harness_model(registry_root, index, harness, model_alias)
489
+ resolution_kind = "latest_model"
490
+ else:
491
+ run_id = latest_run_id_for_harness(registry_root, index, latest_harness)
492
+ resolution_kind = "latest"
493
+ if run_id is None:
494
+ return RunTargetLookupError(
495
+ "no_matching_runs",
496
+ f"No runs found for harness: {latest_harness}",
497
+ )
498
+ alias = alias_for_run(index, run_id)
499
+ return RunTarget(run_id, alias, latest_harness, alias or run_id, resolution_kind)
500
+ if handle is None:
501
+ return RunTargetLookupError(
502
+ "missing_handle",
503
+ "A run handle is required.",
504
+ )
505
+ resolved = resolve_handle(index, handle, registry_root=registry_root)
506
+ if resolved.run_id is None:
507
+ suggestion_items = latest_handle_suggestions(index, registry_root)
508
+ suggestion_items.extend(
509
+ item for item in resolved.suggestions if item not in suggestion_items
510
+ )
511
+ suggestions = ", ".join(suggestion_items[:8]) if suggestion_items else "(none)"
512
+ return RunTargetLookupError(
513
+ "unknown_handle",
514
+ f"Unknown run handle: {handle}. Suggestions: {suggestions}. "
515
+ "Runs are recorded per-workspace under <workspace>/.delegate; "
516
+ "if this run was launched elsewhere, pass --cwd <that workspace>.",
517
+ )
518
+ return RunTarget(
519
+ resolved.run_id,
520
+ resolved.alias,
521
+ resolved.requested_handle,
522
+ resolved.resolved_handle,
523
+ resolved.resolution_kind,
524
+ )
525
+
526
+
527
+ def load_run_state(registry_root: Path, run_id: str) -> JsonObject | None:
528
+ return read_json_object(run_directory(registry_root, run_id) / STATE_FILE)
529
+
530
+
531
+ def load_run_state_or_none(registry_root: Path, run_id: str) -> JsonObject | None:
532
+ return read_json_object_or_none(run_directory(registry_root, run_id) / STATE_FILE)
533
+
534
+
535
+ def load_run_snapshot(registry_root: Path, run_id: str) -> JsonObject | None:
536
+ return read_json_object(run_directory(registry_root, run_id) / SNAPSHOT_FILE)
537
+
538
+
539
+ def load_run_snapshot_or_none(registry_root: Path, run_id: str) -> JsonObject | None:
540
+ return read_json_object_or_none(run_directory(registry_root, run_id) / SNAPSHOT_FILE)
541
+
542
+
543
+ def load_run_manifest(registry_root: Path, run_id: str) -> JsonObject | None:
544
+ return read_json_object(run_directory(registry_root, run_id) / MANIFEST_FILE)
545
+
546
+
547
+ def load_run_manifest_or_none(registry_root: Path, run_id: str) -> JsonObject | None:
548
+ return read_json_object_or_none(run_directory(registry_root, run_id) / MANIFEST_FILE)
549
+
550
+
551
+ def alias_for_run(index: JsonObject, run_id: str) -> str | None:
552
+ entry = index.get("runs", {}).get(run_id)
553
+ if isinstance(entry, dict):
554
+ alias = entry.get("alias")
555
+ if isinstance(alias, str):
556
+ return alias
557
+ for candidate, candidate_id in index.get("aliases", {}).items():
558
+ if candidate_id == run_id and isinstance(candidate, str):
559
+ return candidate
560
+ return None
561
+
562
+
563
+ def timestamp_from_run_id(run_id: str) -> str:
564
+ match = re.match(r"^del_(\d{8}T\d{6}Z)_", run_id)
565
+ if not match:
566
+ return ""
567
+ raw = match.group(1)
568
+ return f"{raw[:4]}-{raw[4:6]}-{raw[6:8]}T{raw[9:11]}:{raw[11:13]}:{raw[13:15]}Z"
569
+
570
+
571
+ def _write_worktree_status(
572
+ registry_root: Path,
573
+ run_id: str,
574
+ status: str,
575
+ *,
576
+ removed_at: str | None = None,
577
+ discarded_dirty_paths: list[str] | None = None,
578
+ ) -> JsonObject:
579
+ valid_statuses = {"present", "removed", "missing", "unknown"}
580
+ if status not in valid_statuses:
581
+ raise ValueError(f"worktree status must be one of: {', '.join(sorted(valid_statuses))}")
582
+ run_path = run_directory(registry_root, run_id)
583
+ state_path = run_path / STATE_FILE
584
+ if not state_path.exists():
585
+ raise FileNotFoundError(f"State file not found for run {run_id}")
586
+ state: JsonObject = json.loads(state_path.read_text(encoding="utf-8"))
587
+ state["worktreeStatus"] = status
588
+ if removed_at is not None:
589
+ state["worktreeRemovedAt"] = removed_at
590
+ if discarded_dirty_paths is not None:
591
+ state["discardedDirtyPaths"] = discarded_dirty_paths
592
+ write_json_atomic(state_path, state)
593
+ return state
594
+
595
+
596
+ def set_worktree_status(
597
+ registry_root: Path,
598
+ run_id: str,
599
+ status: str,
600
+ *,
601
+ removed_at: str | None = None,
602
+ discarded_dirty_paths: list[str] | None = None,
603
+ ) -> JsonObject:
604
+ """Set the worktreeStatus field on a run's state, and optionally worktreeRemovedAt.
605
+
606
+ Acquires the registry lock, loads the run's state, updates fields,
607
+ persists via write_json_atomic, and returns the updated state.
608
+
609
+ Status must be one of: 'present', 'removed', 'missing', 'unknown'.
610
+ """
611
+ with registry_lock(registry_root):
612
+ return _write_worktree_status(
613
+ registry_root,
614
+ run_id,
615
+ status,
616
+ removed_at=removed_at,
617
+ discarded_dirty_paths=discarded_dirty_paths,
618
+ )
619
+
620
+
621
+ def set_worktree_status_locked(
622
+ registry_root: Path,
623
+ run_id: str,
624
+ status: str,
625
+ *,
626
+ removed_at: str | None = None,
627
+ discarded_dirty_paths: list[str] | None = None,
628
+ ) -> JsonObject:
629
+ """Set worktree status when the caller already holds the registry lock."""
630
+ return _write_worktree_status(
631
+ registry_root,
632
+ run_id,
633
+ status,
634
+ removed_at=removed_at,
635
+ discarded_dirty_paths=discarded_dirty_paths,
636
+ )
637
+
638
+
639
+ def latest_run_id_for_harness(registry_root: Path, index: JsonObject, harness: str) -> str | None:
640
+ return _latest_run_id_for_harness(registry_root, index, harness)
641
+
642
+
643
+ def latest_run_id_for_harness_model(
644
+ registry_root: Path,
645
+ index: JsonObject,
646
+ harness: str,
647
+ model_alias: str,
648
+ ) -> str | None:
649
+ return _latest_run_id_for_harness(
650
+ registry_root,
651
+ index,
652
+ harness,
653
+ model_alias=model_alias,
654
+ )
655
+
656
+
657
+ def _latest_run_id_for_harness(
658
+ registry_root: Path,
659
+ index: JsonObject,
660
+ harness: str,
661
+ *,
662
+ model_alias: str | None = None,
663
+ ) -> str | None:
664
+ matches: list[tuple[str, int, int, str]] = []
665
+ # The chronology source for the exact tie this function resolves (same
666
+ # activity timestamp, same alias sequence -- e.g. a legacy literal ``codex``
667
+ # and a v0.10 ``codex-1`` that both rank as alias sequence 1 in the same
668
+ # second) is the explicit ``registrationOrdinal`` stamped by register_run.
669
+ # The ordinal is required because save_index persists with sort_keys=True,
670
+ # which reorders the ``runs`` mapping lexicographically by run_id on disk;
671
+ # an in-memory-only insertion index would silently degrade to random-suffix
672
+ # order after a load_index round trip (the production path). For legacy
673
+ # entries that predate the ordinal field, fall back to the enumeration
674
+ # insertion index of the loaded mapping. This keeps mixed indexes
675
+ # consistent: new runs always carry an explicit ordinal >= 1 + max existing
676
+ # ordinal, so they outrank any legacy tie (whose fallback is the
677
+ # insertion index of a mapping that, post-reload, is lexicographic -- but
678
+ # legacy entries have no ordinal at all, so their uniform fallback only
679
+ # orders legacy entries relative to each other, never above a newer run),
680
+ # which is chronologically correct because new runs ARE later. Using the
681
+ # run_id suffix here would be wrong because that suffix is random, so
682
+ # lexicographic run_id order can rank an older run as latest.
683
+ for insertion_index, (run_id, entry) in enumerate(index.get("runs", {}).items()):
684
+ if not isinstance(entry, dict) or entry.get("harness") != harness:
685
+ continue
686
+ entry_model_alias = entry.get("modelAlias")
687
+ if (
688
+ model_alias is not None
689
+ and entry_model_alias is not None
690
+ and entry_model_alias != model_alias
691
+ ):
692
+ continue
693
+ state = load_run_state_or_none(registry_root, run_id)
694
+ manifest = load_run_manifest_or_none(registry_root, run_id)
695
+ if model_alias is not None and entry_model_alias is None:
696
+ manifest_model_alias = (
697
+ manifest.get("modelAlias") if isinstance(manifest, dict) else None
698
+ )
699
+ if manifest_model_alias != model_alias:
700
+ continue
701
+ sort_ts = run_status.activity_timestamp(state, manifest, run_id)
702
+ alias_sequence = alias_sequence_for_harness(entry.get("alias"), harness)
703
+ effective_ordinal = entry.get("registrationOrdinal", insertion_index)
704
+ if not isinstance(effective_ordinal, int):
705
+ effective_ordinal = insertion_index
706
+ matches.append((sort_ts, alias_sequence, effective_ordinal, run_id))
707
+ if not matches:
708
+ return None
709
+ # Tertiary effective_ordinal makes same-second, same-sequence ties resolve
710
+ # to the latest-registered run instead of falling to the random run-id
711
+ # suffix. reverse=True means the greatest (latest) ordinal wins. For legacy
712
+ # entries the fallback insertion index only ever ranks legacy entries among
713
+ # themselves; any explicit ordinal from a newer run outranks them.
714
+ matches.sort(key=lambda item: (item[0], item[1], item[2]), reverse=True)
715
+ return matches[0][3]
716
+
717
+
718
+ from delegate_agent import run_status # noqa: E402 # facade re-export, placed after core defs
719
+ from delegate_agent.run_status import ( # noqa: E402, F401 # re-exported
720
+ DEFAULT_RUNS_LIMIT,
721
+ LARGE_LOG_WARN_BYTES,
722
+ LARGE_LOG_WARN_MIB,
723
+ STATUS_CANCELLED,
724
+ STATUS_FAILED,
725
+ STATUS_FILTER_ACTIVE,
726
+ STATUS_FILTER_RECENT,
727
+ STATUS_FILTER_RUNNING,
728
+ STATUS_FILTER_STALE,
729
+ STATUS_RUNNING,
730
+ STATUS_STALE,
731
+ STATUS_SUCCEEDED,
732
+ STATUS_UNKNOWN,
733
+ TERMINAL_STATUSES,
734
+ activity_datetime,
735
+ activity_timestamp,
736
+ build_run_summary,
737
+ effective_log_byte_sizes,
738
+ effective_status,
739
+ large_log_warnings,
740
+ list_run_summaries,
741
+ log_byte_sizes,
742
+ process_alive,
743
+ raw_logs_archived,
744
+ raw_status,
745
+ stale_next_actions,
746
+ status_fields,
747
+ )