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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. delegate_agent/__init__.py +5 -0
  2. delegate_agent/archived_logs.py +44 -0
  3. delegate_agent/argv_builders.py +423 -0
  4. delegate_agent/argv_utils.py +36 -0
  5. delegate_agent/capability_commands.py +99 -0
  6. delegate_agent/cli.py +987 -0
  7. delegate_agent/cli_parser.py +1627 -0
  8. delegate_agent/command_errors.py +29 -0
  9. delegate_agent/command_help.py +1264 -0
  10. delegate_agent/config.py +1117 -0
  11. delegate_agent/config_commands.py +143 -0
  12. delegate_agent/constants.py +50 -0
  13. delegate_agent/describe_payload.py +1018 -0
  14. delegate_agent/errors.py +32 -0
  15. delegate_agent/git_utils.py +180 -0
  16. delegate_agent/harness_events.py +719 -0
  17. delegate_agent/inspection_commands.py +104 -0
  18. delegate_agent/isolation.py +316 -0
  19. delegate_agent/json_types.py +18 -0
  20. delegate_agent/log_output.py +78 -0
  21. delegate_agent/private_io.py +109 -0
  22. delegate_agent/profile_commands.py +42 -0
  23. delegate_agent/profile_guard.py +116 -0
  24. delegate_agent/profiles.py +389 -0
  25. delegate_agent/prompt_instructions.py +39 -0
  26. delegate_agent/prompt_transport.py +12 -0
  27. delegate_agent/reasoning.py +916 -0
  28. delegate_agent/redaction.py +193 -0
  29. delegate_agent/rendering.py +573 -0
  30. delegate_agent/request_build.py +1681 -0
  31. delegate_agent/request_models.py +191 -0
  32. delegate_agent/retention.py +321 -0
  33. delegate_agent/run_metadata.py +78 -0
  34. delegate_agent/run_output_commands.py +645 -0
  35. delegate_agent/run_registry.py +747 -0
  36. delegate_agent/run_status.py +300 -0
  37. delegate_agent/runner.py +1830 -0
  38. delegate_agent/safe_workspace.py +821 -0
  39. delegate_agent/snapshot_view.py +229 -0
  40. delegate_agent/wait_cancel_commands.py +559 -0
  41. delegate_agent/worktree_commands.py +218 -0
  42. delegate_agent/worktree_execution.py +654 -0
  43. delegate_agent/worktree_gc.py +529 -0
  44. delegate_agent/worktree_mgmt.py +782 -0
  45. delegate_agent/worktree_records.py +232 -0
  46. delegate_agent/worktree_remove.py +547 -0
  47. delegate_agent/worktree_summary.py +242 -0
  48. delegate_agent/wsl.py +65 -0
  49. delegate_agent_cli-0.11.0.dist-info/METADATA +325 -0
  50. delegate_agent_cli-0.11.0.dist-info/RECORD +54 -0
  51. delegate_agent_cli-0.11.0.dist-info/WHEEL +5 -0
  52. delegate_agent_cli-0.11.0.dist-info/entry_points.txt +2 -0
  53. delegate_agent_cli-0.11.0.dist-info/licenses/LICENSE +21 -0
  54. delegate_agent_cli-0.11.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ """Delegate Agent CLI package."""
