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
|
@@ -0,0 +1,1627 @@
|
|
|
1
|
+
"""Command-line argument parsing.
|
|
2
|
+
|
|
3
|
+
Turns ``argv`` into a typed ``ParsedCommand`` (which subcommand, global options,
|
|
4
|
+
launch options, and per-subcommand argument objects). This layer only parses and
|
|
5
|
+
validates argument shape; workspace/prompt resolution and request construction
|
|
6
|
+
live in ``request_build``. The SAFE-mode security strictness lives in the argv
|
|
7
|
+
builders, not here.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import difflib
|
|
13
|
+
import re
|
|
14
|
+
import shlex
|
|
15
|
+
from typing import NoReturn
|
|
16
|
+
|
|
17
|
+
from delegate_agent import (
|
|
18
|
+
capability_commands,
|
|
19
|
+
command_help,
|
|
20
|
+
config_commands,
|
|
21
|
+
inspection_commands,
|
|
22
|
+
profile_commands,
|
|
23
|
+
reasoning,
|
|
24
|
+
run_output_commands,
|
|
25
|
+
wait_cancel_commands,
|
|
26
|
+
worktree_commands,
|
|
27
|
+
worktree_mgmt,
|
|
28
|
+
)
|
|
29
|
+
from delegate_agent import config as delegate_config
|
|
30
|
+
from delegate_agent.constants import (
|
|
31
|
+
ENGINES_PROSE,
|
|
32
|
+
KNOWN_ENGINES,
|
|
33
|
+
MODELESS_ENGINES,
|
|
34
|
+
validate_mode,
|
|
35
|
+
)
|
|
36
|
+
from delegate_agent.errors import DelegateError
|
|
37
|
+
from delegate_agent.request_models import (
|
|
38
|
+
GlobalOptions,
|
|
39
|
+
InspectionOptions,
|
|
40
|
+
LaunchOptions,
|
|
41
|
+
ParsedCommand,
|
|
42
|
+
RunJsonOptions,
|
|
43
|
+
)
|
|
44
|
+
from delegate_agent.run_output_commands import RUN_OUTPUT_DEFAULT_TAIL_LINES
|
|
45
|
+
|
|
46
|
+
MISPLACED_GLOBAL_OPTIONS = frozenset(
|
|
47
|
+
{
|
|
48
|
+
"--json",
|
|
49
|
+
"--cwd",
|
|
50
|
+
"--isolation",
|
|
51
|
+
"--pass-through",
|
|
52
|
+
"--completion-report",
|
|
53
|
+
"--no-completion-report",
|
|
54
|
+
"--auth-profile",
|
|
55
|
+
"--group",
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
VALUE_GLOBAL_OPTIONS = frozenset(
|
|
60
|
+
{"--cwd", "--isolation", "--completion-report", "--auth-profile", "--group"}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
AUTH_PROFILE_SUBCOMMANDS = frozenset(KNOWN_ENGINES) | frozenset(
|
|
64
|
+
{"dry-run", "run", "profiles", "capabilities"}
|
|
65
|
+
)
|
|
66
|
+
GROUP_RE = re.compile(r"^[A-Za-z0-9._-]{1,64}$")
|
|
67
|
+
GROUP_SUBCOMMANDS = frozenset(KNOWN_ENGINES) | frozenset({"dry-run", "run"})
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def validate_group(value: str, *, option: str = "--group") -> str:
|
|
71
|
+
if not GROUP_RE.fullmatch(value):
|
|
72
|
+
raise DelegateError(
|
|
73
|
+
"invalid_group",
|
|
74
|
+
f"{option} must match [A-Za-z0-9._-]{{1,64}}.",
|
|
75
|
+
)
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def infer_global_json(argv: list[str]) -> bool:
|
|
80
|
+
i = 0
|
|
81
|
+
while i < len(argv):
|
|
82
|
+
token = argv[i]
|
|
83
|
+
if token == "--json":
|
|
84
|
+
return True
|
|
85
|
+
if token in VALUE_GLOBAL_OPTIONS:
|
|
86
|
+
i += 2
|
|
87
|
+
continue
|
|
88
|
+
if token in MISPLACED_GLOBAL_OPTIONS:
|
|
89
|
+
i += 1
|
|
90
|
+
continue
|
|
91
|
+
if token in ("--help", "-h", "--version"):
|
|
92
|
+
return False
|
|
93
|
+
break
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def canonical_help_topic(path: str | None) -> str | None:
|
|
98
|
+
"""Resolve a detected command path to its nearest valid COMMAND_SPECS key.
|
|
99
|
+
|
|
100
|
+
Returns the path unchanged when it is empty/None (overview) or an exact spec
|
|
101
|
+
key. Otherwise it falls back to the nearest valid prefix (e.g. an unknown
|
|
102
|
+
worktree action maps to ``worktree``); a leftover with no spec is returned
|
|
103
|
+
as-is so emit_command_help can surface unknown_help_topic.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
if not path:
|
|
107
|
+
return path
|
|
108
|
+
if path in command_help.COMMAND_SPECS:
|
|
109
|
+
return path
|
|
110
|
+
parts = path.split()
|
|
111
|
+
while len(parts) > 1:
|
|
112
|
+
parts.pop()
|
|
113
|
+
candidate = " ".join(parts)
|
|
114
|
+
if candidate in command_help.COMMAND_SPECS:
|
|
115
|
+
return candidate
|
|
116
|
+
return path
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def help_command(json_mode: bool, topic: str | None) -> ParsedCommand:
|
|
120
|
+
"""Build a help ParsedCommand, mapping topic to a canonical spec key."""
|
|
121
|
+
|
|
122
|
+
return ParsedCommand(
|
|
123
|
+
"help",
|
|
124
|
+
global_options=GlobalOptions(json_mode=json_mode),
|
|
125
|
+
help_topic=canonical_help_topic(topic),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def parse_help_subcommand(rest: list[str], json_mode: bool) -> ParsedCommand:
|
|
130
|
+
"""Parse ``help [<command> [<subcommand>]]`` (D3).
|
|
131
|
+
|
|
132
|
+
``--json`` and help tokens are honored from anywhere in ``rest``; remaining
|
|
133
|
+
tokens name the topic. An empty topic yields the overview, unless a help
|
|
134
|
+
token was present, in which case ``delegate help --help`` describes the help
|
|
135
|
+
command itself.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
help_requested = False
|
|
139
|
+
topic_parts: list[str] = []
|
|
140
|
+
for token in rest:
|
|
141
|
+
if token == "--json":
|
|
142
|
+
json_mode = True
|
|
143
|
+
continue
|
|
144
|
+
if command_help.is_help_token(token):
|
|
145
|
+
help_requested = True
|
|
146
|
+
continue
|
|
147
|
+
topic_parts.append(token)
|
|
148
|
+
|
|
149
|
+
if topic_parts:
|
|
150
|
+
topic = " ".join(topic_parts)
|
|
151
|
+
elif help_requested:
|
|
152
|
+
topic = "help"
|
|
153
|
+
else:
|
|
154
|
+
topic = None
|
|
155
|
+
return help_command(json_mode, topic)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
SIMPLE_INSPECTION_SUBCOMMANDS = frozenset({"models", "describe", "agent-help"})
|
|
159
|
+
INSPECTION_OPTION_SUBCOMMANDS = frozenset({"models", "describe"})
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def parse_simple_inspection_subcommand(
|
|
163
|
+
name: str,
|
|
164
|
+
rest: list[str],
|
|
165
|
+
*,
|
|
166
|
+
json_mode: bool,
|
|
167
|
+
cwd: str | None,
|
|
168
|
+
pass_through: bool,
|
|
169
|
+
completion_report: str | None,
|
|
170
|
+
isolation: str | None,
|
|
171
|
+
auth_profile: str | None,
|
|
172
|
+
) -> ParsedCommand:
|
|
173
|
+
rest, json_mode = consume_json_option(rest, json_mode)
|
|
174
|
+
if any(command_help.is_help_token(token) for token in rest):
|
|
175
|
+
return help_command(json_mode, name)
|
|
176
|
+
summary = False
|
|
177
|
+
if name in INSPECTION_OPTION_SUBCOMMANDS:
|
|
178
|
+
for token in rest:
|
|
179
|
+
if token == "--summary":
|
|
180
|
+
summary = True
|
|
181
|
+
continue
|
|
182
|
+
require_no_extra([token], name)
|
|
183
|
+
else:
|
|
184
|
+
require_no_extra(rest, name)
|
|
185
|
+
return ParsedCommand(
|
|
186
|
+
name,
|
|
187
|
+
global_options=GlobalOptions(
|
|
188
|
+
json_mode=json_mode,
|
|
189
|
+
cwd=cwd,
|
|
190
|
+
pass_through=pass_through,
|
|
191
|
+
completion_report=completion_report,
|
|
192
|
+
isolation=isolation,
|
|
193
|
+
auth_profile=auth_profile,
|
|
194
|
+
),
|
|
195
|
+
inspection=InspectionOptions(summary=summary),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def parse_capabilities_subcommand(
|
|
200
|
+
rest: list[str],
|
|
201
|
+
*,
|
|
202
|
+
json_mode: bool,
|
|
203
|
+
cwd: str | None,
|
|
204
|
+
pass_through: bool,
|
|
205
|
+
completion_report: str | None,
|
|
206
|
+
isolation: str | None,
|
|
207
|
+
auth_profile: str | None,
|
|
208
|
+
) -> ParsedCommand:
|
|
209
|
+
rest, json_mode = consume_json_option(rest, json_mode)
|
|
210
|
+
if any(command_help.is_help_token(token) for token in rest):
|
|
211
|
+
return help_command(json_mode, "capabilities")
|
|
212
|
+
refresh = False
|
|
213
|
+
if rest == ["refresh"]:
|
|
214
|
+
refresh = True
|
|
215
|
+
elif rest:
|
|
216
|
+
require_no_extra(rest, "capabilities")
|
|
217
|
+
if auth_profile is not None and not refresh:
|
|
218
|
+
raise DelegateError(
|
|
219
|
+
"invalid_option_combination",
|
|
220
|
+
"--auth-profile is only supported with capabilities refresh; "
|
|
221
|
+
"the cached capabilities report does not spawn a harness.",
|
|
222
|
+
)
|
|
223
|
+
return ParsedCommand(
|
|
224
|
+
"capabilities",
|
|
225
|
+
global_options=GlobalOptions(
|
|
226
|
+
json_mode=json_mode,
|
|
227
|
+
cwd=cwd,
|
|
228
|
+
pass_through=pass_through,
|
|
229
|
+
completion_report=completion_report,
|
|
230
|
+
isolation=isolation,
|
|
231
|
+
auth_profile=auth_profile,
|
|
232
|
+
),
|
|
233
|
+
capabilities=capability_commands.CapabilitiesCommand(
|
|
234
|
+
refresh=refresh,
|
|
235
|
+
json_mode=json_mode,
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def parse_config_subcommand(
|
|
241
|
+
rest: list[str],
|
|
242
|
+
*,
|
|
243
|
+
json_mode: bool,
|
|
244
|
+
cwd: str | None,
|
|
245
|
+
pass_through: bool,
|
|
246
|
+
completion_report: str | None,
|
|
247
|
+
isolation: str | None,
|
|
248
|
+
auth_profile: str | None,
|
|
249
|
+
) -> ParsedCommand:
|
|
250
|
+
rest, json_mode = consume_json_option(rest, json_mode)
|
|
251
|
+
if not rest or command_help.is_help_token(rest[0]):
|
|
252
|
+
return help_command(json_mode, "config")
|
|
253
|
+
if cwd is not None or pass_through or completion_report is not None or isolation is not None:
|
|
254
|
+
raise DelegateError(
|
|
255
|
+
"invalid_option_combination",
|
|
256
|
+
"delegate config does not use --cwd, --isolation, --pass-through, or completion-report options.",
|
|
257
|
+
)
|
|
258
|
+
if auth_profile is not None:
|
|
259
|
+
raise DelegateError(
|
|
260
|
+
"invalid_option_combination",
|
|
261
|
+
"--auth-profile is not supported with delegate config.",
|
|
262
|
+
)
|
|
263
|
+
action = rest[0]
|
|
264
|
+
if action not in {"init", "sync-profiles"}:
|
|
265
|
+
raise DelegateError("unknown_config_action", f"Unknown config action: {action}")
|
|
266
|
+
force = False
|
|
267
|
+
if action == "sync-profiles":
|
|
268
|
+
for token in rest[1:]:
|
|
269
|
+
if command_help.is_help_token(token):
|
|
270
|
+
return help_command(json_mode, "config sync-profiles")
|
|
271
|
+
require_no_extra([token], "config sync-profiles")
|
|
272
|
+
return ParsedCommand(
|
|
273
|
+
"config",
|
|
274
|
+
global_options=GlobalOptions(json_mode=json_mode),
|
|
275
|
+
config_command=config_commands.ConfigCommand(
|
|
276
|
+
action="sync-profiles",
|
|
277
|
+
json_mode=json_mode,
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
for token in rest[1:]:
|
|
281
|
+
if command_help.is_help_token(token):
|
|
282
|
+
return help_command(json_mode, "config init")
|
|
283
|
+
if token == "--force":
|
|
284
|
+
force = True
|
|
285
|
+
continue
|
|
286
|
+
require_no_extra([token], "config init")
|
|
287
|
+
return ParsedCommand(
|
|
288
|
+
"config",
|
|
289
|
+
global_options=GlobalOptions(json_mode=json_mode),
|
|
290
|
+
config_command=config_commands.ConfigCommand(
|
|
291
|
+
action="init",
|
|
292
|
+
force=force,
|
|
293
|
+
json_mode=json_mode,
|
|
294
|
+
),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def parse_cli(argv: list[str]) -> ParsedCommand:
|
|
299
|
+
if not argv or argv[0] in ("--help", "-h"):
|
|
300
|
+
return ParsedCommand("help")
|
|
301
|
+
if argv[0] == "--version":
|
|
302
|
+
return ParsedCommand("version")
|
|
303
|
+
|
|
304
|
+
json_mode = False
|
|
305
|
+
cwd: str | None = None
|
|
306
|
+
pass_through = False
|
|
307
|
+
completion_report: str | None = None
|
|
308
|
+
isolation: str | None = None
|
|
309
|
+
auth_profile: str | None = None
|
|
310
|
+
group: str | None = None
|
|
311
|
+
i = 0
|
|
312
|
+
while i < len(argv):
|
|
313
|
+
token = argv[i]
|
|
314
|
+
if token == "--json":
|
|
315
|
+
json_mode = True
|
|
316
|
+
i += 1
|
|
317
|
+
continue
|
|
318
|
+
if token == "--cwd":
|
|
319
|
+
if i + 1 >= len(argv):
|
|
320
|
+
raise DelegateError("missing_cwd", "--cwd requires a path.")
|
|
321
|
+
cwd = argv[i + 1]
|
|
322
|
+
i += 2
|
|
323
|
+
continue
|
|
324
|
+
if token == "--pass-through":
|
|
325
|
+
pass_through = True
|
|
326
|
+
i += 1
|
|
327
|
+
continue
|
|
328
|
+
if token == "--no-completion-report":
|
|
329
|
+
completion_report = delegate_config.COMPLETION_REPORT_MODE_NONE
|
|
330
|
+
i += 1
|
|
331
|
+
continue
|
|
332
|
+
if token == "--completion-report":
|
|
333
|
+
if i + 1 >= len(argv):
|
|
334
|
+
raise DelegateError(
|
|
335
|
+
"missing_completion_report", "--completion-report requires markdown or none."
|
|
336
|
+
)
|
|
337
|
+
completion_report = argv[i + 1]
|
|
338
|
+
if completion_report not in delegate_config.COMPLETION_REPORT_MODES:
|
|
339
|
+
raise DelegateError(
|
|
340
|
+
"invalid_completion_report",
|
|
341
|
+
"--completion-report must be markdown or none.",
|
|
342
|
+
)
|
|
343
|
+
i += 2
|
|
344
|
+
continue
|
|
345
|
+
if token == "--isolation":
|
|
346
|
+
if i + 1 >= len(argv):
|
|
347
|
+
raise DelegateError("missing_isolation_value", "--isolation requires a value.")
|
|
348
|
+
isolation = argv[i + 1]
|
|
349
|
+
if isolation not in delegate_config.VALID_ISOLATION_VALUES:
|
|
350
|
+
raise DelegateError(
|
|
351
|
+
"invalid_isolation",
|
|
352
|
+
"--isolation must be auto, none, or worktree.",
|
|
353
|
+
)
|
|
354
|
+
i += 2
|
|
355
|
+
continue
|
|
356
|
+
if token == "--auth-profile":
|
|
357
|
+
if i + 1 >= len(argv):
|
|
358
|
+
raise DelegateError(
|
|
359
|
+
"missing_auth_profile", "--auth-profile requires a profile name."
|
|
360
|
+
)
|
|
361
|
+
auth_profile = argv[i + 1]
|
|
362
|
+
if not auth_profile or auth_profile.startswith("-"):
|
|
363
|
+
raise DelegateError(
|
|
364
|
+
"missing_auth_profile", "--auth-profile requires a profile name."
|
|
365
|
+
)
|
|
366
|
+
i += 2
|
|
367
|
+
continue
|
|
368
|
+
if token == "--group":
|
|
369
|
+
if i + 1 >= len(argv):
|
|
370
|
+
raise DelegateError("missing_group", "--group requires a group name.")
|
|
371
|
+
group = validate_group(argv[i + 1])
|
|
372
|
+
i += 2
|
|
373
|
+
continue
|
|
374
|
+
break
|
|
375
|
+
|
|
376
|
+
if json_mode and pass_through:
|
|
377
|
+
raise DelegateError(
|
|
378
|
+
"invalid_option_combination",
|
|
379
|
+
"--pass-through is incompatible with --json.",
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
if i >= len(argv):
|
|
383
|
+
raise DelegateError("missing_subcommand", "Missing subcommand.")
|
|
384
|
+
|
|
385
|
+
subcommand = argv[i]
|
|
386
|
+
if subcommand == "list":
|
|
387
|
+
subcommand = "runs"
|
|
388
|
+
rest = argv[i + 1 :]
|
|
389
|
+
if subcommand.startswith("-"):
|
|
390
|
+
raise DelegateError(
|
|
391
|
+
"unknown_option", f"Unknown global option before subcommand: {subcommand}"
|
|
392
|
+
)
|
|
393
|
+
if auth_profile is not None and subcommand not in AUTH_PROFILE_SUBCOMMANDS:
|
|
394
|
+
raise DelegateError(
|
|
395
|
+
"invalid_option_combination",
|
|
396
|
+
f"--auth-profile is not supported with delegate {subcommand}; "
|
|
397
|
+
"use it with launches, dry-run, run --input-json, profiles, or capabilities refresh.",
|
|
398
|
+
)
|
|
399
|
+
if group is not None and subcommand not in GROUP_SUBCOMMANDS:
|
|
400
|
+
raise DelegateError(
|
|
401
|
+
"invalid_option_combination",
|
|
402
|
+
f"--group is not supported with delegate {subcommand}; use command-specific "
|
|
403
|
+
"--group selectors where available.",
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if subcommand == "help":
|
|
407
|
+
return parse_help_subcommand(rest, json_mode)
|
|
408
|
+
|
|
409
|
+
if subcommand in SIMPLE_INSPECTION_SUBCOMMANDS:
|
|
410
|
+
return parse_simple_inspection_subcommand(
|
|
411
|
+
subcommand,
|
|
412
|
+
rest,
|
|
413
|
+
json_mode=json_mode,
|
|
414
|
+
cwd=cwd,
|
|
415
|
+
pass_through=pass_through,
|
|
416
|
+
completion_report=completion_report,
|
|
417
|
+
isolation=isolation,
|
|
418
|
+
auth_profile=auth_profile,
|
|
419
|
+
)
|
|
420
|
+
if subcommand == "capabilities":
|
|
421
|
+
return parse_capabilities_subcommand(
|
|
422
|
+
rest,
|
|
423
|
+
json_mode=json_mode,
|
|
424
|
+
cwd=cwd,
|
|
425
|
+
pass_through=pass_through,
|
|
426
|
+
completion_report=completion_report,
|
|
427
|
+
isolation=isolation,
|
|
428
|
+
auth_profile=auth_profile,
|
|
429
|
+
)
|
|
430
|
+
if subcommand == "config":
|
|
431
|
+
return parse_config_subcommand(
|
|
432
|
+
rest,
|
|
433
|
+
json_mode=json_mode,
|
|
434
|
+
cwd=cwd,
|
|
435
|
+
pass_through=pass_through,
|
|
436
|
+
completion_report=completion_report,
|
|
437
|
+
isolation=isolation,
|
|
438
|
+
auth_profile=auth_profile,
|
|
439
|
+
)
|
|
440
|
+
if subcommand == "run":
|
|
441
|
+
return parse_run(
|
|
442
|
+
rest, json_mode, cwd, pass_through, completion_report, isolation, auth_profile, group
|
|
443
|
+
)
|
|
444
|
+
if subcommand in MODELESS_ENGINES:
|
|
445
|
+
return parse_modeless_engine(
|
|
446
|
+
subcommand,
|
|
447
|
+
rest,
|
|
448
|
+
json_mode,
|
|
449
|
+
cwd,
|
|
450
|
+
dry_run=False,
|
|
451
|
+
pass_through=pass_through,
|
|
452
|
+
completion_report=completion_report,
|
|
453
|
+
isolation=isolation,
|
|
454
|
+
auth_profile=auth_profile,
|
|
455
|
+
group=group,
|
|
456
|
+
)
|
|
457
|
+
if subcommand == "droid":
|
|
458
|
+
return parse_droid(
|
|
459
|
+
rest,
|
|
460
|
+
json_mode,
|
|
461
|
+
cwd,
|
|
462
|
+
dry_run=False,
|
|
463
|
+
pass_through=pass_through,
|
|
464
|
+
completion_report=completion_report,
|
|
465
|
+
isolation=isolation,
|
|
466
|
+
auth_profile=auth_profile,
|
|
467
|
+
)
|
|
468
|
+
if subcommand == "dry-run":
|
|
469
|
+
return parse_dry_run(
|
|
470
|
+
rest, json_mode, cwd, pass_through, completion_report, isolation, auth_profile, group
|
|
471
|
+
)
|
|
472
|
+
if subcommand == "snapshot":
|
|
473
|
+
return parse_snapshot(rest, json_mode, cwd)
|
|
474
|
+
if subcommand == "runs":
|
|
475
|
+
return parse_runs(rest, json_mode, cwd)
|
|
476
|
+
if subcommand == "run-output":
|
|
477
|
+
return parse_run_output(rest, json_mode, cwd)
|
|
478
|
+
if subcommand == "wait":
|
|
479
|
+
if isolation is not None:
|
|
480
|
+
raise DelegateError(
|
|
481
|
+
"invalid_option_combination",
|
|
482
|
+
"--isolation is not supported with delegate wait.",
|
|
483
|
+
)
|
|
484
|
+
return parse_wait(rest, json_mode, cwd)
|
|
485
|
+
if subcommand == "cancel":
|
|
486
|
+
if isolation is not None:
|
|
487
|
+
raise DelegateError(
|
|
488
|
+
"invalid_option_combination",
|
|
489
|
+
"--isolation is not supported with delegate cancel.",
|
|
490
|
+
)
|
|
491
|
+
return parse_cancel(rest, json_mode, cwd)
|
|
492
|
+
if subcommand == "worktree":
|
|
493
|
+
if isolation is not None:
|
|
494
|
+
raise DelegateError(
|
|
495
|
+
"invalid_option_combination",
|
|
496
|
+
"--isolation is not supported with delegate worktree commands.",
|
|
497
|
+
)
|
|
498
|
+
return parse_worktree(rest, json_mode, cwd)
|
|
499
|
+
if subcommand == "profiles":
|
|
500
|
+
if isolation is not None:
|
|
501
|
+
raise DelegateError(
|
|
502
|
+
"invalid_option_combination",
|
|
503
|
+
"--isolation is not supported with delegate profiles.",
|
|
504
|
+
)
|
|
505
|
+
return parse_profiles(rest, json_mode, cwd, auth_profile)
|
|
506
|
+
|
|
507
|
+
raise DelegateError("unknown_subcommand", unknown_subcommand_message(subcommand))
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def unknown_subcommand_message(subcommand: str) -> str:
|
|
511
|
+
suggestion_only = {
|
|
512
|
+
"codex-auth": "use: delegate profiles",
|
|
513
|
+
"kill": "use: delegate cancel ...",
|
|
514
|
+
}
|
|
515
|
+
if subcommand.startswith("droid-"):
|
|
516
|
+
model = subcommand.removeprefix("droid-")
|
|
517
|
+
suggestion_only[subcommand] = f"use: delegate droid {model} ..."
|
|
518
|
+
candidates = sorted(set(command_help.COMMAND_SPECS) | set(KNOWN_ENGINES))
|
|
519
|
+
matches = difflib.get_close_matches(subcommand, candidates, n=3)
|
|
520
|
+
parts = [f"Unknown subcommand: {subcommand}"]
|
|
521
|
+
if subcommand in suggestion_only:
|
|
522
|
+
parts.append(suggestion_only[subcommand])
|
|
523
|
+
elif matches:
|
|
524
|
+
parts.append(f"Did you mean: {', '.join(matches)}?")
|
|
525
|
+
top_level = sorted({name.split()[0] for name in command_help.COMMAND_SPECS})
|
|
526
|
+
if len(top_level) <= 20:
|
|
527
|
+
parts.append(f"Valid subcommands: {', '.join(top_level)}.")
|
|
528
|
+
return " ".join(parts)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def has_misplaced_global_option(tokens: list[str]) -> bool:
|
|
532
|
+
return any(token in MISPLACED_GLOBAL_OPTIONS for token in tokens)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _shell_command(argv: list[str]) -> str:
|
|
536
|
+
return shlex.join(["delegate", *argv])
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def corrected_global_command(argv: list[str]) -> str:
|
|
540
|
+
globals_out: list[str] = []
|
|
541
|
+
rest: list[str] = []
|
|
542
|
+
i = 0
|
|
543
|
+
while i < len(argv):
|
|
544
|
+
token = argv[i]
|
|
545
|
+
if token in VALUE_GLOBAL_OPTIONS and i + 1 < len(argv):
|
|
546
|
+
globals_out.extend([token, argv[i + 1]])
|
|
547
|
+
i += 2
|
|
548
|
+
continue
|
|
549
|
+
if token in MISPLACED_GLOBAL_OPTIONS:
|
|
550
|
+
globals_out.append(token)
|
|
551
|
+
i += 1
|
|
552
|
+
continue
|
|
553
|
+
rest.append(token)
|
|
554
|
+
i += 1
|
|
555
|
+
return _shell_command([*globals_out, *rest])
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def raise_misplaced_global_option(message: str, argv: list[str] | None = None) -> NoReturn:
|
|
559
|
+
guidance = (
|
|
560
|
+
"Move global options before the subcommand "
|
|
561
|
+
"(for example: delegate --json --cwd PATH <subcommand> ...)."
|
|
562
|
+
)
|
|
563
|
+
corrected = f" Corrected command: {corrected_global_command(argv)}." if argv else ""
|
|
564
|
+
raise DelegateError("misplaced_global_option", f"{message} {guidance}{corrected}")
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def unknown_option_message(command: str, option: str) -> str:
|
|
568
|
+
spec = command_help.COMMAND_SPECS.get(command)
|
|
569
|
+
candidates = [opt.flag for opt in spec.options] if spec is not None else []
|
|
570
|
+
matches = difflib.get_close_matches(option, candidates, n=3)
|
|
571
|
+
if command == "run-output" and option == "--full":
|
|
572
|
+
matches = ["--raw", *[match for match in matches if match != "--raw"]]
|
|
573
|
+
suffix = f" Did you mean: {', '.join(matches)}?" if matches else ""
|
|
574
|
+
return f"{command} does not support option: {option}.{suffix}"
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def consume_json_option(rest: list[str], json_mode: bool) -> tuple[list[str], bool]:
|
|
578
|
+
"""Accept `--json` after contained inspection commands.
|
|
579
|
+
|
|
580
|
+
Launching commands still require global options before the subcommand so
|
|
581
|
+
prompt boundaries stay unambiguous. For no-launch inspection commands,
|
|
582
|
+
accepting `delegate describe --json` and friends removes a common operator
|
|
583
|
+
foot-gun without changing child-runtime invocation semantics.
|
|
584
|
+
"""
|
|
585
|
+
|
|
586
|
+
normalized: list[str] = []
|
|
587
|
+
for token in rest:
|
|
588
|
+
if token == "--json":
|
|
589
|
+
json_mode = True
|
|
590
|
+
continue
|
|
591
|
+
normalized.append(token)
|
|
592
|
+
return normalized, json_mode
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def require_no_extra(rest: list[str], name: str) -> None:
|
|
596
|
+
if rest:
|
|
597
|
+
if has_misplaced_global_option(rest):
|
|
598
|
+
raise_misplaced_global_option("Global options must appear before the subcommand.")
|
|
599
|
+
raise DelegateError(
|
|
600
|
+
"unexpected_argument", f"{name} does not accept arguments: {' '.join(rest)}"
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def parse_run(
|
|
605
|
+
rest: list[str],
|
|
606
|
+
json_mode: bool,
|
|
607
|
+
cwd: str | None,
|
|
608
|
+
pass_through: bool,
|
|
609
|
+
completion_report: str | None,
|
|
610
|
+
isolation: str | None,
|
|
611
|
+
auth_profile: str | None,
|
|
612
|
+
group: str | None = None,
|
|
613
|
+
) -> ParsedCommand:
|
|
614
|
+
# Help wins before required-arg validation: `run --help` needs no --input-json.
|
|
615
|
+
if any(command_help.is_help_token(token) for token in rest):
|
|
616
|
+
return help_command(json_mode, "run")
|
|
617
|
+
if len(rest) != 2 or rest[0] != "--input-json":
|
|
618
|
+
if has_misplaced_global_option(rest):
|
|
619
|
+
raise_misplaced_global_option("Global options must appear before the subcommand.")
|
|
620
|
+
raise DelegateError("invalid_run_args", "run requires: --input-json FILE")
|
|
621
|
+
return ParsedCommand(
|
|
622
|
+
"run",
|
|
623
|
+
global_options=GlobalOptions(
|
|
624
|
+
json_mode=json_mode,
|
|
625
|
+
cwd=cwd,
|
|
626
|
+
pass_through=pass_through,
|
|
627
|
+
completion_report=completion_report,
|
|
628
|
+
isolation=isolation,
|
|
629
|
+
auth_profile=auth_profile,
|
|
630
|
+
group=group,
|
|
631
|
+
),
|
|
632
|
+
run_json=RunJsonOptions(rest[1]),
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def parse_modeless_engine(
|
|
637
|
+
engine: str,
|
|
638
|
+
rest: list[str],
|
|
639
|
+
json_mode: bool,
|
|
640
|
+
cwd: str | None,
|
|
641
|
+
dry_run: bool,
|
|
642
|
+
pass_through: bool,
|
|
643
|
+
completion_report: str | None,
|
|
644
|
+
isolation: str | None,
|
|
645
|
+
auth_profile: str | None,
|
|
646
|
+
group: str | None = None,
|
|
647
|
+
*,
|
|
648
|
+
help_topic: str | None = None,
|
|
649
|
+
) -> ParsedCommand:
|
|
650
|
+
"""Parse the shared cursor/codex grammar: <mode> [--prompt-file PATH] [prompt...]."""
|
|
651
|
+
topic = help_topic if help_topic is not None else engine
|
|
652
|
+
# Help wins before a mode is consumed: `cursor --help` needs no mode.
|
|
653
|
+
if rest and command_help.is_help_token(rest[0]):
|
|
654
|
+
return help_command(json_mode, topic)
|
|
655
|
+
if not rest:
|
|
656
|
+
raise DelegateError("missing_mode", f"{engine} requires mode: safe, work, or call.")
|
|
657
|
+
mode = rest[0]
|
|
658
|
+
if mode.startswith("-"):
|
|
659
|
+
raise DelegateError(
|
|
660
|
+
"misplaced_global_option", "Global options must appear before the subcommand."
|
|
661
|
+
)
|
|
662
|
+
validate_mode(mode)
|
|
663
|
+
# Help wins immediately after the mode, before prompt capture begins:
|
|
664
|
+
# `cursor safe --help`. Once a prompt positional begins, a later --help is
|
|
665
|
+
# prompt text (`cursor work explain --help`).
|
|
666
|
+
if len(rest) >= 2 and command_help.is_help_token(rest[1]):
|
|
667
|
+
return help_command(json_mode, topic)
|
|
668
|
+
(
|
|
669
|
+
prompt_file,
|
|
670
|
+
output_schema,
|
|
671
|
+
reasoning_effort,
|
|
672
|
+
progress_intent,
|
|
673
|
+
forbid_commit,
|
|
674
|
+
prompt_parts,
|
|
675
|
+
json_mode,
|
|
676
|
+
isolation,
|
|
677
|
+
read_only,
|
|
678
|
+
include_dirty,
|
|
679
|
+
) = parse_prompt_tail(rest[1:], json_mode, isolation, command_prefix=[engine, mode])
|
|
680
|
+
forbid_commit_implied_isolation = False
|
|
681
|
+
if forbid_commit and mode == "work" and isolation is None:
|
|
682
|
+
isolation = "worktree"
|
|
683
|
+
forbid_commit_implied_isolation = True
|
|
684
|
+
if forbid_commit and mode == "work" and isolation == "none":
|
|
685
|
+
raise DelegateError(
|
|
686
|
+
"invalid_option_combination",
|
|
687
|
+
"--forbid-commit cannot be combined with --isolation none. "
|
|
688
|
+
f"Corrected command: {_shell_command(['--isolation', 'worktree', engine, mode, '--forbid-commit', *(prompt_parts or [])])}.",
|
|
689
|
+
)
|
|
690
|
+
return ParsedCommand(
|
|
691
|
+
engine,
|
|
692
|
+
global_options=GlobalOptions(
|
|
693
|
+
json_mode=json_mode,
|
|
694
|
+
cwd=cwd,
|
|
695
|
+
pass_through=pass_through,
|
|
696
|
+
completion_report=completion_report,
|
|
697
|
+
isolation=isolation,
|
|
698
|
+
auth_profile=auth_profile,
|
|
699
|
+
group=group,
|
|
700
|
+
),
|
|
701
|
+
launch=LaunchOptions(
|
|
702
|
+
engine=engine,
|
|
703
|
+
mode=mode,
|
|
704
|
+
prompt_parts=prompt_parts,
|
|
705
|
+
prompt_file=prompt_file,
|
|
706
|
+
output_schema=output_schema,
|
|
707
|
+
reasoning_effort=reasoning_effort,
|
|
708
|
+
progress_intent=progress_intent,
|
|
709
|
+
forbid_commit=forbid_commit,
|
|
710
|
+
forbid_commit_implied_isolation=forbid_commit_implied_isolation,
|
|
711
|
+
include_dirty=include_dirty,
|
|
712
|
+
read_only=read_only,
|
|
713
|
+
dry_run=dry_run,
|
|
714
|
+
),
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def parse_droid(
|
|
719
|
+
rest: list[str],
|
|
720
|
+
json_mode: bool,
|
|
721
|
+
cwd: str | None,
|
|
722
|
+
dry_run: bool,
|
|
723
|
+
pass_through: bool,
|
|
724
|
+
completion_report: str | None,
|
|
725
|
+
isolation: str | None,
|
|
726
|
+
auth_profile: str | None,
|
|
727
|
+
group: str | None = None,
|
|
728
|
+
*,
|
|
729
|
+
help_topic: str | None = None,
|
|
730
|
+
) -> ParsedCommand:
|
|
731
|
+
topic = help_topic if help_topic is not None else "droid"
|
|
732
|
+
# Help wins before the alias is consumed: `droid --help` needs no alias.
|
|
733
|
+
if rest and command_help.is_help_token(rest[0]):
|
|
734
|
+
return help_command(json_mode, topic)
|
|
735
|
+
if len(rest) < 2:
|
|
736
|
+
raise DelegateError("missing_droid_args", "droid requires MODEL_ALIAS and mode.")
|
|
737
|
+
model_alias = rest[0]
|
|
738
|
+
if model_alias.startswith("-"):
|
|
739
|
+
raise DelegateError(
|
|
740
|
+
"misplaced_global_option", "Global options must appear before the subcommand."
|
|
741
|
+
)
|
|
742
|
+
# Help wins after the alias, before the mode: `droid x --help`.
|
|
743
|
+
if command_help.is_help_token(rest[1]):
|
|
744
|
+
return help_command(json_mode, topic)
|
|
745
|
+
mode = rest[1]
|
|
746
|
+
if mode.startswith("-"):
|
|
747
|
+
raise DelegateError(
|
|
748
|
+
"misplaced_global_option", "Global options must appear before the subcommand."
|
|
749
|
+
)
|
|
750
|
+
validate_mode(mode)
|
|
751
|
+
# Help wins after the mode, before prompt capture: `droid x safe --help`.
|
|
752
|
+
if len(rest) >= 3 and command_help.is_help_token(rest[2]):
|
|
753
|
+
return help_command(json_mode, topic)
|
|
754
|
+
(
|
|
755
|
+
prompt_file,
|
|
756
|
+
output_schema,
|
|
757
|
+
reasoning_effort,
|
|
758
|
+
progress_intent,
|
|
759
|
+
forbid_commit,
|
|
760
|
+
prompt_parts,
|
|
761
|
+
json_mode,
|
|
762
|
+
isolation,
|
|
763
|
+
read_only,
|
|
764
|
+
include_dirty,
|
|
765
|
+
) = parse_prompt_tail(
|
|
766
|
+
rest[2:], json_mode, isolation, command_prefix=["droid", model_alias, mode]
|
|
767
|
+
)
|
|
768
|
+
forbid_commit_implied_isolation = False
|
|
769
|
+
if forbid_commit and mode == "work" and isolation is None:
|
|
770
|
+
isolation = "worktree"
|
|
771
|
+
forbid_commit_implied_isolation = True
|
|
772
|
+
if forbid_commit and mode == "work" and isolation == "none":
|
|
773
|
+
raise DelegateError(
|
|
774
|
+
"invalid_option_combination",
|
|
775
|
+
"--forbid-commit cannot be combined with --isolation none. "
|
|
776
|
+
f"Corrected command: {_shell_command(['--isolation', 'worktree', 'droid', model_alias, mode, '--forbid-commit', *(prompt_parts or [])])}.",
|
|
777
|
+
)
|
|
778
|
+
return ParsedCommand(
|
|
779
|
+
"droid",
|
|
780
|
+
global_options=GlobalOptions(
|
|
781
|
+
json_mode=json_mode,
|
|
782
|
+
cwd=cwd,
|
|
783
|
+
pass_through=pass_through,
|
|
784
|
+
completion_report=completion_report,
|
|
785
|
+
isolation=isolation,
|
|
786
|
+
auth_profile=auth_profile,
|
|
787
|
+
group=group,
|
|
788
|
+
),
|
|
789
|
+
launch=LaunchOptions(
|
|
790
|
+
engine="droid",
|
|
791
|
+
mode=mode,
|
|
792
|
+
model_alias=model_alias,
|
|
793
|
+
prompt_parts=prompt_parts,
|
|
794
|
+
prompt_file=prompt_file,
|
|
795
|
+
output_schema=output_schema,
|
|
796
|
+
reasoning_effort=reasoning_effort,
|
|
797
|
+
progress_intent=progress_intent,
|
|
798
|
+
forbid_commit=forbid_commit,
|
|
799
|
+
forbid_commit_implied_isolation=forbid_commit_implied_isolation,
|
|
800
|
+
include_dirty=include_dirty,
|
|
801
|
+
read_only=read_only,
|
|
802
|
+
dry_run=dry_run,
|
|
803
|
+
),
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def parse_dry_run(
|
|
808
|
+
rest: list[str],
|
|
809
|
+
json_mode: bool,
|
|
810
|
+
cwd: str | None,
|
|
811
|
+
pass_through: bool,
|
|
812
|
+
completion_report: str | None,
|
|
813
|
+
isolation: str | None,
|
|
814
|
+
auth_profile: str | None,
|
|
815
|
+
group: str | None = None,
|
|
816
|
+
) -> ParsedCommand:
|
|
817
|
+
# Help wins before the engine is consumed: `dry-run --help`.
|
|
818
|
+
if rest and command_help.is_help_token(rest[0]):
|
|
819
|
+
return help_command(json_mode, "dry-run")
|
|
820
|
+
if not rest:
|
|
821
|
+
raise DelegateError(
|
|
822
|
+
"missing_engine",
|
|
823
|
+
"dry-run requires cursor, droid, codex, kimi, claude, or grok.",
|
|
824
|
+
)
|
|
825
|
+
engine = rest[0]
|
|
826
|
+
if engine.startswith("-"):
|
|
827
|
+
raise DelegateError(
|
|
828
|
+
"misplaced_global_option", "Global options must appear before the subcommand."
|
|
829
|
+
)
|
|
830
|
+
if engine in MODELESS_ENGINES:
|
|
831
|
+
return parse_modeless_engine(
|
|
832
|
+
engine,
|
|
833
|
+
rest[1:],
|
|
834
|
+
json_mode,
|
|
835
|
+
cwd,
|
|
836
|
+
dry_run=True,
|
|
837
|
+
pass_through=pass_through,
|
|
838
|
+
completion_report=completion_report,
|
|
839
|
+
isolation=isolation,
|
|
840
|
+
auth_profile=auth_profile,
|
|
841
|
+
group=group,
|
|
842
|
+
help_topic="dry-run",
|
|
843
|
+
)
|
|
844
|
+
if engine == "droid":
|
|
845
|
+
return parse_droid(
|
|
846
|
+
rest[1:],
|
|
847
|
+
json_mode,
|
|
848
|
+
cwd,
|
|
849
|
+
dry_run=True,
|
|
850
|
+
pass_through=pass_through,
|
|
851
|
+
completion_report=completion_report,
|
|
852
|
+
isolation=isolation,
|
|
853
|
+
auth_profile=auth_profile,
|
|
854
|
+
group=group,
|
|
855
|
+
help_topic="dry-run",
|
|
856
|
+
)
|
|
857
|
+
raise DelegateError(
|
|
858
|
+
"invalid_engine",
|
|
859
|
+
f"dry-run engine must be {ENGINES_PROSE}.",
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def parse_prompt_tail(
|
|
864
|
+
rest: list[str],
|
|
865
|
+
json_mode: bool,
|
|
866
|
+
isolation: str | None,
|
|
867
|
+
*,
|
|
868
|
+
command_prefix: list[str] | None = None,
|
|
869
|
+
) -> tuple[
|
|
870
|
+
str | None,
|
|
871
|
+
str | None,
|
|
872
|
+
str | None,
|
|
873
|
+
str | None,
|
|
874
|
+
bool,
|
|
875
|
+
list[str],
|
|
876
|
+
bool,
|
|
877
|
+
str | None,
|
|
878
|
+
bool,
|
|
879
|
+
bool,
|
|
880
|
+
]:
|
|
881
|
+
prompt_file: str | None = None
|
|
882
|
+
output_schema: str | None = None
|
|
883
|
+
reasoning_effort: str | None = None
|
|
884
|
+
progress_intent: str | None = None
|
|
885
|
+
forbid_commit = False
|
|
886
|
+
include_dirty = False
|
|
887
|
+
read_only = False
|
|
888
|
+
prompt_parts: list[str] = []
|
|
889
|
+
i = 0
|
|
890
|
+
while i < len(rest):
|
|
891
|
+
token = rest[i]
|
|
892
|
+
# `--json` is unambiguous anywhere before inline prompt text starts (e.g.
|
|
893
|
+
# after --prompt-file), so accept it here instead of forcing it ahead of the
|
|
894
|
+
# subcommand. Once prompt text begins it lands in prompt_parts and the
|
|
895
|
+
# post-loop misplaced-global guard rejects it, since then it could be prompt
|
|
896
|
+
# text. Mirrors consume_json_option for inspection commands.
|
|
897
|
+
if token == "--json":
|
|
898
|
+
json_mode = True
|
|
899
|
+
i += 1
|
|
900
|
+
continue
|
|
901
|
+
if token == "--prompt-file":
|
|
902
|
+
if prompt_parts:
|
|
903
|
+
raise DelegateError(
|
|
904
|
+
"ambiguous_prompt_source",
|
|
905
|
+
"--prompt-file must appear before direct prompt text."
|
|
906
|
+
+ corrected_prompt_file_suffix(command_prefix, rest),
|
|
907
|
+
)
|
|
908
|
+
if prompt_file is not None:
|
|
909
|
+
raise DelegateError("ambiguous_prompt_source", "Only one --prompt-file is allowed.")
|
|
910
|
+
if i + 1 >= len(rest):
|
|
911
|
+
raise DelegateError("missing_prompt_file", "--prompt-file requires a path.")
|
|
912
|
+
prompt_file = rest[i + 1]
|
|
913
|
+
i += 2
|
|
914
|
+
continue
|
|
915
|
+
if token == "--isolation":
|
|
916
|
+
if i + 1 >= len(rest):
|
|
917
|
+
raise DelegateError("missing_isolation_value", "--isolation requires a value.")
|
|
918
|
+
isolation = rest[i + 1]
|
|
919
|
+
if isolation not in delegate_config.VALID_ISOLATION_VALUES:
|
|
920
|
+
raise DelegateError(
|
|
921
|
+
"invalid_isolation",
|
|
922
|
+
"--isolation must be auto, none, or worktree.",
|
|
923
|
+
)
|
|
924
|
+
i += 2
|
|
925
|
+
continue
|
|
926
|
+
if token == "--output-schema":
|
|
927
|
+
if output_schema is not None:
|
|
928
|
+
raise DelegateError("invalid_output_schema", "Only one --output-schema is allowed.")
|
|
929
|
+
if i + 1 >= len(rest):
|
|
930
|
+
raise DelegateError("missing_output_schema", "--output-schema requires a path.")
|
|
931
|
+
output_schema = rest[i + 1]
|
|
932
|
+
i += 2
|
|
933
|
+
continue
|
|
934
|
+
if token == "--reasoning-effort":
|
|
935
|
+
if reasoning_effort is not None:
|
|
936
|
+
raise DelegateError(
|
|
937
|
+
"invalid_reasoning_effort",
|
|
938
|
+
"Only one --reasoning-effort is allowed.",
|
|
939
|
+
)
|
|
940
|
+
if i + 1 >= len(rest):
|
|
941
|
+
raise DelegateError(
|
|
942
|
+
"missing_reasoning_effort",
|
|
943
|
+
"--reasoning-effort requires a value.",
|
|
944
|
+
)
|
|
945
|
+
value = rest[i + 1]
|
|
946
|
+
if value.startswith("-") or command_help.is_help_token(value):
|
|
947
|
+
raise DelegateError(
|
|
948
|
+
"missing_reasoning_effort",
|
|
949
|
+
"--reasoning-effort requires a value.",
|
|
950
|
+
)
|
|
951
|
+
try:
|
|
952
|
+
reasoning_effort = reasoning.normalize_effort(value)
|
|
953
|
+
except reasoning.ReasoningCapabilityError as exc:
|
|
954
|
+
corrected = corrected_drop_option_suffix(command_prefix, rest, i, takes_value=True)
|
|
955
|
+
raise DelegateError(exc.error, f"{exc.message}{corrected}") from exc
|
|
956
|
+
i += 2
|
|
957
|
+
continue
|
|
958
|
+
if token == "--progress":
|
|
959
|
+
if progress_intent == "on":
|
|
960
|
+
raise DelegateError(
|
|
961
|
+
"invalid_option_combination",
|
|
962
|
+
"Only one --progress flag is allowed.",
|
|
963
|
+
)
|
|
964
|
+
if progress_intent == "off":
|
|
965
|
+
raise DelegateError(
|
|
966
|
+
"invalid_option_combination",
|
|
967
|
+
"--progress and --no-progress cannot be combined.",
|
|
968
|
+
)
|
|
969
|
+
progress_intent = "on"
|
|
970
|
+
i += 1
|
|
971
|
+
continue
|
|
972
|
+
if token == "--no-progress":
|
|
973
|
+
if progress_intent == "off":
|
|
974
|
+
raise DelegateError(
|
|
975
|
+
"invalid_option_combination",
|
|
976
|
+
"Only one --no-progress flag is allowed.",
|
|
977
|
+
)
|
|
978
|
+
if progress_intent == "on":
|
|
979
|
+
raise DelegateError(
|
|
980
|
+
"invalid_option_combination",
|
|
981
|
+
"--progress and --no-progress cannot be combined.",
|
|
982
|
+
)
|
|
983
|
+
progress_intent = "off"
|
|
984
|
+
i += 1
|
|
985
|
+
continue
|
|
986
|
+
if token == "--forbid-commit":
|
|
987
|
+
if forbid_commit:
|
|
988
|
+
raise DelegateError(
|
|
989
|
+
"invalid_option_combination",
|
|
990
|
+
"Only one --forbid-commit flag is allowed.",
|
|
991
|
+
)
|
|
992
|
+
forbid_commit = True
|
|
993
|
+
i += 1
|
|
994
|
+
continue
|
|
995
|
+
if token == "--include-dirty":
|
|
996
|
+
if include_dirty:
|
|
997
|
+
raise DelegateError(
|
|
998
|
+
"invalid_option_combination",
|
|
999
|
+
"Only one --include-dirty flag is allowed.",
|
|
1000
|
+
)
|
|
1001
|
+
include_dirty = True
|
|
1002
|
+
i += 1
|
|
1003
|
+
continue
|
|
1004
|
+
if token == "--read-only":
|
|
1005
|
+
read_only = True
|
|
1006
|
+
i += 1
|
|
1007
|
+
continue
|
|
1008
|
+
prompt_parts = rest[i:]
|
|
1009
|
+
break
|
|
1010
|
+
if "--prompt-file" in prompt_parts:
|
|
1011
|
+
raise DelegateError(
|
|
1012
|
+
"ambiguous_prompt_source",
|
|
1013
|
+
"--prompt-file must appear before direct prompt text."
|
|
1014
|
+
+ corrected_prompt_file_suffix(command_prefix, rest),
|
|
1015
|
+
)
|
|
1016
|
+
if "--output-schema" in prompt_parts:
|
|
1017
|
+
raise DelegateError(
|
|
1018
|
+
"invalid_output_schema", "--output-schema must appear before direct prompt text."
|
|
1019
|
+
)
|
|
1020
|
+
if has_misplaced_global_option(prompt_parts):
|
|
1021
|
+
raise_misplaced_global_option(
|
|
1022
|
+
"Global options must appear before the subcommand; use --prompt-file for literal flag text.",
|
|
1023
|
+
)
|
|
1024
|
+
return (
|
|
1025
|
+
prompt_file,
|
|
1026
|
+
output_schema,
|
|
1027
|
+
reasoning_effort,
|
|
1028
|
+
progress_intent,
|
|
1029
|
+
forbid_commit,
|
|
1030
|
+
prompt_parts,
|
|
1031
|
+
json_mode,
|
|
1032
|
+
isolation,
|
|
1033
|
+
read_only,
|
|
1034
|
+
include_dirty,
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def parse_snapshot(rest: list[str], json_mode: bool, cwd: str | None) -> ParsedCommand:
|
|
1039
|
+
# Help wins over the optional handle/flags: a help token anywhere is help.
|
|
1040
|
+
rest, json_mode = consume_json_option(rest, json_mode)
|
|
1041
|
+
if any(command_help.is_help_token(token) for token in rest):
|
|
1042
|
+
return help_command(json_mode, "snapshot")
|
|
1043
|
+
latest_harness: str | None = None
|
|
1044
|
+
no_redact = False
|
|
1045
|
+
handle: str | None = None
|
|
1046
|
+
i = 0
|
|
1047
|
+
while i < len(rest):
|
|
1048
|
+
token = rest[i]
|
|
1049
|
+
if token == "--latest":
|
|
1050
|
+
if i + 1 >= len(rest):
|
|
1051
|
+
raise DelegateError("missing_harness", "snapshot --latest requires a harness name.")
|
|
1052
|
+
latest_harness = rest[i + 1]
|
|
1053
|
+
i += 2
|
|
1054
|
+
continue
|
|
1055
|
+
if token == "--no-redact":
|
|
1056
|
+
no_redact = True
|
|
1057
|
+
i += 1
|
|
1058
|
+
continue
|
|
1059
|
+
if token.startswith("-"):
|
|
1060
|
+
raise DelegateError("unknown_option", unknown_option_message("snapshot", token))
|
|
1061
|
+
if handle is not None:
|
|
1062
|
+
raise DelegateError(
|
|
1063
|
+
"unexpected_argument", f"snapshot accepts one handle: {' '.join(rest)}"
|
|
1064
|
+
)
|
|
1065
|
+
handle = token
|
|
1066
|
+
i += 1
|
|
1067
|
+
if latest_harness is None and handle is None:
|
|
1068
|
+
raise DelegateError(
|
|
1069
|
+
"missing_handle", "snapshot requires <alias-or-runId> or --latest <harness>."
|
|
1070
|
+
)
|
|
1071
|
+
if latest_harness is not None and handle is not None:
|
|
1072
|
+
raise DelegateError(
|
|
1073
|
+
"ambiguous_snapshot_target",
|
|
1074
|
+
"Use either --latest <harness> or an exact handle, not both.",
|
|
1075
|
+
)
|
|
1076
|
+
return ParsedCommand(
|
|
1077
|
+
"snapshot",
|
|
1078
|
+
global_options=GlobalOptions(json_mode=json_mode, cwd=cwd),
|
|
1079
|
+
snapshot=inspection_commands.SnapshotCommand(
|
|
1080
|
+
handle=handle,
|
|
1081
|
+
latest_harness=latest_harness,
|
|
1082
|
+
no_redact=no_redact,
|
|
1083
|
+
json_mode=json_mode,
|
|
1084
|
+
),
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def parse_runs(rest: list[str], json_mode: bool, cwd: str | None) -> ParsedCommand:
|
|
1089
|
+
# Help wins over flags: a help token anywhere is help.
|
|
1090
|
+
rest, json_mode = consume_json_option(rest, json_mode)
|
|
1091
|
+
if any(command_help.is_help_token(token) for token in rest):
|
|
1092
|
+
return help_command(json_mode, "runs")
|
|
1093
|
+
active = False
|
|
1094
|
+
recent = False
|
|
1095
|
+
running = False
|
|
1096
|
+
stale = False
|
|
1097
|
+
harness: str | None = None
|
|
1098
|
+
group: str | None = None
|
|
1099
|
+
limit: int | None = None
|
|
1100
|
+
i = 0
|
|
1101
|
+
while i < len(rest):
|
|
1102
|
+
token = rest[i]
|
|
1103
|
+
if token == "--active":
|
|
1104
|
+
active = True
|
|
1105
|
+
i += 1
|
|
1106
|
+
continue
|
|
1107
|
+
if token == "--running":
|
|
1108
|
+
running = True
|
|
1109
|
+
i += 1
|
|
1110
|
+
continue
|
|
1111
|
+
if token == "--stale":
|
|
1112
|
+
stale = True
|
|
1113
|
+
i += 1
|
|
1114
|
+
continue
|
|
1115
|
+
if token == "--recent":
|
|
1116
|
+
recent = True
|
|
1117
|
+
i += 1
|
|
1118
|
+
continue
|
|
1119
|
+
if token == "--harness":
|
|
1120
|
+
if i + 1 >= len(rest):
|
|
1121
|
+
raise DelegateError("missing_harness", "runs --harness requires a harness name.")
|
|
1122
|
+
harness = rest[i + 1]
|
|
1123
|
+
if harness not in KNOWN_ENGINES:
|
|
1124
|
+
raise DelegateError(
|
|
1125
|
+
"invalid_harness",
|
|
1126
|
+
f"runs --harness must be one of {', '.join(KNOWN_ENGINES)}.",
|
|
1127
|
+
)
|
|
1128
|
+
i += 2
|
|
1129
|
+
continue
|
|
1130
|
+
if token == "--group":
|
|
1131
|
+
if i + 1 >= len(rest):
|
|
1132
|
+
raise DelegateError("missing_group", "runs --group requires a group name.")
|
|
1133
|
+
group = validate_group(rest[i + 1])
|
|
1134
|
+
i += 2
|
|
1135
|
+
continue
|
|
1136
|
+
if token == "--limit":
|
|
1137
|
+
limit, i = parse_required_positive_int_option(
|
|
1138
|
+
rest,
|
|
1139
|
+
i,
|
|
1140
|
+
option_label="runs --limit",
|
|
1141
|
+
missing_error="missing_limit",
|
|
1142
|
+
invalid_error="invalid_limit",
|
|
1143
|
+
)
|
|
1144
|
+
continue
|
|
1145
|
+
raise DelegateError("unknown_option", f"runs does not support option: {token}")
|
|
1146
|
+
selected_modes = [
|
|
1147
|
+
label
|
|
1148
|
+
for label, selected in (
|
|
1149
|
+
("--active", active),
|
|
1150
|
+
("--running", running),
|
|
1151
|
+
("--stale", stale),
|
|
1152
|
+
("--recent", recent),
|
|
1153
|
+
)
|
|
1154
|
+
if selected
|
|
1155
|
+
]
|
|
1156
|
+
if len(selected_modes) > 1:
|
|
1157
|
+
raise DelegateError(
|
|
1158
|
+
"invalid_option_combination",
|
|
1159
|
+
f"runs filters are mutually exclusive: {', '.join(selected_modes)}.",
|
|
1160
|
+
)
|
|
1161
|
+
return ParsedCommand(
|
|
1162
|
+
"runs",
|
|
1163
|
+
global_options=GlobalOptions(json_mode=json_mode, cwd=cwd),
|
|
1164
|
+
runs=inspection_commands.RunsCommand(
|
|
1165
|
+
active=active,
|
|
1166
|
+
running=running,
|
|
1167
|
+
stale=stale,
|
|
1168
|
+
harness=harness,
|
|
1169
|
+
group=group,
|
|
1170
|
+
limit=limit,
|
|
1171
|
+
json_mode=json_mode,
|
|
1172
|
+
),
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def parse_run_output(rest: list[str], json_mode: bool, cwd: str | None) -> ParsedCommand:
|
|
1177
|
+
# Help wins over the required handle/selectors: a help token anywhere is help.
|
|
1178
|
+
rest, json_mode = consume_json_option(rest, json_mode)
|
|
1179
|
+
if any(command_help.is_help_token(token) for token in rest):
|
|
1180
|
+
return help_command(json_mode, "run-output")
|
|
1181
|
+
if not rest:
|
|
1182
|
+
raise DelegateError("missing_handle", "run-output requires <alias-or-runId> or --latest.")
|
|
1183
|
+
handle: str | None = None
|
|
1184
|
+
latest_harness: str | None = None
|
|
1185
|
+
completion_report = False
|
|
1186
|
+
stdout_flag = False
|
|
1187
|
+
stderr_flag = False
|
|
1188
|
+
tail: int | None = None
|
|
1189
|
+
max_chars: int | None = None
|
|
1190
|
+
raw = False
|
|
1191
|
+
no_redact = False
|
|
1192
|
+
i = 0
|
|
1193
|
+
while i < len(rest):
|
|
1194
|
+
token = rest[i]
|
|
1195
|
+
if token == "--latest":
|
|
1196
|
+
if i + 1 >= len(rest):
|
|
1197
|
+
raise DelegateError(
|
|
1198
|
+
"missing_harness", "run-output --latest requires a harness name."
|
|
1199
|
+
)
|
|
1200
|
+
latest_harness = rest[i + 1]
|
|
1201
|
+
i += 2
|
|
1202
|
+
continue
|
|
1203
|
+
if token == "--completion-report":
|
|
1204
|
+
completion_report = True
|
|
1205
|
+
i += 1
|
|
1206
|
+
continue
|
|
1207
|
+
if token == "--stdout":
|
|
1208
|
+
stdout_flag = True
|
|
1209
|
+
i += 1
|
|
1210
|
+
continue
|
|
1211
|
+
if token == "--stderr":
|
|
1212
|
+
stderr_flag = True
|
|
1213
|
+
i += 1
|
|
1214
|
+
continue
|
|
1215
|
+
if token == "--raw":
|
|
1216
|
+
raw = True
|
|
1217
|
+
i += 1
|
|
1218
|
+
continue
|
|
1219
|
+
if token == "--no-redact":
|
|
1220
|
+
no_redact = True
|
|
1221
|
+
i += 1
|
|
1222
|
+
continue
|
|
1223
|
+
if token == "--tail":
|
|
1224
|
+
tail, i = parse_required_positive_int_option(
|
|
1225
|
+
rest,
|
|
1226
|
+
i,
|
|
1227
|
+
option_label="run-output --tail",
|
|
1228
|
+
missing_error="missing_tail",
|
|
1229
|
+
invalid_error="invalid_tail",
|
|
1230
|
+
missing_value_description="a line count",
|
|
1231
|
+
)
|
|
1232
|
+
continue
|
|
1233
|
+
if token == "--max-chars":
|
|
1234
|
+
max_chars, i = parse_required_positive_int_option(
|
|
1235
|
+
rest,
|
|
1236
|
+
i,
|
|
1237
|
+
option_label="run-output --max-chars",
|
|
1238
|
+
missing_error="missing_max_chars",
|
|
1239
|
+
invalid_error="invalid_max_chars",
|
|
1240
|
+
missing_value_description="a positive integer",
|
|
1241
|
+
)
|
|
1242
|
+
continue
|
|
1243
|
+
if token.startswith("-"):
|
|
1244
|
+
raise DelegateError("unknown_option", unknown_option_message("run-output", token))
|
|
1245
|
+
if handle is not None:
|
|
1246
|
+
raise DelegateError(
|
|
1247
|
+
"unexpected_argument", f"run-output accepts one handle: {' '.join(rest)}"
|
|
1248
|
+
)
|
|
1249
|
+
handle = token
|
|
1250
|
+
i += 1
|
|
1251
|
+
if latest_harness is None and handle is None:
|
|
1252
|
+
raise DelegateError("missing_handle", "run-output requires <alias-or-runId> or --latest.")
|
|
1253
|
+
if latest_harness is not None and handle is not None:
|
|
1254
|
+
raise DelegateError(
|
|
1255
|
+
"ambiguous_run_output_target",
|
|
1256
|
+
"Use either --latest <harness> or an exact handle, not both.",
|
|
1257
|
+
)
|
|
1258
|
+
default_output = not (completion_report or stdout_flag or stderr_flag or raw)
|
|
1259
|
+
if default_output:
|
|
1260
|
+
completion_report = True
|
|
1261
|
+
if raw and tail is not None:
|
|
1262
|
+
raise DelegateError(
|
|
1263
|
+
"invalid_option_combination",
|
|
1264
|
+
"run-output --raw cannot be combined with --tail.",
|
|
1265
|
+
)
|
|
1266
|
+
if raw and max_chars is not None:
|
|
1267
|
+
raise DelegateError(
|
|
1268
|
+
"invalid_option_combination",
|
|
1269
|
+
"run-output --raw cannot be combined with --max-chars.",
|
|
1270
|
+
)
|
|
1271
|
+
if not (stdout_flag or stderr_flag or raw) and (tail is not None or max_chars is not None):
|
|
1272
|
+
raise DelegateError(
|
|
1273
|
+
"invalid_option_combination",
|
|
1274
|
+
"run-output --tail/--max-chars only apply to --stdout or --stderr; "
|
|
1275
|
+
"add --stdout/--stderr or remove the ignored flag.",
|
|
1276
|
+
)
|
|
1277
|
+
if (stdout_flag or stderr_flag) and not raw and tail is None:
|
|
1278
|
+
tail = RUN_OUTPUT_DEFAULT_TAIL_LINES
|
|
1279
|
+
return ParsedCommand(
|
|
1280
|
+
"run-output",
|
|
1281
|
+
global_options=GlobalOptions(json_mode=json_mode, cwd=cwd),
|
|
1282
|
+
run_output=run_output_commands.RunOutputCommand(
|
|
1283
|
+
handle=handle,
|
|
1284
|
+
latest_harness=latest_harness,
|
|
1285
|
+
json_mode=json_mode,
|
|
1286
|
+
completion_report=completion_report,
|
|
1287
|
+
stdout=stdout_flag,
|
|
1288
|
+
stderr=stderr_flag,
|
|
1289
|
+
tail=tail,
|
|
1290
|
+
max_chars=max_chars,
|
|
1291
|
+
raw=raw,
|
|
1292
|
+
no_redact=no_redact,
|
|
1293
|
+
default=default_output,
|
|
1294
|
+
),
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
def parse_wait(rest: list[str], json_mode: bool, cwd: str | None) -> ParsedCommand:
|
|
1299
|
+
rest, json_mode = consume_json_option(rest, json_mode)
|
|
1300
|
+
if any(command_help.is_help_token(token) for token in rest):
|
|
1301
|
+
return help_command(json_mode, "wait")
|
|
1302
|
+
handles: list[str] = []
|
|
1303
|
+
latest_harness: str | None = None
|
|
1304
|
+
group: str | None = None
|
|
1305
|
+
timeout = wait_cancel_commands.WAIT_DEFAULT_TIMEOUT_SECONDS
|
|
1306
|
+
interval = wait_cancel_commands.WAIT_DEFAULT_INTERVAL_SECONDS
|
|
1307
|
+
completion_report = False
|
|
1308
|
+
i = 0
|
|
1309
|
+
while i < len(rest):
|
|
1310
|
+
token = rest[i]
|
|
1311
|
+
if token == "--latest":
|
|
1312
|
+
if i + 1 >= len(rest):
|
|
1313
|
+
raise DelegateError("missing_harness", "wait --latest requires a harness name.")
|
|
1314
|
+
latest_harness = rest[i + 1]
|
|
1315
|
+
i += 2
|
|
1316
|
+
continue
|
|
1317
|
+
if token == "--timeout":
|
|
1318
|
+
timeout, i = parse_required_positive_int_option(
|
|
1319
|
+
rest,
|
|
1320
|
+
i,
|
|
1321
|
+
option_label="wait --timeout",
|
|
1322
|
+
missing_error="missing_timeout",
|
|
1323
|
+
invalid_error="invalid_timeout",
|
|
1324
|
+
missing_value_description="seconds",
|
|
1325
|
+
)
|
|
1326
|
+
continue
|
|
1327
|
+
if token == "--group":
|
|
1328
|
+
if i + 1 >= len(rest):
|
|
1329
|
+
raise DelegateError("missing_group", "wait --group requires a group name.")
|
|
1330
|
+
group = validate_group(rest[i + 1])
|
|
1331
|
+
i += 2
|
|
1332
|
+
continue
|
|
1333
|
+
if token == "--interval":
|
|
1334
|
+
interval, i = parse_required_positive_int_option(
|
|
1335
|
+
rest,
|
|
1336
|
+
i,
|
|
1337
|
+
option_label="wait --interval",
|
|
1338
|
+
missing_error="missing_interval",
|
|
1339
|
+
invalid_error="invalid_interval",
|
|
1340
|
+
missing_value_description="seconds",
|
|
1341
|
+
)
|
|
1342
|
+
interval = max(interval, wait_cancel_commands.WAIT_MIN_INTERVAL_SECONDS)
|
|
1343
|
+
continue
|
|
1344
|
+
if token == "--completion-report":
|
|
1345
|
+
completion_report = True
|
|
1346
|
+
i += 1
|
|
1347
|
+
continue
|
|
1348
|
+
if token.startswith("-"):
|
|
1349
|
+
raise DelegateError("unknown_option", unknown_option_message("wait", token))
|
|
1350
|
+
handles.append(token)
|
|
1351
|
+
i += 1
|
|
1352
|
+
if not handles and latest_harness is None and group is None:
|
|
1353
|
+
raise DelegateError(
|
|
1354
|
+
"missing_handle", "wait requires a handle, --latest <harness>, or --group <name>."
|
|
1355
|
+
)
|
|
1356
|
+
return ParsedCommand(
|
|
1357
|
+
"wait",
|
|
1358
|
+
global_options=GlobalOptions(json_mode=json_mode, cwd=cwd),
|
|
1359
|
+
wait_command=wait_cancel_commands.WaitCommand(
|
|
1360
|
+
handles=tuple(handles),
|
|
1361
|
+
latest_harness=latest_harness,
|
|
1362
|
+
group=group,
|
|
1363
|
+
timeout_seconds=timeout,
|
|
1364
|
+
interval_seconds=interval,
|
|
1365
|
+
completion_report=completion_report,
|
|
1366
|
+
json_mode=json_mode,
|
|
1367
|
+
),
|
|
1368
|
+
)
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
def parse_cancel(rest: list[str], json_mode: bool, cwd: str | None) -> ParsedCommand:
|
|
1372
|
+
rest, json_mode = consume_json_option(rest, json_mode)
|
|
1373
|
+
if any(command_help.is_help_token(token) for token in rest):
|
|
1374
|
+
return help_command(json_mode, "cancel")
|
|
1375
|
+
handles: list[str] = []
|
|
1376
|
+
for token in rest:
|
|
1377
|
+
if token.startswith("-"):
|
|
1378
|
+
raise DelegateError("unknown_option", unknown_option_message("cancel", token))
|
|
1379
|
+
handles.append(token)
|
|
1380
|
+
if not handles:
|
|
1381
|
+
raise DelegateError("missing_handle", "cancel requires at least one run handle.")
|
|
1382
|
+
return ParsedCommand(
|
|
1383
|
+
"cancel",
|
|
1384
|
+
global_options=GlobalOptions(json_mode=json_mode, cwd=cwd),
|
|
1385
|
+
cancel_command=wait_cancel_commands.CancelCommand(tuple(handles), json_mode=json_mode),
|
|
1386
|
+
)
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def corrected_prompt_file_suffix(command_prefix: list[str] | None, rest: list[str]) -> str:
|
|
1390
|
+
if not command_prefix or "--prompt-file" not in rest:
|
|
1391
|
+
return ""
|
|
1392
|
+
prompt_index = rest.index("--prompt-file")
|
|
1393
|
+
if prompt_index + 1 >= len(rest):
|
|
1394
|
+
return ""
|
|
1395
|
+
prompt_file = rest[prompt_index + 1]
|
|
1396
|
+
before = [token for token in rest[:prompt_index] if token != "--json"]
|
|
1397
|
+
after = rest[prompt_index + 2 :]
|
|
1398
|
+
corrected = [*command_prefix, "--prompt-file", prompt_file, *before, *after]
|
|
1399
|
+
return f" Corrected command: {_shell_command(corrected)}."
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
def corrected_drop_option_suffix(
|
|
1403
|
+
command_prefix: list[str] | None,
|
|
1404
|
+
rest: list[str],
|
|
1405
|
+
index: int,
|
|
1406
|
+
*,
|
|
1407
|
+
takes_value: bool,
|
|
1408
|
+
) -> str:
|
|
1409
|
+
if not command_prefix:
|
|
1410
|
+
return ""
|
|
1411
|
+
drop = 2 if takes_value else 1
|
|
1412
|
+
corrected = [*command_prefix, *rest[:index], *rest[index + drop :]]
|
|
1413
|
+
return f" Corrected command: {_shell_command(corrected)}."
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
def parse_non_negative_int(value: str, *, option: str) -> int:
|
|
1417
|
+
try:
|
|
1418
|
+
parsed = int(value)
|
|
1419
|
+
except ValueError:
|
|
1420
|
+
raise DelegateError("invalid_option_value", f"{option} must be an integer.") from None
|
|
1421
|
+
if parsed < 0:
|
|
1422
|
+
raise DelegateError("invalid_option_value", f"{option} must be non-negative.")
|
|
1423
|
+
return parsed
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
def parse_positive_int(value: str, *, option: str) -> int:
|
|
1427
|
+
parsed = parse_non_negative_int(value, option=option)
|
|
1428
|
+
if parsed < 1:
|
|
1429
|
+
raise DelegateError("invalid_option_value", f"{option} must be at least 1.")
|
|
1430
|
+
return parsed
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
def parse_required_positive_int_option(
|
|
1434
|
+
rest: list[str],
|
|
1435
|
+
index: int,
|
|
1436
|
+
*,
|
|
1437
|
+
option_label: str,
|
|
1438
|
+
missing_error: str,
|
|
1439
|
+
invalid_error: str,
|
|
1440
|
+
missing_value_description: str = "a positive integer",
|
|
1441
|
+
) -> tuple[int, int]:
|
|
1442
|
+
if index + 1 >= len(rest):
|
|
1443
|
+
raise DelegateError(
|
|
1444
|
+
missing_error,
|
|
1445
|
+
f"{option_label} requires {missing_value_description}.",
|
|
1446
|
+
)
|
|
1447
|
+
try:
|
|
1448
|
+
parsed = int(rest[index + 1])
|
|
1449
|
+
except ValueError as exc:
|
|
1450
|
+
raise DelegateError(invalid_error, f"{option_label} must be a positive integer.") from exc
|
|
1451
|
+
if parsed < 1:
|
|
1452
|
+
raise DelegateError(invalid_error, f"{option_label} must be at least 1.")
|
|
1453
|
+
return parsed, index + 2
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
def _require_option_value(rest: list[str], index: int, option: str) -> str:
|
|
1457
|
+
if index + 1 >= len(rest):
|
|
1458
|
+
raise DelegateError("missing_option_value", f"{option} requires a value.")
|
|
1459
|
+
value = rest[index + 1]
|
|
1460
|
+
if value.startswith("-"):
|
|
1461
|
+
raise DelegateError("missing_option_value", f"{option} requires a value.")
|
|
1462
|
+
return value
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
WorktreeOptionSpec = tuple[str, str]
|
|
1466
|
+
|
|
1467
|
+
WORKTREE_OPTION_SPECS: dict[str, dict[str, WorktreeOptionSpec]] = {
|
|
1468
|
+
"list": {
|
|
1469
|
+
"--harness": ("str", "harness"),
|
|
1470
|
+
"--group": ("group", "group"),
|
|
1471
|
+
"--status": ("status", "status"),
|
|
1472
|
+
"--limit": ("positive_int", "limit"),
|
|
1473
|
+
"--no-auto-prune": ("flag", "no_auto_prune"),
|
|
1474
|
+
},
|
|
1475
|
+
"show": {
|
|
1476
|
+
"--latest": ("str", "latest_harness"),
|
|
1477
|
+
},
|
|
1478
|
+
"remove": {
|
|
1479
|
+
"--group": ("group", "group"),
|
|
1480
|
+
"--discard-uncommitted": ("flag", "discard_uncommitted"),
|
|
1481
|
+
"--force-branch": ("flag", "force_branch"),
|
|
1482
|
+
"--force": ("flag", "force"),
|
|
1483
|
+
"--keep-branch": ("flag", "keep_branch"),
|
|
1484
|
+
},
|
|
1485
|
+
"prune": {
|
|
1486
|
+
"--merged": ("flag", "merged"),
|
|
1487
|
+
"--older-than": ("non_negative_int", "older_than_days"),
|
|
1488
|
+
"--harness": ("str", "harness"),
|
|
1489
|
+
"--group": ("group", "group"),
|
|
1490
|
+
"--include-detached": ("flag", "include_detached"),
|
|
1491
|
+
"--dry-run": ("flag", "dry_run"),
|
|
1492
|
+
"--discard-uncommitted": ("flag", "discard_uncommitted"),
|
|
1493
|
+
"--force-branch": ("flag", "force_branch"),
|
|
1494
|
+
"--force": ("flag", "force"),
|
|
1495
|
+
},
|
|
1496
|
+
"gc": {
|
|
1497
|
+
"--dry-run": ("flag", "dry_run"),
|
|
1498
|
+
},
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
def _apply_worktree_option(
|
|
1503
|
+
options: dict[str, object],
|
|
1504
|
+
args: list[str],
|
|
1505
|
+
index: int,
|
|
1506
|
+
option: str,
|
|
1507
|
+
spec: WorktreeOptionSpec,
|
|
1508
|
+
) -> int:
|
|
1509
|
+
kind, attr = spec
|
|
1510
|
+
if kind == "flag":
|
|
1511
|
+
options[attr] = True
|
|
1512
|
+
return index + 1
|
|
1513
|
+
value = _require_option_value(args, index, option)
|
|
1514
|
+
if kind == "group":
|
|
1515
|
+
options[attr] = validate_group(value, option=option)
|
|
1516
|
+
elif kind == "str":
|
|
1517
|
+
options[attr] = value
|
|
1518
|
+
elif kind == "status":
|
|
1519
|
+
if value not in worktree_mgmt.VALID_STATUSES:
|
|
1520
|
+
raise DelegateError(
|
|
1521
|
+
"invalid_option_value",
|
|
1522
|
+
"--status must be present, removed, missing, or unknown.",
|
|
1523
|
+
)
|
|
1524
|
+
options[attr] = value
|
|
1525
|
+
elif kind == "positive_int":
|
|
1526
|
+
options[attr] = parse_positive_int(value, option=option)
|
|
1527
|
+
elif kind == "non_negative_int":
|
|
1528
|
+
options[attr] = parse_non_negative_int(value, option=option)
|
|
1529
|
+
else: # pragma: no cover - table construction bug
|
|
1530
|
+
raise AssertionError(f"unknown worktree option kind: {kind}")
|
|
1531
|
+
return index + 2
|
|
1532
|
+
|
|
1533
|
+
|
|
1534
|
+
def parse_worktree(rest: list[str], json_mode: bool, cwd: str | None) -> ParsedCommand:
|
|
1535
|
+
# Help wins before an action is consumed: `worktree --help`.
|
|
1536
|
+
if rest and command_help.is_help_token(rest[0]):
|
|
1537
|
+
return help_command(json_mode, "worktree")
|
|
1538
|
+
if not rest:
|
|
1539
|
+
raise DelegateError(
|
|
1540
|
+
"missing_worktree_action", "worktree requires list, show, remove, prune, or gc."
|
|
1541
|
+
)
|
|
1542
|
+
action = rest[0]
|
|
1543
|
+
args = rest[1:]
|
|
1544
|
+
# Destructive safety: a help token anywhere in an action's args makes help
|
|
1545
|
+
# win, so no removal/prune ever fires when the user asked for help. An
|
|
1546
|
+
# unknown action with a help token falls back to the worktree overview.
|
|
1547
|
+
if any(command_help.is_help_token(token) for token in args):
|
|
1548
|
+
topic = f"worktree {action}" if action in WORKTREE_OPTION_SPECS else "worktree"
|
|
1549
|
+
return help_command(json_mode, topic)
|
|
1550
|
+
if action not in WORKTREE_OPTION_SPECS:
|
|
1551
|
+
raise DelegateError("unknown_worktree_action", f"Unknown worktree action: {action}")
|
|
1552
|
+
options: dict[str, object] = {}
|
|
1553
|
+
positional: list[str] = []
|
|
1554
|
+
i = 0
|
|
1555
|
+
action_specs = WORKTREE_OPTION_SPECS[action]
|
|
1556
|
+
while i < len(args):
|
|
1557
|
+
token = args[i]
|
|
1558
|
+
spec = action_specs.get(token)
|
|
1559
|
+
if spec is not None:
|
|
1560
|
+
i = _apply_worktree_option(options, args, i, token, spec)
|
|
1561
|
+
continue
|
|
1562
|
+
if token in MISPLACED_GLOBAL_OPTIONS:
|
|
1563
|
+
raise_misplaced_global_option(f"{token} must appear before the subcommand.")
|
|
1564
|
+
if token.startswith("--"):
|
|
1565
|
+
raise DelegateError(
|
|
1566
|
+
"unknown_option", f"worktree {action} does not support option: {token}"
|
|
1567
|
+
)
|
|
1568
|
+
positional.append(token)
|
|
1569
|
+
i += 1
|
|
1570
|
+
|
|
1571
|
+
if action in {"list", "prune", "gc"} and positional:
|
|
1572
|
+
raise DelegateError(
|
|
1573
|
+
"unexpected_argument", f"worktree {action} does not accept positional arguments."
|
|
1574
|
+
)
|
|
1575
|
+
if action == "show":
|
|
1576
|
+
if options.get("latest_harness") is not None:
|
|
1577
|
+
if positional:
|
|
1578
|
+
raise DelegateError(
|
|
1579
|
+
"invalid_option_combination",
|
|
1580
|
+
"worktree show accepts either --latest HARNESS or a handle, not both.",
|
|
1581
|
+
)
|
|
1582
|
+
elif len(positional) != 1:
|
|
1583
|
+
raise DelegateError("missing_handle", "worktree show requires an alias or run id.")
|
|
1584
|
+
else:
|
|
1585
|
+
options["handle"] = positional[0]
|
|
1586
|
+
if action == "remove":
|
|
1587
|
+
if options.get("group") is not None:
|
|
1588
|
+
if positional:
|
|
1589
|
+
raise DelegateError(
|
|
1590
|
+
"invalid_option_combination",
|
|
1591
|
+
"worktree remove accepts either --group NAME or a handle, not both.",
|
|
1592
|
+
)
|
|
1593
|
+
elif len(positional) != 1:
|
|
1594
|
+
raise DelegateError("missing_handle", "worktree remove requires an alias or run id.")
|
|
1595
|
+
else:
|
|
1596
|
+
options["handle"] = positional[0]
|
|
1597
|
+
if options.get("keep_branch") and (options.get("force_branch") or options.get("force")):
|
|
1598
|
+
raise DelegateError(
|
|
1599
|
+
"invalid_option_combination",
|
|
1600
|
+
"worktree remove --keep-branch is mutually exclusive with --force-branch/--force.",
|
|
1601
|
+
)
|
|
1602
|
+
return ParsedCommand(
|
|
1603
|
+
"worktree",
|
|
1604
|
+
global_options=GlobalOptions(json_mode=json_mode, cwd=cwd),
|
|
1605
|
+
worktree=worktree_commands.WorktreeCommand(
|
|
1606
|
+
action=action,
|
|
1607
|
+
json_mode=json_mode,
|
|
1608
|
+
**options,
|
|
1609
|
+
),
|
|
1610
|
+
)
|
|
1611
|
+
|
|
1612
|
+
|
|
1613
|
+
def parse_profiles(
|
|
1614
|
+
rest: list[str],
|
|
1615
|
+
json_mode: bool,
|
|
1616
|
+
cwd: str | None,
|
|
1617
|
+
auth_profile: str | None,
|
|
1618
|
+
) -> ParsedCommand:
|
|
1619
|
+
rest, json_mode = consume_json_option(rest, json_mode)
|
|
1620
|
+
if any(command_help.is_help_token(token) for token in rest):
|
|
1621
|
+
return help_command(json_mode, "profiles")
|
|
1622
|
+
require_no_extra(rest, "profiles")
|
|
1623
|
+
return ParsedCommand(
|
|
1624
|
+
"profiles",
|
|
1625
|
+
global_options=GlobalOptions(json_mode=json_mode, cwd=cwd, auth_profile=auth_profile),
|
|
1626
|
+
profiles_command=profile_commands.ProfilesCommand(json_mode=json_mode),
|
|
1627
|
+
)
|