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.
- delegate_agent/__init__.py +5 -0
- delegate_agent/archived_logs.py +44 -0
- delegate_agent/argv_builders.py +423 -0
- delegate_agent/argv_utils.py +36 -0
- delegate_agent/capability_commands.py +99 -0
- delegate_agent/cli.py +987 -0
- delegate_agent/cli_parser.py +1627 -0
- delegate_agent/command_errors.py +29 -0
- delegate_agent/command_help.py +1264 -0
- delegate_agent/config.py +1117 -0
- delegate_agent/config_commands.py +143 -0
- delegate_agent/constants.py +50 -0
- delegate_agent/describe_payload.py +1018 -0
- delegate_agent/errors.py +32 -0
- delegate_agent/git_utils.py +180 -0
- delegate_agent/harness_events.py +719 -0
- delegate_agent/inspection_commands.py +104 -0
- delegate_agent/isolation.py +316 -0
- delegate_agent/json_types.py +18 -0
- delegate_agent/log_output.py +78 -0
- delegate_agent/private_io.py +109 -0
- delegate_agent/profile_commands.py +42 -0
- delegate_agent/profile_guard.py +116 -0
- delegate_agent/profiles.py +389 -0
- delegate_agent/prompt_instructions.py +39 -0
- delegate_agent/prompt_transport.py +12 -0
- delegate_agent/reasoning.py +916 -0
- delegate_agent/redaction.py +193 -0
- delegate_agent/rendering.py +573 -0
- delegate_agent/request_build.py +1681 -0
- delegate_agent/request_models.py +191 -0
- delegate_agent/retention.py +321 -0
- delegate_agent/run_metadata.py +78 -0
- delegate_agent/run_output_commands.py +645 -0
- delegate_agent/run_registry.py +747 -0
- delegate_agent/run_status.py +300 -0
- delegate_agent/runner.py +1830 -0
- delegate_agent/safe_workspace.py +821 -0
- delegate_agent/snapshot_view.py +229 -0
- delegate_agent/wait_cancel_commands.py +559 -0
- delegate_agent/worktree_commands.py +218 -0
- delegate_agent/worktree_execution.py +654 -0
- delegate_agent/worktree_gc.py +529 -0
- delegate_agent/worktree_mgmt.py +782 -0
- delegate_agent/worktree_records.py +232 -0
- delegate_agent/worktree_remove.py +547 -0
- delegate_agent/worktree_summary.py +242 -0
- delegate_agent/wsl.py +65 -0
- delegate_agent_cli-0.11.0.dist-info/METADATA +325 -0
- delegate_agent_cli-0.11.0.dist-info/RECORD +54 -0
- delegate_agent_cli-0.11.0.dist-info/WHEEL +5 -0
- delegate_agent_cli-0.11.0.dist-info/entry_points.txt +2 -0
- delegate_agent_cli-0.11.0.dist-info/licenses/LICENSE +21 -0
- 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())
|