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
delegate_agent/cli.py ADDED
@@ -0,0 +1,987 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import json # noqa: F401 # re-exported for tests (delegate.json)
5
+ import os
6
+ import shlex
7
+ import shutil
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import TextIO
11
+
12
+ from delegate_agent import (
13
+ VERSION,
14
+ argv_utils,
15
+ capability_commands,
16
+ command_errors,
17
+ command_help,
18
+ config_commands,
19
+ inspection_commands,
20
+ profile_commands,
21
+ profile_guard,
22
+ profiles,
23
+ reasoning,
24
+ redaction,
25
+ run_output_commands,
26
+ run_registry,
27
+ wait_cancel_commands,
28
+ worktree_commands,
29
+ worktree_execution,
30
+ worktree_mgmt,
31
+ wsl,
32
+ )
33
+ from delegate_agent import config as delegate_config
34
+ from delegate_agent import rendering as delegate_rendering
35
+ from delegate_agent import retention as delegate_retention
36
+ from delegate_agent import runner as delegate_runner
37
+ from delegate_agent.argv_builders import ( # noqa: F401 # re-exported for tests / back-compat
38
+ SAFE_REVIEW_PREFIX_BY_ENGINE,
39
+ _claude_harness_bypass_enabled,
40
+ _grok_harness_bypass_enabled,
41
+ build_claude_argv,
42
+ build_codex_argv,
43
+ build_cursor_argv,
44
+ build_droid_argv,
45
+ build_grok_argv,
46
+ build_kimi_argv,
47
+ prefix_cursor_safe_prompt,
48
+ prefix_droid_safe_prompt,
49
+ redacted_prompt_argv,
50
+ )
51
+ from delegate_agent.argv_utils import public_argv
52
+ from delegate_agent.cli_parser import ( # noqa: F401 # re-exported for tests / back-compat
53
+ has_misplaced_global_option,
54
+ infer_global_json,
55
+ parse_cli,
56
+ parse_required_positive_int_option,
57
+ )
58
+ from delegate_agent.constants import BINARY_CONFIG_ENGINES, KNOWN_ENGINES, MODE_CALL, MODE_SAFE
59
+ from delegate_agent.describe_payload import ( # noqa: F401 # re-exported for tests / back-compat
60
+ _claude_runtime_policy,
61
+ describe_payload,
62
+ emit_agent_help,
63
+ emit_command_help,
64
+ emit_describe,
65
+ emit_models,
66
+ models_payload,
67
+ )
68
+ from delegate_agent.errors import (
69
+ EXIT_MISSING_BINARY,
70
+ EXIT_OK,
71
+ EXIT_USAGE,
72
+ DelegateError,
73
+ )
74
+ from delegate_agent.git_utils import capture_git_metadata # noqa: F401 # re-exported for tests
75
+ from delegate_agent.isolation import ( # noqa: F401 # re-exported for tests
76
+ IsolationContext,
77
+ build_isolation_context,
78
+ )
79
+ from delegate_agent.json_types import JsonObject
80
+ from delegate_agent.prompt_transport import ( # noqa: F401 # CURSOR_PROMPT_REDACTION re-exported for tests
81
+ CURSOR_PROMPT_REDACTION,
82
+ DROID_PROMPT_FILE_ARG_PLACEHOLDER,
83
+ DROID_PROMPT_FILE_DISPLAY,
84
+ KIMI_PROMPT_REDACTION,
85
+ PROMPT_FILE_ARG_PLACEHOLDER,
86
+ PROMPT_FILE_DISPLAY,
87
+ PROMPT_TRANSPORT_ARGV,
88
+ PROMPT_TRANSPORT_FILE,
89
+ PROMPT_TRANSPORT_STDIN,
90
+ )
91
+ from delegate_agent.request_build import ( # noqa: F401 # re-exported for tests / back-compat
92
+ CALL_TEMP_CWD_PLACEHOLDER,
93
+ RUN_INPUT_KEYS,
94
+ _load_input_json_object,
95
+ _resolve_default_model,
96
+ build_request,
97
+ effective_prompt,
98
+ load_config,
99
+ read_stdin_source,
100
+ request_from_input_json,
101
+ request_from_parsed,
102
+ resolve_completion_report_mode,
103
+ resolve_prompt,
104
+ resolve_workspace,
105
+ validate_config,
106
+ validate_prompt,
107
+ )
108
+ from delegate_agent.request_models import ( # noqa: F401 # re-exported for tests / back-compat
109
+ GlobalOptions,
110
+ InspectionOptions,
111
+ LaunchOptions,
112
+ ParsedCommand,
113
+ Request,
114
+ ResolvedWorkspace,
115
+ RunJsonOptions,
116
+ )
117
+ from delegate_agent.safe_workspace import ( # noqa: F401 # re-exported for tests / back-compat
118
+ CURSOR_SAFE_CLI_CONFIG,
119
+ SAFE_BLOCKED_SYMLINK_PLACEHOLDER,
120
+ SAFE_EXTERNAL_SYMLINK_WARNING_PREFIX,
121
+ SAFE_UNBORN_GIT_WARNING,
122
+ block_external_symlinks,
123
+ cleanup_safe_isolated_workspace,
124
+ create_directory_safe_workspace,
125
+ create_git_safe_workspace,
126
+ external_symlink_warnings,
127
+ mirror_path_preserving_symlinks,
128
+ read_git_tracked_diff,
129
+ safe_isolated_request,
130
+ write_cursor_safe_project_config,
131
+ )
132
+
133
+ _replace_ws_by_engine = argv_utils.replace_workspace_arg_in_argv
134
+
135
+ DEFAULT_CONFIG = delegate_config.embedded_default_config()
136
+ CONFIG_ENV = delegate_config.CONFIG_ENV
137
+
138
+ MISSING_BINARY_PROBE_DIRS = (
139
+ "~/.claude/local",
140
+ "~/.grok/bin",
141
+ "~/.kimi-code/bin",
142
+ "~/.local/bin",
143
+ "~/bin",
144
+ "/opt/homebrew/bin",
145
+ )
146
+
147
+
148
+ HELP = command_help.render_overview_text()
149
+
150
+
151
+ def config_path() -> Path:
152
+ return delegate_config.config_path()
153
+
154
+
155
+ def workspace_path_for_config(global_cwd: str | None) -> Path | None:
156
+ try:
157
+ return Path(resolve_workspace(global_cwd).path)
158
+ except DelegateError:
159
+ return None
160
+
161
+
162
+ def maybe_run_retention_pass(registry_root: Path, config: JsonObject) -> None:
163
+ delegate_retention.run_retention_pass(registry_root, config)
164
+
165
+
166
+ def emit_snapshot(parsed: ParsedCommand, workspace: ResolvedWorkspace, stdout: TextIO) -> int:
167
+ command = parsed.snapshot
168
+ if command is None:
169
+ raise DelegateError("invalid_command", "snapshot options are required.")
170
+ return inspection_commands.emit_snapshot(
171
+ command,
172
+ workspace_path=workspace.path,
173
+ stdout=stdout,
174
+ )
175
+
176
+
177
+ def emit_runs(parsed: ParsedCommand, workspace: ResolvedWorkspace, stdout: TextIO) -> int:
178
+ command = parsed.runs
179
+ if command is None:
180
+ raise DelegateError("invalid_command", "runs options are required.")
181
+ return inspection_commands.emit_runs(
182
+ command,
183
+ workspace_path=workspace.path,
184
+ stdout=stdout,
185
+ )
186
+
187
+
188
+ RECOVERY_STDOUT_TAIL_LINES = run_output_commands.RECOVERY_STDOUT_TAIL_LINES
189
+ RECOVERY_STDOUT_TAIL_BYTES = run_output_commands.RECOVERY_STDOUT_TAIL_BYTES
190
+ RUN_OUTPUT_DEFAULT_TAIL_LINES = run_output_commands.RUN_OUTPUT_DEFAULT_TAIL_LINES
191
+
192
+
193
+ def emit_run_output(parsed: ParsedCommand, workspace: ResolvedWorkspace, stdout: TextIO) -> int:
194
+ command = parsed.run_output
195
+ if command is None:
196
+ raise DelegateError("invalid_command", "run-output options are required.")
197
+ return run_output_commands.emit(
198
+ command,
199
+ workspace_path=workspace.path,
200
+ stdout=stdout,
201
+ )
202
+
203
+
204
+ def emit_wait(parsed: ParsedCommand, workspace: ResolvedWorkspace, stdout: TextIO) -> int:
205
+ command = parsed.wait_command
206
+ if command is None:
207
+ raise DelegateError("invalid_command", "wait options are required.")
208
+ return wait_cancel_commands.emit_wait(command, workspace_path=workspace.path, stdout=stdout)
209
+
210
+
211
+ def emit_cancel(parsed: ParsedCommand, workspace: ResolvedWorkspace, stdout: TextIO) -> int:
212
+ command = parsed.cancel_command
213
+ if command is None:
214
+ raise DelegateError("invalid_command", "cancel options are required.")
215
+ return wait_cancel_commands.emit_cancel(command, workspace_path=workspace.path, stdout=stdout)
216
+
217
+
218
+ def emit_worktree(
219
+ parsed: ParsedCommand,
220
+ workspace: ResolvedWorkspace,
221
+ config: JsonObject,
222
+ stdout: TextIO,
223
+ ) -> int:
224
+ command = parsed.worktree
225
+ if command is None:
226
+ raise DelegateError("invalid_command", "worktree options are required.")
227
+ return worktree_commands.emit(
228
+ command,
229
+ workspace_path=workspace.path,
230
+ config=config,
231
+ stdout=stdout,
232
+ )
233
+
234
+
235
+ def emit_profiles_command(
236
+ parsed: ParsedCommand,
237
+ config: JsonObject,
238
+ config_source: str,
239
+ stdout: TextIO,
240
+ ) -> int:
241
+ command = parsed.profiles_command
242
+ if command is None:
243
+ raise DelegateError("invalid_command", "profiles options are required.")
244
+ resolution = profiles.resolve_active_profile(
245
+ config,
246
+ os.environ,
247
+ cli_override=parsed.global_options.auth_profile,
248
+ )
249
+ return profile_commands.emit(
250
+ command,
251
+ resolution=resolution,
252
+ config_source=config_source,
253
+ stdout=stdout,
254
+ )
255
+
256
+
257
+ def emit_config_command(parsed: ParsedCommand, stdout: TextIO) -> int:
258
+ command = parsed.config_command
259
+ if command is None:
260
+ raise DelegateError("invalid_command", "config options are required.")
261
+ return config_commands.emit(command, stdout)
262
+
263
+
264
+ def dry_run_payload(request: Request) -> JsonObject:
265
+ payload: JsonObject = {
266
+ "ok": True,
267
+ "dryRun": True,
268
+ "cwd": request.workspace,
269
+ "workspaceKind": request.workspace_kind,
270
+ "engine": request.engine,
271
+ "mode": request.mode,
272
+ "model": request.model,
273
+ "argv": public_argv(request),
274
+ "promptTransport": request.prompt_transport,
275
+ }
276
+ reasoning.add_reasoning_payload_fields(payload, request)
277
+ if request.warnings:
278
+ payload["warnings"] = list(request.warnings)
279
+ if request.progress:
280
+ payload["progressRequested"] = True
281
+ if request.forbid_commit:
282
+ payload["commitPolicy"] = {"forbidCommit": True}
283
+ if request.include_dirty:
284
+ payload["includeDirty"] = True
285
+ if request.group is not None:
286
+ payload["group"] = request.group
287
+ if request.auth_profile is not None:
288
+ payload["authProfile"] = request.auth_profile
289
+ if request.fallback_auth_profile is not None:
290
+ payload["fallbackProfile"] = request.fallback_auth_profile
291
+ if request.profile_resolution.name is not None:
292
+ payload["profileEnv"] = redaction.redact_env_map(request.profile_resolution.env)
293
+
294
+ # Structured isolation fields from the isolation context.
295
+ if request.isolation_context is not None:
296
+ ctx = request.isolation_context
297
+
298
+ # Keep the existing human-readable `isolation` note for legacy use.
299
+ if ctx.effective_isolation == "worktree" and ctx.isolation_lifecycle == "persistent":
300
+ payload["isolation"] = "worktree persistent"
301
+ elif ctx.effective_isolation == "worktree":
302
+ payload["isolation"] = "worktree temporary"
303
+ elif ctx.effective_isolation == "none":
304
+ payload["isolation"] = "none"
305
+ else:
306
+ payload["isolation"] = "source workspace"
307
+
308
+ payload["isolationMode"] = ctx.isolation_mode
309
+ payload["effectiveIsolation"] = ctx.effective_isolation
310
+ payload["isolationLifecycle"] = ctx.isolation_lifecycle
311
+ payload["preservedWorkspace"] = ctx.preserved_workspace
312
+
313
+ # Only persistent worktree isolation has a planned Delegate-managed
314
+ # branch/path. Temporary safe-mode isolation is created ephemerally at
315
+ # execution time and must not claim a persistent worktree plan.
316
+ if ctx.isolation_lifecycle == "persistent":
317
+ planned_cwd = ctx.planned_execution_cwd or "<planned-worktree-path>"
318
+ planned_branch = ctx.planned_branch or "<planned-branch>"
319
+ payload["plannedExecutionCwd"] = planned_cwd
320
+ payload["plannedBranch"] = planned_branch
321
+ # Rewrite argv to show the planned workspace path, not the source.
322
+ payload["argv"] = _replace_ws_by_engine(request.engine, payload["argv"], planned_cwd)
323
+ else:
324
+ payload["plannedExecutionCwd"] = None
325
+ payload["plannedBranch"] = None
326
+
327
+ # Always emit isolatedWorkspace as explicit boolean (mirrors preservedWorkspace).
328
+ payload["isolatedWorkspace"] = ctx.isolation_lifecycle in ("temporary", "persistent")
329
+ else:
330
+ # Fallback when no isolation context is provided (e.g. direct build_request calls in tests).
331
+ # Use embedded-default logic: safe local harnesses -> worktree temporary, others -> none.
332
+ if request.mode == MODE_CALL:
333
+ payload["isolatedWorkspace"] = False
334
+ payload["isolation"] = "call temporary cwd"
335
+ payload["isolationMode"] = "none"
336
+ payload["effectiveIsolation"] = "none"
337
+ payload["isolationLifecycle"] = "none"
338
+ payload["preservedWorkspace"] = False
339
+ elif request.engine in KNOWN_ENGINES and request.mode == MODE_SAFE:
340
+ payload["isolatedWorkspace"] = True
341
+ payload["isolation"] = (
342
+ "Execution uses a temporary detached git worktree or directory copy; "
343
+ "the child runs outside the original workspace; tracked runs may write .delegate metadata."
344
+ )
345
+ payload["isolationMode"] = "worktree"
346
+ payload["effectiveIsolation"] = "worktree"
347
+ payload["isolationLifecycle"] = "temporary"
348
+ payload["preservedWorkspace"] = False
349
+ else:
350
+ payload["isolatedWorkspace"] = False
351
+ payload["isolation"] = "source workspace"
352
+ payload["isolationMode"] = "none"
353
+ payload["effectiveIsolation"] = "none"
354
+ payload["isolationLifecycle"] = "none"
355
+ payload["preservedWorkspace"] = False
356
+ payload["plannedExecutionCwd"] = None
357
+ payload["plannedBranch"] = None
358
+
359
+ return payload
360
+
361
+
362
+ def _binary_config_key(engine: str | None) -> str | None:
363
+ if engine == "cursor":
364
+ return "cursor.argvPrefix"
365
+ if engine in BINARY_CONFIG_ENGINES:
366
+ return f"{engine}.binary"
367
+ return None
368
+
369
+
370
+ def _binary_config_path(config_source: str | None) -> str:
371
+ if config_source and config_source not in {"embedded-default", "cli-overrides"}:
372
+ return config_source
373
+ return str(config_path())
374
+
375
+
376
+ def _suggest_binary_path(binary: str) -> str | None:
377
+ if not binary or os.path.isabs(binary) or os.sep in binary:
378
+ return None
379
+ for directory in MISSING_BINARY_PROBE_DIRS:
380
+ candidate = Path(directory).expanduser() / binary
381
+ if candidate.is_file() and os.access(candidate, os.X_OK):
382
+ return str(candidate)
383
+ return None
384
+
385
+
386
+ def _missing_binary_error(
387
+ binary: str,
388
+ *,
389
+ engine: str | None = None,
390
+ config_source: str | None = None,
391
+ search_context: str = "delegate process",
392
+ ) -> DelegateError:
393
+ config_key = _binary_config_key(engine)
394
+ config_path_hint = _binary_config_path(config_source)
395
+ suggested = _suggest_binary_path(binary)
396
+
397
+ parts = [
398
+ f"Missing binary: {binary}",
399
+ f"(searched PATH of the {search_context}, not your interactive shell).",
400
+ ]
401
+ if config_key is not None:
402
+ parts.append(f"Fix: set an absolute path in {config_path_hint} under {config_key!r}.")
403
+ else:
404
+ parts.append("Fix: set the configured binary to an absolute path or add it to PATH.")
405
+ if suggested is not None:
406
+ parts.append(f"Found candidate at {suggested}; set the config value to that absolute path.")
407
+
408
+ diagnostics: JsonObject = {
409
+ "binary": binary,
410
+ "configPath": config_path_hint,
411
+ }
412
+ if config_key is not None:
413
+ diagnostics["configKey"] = config_key
414
+ if suggested is not None:
415
+ diagnostics["suggestedBinaryPath"] = suggested
416
+
417
+ next_actions = [
418
+ f"command -v {shlex.quote(binary)}",
419
+ ]
420
+ if config_key is not None:
421
+ next_actions.append(f"set {config_key} to an absolute path in {config_path_hint}")
422
+
423
+ return DelegateError(
424
+ "missing_binary",
425
+ " ".join(parts),
426
+ EXIT_MISSING_BINARY,
427
+ diagnostics=diagnostics,
428
+ next_actions=next_actions,
429
+ )
430
+
431
+
432
+ def ensure_binary(
433
+ argv: list[str],
434
+ *,
435
+ engine: str | None = None,
436
+ config_source: str | None = None,
437
+ env_overrides: dict[str, str] | None = None,
438
+ ) -> None:
439
+ if not argv:
440
+ raise DelegateError("missing_binary", "Empty argv.", EXIT_MISSING_BINARY)
441
+ env = profiles.child_environment(overrides=env_overrides) if env_overrides else os.environ
442
+ if shutil.which(argv[0], path=env.get("PATH")) is None:
443
+ search_context = (
444
+ "child environment" if env_overrides and "PATH" in env_overrides else "delegate process"
445
+ )
446
+ raise _missing_binary_error(
447
+ argv[0],
448
+ engine=engine,
449
+ config_source=config_source,
450
+ search_context=search_context,
451
+ )
452
+
453
+
454
+ def make_run_context(
455
+ registry_root: Path,
456
+ request: Request,
457
+ *,
458
+ run_id: str,
459
+ alias: str,
460
+ source_workspace: ResolvedWorkspace,
461
+ creation_context: JsonObject | None = None,
462
+ ) -> delegate_runner.RunContext:
463
+ source_cwd = (
464
+ request.isolation_context.source_workspace
465
+ if request.isolation_context is not None
466
+ else source_workspace.path
467
+ )
468
+ execution_cwd = request.workspace
469
+ # isolated_workspace must reflect the EFFECTIVE behavior, not the
470
+ # mere presence of an isolation_context object. Only "temporary" or
471
+ # "persistent" lifecycle means a physically separate execution workspace.
472
+ isolated_workspace = (
473
+ request.isolation_context.isolation_lifecycle in ("temporary", "persistent")
474
+ if request.isolation_context is not None
475
+ else False
476
+ )
477
+ # Extract isolation metadata from the isolation context.
478
+ iso_ctx = request.isolation_context
479
+ if iso_ctx is not None:
480
+ isolation_mode = iso_ctx.isolation_mode
481
+ effective_isolation = iso_ctx.effective_isolation
482
+ isolation_lifecycle = iso_ctx.isolation_lifecycle
483
+ preserved_workspace = iso_ctx.preserved_workspace
484
+ branch = iso_ctx.planned_branch
485
+ source_git_root = iso_ctx.source_git_root
486
+ safe_workspace_method = iso_ctx.safe_workspace_method
487
+ warnings = iso_ctx.warnings
488
+ else:
489
+ isolation_mode = "none"
490
+ effective_isolation = "none"
491
+ isolation_lifecycle = "none"
492
+ preserved_workspace = False
493
+ branch = None
494
+ source_git_root = None
495
+ safe_workspace_method = None
496
+ warnings = ()
497
+
498
+ return delegate_runner.RunContext(
499
+ registry_root=registry_root,
500
+ run_id=run_id,
501
+ alias=alias,
502
+ harness=request.engine,
503
+ engine=request.engine,
504
+ mode=request.mode,
505
+ model=request.model,
506
+ source_cwd=source_cwd,
507
+ execution_cwd=execution_cwd,
508
+ workspace_kind=source_workspace.kind,
509
+ isolated_workspace=isolated_workspace,
510
+ started_at=run_registry.utc_now_iso(),
511
+ model_alias=request.model_alias,
512
+ model_resolved=request.model,
513
+ creation_context=creation_context,
514
+ source_git_root=source_git_root,
515
+ isolation_mode=isolation_mode,
516
+ effective_isolation=effective_isolation,
517
+ isolation_lifecycle=isolation_lifecycle,
518
+ preserved_workspace=preserved_workspace,
519
+ branch=branch,
520
+ safe_workspace_method=safe_workspace_method,
521
+ warnings=(*warnings, *request.warnings),
522
+ reasoning_effort=request.reasoning_effort,
523
+ reasoning_effort_source=request.reasoning_effort_source,
524
+ reasoning_capability_source=request.reasoning_capability_source,
525
+ reasoning_transport=request.reasoning_transport,
526
+ prompt_transport=request.prompt_transport,
527
+ forbid_commit=request.forbid_commit,
528
+ progress_initial_delay_sec=request.progress_initial_delay_sec,
529
+ progress_interval_sec=request.progress_interval_sec,
530
+ env_overrides=dict(request.env_overrides or {}),
531
+ fallback_env_overrides=dict(
532
+ profiles.codex_fallback_env_overrides(request.profile_resolution) or {}
533
+ ),
534
+ auth_profile=request.auth_profile,
535
+ fallback_auth_profile=request.fallback_auth_profile,
536
+ include_dirty=request.include_dirty,
537
+ group=request.group,
538
+ )
539
+
540
+
541
+ def execute_request(
542
+ request: Request,
543
+ json_mode: bool,
544
+ *,
545
+ config: JsonObject,
546
+ config_source: str | None = None,
547
+ pass_through: bool,
548
+ completion_report_mode: str,
549
+ source_workspace: ResolvedWorkspace,
550
+ stdout: TextIO,
551
+ stderr: TextIO,
552
+ ) -> tuple[int, JsonObject | None]:
553
+ if request.mode == MODE_CALL:
554
+ try:
555
+ if request.engine == "codex":
556
+ profiles.preflight_codex_request(request, config.get("codex", {}))
557
+ if pass_through:
558
+ raise DelegateError(
559
+ "invalid_option_combination",
560
+ "--pass-through is not supported with call mode.",
561
+ )
562
+ ensure_binary(
563
+ request.argv,
564
+ engine=request.engine,
565
+ config_source=config_source,
566
+ env_overrides=request.env_overrides,
567
+ )
568
+ try:
569
+ result = delegate_runner.execute_call(
570
+ request.argv,
571
+ request.workspace,
572
+ harness=request.engine,
573
+ stdin_text=request.stdin_text,
574
+ prompt_file_text=request.prompt_file_text,
575
+ prompt_file_placeholder=PROMPT_FILE_ARG_PLACEHOLDER,
576
+ env_overrides=request.env_overrides,
577
+ )
578
+ except delegate_runner.RunnerLaunchError as exc:
579
+ raise DelegateError(exc.error, exc.message) from exc
580
+ status = delegate_runner.status_from_exit(result.exit_code)
581
+ if json_mode:
582
+ payload: JsonObject = {
583
+ "ok": result.exit_code == 0,
584
+ "status": status,
585
+ "exitCode": result.exit_code,
586
+ "engine": request.engine,
587
+ "mode": request.mode,
588
+ "model": request.model,
589
+ "text": result.text,
590
+ "textChars": result.text_chars,
591
+ "textTruncated": result.text_truncated,
592
+ "stdoutBytes": result.stdout_bytes,
593
+ "stderrBytes": result.stderr_bytes,
594
+ "durationMs": result.duration_ms,
595
+ }
596
+ if request.warnings:
597
+ payload["warnings"] = list(request.warnings)
598
+ if result.warnings:
599
+ # Dedupe while preserving order so a warning present in both
600
+ # request.warnings and result.warnings is not emitted twice.
601
+ merged: list[str] = []
602
+ seen: set[str] = set()
603
+ for warning in [*payload.get("warnings", []), *result.warnings]:
604
+ if isinstance(warning, str) and warning not in seen:
605
+ seen.add(warning)
606
+ merged.append(warning)
607
+ payload["warnings"] = merged
608
+ reasoning.add_reasoning_payload_fields(payload, request)
609
+ if result.exit_code != 0:
610
+ payload["error"] = "child_failed"
611
+ payload["message"] = "Child command failed."
612
+ payload["stderrTail"] = result.stderr_tail
613
+ return result.exit_code, payload
614
+ for warning in result.warnings:
615
+ print(f"warning: {warning}", file=stderr)
616
+ if result.exit_code != 0 and result.stderr_tail:
617
+ print(result.stderr_tail, file=stderr)
618
+ if result.text:
619
+ print(result.text, file=stdout)
620
+ return result.exit_code, None
621
+ finally:
622
+ if request.cleanup_workspace:
623
+ shutil.rmtree(request.workspace, ignore_errors=True)
624
+ if request.engine == "codex":
625
+ profiles.preflight_codex_request(request, config.get("codex", {}))
626
+ ctx = request.isolation_context
627
+
628
+ # --- Persistent worktree path (work + worktree) ---
629
+ if ctx is not None and ctx.isolation_lifecycle == "persistent":
630
+ try:
631
+ return worktree_execution.execute_persistent_worktree(
632
+ worktree_execution.PersistentWorktreeExecution(
633
+ request=request,
634
+ json_mode=json_mode,
635
+ config=config,
636
+ pass_through=pass_through,
637
+ completion_report_mode=completion_report_mode,
638
+ source_workspace=source_workspace,
639
+ stdout=stdout,
640
+ stderr=stderr,
641
+ binary_validator=lambda argv, engine: ensure_binary(
642
+ argv,
643
+ engine=engine,
644
+ config_source=config_source,
645
+ env_overrides=request.env_overrides,
646
+ ),
647
+ )
648
+ )
649
+ except worktree_execution.PersistentWorktreeError as exc:
650
+ raise DelegateError(exc.error, exc.message) from exc
651
+
652
+ with safe_isolated_request(request) as isolated_request:
653
+ ensure_binary(
654
+ isolated_request.argv,
655
+ engine=isolated_request.engine,
656
+ config_source=config_source,
657
+ env_overrides=isolated_request.env_overrides,
658
+ )
659
+ if pass_through:
660
+ if json_mode:
661
+ raise DelegateError(
662
+ "invalid_option_combination",
663
+ "--pass-through is incompatible with --json.",
664
+ )
665
+ try:
666
+ exit_code = delegate_runner.execute_passthrough(
667
+ isolated_request.argv,
668
+ isolated_request.workspace,
669
+ stdin_text=isolated_request.stdin_text,
670
+ prompt_file_text=isolated_request.prompt_file_text,
671
+ prompt_file_placeholder=PROMPT_FILE_ARG_PLACEHOLDER,
672
+ env_overrides=isolated_request.env_overrides,
673
+ )
674
+ except delegate_runner.RunnerLaunchError as exc:
675
+ raise DelegateError(exc.error, exc.message) from exc
676
+ return exit_code, None
677
+ registry_root = run_registry.ensure_registry(
678
+ Path(source_workspace.path),
679
+ workspace_kind=source_workspace.kind,
680
+ )
681
+ maybe_run_retention_pass(registry_root, config)
682
+ run_id, alias = run_registry.register_run(
683
+ registry_root,
684
+ harness=isolated_request.engine,
685
+ metadata={
686
+ "mode": isolated_request.mode,
687
+ "model": isolated_request.model,
688
+ "modelAlias": isolated_request.model_alias,
689
+ "modelResolved": isolated_request.model,
690
+ "group": isolated_request.group,
691
+ "cwd": (
692
+ isolated_request.isolation_context.source_workspace
693
+ if isolated_request.isolation_context is not None
694
+ else source_workspace.path
695
+ ),
696
+ },
697
+ )
698
+ ctx_runner = make_run_context(
699
+ registry_root,
700
+ isolated_request,
701
+ run_id=run_id,
702
+ alias=alias,
703
+ source_workspace=source_workspace,
704
+ )
705
+ try:
706
+ return delegate_runner.execute_tracked(
707
+ isolated_request.argv,
708
+ isolated_request.workspace,
709
+ ctx_runner,
710
+ json_mode=json_mode,
711
+ stdout=stdout,
712
+ stderr=stderr,
713
+ completion_report_mode=completion_report_mode,
714
+ stdin_text=isolated_request.stdin_text,
715
+ prompt_file_text=isolated_request.prompt_file_text,
716
+ prompt_file_placeholder=PROMPT_FILE_ARG_PLACEHOLDER,
717
+ manifest_argv=public_argv(isolated_request),
718
+ progress=isolated_request.progress,
719
+ progress_initial_delay_sec=isolated_request.progress_initial_delay_sec,
720
+ progress_interval_sec=isolated_request.progress_interval_sec,
721
+ )
722
+ except delegate_runner.RunnerLaunchError as exc:
723
+ raise DelegateError(exc.error, exc.message) from exc
724
+
725
+
726
+ def shell_join(argv: list[str]) -> str:
727
+ return " ".join(shlex.quote(arg) for arg in argv)
728
+
729
+
730
+ def pre_read_run_json_for_config(
731
+ input_json_path: str,
732
+ cli_cwd: str | None,
733
+ cli_isolation: str | None = None,
734
+ ) -> tuple[ResolvedWorkspace, JsonObject, str]:
735
+ """Pre-read run input JSON for config discovery: extract cwd/isolation, resolve workspace,
736
+ load config from that workspace, validate config. Returns (workspace, config, source)."""
737
+ if wsl.should_reject_windows_path(input_json_path):
738
+ raise DelegateError(
739
+ "windows_path", wsl.windows_path_message("--input-json", input_json_path)
740
+ )
741
+ path = Path(input_json_path).expanduser()
742
+ raw = _load_input_json_object(path)
743
+ if raw.get("mode") == MODE_CALL:
744
+ if cli_cwd is not None:
745
+ raise DelegateError("invalid_option_combination", "call mode does not use --cwd.")
746
+ if cli_isolation is not None:
747
+ raise DelegateError(
748
+ "invalid_option_combination",
749
+ "call mode does not use --isolation.",
750
+ )
751
+ if raw.get("cwd") is not None:
752
+ raise DelegateError(
753
+ "invalid_option_combination", "call mode input JSON must not include cwd."
754
+ )
755
+ if "isolation" in raw:
756
+ raise DelegateError(
757
+ "invalid_option_combination",
758
+ "call mode input JSON must not include isolation.",
759
+ )
760
+ config, source = load_config(workspace=None)
761
+ validate_config(config)
762
+ return ResolvedWorkspace(CALL_TEMP_CWD_PLACEHOLDER, "directory"), config, source
763
+
764
+ # Read ONLY cwd and isolation for config discovery.
765
+ json_cwd = raw.get("cwd")
766
+ if json_cwd is not None and not isinstance(json_cwd, str):
767
+ raise DelegateError("invalid_cwd", "cwd must be a string.")
768
+
769
+ # Reject explicit null isolation in the JSON pre-read.
770
+ if "isolation" in raw and raw["isolation"] is None:
771
+ raise DelegateError(
772
+ "invalid_isolation",
773
+ "isolation in input JSON must be auto, none, or worktree (null is not allowed).",
774
+ )
775
+
776
+ workspace = resolve_workspace(cli_cwd, json_cwd)
777
+ config, source = load_config(workspace=Path(workspace.path))
778
+ validate_config(config)
779
+ return workspace, config, source
780
+
781
+
782
+ def emit_error(error: DelegateError, json_mode: bool, stdout: TextIO, stderr: TextIO) -> int:
783
+ if json_mode:
784
+ payload: JsonObject = {
785
+ "ok": False,
786
+ "error": error.error,
787
+ "message": error.message,
788
+ "exitCode": error.exit_code,
789
+ }
790
+ if error.diagnostics is not None:
791
+ payload["diagnostics"] = error.diagnostics
792
+ for key, value in error.diagnostics.items():
793
+ if key not in payload:
794
+ payload[key] = value
795
+ if error.next_actions:
796
+ payload["nextActions"] = error.next_actions
797
+ delegate_rendering.print_json(
798
+ payload,
799
+ stdout,
800
+ )
801
+ else:
802
+ print(f"{error.error}: {error.message}", file=stderr)
803
+ return error.exit_code
804
+
805
+
806
+ def main(
807
+ argv: list[str] | None = None,
808
+ stdin: TextIO | None = None,
809
+ stdout: TextIO | None = None,
810
+ stderr: TextIO | None = None,
811
+ ) -> int:
812
+ argv = list(sys.argv[1:] if argv is None else argv)
813
+ stdin = sys.stdin if stdin is None else stdin
814
+ stdout = sys.stdout if stdout is None else stdout
815
+ stderr = sys.stderr if stderr is None else stderr
816
+ json_mode = infer_global_json(argv)
817
+ try:
818
+ parsed = parse_cli(argv)
819
+ global_options = parsed.global_options
820
+ json_mode = global_options.json_mode
821
+ profile_guard.enforce_profile_guard(parsed, stderr=stderr)
822
+ if parsed.subcommand == "help":
823
+ return emit_command_help(parsed.help_topic, global_options.json_mode, stdout)
824
+ if parsed.subcommand == "version":
825
+ print(VERSION, file=stdout)
826
+ return EXIT_OK
827
+ if parsed.subcommand == "config":
828
+ return emit_config_command(parsed, stdout)
829
+
830
+ # For run --input-json, pre-read the JSON to discover config from the
831
+ # JSON-resolved workspace before loading/finalizing config.
832
+ if parsed.subcommand == "run":
833
+ run_json = parsed.run_json
834
+ if run_json is None:
835
+ raise DelegateError("invalid_command", "run --input-json options are required.")
836
+ workspace, config, source = pre_read_run_json_for_config(
837
+ run_json.input_json,
838
+ global_options.cwd,
839
+ global_options.isolation,
840
+ )
841
+ config_workspace = Path(workspace.path)
842
+ else:
843
+ if parsed.launch is not None and parsed.launch.mode == MODE_CALL:
844
+ if global_options.cwd is not None:
845
+ raise DelegateError(
846
+ "invalid_option_combination",
847
+ "call mode does not use --cwd.",
848
+ )
849
+ config_workspace = None
850
+ else:
851
+ config_workspace = workspace_path_for_config(global_options.cwd)
852
+ config, source = load_config(workspace=config_workspace)
853
+ validate_config(config)
854
+
855
+ if parsed.subcommand == "models":
856
+ inspection = parsed.inspection or InspectionOptions()
857
+ return emit_models(
858
+ config,
859
+ source,
860
+ global_options.json_mode,
861
+ stdout,
862
+ workspace=config_workspace,
863
+ summary=inspection.summary,
864
+ )
865
+ if parsed.subcommand == "describe":
866
+ inspection = parsed.inspection or InspectionOptions()
867
+ return emit_describe(
868
+ config,
869
+ source,
870
+ global_options.json_mode,
871
+ stdout,
872
+ workspace=config_workspace,
873
+ summary=inspection.summary,
874
+ )
875
+ if parsed.subcommand == "agent-help":
876
+ return emit_agent_help(stdout)
877
+
878
+ if parsed.subcommand != "run":
879
+ if parsed.launch is not None and parsed.launch.mode == MODE_CALL:
880
+ workspace = ResolvedWorkspace(CALL_TEMP_CWD_PLACEHOLDER, "directory")
881
+ else:
882
+ workspace = resolve_workspace(global_options.cwd)
883
+
884
+ if parsed.subcommand == "capabilities":
885
+ command = parsed.capabilities
886
+ if command is None:
887
+ raise DelegateError("invalid_command", "capabilities options are required.")
888
+ return capability_commands.emit(
889
+ command,
890
+ config=config,
891
+ config_source=source,
892
+ workspace=workspace.path,
893
+ auth_profile_override=global_options.auth_profile,
894
+ stdout=stdout,
895
+ )
896
+
897
+ if parsed.subcommand in {"snapshot", "runs", "run-output", "wait", "cancel", "worktree"}:
898
+ existing_registry = run_registry.registry_root_if_exists(Path(workspace.path))
899
+ if existing_registry is not None:
900
+ maybe_run_retention_pass(existing_registry, config)
901
+ if parsed.subcommand == "snapshot":
902
+ return emit_snapshot(parsed, workspace, stdout)
903
+ if parsed.subcommand == "runs":
904
+ return emit_runs(parsed, workspace, stdout)
905
+ if parsed.subcommand == "run-output":
906
+ return emit_run_output(parsed, workspace, stdout)
907
+ if parsed.subcommand == "wait":
908
+ return emit_wait(parsed, workspace, stdout)
909
+ if parsed.subcommand == "cancel":
910
+ return emit_cancel(parsed, workspace, stdout)
911
+ if parsed.subcommand == "worktree":
912
+ return emit_worktree(parsed, workspace, config, stdout)
913
+
914
+ if parsed.subcommand == "profiles":
915
+ return emit_profiles_command(parsed, config, source, stdout)
916
+
917
+ request = request_from_parsed(parsed, config, stdin)
918
+ if request.dry_run:
919
+ payload = dry_run_payload(request)
920
+ if global_options.json_mode:
921
+ delegate_rendering.print_json(payload, stdout)
922
+ else:
923
+ print(f"cwd: {request.workspace} ({request.workspace_kind})", file=stdout)
924
+ if payload.get("isolatedWorkspace"):
925
+ print(f"isolation: {payload['isolation']}", file=stdout)
926
+ lifecycle = payload.get("isolationLifecycle", "")
927
+ if lifecycle:
928
+ print(f"isolationLifecycle: {lifecycle}", file=stdout)
929
+ if payload.get("plannedBranch"):
930
+ print(f"plannedBranch: {payload['plannedBranch']}", file=stdout)
931
+ if payload.get("plannedExecutionCwd"):
932
+ print(f"plannedExecutionCwd: {payload['plannedExecutionCwd']}", file=stdout)
933
+ # Use the payload's rewritten argv (which shows planned paths) when
934
+ # worktree isolation is active; otherwise use the source request.argv.
935
+ display_argv = payload.get("argv", request.argv)
936
+ print(f"argv: {shell_join(display_argv)}", file=stdout)
937
+ for warning in payload.get("warnings", ()):
938
+ print(f"warning: {warning}", file=stdout)
939
+ return EXIT_OK
940
+
941
+ completion_report_mode = resolve_completion_report_mode(parsed, config)
942
+ exit_code, payload = execute_request(
943
+ request,
944
+ global_options.json_mode,
945
+ config=config,
946
+ config_source=source,
947
+ pass_through=global_options.pass_through,
948
+ completion_report_mode=completion_report_mode,
949
+ source_workspace=workspace,
950
+ stdout=stdout,
951
+ stderr=stderr,
952
+ )
953
+ if global_options.json_mode and payload is not None:
954
+ delegate_rendering.print_json(payload, stdout)
955
+ return exit_code
956
+ except worktree_mgmt.WorktreeManagementError as exc:
957
+ if json_mode:
958
+ delegate_rendering.print_json(exc.payload, stdout)
959
+ else:
960
+ print(f"{exc.code}: {exc.message}", file=stderr)
961
+ exit_code = exc.payload.get("exitCode")
962
+ return exit_code if isinstance(exit_code, int) else EXIT_USAGE
963
+ except run_registry.RegistryJsonError as exc:
964
+ return emit_error(
965
+ DelegateError("invalid_run_registry", str(exc)),
966
+ json_mode,
967
+ stdout,
968
+ stderr,
969
+ )
970
+ except command_errors.CommandError as exc:
971
+ return emit_error(
972
+ DelegateError(
973
+ exc.error,
974
+ exc.message,
975
+ diagnostics=getattr(exc, "diagnostics", None),
976
+ next_actions=getattr(exc, "next_actions", None),
977
+ ),
978
+ json_mode,
979
+ stdout,
980
+ stderr,
981
+ )
982
+ except DelegateError as exc:
983
+ return emit_error(exc, json_mode, stdout, stderr)
984
+
985
+
986
+ if __name__ == "__main__":
987
+ raise SystemExit(main())