2
+
3
+ VERSION = "0.11.0"
4
+
5
+ __all__ = ["VERSION"]
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import tarfile
4
+ from pathlib import Path
5
+
6
+ from delegate_agent.json_types import JsonObject, is_non_negative_int
7
+
8
+
9
+ def archive_dir(registry_root: Path) -> Path:
10
+ return registry_root / "archive"
11
+
12
+
13
+ def archive_path(registry_root: Path, run_id: str) -> Path:
14
+ return archive_dir(registry_root) / f"{run_id}.tar.gz"
15
+
16
+
17
+ def state_log_byte_sizes(state: JsonObject | None) -> tuple[int, int] | None:
18
+ if not state:
19
+ return None
20
+ stdout_bytes = state.get("stdoutBytes")
21
+ stderr_bytes = state.get("stderrBytes")
22
+ if is_non_negative_int(stdout_bytes) and is_non_negative_int(stderr_bytes):
23
+ return stdout_bytes, stderr_bytes
24
+ return None
25
+
26
+
27
+ def archive_log_byte_sizes(
28
+ archive_file: Path,
29
+ *,
30
+ stdout_log: str,
31
+ stderr_log: str,
32
+ ) -> tuple[int, int]:
33
+ try:
34
+ with tarfile.open(archive_file, "r:gz") as archive:
35
+ stdout_bytes = 0
36
+ stderr_bytes = 0
37
+ for member in archive.getmembers():
38
+ if member.name == stdout_log:
39
+ stdout_bytes = member.size
40
+ elif member.name == stderr_log:
41
+ stderr_bytes = member.size
42
+ return stdout_bytes, stderr_bytes
43
+ except FileNotFoundError:
44
+ return 0, 0
@@ -0,0 +1,423 @@
1
+ """Per-engine command-line construction.
2
+
3
+ Given a resolved config section, mode, workspace, model, and prompt, build the
4
+ exact argv each harness CLI (cursor / droid / kimi / claude / codex) expects.
5
+ The builders stay deliberately explicit rather than table-driven: prompt
6
+ transport, sandbox flags, MCP handling, and reasoning wiring differ materially
7
+ per engine, and the safe-mode flag sets here are security-load-bearing — codex
8
+ never emits its bypass flags in safe mode, cursor/droid/kimi/claude apply their
9
+ read-only flag sets. Preserve those branches exactly.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from delegate_agent import reasoning
15
+ from delegate_agent.constants import MODE_CALL, MODE_SAFE, MODE_WORK, validate_mode
16
+ from delegate_agent.errors import DelegateError
17
+ from delegate_agent.json_types import JsonObject
18
+ from delegate_agent.prompt_instructions import SKILL_REVIEW_PREFIX
19
+ from delegate_agent.prompt_transport import (
20
+ CURSOR_PROMPT_REDACTION,
21
+ DROID_PROMPT_FILE_ARG_PLACEHOLDER,
22
+ PROMPT_FILE_ARG_PLACEHOLDER,
23
+ PROMPT_TRANSPORT_ARGV,
24
+ PROMPT_TRANSPORT_FILE,
25
+ PROMPT_TRANSPORT_STDIN,
26
+ )
27
+
28
+ # Each engine's safe-review prefix is one shared body behind a per-engine label.
29
+ # Cursor uses "Delegate review mode"; the rest use "Delegate <Engine> safe mode".
30
+ _SAFE_REVIEW_BODY = (
31
+ "(read-only review/investigation): "
32
+ "Inspect, map, and reason about the workspace. "
33
+ "You may propose patches or commands in text, but do not edit, create, delete, "
34
+ "format, commit, or otherwise mutate files or repo state. "
35
+ "If a write is blocked, do not retry or work around it; continue with a "
36
+ "text-only report.\n\n"
37
+ )
38
+ _SAFE_REVIEW_LABEL_BY_ENGINE = {
39
+ "cursor": "Delegate review mode",
40
+ "codex": "Delegate Codex safe mode",
41
+ "claude": "Delegate Claude safe mode",
42
+ "grok": "Delegate Grok safe mode",
43
+ "droid": "Delegate Droid safe mode",
44
+ "kimi": "Delegate Kimi safe mode",
45
+ }
46
+ SAFE_REVIEW_PREFIX_BY_ENGINE: dict[str, str] = {
47
+ engine: f"{label} {_SAFE_REVIEW_BODY}" for engine, label in _SAFE_REVIEW_LABEL_BY_ENGINE.items()
48
+ }
49
+
50
+ CLAUDE_SAFE_TOOLS = "Read,Grep,Glob,Bash"
51
+
52
+ CLAUDE_SAFE_ALLOWED_TOOLS = (
53
+ "Bash(git diff:*),Bash(git status:*),Bash(git show:*),Bash(git log:*),"
54
+ "Bash(rg:*),Bash(grep:*),Bash(ls:*)"
55
+ )
56
+
57
+
58
+ def redacted_prompt_argv(argv: list[str], replacement: str = CURSOR_PROMPT_REDACTION) -> list[str]:
59
+ if not argv:
60
+ return []
61
+ redacted = list(argv)
62
+ redacted[-1] = replacement
63
+ return redacted
64
+
65
+
66
+ def _prefix_safe_prompt(prompt: str, engine: str) -> str:
67
+ """Idempotently prepend the engine's safe-review prefix."""
68
+ safe_prefix = SAFE_REVIEW_PREFIX_BY_ENGINE[engine]
69
+ if prompt.startswith(safe_prefix):
70
+ return prompt
71
+ return f"{safe_prefix}{prompt}"
72
+
73
+
74
+ def prefix_cursor_safe_prompt(prompt: str) -> str:
75
+ return _prefix_safe_prompt(prompt, "cursor")
76
+
77
+
78
+ def prefix_kimi_safe_prompt(prompt: str) -> str:
79
+ return _prefix_safe_prompt(prompt, "kimi")
80
+
81
+
82
+ def prefix_droid_safe_prompt(prompt: str) -> str:
83
+ safe_prefix = SAFE_REVIEW_PREFIX_BY_ENGINE["droid"]
84
+ if prompt.startswith(safe_prefix):
85
+ return prompt
86
+ skill_prefix = SKILL_REVIEW_PREFIX
87
+ if prompt.startswith(skill_prefix):
88
+ insert_at = len(skill_prefix)
89
+ if prompt[insert_at:].startswith(safe_prefix):
90
+ return prompt
91
+ return prompt[:insert_at] + safe_prefix + prompt[insert_at:]
92
+ return f"{safe_prefix}{prompt}"
93
+
94
+
95
+ def _harness_bypass_enabled(config: JsonObject, mode: str, engine: str) -> bool:
96
+ """Return true only for explicit harness-scoped bypass policy.
97
+
98
+ Delegate's historical policy profiles were Codex-oriented. Requiring the
99
+ bypass to live under policy.harness.<engine>.work prevents a global
100
+ external-sandbox profile from silently broadening native harness permissions.
101
+ """
102
+ if mode != MODE_WORK:
103
+ return False
104
+ policy = config.get("policy")
105
+ if not isinstance(policy, dict):
106
+ return False
107
+ harnesses = policy.get("harness")
108
+ if not isinstance(harnesses, dict):
109
+ return False
110
+ engine_policy = harnesses.get(engine)
111
+ if not isinstance(engine_policy, dict):
112
+ return False
113
+ work_policy = engine_policy.get(MODE_WORK)
114
+ return isinstance(work_policy, dict) and work_policy.get("bypassApprovalsAndSandbox") is True
115
+
116
+
117
+ def _claude_harness_bypass_enabled(config: JsonObject, mode: str) -> bool:
118
+ return _harness_bypass_enabled(config, mode, "claude")
119
+
120
+
121
+ def _grok_harness_bypass_enabled(config: JsonObject, mode: str) -> bool:
122
+ return _harness_bypass_enabled(config, mode, "grok")
123
+
124
+
125
+ def build_cursor_argv(
126
+ prefix: list[str],
127
+ mode: str,
128
+ workspace: str,
129
+ model: str,
130
+ prompt: str,
131
+ *,
132
+ stream_capture: bool = True,
133
+ call_read_only: bool = False,
134
+ ) -> list[str]:
135
+ argv = [*prefix, "--workspace", workspace, "-p", "--trust"]
136
+ if mode == MODE_WORK:
137
+ argv.extend(["--approve-mcps", "--force"])
138
+ elif mode == MODE_SAFE:
139
+ prompt = prefix_cursor_safe_prompt(prompt)
140
+ elif mode == MODE_CALL:
141
+ # Call defaults to work-level capability ("work minus a repo"); --read-only
142
+ # drops the write flags for the stateless judge/completion contract.
143
+ if not call_read_only:
144
+ argv.extend(["--approve-mcps", "--force"])
145
+ else:
146
+ validate_mode(mode)
147
+ if stream_capture:
148
+ argv.extend(["--model", model, "--print", "--output-format", "stream-json", prompt])
149
+ else:
150
+ argv.extend(["--model", model, "--output-format", "text", prompt])
151
+ return argv
152
+
153
+
154
+ def build_droid_argv(
155
+ binary: str,
156
+ mode: str,
157
+ workspace: str,
158
+ model: str,
159
+ prompt: str,
160
+ *,
161
+ stream_capture: bool = True,
162
+ reasoning_capability: reasoning.ReasoningCapability | None = None,
163
+ prompt_transport: str = PROMPT_TRANSPORT_ARGV,
164
+ call_read_only: bool = False,
165
+ ) -> list[str]:
166
+ argv = [binary, "exec", "--cwd", workspace]
167
+ if mode == MODE_WORK:
168
+ argv.append("--skip-permissions-unsafe")
169
+ elif mode == MODE_SAFE:
170
+ prompt = prefix_droid_safe_prompt(prompt)
171
+ elif mode == MODE_CALL:
172
+ if not call_read_only:
173
+ argv.append("--skip-permissions-unsafe")
174
+ else:
175
+ validate_mode(mode)
176
+ if reasoning_capability is not None:
177
+ argv.extend(["--reasoning-effort", reasoning_capability.effort])
178
+ if stream_capture:
179
+ argv.extend(["--model", model, "--output-format", "stream-json"])
180
+ else:
181
+ argv.extend(["--model", model])
182
+ if prompt_transport == PROMPT_TRANSPORT_ARGV:
183
+ argv.append(prompt)
184
+ elif prompt_transport == PROMPT_TRANSPORT_FILE:
185
+ argv.extend(["--file", DROID_PROMPT_FILE_ARG_PLACEHOLDER])
186
+ elif prompt_transport != PROMPT_TRANSPORT_STDIN:
187
+ raise DelegateError(
188
+ "invalid_prompt_transport",
189
+ f"Unsupported Droid prompt transport: {prompt_transport}",
190
+ )
191
+ return argv
192
+
193
+
194
+ def build_kimi_argv(
195
+ kimi: JsonObject,
196
+ mode: str,
197
+ workspace: str,
198
+ model: str | None,
199
+ prompt: str,
200
+ *,
201
+ stream_capture: bool = True,
202
+ ) -> list[str]:
203
+ argv = [str(kimi["binary"])]
204
+ if mode == MODE_SAFE:
205
+ prompt = prefix_kimi_safe_prompt(prompt)
206
+ elif mode not in (MODE_WORK, MODE_CALL):
207
+ validate_mode(mode)
208
+ if model:
209
+ argv.extend(["--model", model])
210
+ if stream_capture:
211
+ argv.extend(["--output-format", "stream-json"])
212
+ argv.extend(["--prompt", prompt])
213
+ return argv
214
+
215
+
216
+ def build_claude_argv(
217
+ claude: JsonObject,
218
+ mode: str,
219
+ model: str | None,
220
+ policy: JsonObject,
221
+ *,
222
+ stream_capture: bool = True,
223
+ reasoning_effort: str | None = None,
224
+ allow_bypass_permissions: bool = False,
225
+ call_read_only: bool = False,
226
+ ) -> list[str]:
227
+ argv = [
228
+ str(claude["binary"]),
229
+ "-p",
230
+ "--output-format",
231
+ "stream-json" if stream_capture else "text",
232
+ "--input-format",
233
+ "text",
234
+ ]
235
+ if mode == MODE_SAFE:
236
+ argv.extend(
237
+ [
238
+ "--permission-mode",
239
+ "plan",
240
+ "--tools",
241
+ CLAUDE_SAFE_TOOLS,
242
+ "--allowedTools",
243
+ CLAUDE_SAFE_ALLOWED_TOOLS,
244
+ "--strict-mcp-config",
245
+ ]
246
+ )
247
+ elif mode == MODE_WORK:
248
+ permission_mode = (
249
+ "bypassPermissions"
250
+ if allow_bypass_permissions and policy.get("bypassApprovalsAndSandbox") is True
251
+ else str(claude.get("workPermissionMode", "auto"))
252
+ )
253
+ argv.extend(["--permission-mode", permission_mode])
254
+ elif mode == MODE_CALL:
255
+ if call_read_only:
256
+ argv.extend(
257
+ [
258
+ "--permission-mode",
259
+ "plan",
260
+ "--tools",
261
+ CLAUDE_SAFE_TOOLS,
262
+ "--allowedTools",
263
+ CLAUDE_SAFE_ALLOWED_TOOLS,
264
+ "--strict-mcp-config",
265
+ ]
266
+ )
267
+ else:
268
+ argv.extend(["--permission-mode", str(claude.get("workPermissionMode", "auto"))])
269
+ else:
270
+ validate_mode(mode)
271
+ if claude.get("noSessionPersistence", True) is True:
272
+ argv.append("--no-session-persistence")
273
+ if claude.get("bare", False) is True:
274
+ argv.append("--bare")
275
+ if model:
276
+ argv.extend(["--model", model])
277
+ if reasoning_effort is not None:
278
+ argv.extend(["--effort", reasoning_effort])
279
+ return argv
280
+
281
+
282
+ def build_grok_argv(
283
+ grok: JsonObject,
284
+ mode: str,
285
+ workspace: str,
286
+ model: str | None,
287
+ policy: JsonObject,
288
+ *,
289
+ stream_capture: bool = True,
290
+ reasoning_effort: str | None = None,
291
+ allow_bypass_permissions: bool = False,
292
+ prompt_transport: str = PROMPT_TRANSPORT_FILE,
293
+ call_read_only: bool = False,
294
+ ) -> list[str]:
295
+ argv = [str(grok["binary"]), "--cwd", workspace]
296
+ if stream_capture:
297
+ argv.extend(["--output-format", "streaming-json"])
298
+ else:
299
+ argv.extend(["--output-format", "plain"])
300
+ if mode == MODE_SAFE:
301
+ argv.extend(
302
+ [
303
+ "--permission-mode",
304
+ str(grok.get("safePermissionMode", "dontAsk")),
305
+ "--sandbox",
306
+ str(grok.get("safeSandbox", "read-only")),
307
+ ]
308
+ )
309
+ elif mode == MODE_WORK:
310
+ if allow_bypass_permissions and policy.get("bypassApprovalsAndSandbox") is True:
311
+ argv.extend(["--permission-mode", "bypassPermissions", "--always-approve"])
312
+ else:
313
+ argv.extend(["--permission-mode", str(grok.get("workPermissionMode", "auto"))])
314
+ work_sandbox = grok.get("workSandbox")
315
+ if isinstance(work_sandbox, str) and work_sandbox:
316
+ argv.extend(["--sandbox", work_sandbox])
317
+ elif mode == MODE_CALL:
318
+ if call_read_only:
319
+ argv.extend(
320
+ [
321
+ "--permission-mode",
322
+ str(grok.get("safePermissionMode", "dontAsk")),
323
+ "--sandbox",
324
+ str(grok.get("safeSandbox", "read-only")),
325
+ ]
326
+ )
327
+ else:
328
+ argv.extend(["--permission-mode", str(grok.get("workPermissionMode", "auto"))])
329
+ work_sandbox = grok.get("workSandbox")
330
+ if isinstance(work_sandbox, str) and work_sandbox:
331
+ argv.extend(["--sandbox", work_sandbox])
332
+ else:
333
+ validate_mode(mode)
334
+ if policy.get("webSearch") is not True and grok.get("disableWebSearch", True) is True:
335
+ argv.append("--disable-web-search")
336
+ if grok.get("noSubagents", False) is True:
337
+ argv.append("--no-subagents")
338
+ if model:
339
+ argv.extend(["--model", model])
340
+ if reasoning_effort is not None:
341
+ argv.extend(["--effort", reasoning_effort])
342
+ if prompt_transport != PROMPT_TRANSPORT_FILE:
343
+ raise DelegateError(
344
+ "invalid_prompt_transport",
345
+ f"Unsupported Grok prompt transport: {prompt_transport}",
346
+ )
347
+ argv.extend(["--prompt-file", PROMPT_FILE_ARG_PLACEHOLDER])
348
+ return argv
349
+
350
+
351
+ def build_codex_argv(
352
+ codex: JsonObject,
353
+ mode: str,
354
+ workspace: str,
355
+ model: str | None,
356
+ prompt: str,
357
+ policy: JsonObject,
358
+ *,
359
+ workspace_kind: str,
360
+ stream_capture: bool = True,
361
+ reasoning_capability: reasoning.ReasoningCapability | None = None,
362
+ prompt_transport: str = PROMPT_TRANSPORT_ARGV,
363
+ output_schema: str | None = None,
364
+ call_read_only: bool = False,
365
+ ) -> list[str]:
366
+ binary = str(codex["binary"])
367
+ argv = [binary]
368
+ # Safe mode is read-only by contract: never emit the dangerous bypass flags,
369
+ # even if a policy block somehow carries them. Config validation rejects such
370
+ # configs up front, but enforce the invariant structurally here too.
371
+ # Work-level call ("work minus a repo") gets the workSandbox but never the
372
+ # bypass flags — those stay bound to real work mode.
373
+ elevated = mode == MODE_WORK
374
+ call_write = mode == MODE_CALL and not call_read_only
375
+ write_sandbox = mode == MODE_WORK or call_write
376
+ bypass_sandbox = elevated and policy.get("bypassApprovalsAndSandbox") is True
377
+ bypass_hook_trust = elevated and policy.get("bypassHookTrust") is True
378
+ if policy.get("webSearch") is True:
379
+ argv.append("--search")
380
+ if not bypass_sandbox:
381
+ argv.extend(["--ask-for-approval", "never"])
382
+ if codex.get("profile"):
383
+ argv.extend(["--profile", str(codex["profile"])])
384
+ if model:
385
+ argv.extend(["--model", model])
386
+ if reasoning_capability is not None:
387
+ argv.extend(
388
+ [
389
+ "-c",
390
+ f'model_reasoning_effort="{reasoning_capability.effort}"',
391
+ ]
392
+ )
393
+ argv.append("exec")
394
+ argv.extend(["--cd", workspace])
395
+ if output_schema is not None:
396
+ argv.extend(["--output-schema", output_schema])
397
+ if codex.get("ignoreUserConfig") is True:
398
+ argv.append("--ignore-user-config")
399
+ if workspace_kind != "git":
400
+ argv.append("--skip-git-repo-check")
401
+ if bypass_sandbox:
402
+ argv.append("--dangerously-bypass-approvals-and-sandbox")
403
+ else:
404
+ sandbox = codex["workSandbox"] if write_sandbox else "read-only"
405
+ argv.extend(["--sandbox", str(sandbox)])
406
+ if write_sandbox and sandbox == "workspace-write" and policy.get("networkAccess") is True:
407
+ argv.extend(["-c", "sandbox_workspace_write.network_access=true"])
408
+ if bypass_hook_trust:
409
+ argv.append("--dangerously-bypass-hook-trust")
410
+ if stream_capture:
411
+ argv.extend(["--color", "never", "--json"])
412
+ if codex.get("ephemeral", True) is True:
413
+ argv.append("--ephemeral")
414
+ if prompt_transport == PROMPT_TRANSPORT_ARGV:
415
+ argv.append(prompt)
416
+ elif prompt_transport == PROMPT_TRANSPORT_STDIN:
417
+ argv.append("-")
418
+ else:
419
+ raise DelegateError(
420
+ "invalid_prompt_transport",
421
+ f"Unsupported Codex prompt transport: {prompt_transport}",
422
+ )
423
+ return argv
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from delegate_agent.request_models import Request
7
+
8
+ WORKSPACE_FLAG_BY_ENGINE: dict[str, str | None] = {
9
+ "cursor": "--workspace",
10
+ "codex": "--cd",
11
+ "droid": "--cwd",
12
+ "kimi": None,
13
+ "claude": None,
14
+ "grok": "--cwd",
15
+ }
16
+
17
+
18
+ def replace_argv_after_flag(argv: list[str], flag: str, value: str) -> list[str]:
19
+ updated = list(argv)
20
+ for index, token in enumerate(updated):
21
+ if token == flag and index + 1 < len(updated):
22
+ updated[index + 1] = value
23
+ return updated
24
+ return updated
25
+
26
+
27
+ def replace_workspace_arg_in_argv(engine: str, argv: list[str], value: str) -> list[str]:
28
+ flag = WORKSPACE_FLAG_BY_ENGINE.get(engine)
29
+ if flag is None:
30
+ return list(argv)
31
+ return replace_argv_after_flag(argv, flag, value)
32
+
33
+
34
+ def public_argv(request: Request) -> list[str]:
35
+ """Return the display-safe argv for a request (redacted form when present)."""
36
+ return list(request.display_argv if request.display_argv is not None else request.argv)
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TextIO
5
+
6
+ from delegate_agent import command_errors, profiles, reasoning
7
+ from delegate_agent import rendering as delegate_rendering
8
+ from delegate_agent.config import harness_binary
9
+ from delegate_agent.json_types import JsonObject
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class CapabilitiesCommand:
14
+ refresh: bool = False
15
+ json_mode: bool = False
16
+
17
+
18
+ class CapabilitiesError(command_errors.CommandError):
19
+ pass
20
+
21
+
22
+ def capabilities_payload(config: JsonObject, config_source: str, workspace: str) -> JsonObject:
23
+ cache = reasoning.load_reasoning_capability_cache(workspace)
24
+ return {
25
+ "ok": True,
26
+ "configSource": config_source,
27
+ "cachePath": str(reasoning.reasoning_capability_cache_path(workspace)),
28
+ "reasoning": reasoning.build_reasoning_capabilities_payload(config, cache),
29
+ }
30
+
31
+
32
+ def _refresh_payload(
33
+ config: JsonObject,
34
+ workspace: str,
35
+ *,
36
+ auth_profile_override: str | None = None,
37
+ ) -> JsonObject:
38
+ profile = profiles.resolve_active_profile(
39
+ config,
40
+ profiles.child_environment(),
41
+ cli_override=auth_profile_override,
42
+ )
43
+ try:
44
+ existing_cache = reasoning.load_reasoning_capability_cache(workspace)
45
+ refreshed_cache = reasoning.refresh_reasoning_capabilities(
46
+ cwd=workspace,
47
+ codex_binary=harness_binary(config, "codex"),
48
+ env=profiles.child_environment(overrides=profile.env),
49
+ )
50
+ cache = reasoning.merge_reasoning_capability_cache(existing_cache, refreshed_cache)
51
+ cache_path = reasoning.write_reasoning_capability_cache(workspace, cache)
52
+ except reasoning.ReasoningCapabilityError as exc:
53
+ raise CapabilitiesError(exc.error, exc.message) from exc
54
+ return {
55
+ "ok": True,
56
+ "refreshed": True,
57
+ "cachePath": str(cache_path),
58
+ "reasoning": reasoning.build_reasoning_capabilities_payload(config, cache),
59
+ }
60
+
61
+
62
+ def emit(
63
+ command: CapabilitiesCommand,
64
+ *,
65
+ config: JsonObject,
66
+ config_source: str,
67
+ workspace: str,
68
+ auth_profile_override: str | None = None,
69
+ stdout: TextIO,
70
+ ) -> int:
71
+ payload = (
72
+ _refresh_payload(config, workspace, auth_profile_override=auth_profile_override)
73
+ if command.refresh
74
+ else capabilities_payload(config, config_source, workspace)
75
+ )
76
+
77
+ if command.json_mode:
78
+ delegate_rendering.print_json(payload, stdout)
79
+ else:
80
+ print(f"reasoning capabilities: {payload['cachePath']}", file=stdout)
81
+ harnesses = payload["reasoning"]["harnesses"]
82
+ if not isinstance(harnesses, dict):
83
+ return 0
84
+ for harness, harness_payload in harnesses.items():
85
+ if not isinstance(harness_payload, dict):
86
+ continue
87
+ supported = harness_payload.get("supported")
88
+ warning = harness_payload.get("warning")
89
+ if isinstance(warning, str):
90
+ print(f"{harness}: {warning}", file=stdout)
91
+ continue
92
+ if isinstance(supported, list):
93
+ print(f"{harness}: {len(supported)} static effort level(s)", file=stdout)
94
+ continue
95
+ models = harness_payload.get("models")
96
+ if not isinstance(models, dict):
97
+ continue
98
+ print(f"{harness}: {len(models)} model(s)", file=stdout)
99
+ return 0