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,1018 @@
1
+ """Introspection surfaces.
2
+
3
+ Builds the JSON payloads behind ``delegate runtime``, ``config``, ``models``,
4
+ and ``describe``, plus the stdout emitters for models/describe/command-help/
5
+ agent-help. Reference output only — no run execution happens here.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import TextIO
14
+
15
+ from delegate_agent import VERSION, command_help, reasoning, redaction
16
+ from delegate_agent import config as delegate_config
17
+ from delegate_agent import rendering as delegate_rendering
18
+ from delegate_agent.argv_builders import (
19
+ _claude_harness_bypass_enabled,
20
+ _grok_harness_bypass_enabled,
21
+ build_claude_argv,
22
+ build_codex_argv,
23
+ build_grok_argv,
24
+ )
25
+ from delegate_agent.command_help import SAFE_WORKSPACE_SYNC_NOTE
26
+ from delegate_agent.config import config_path
27
+ from delegate_agent.constants import (
28
+ KNOWN_ENGINES,
29
+ MODE_CALL,
30
+ MODE_SAFE,
31
+ MODE_WORK,
32
+ MODEL_SUMMARY_ENGINES,
33
+ )
34
+ from delegate_agent.errors import EXIT_OK, DelegateError
35
+ from delegate_agent.json_types import JsonObject
36
+ from delegate_agent.prompt_transport import (
37
+ DROID_PROMPT_FILE_DISPLAY,
38
+ PROMPT_FILE_ARG_PLACEHOLDER,
39
+ PROMPT_FILE_DISPLAY,
40
+ PROMPT_TRANSPORT_ARGV,
41
+ PROMPT_TRANSPORT_FILE,
42
+ PROMPT_TRANSPORT_STDIN,
43
+ )
44
+ from delegate_agent.request_build import _resolve_default_model
45
+
46
+ CONFIG_ENV = delegate_config.CONFIG_ENV
47
+
48
+
49
+ def _scrub_discovery_payload(payload: JsonObject) -> JsonObject:
50
+ scrubbed = redaction.redact_value(payload)
51
+ return scrubbed if isinstance(scrubbed, dict) else {"ok": False}
52
+
53
+
54
+ def _text_or_none(value: object) -> str:
55
+ return value if isinstance(value, str) and value else "(none)"
56
+
57
+
58
+ def _text_argv_prefix_label(value: object) -> str:
59
+ if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
60
+ return "(none)"
61
+ return " ".join(value) if value else "(none)"
62
+
63
+
64
+ def _global_options() -> list[str]:
65
+ return [option.flag for option in command_help.GLOBAL_OPTIONS]
66
+
67
+
68
+ def _commands_catalog() -> list[JsonObject]:
69
+ return [
70
+ {
71
+ "name": spec.name,
72
+ "command": spec.name,
73
+ "summary": spec.summary,
74
+ "usage": list(spec.usage),
75
+ "arguments": [
76
+ {
77
+ "name": arg.name,
78
+ "required": arg.required,
79
+ "description": arg.description,
80
+ }
81
+ for arg in spec.arguments
82
+ ],
83
+ "options": [
84
+ {
85
+ "flag": option.flag,
86
+ "arg": option.arg,
87
+ "description": option.description,
88
+ }
89
+ for option in spec.options
90
+ ],
91
+ "launchOptions": [option.flag for option in spec.options],
92
+ }
93
+ for spec in command_help.COMMAND_SPECS.values()
94
+ ]
95
+
96
+
97
+ def _launch_options() -> list[str]:
98
+ flags: list[str] = []
99
+ for command in ("cursor", "droid", "codex", "claude", "grok", "kimi"):
100
+ spec = command_help.COMMAND_SPECS[command]
101
+ for option in spec.options:
102
+ if option.flag not in flags:
103
+ flags.append(option.flag)
104
+ return flags
105
+
106
+
107
+ def _profiles_config_payload(config: JsonObject) -> JsonObject:
108
+ section = config.get("profiles")
109
+ if not isinstance(section, dict):
110
+ return {"detectFrom": [], "default": None, "definedProfiles": [], "envKeys": {}}
111
+ detect_from = section.get("detectFrom")
112
+ definitions = section.get("definitions")
113
+ profile_names = sorted(definitions) if isinstance(definitions, dict) else []
114
+ env_keys: JsonObject = {}
115
+ if isinstance(definitions, dict):
116
+ for name in profile_names:
117
+ entry = definitions.get(name)
118
+ env = entry.get("env") if isinstance(entry, dict) else None
119
+ env_keys[name] = sorted(env) if isinstance(env, dict) else []
120
+ return {
121
+ "detectFrom": list(detect_from) if isinstance(detect_from, list) else [],
122
+ "default": section.get("default") if isinstance(section.get("default"), str) else None,
123
+ "definedProfiles": profile_names,
124
+ "envKeys": env_keys,
125
+ }
126
+
127
+
128
+ def runtime_payload() -> JsonObject:
129
+ # modulePath reports the CLI entrypoint (cli.py), a sibling of this module;
130
+ # callers and tests rely on it pointing at cli.py, not at describe_payload.py.
131
+ module_path = Path(__file__).resolve().with_name("cli.py")
132
+ return {
133
+ "version": VERSION,
134
+ "modulePath": str(module_path),
135
+ "packageRoot": str(module_path.parents[1]),
136
+ "executable": sys.argv[0],
137
+ "pythonExecutable": sys.executable,
138
+ }
139
+
140
+
141
+ def config_resolution_payload(config_source: str, workspace: Path | None = None) -> JsonObject:
142
+ layers: list[JsonObject] = [{"name": "embedded-default", "applied": True}]
143
+ global_path = delegate_config.default_config_path()
144
+ global_exists = global_path.exists()
145
+ layers.append(
146
+ {
147
+ "name": "user",
148
+ "path": str(global_path),
149
+ "exists": global_exists,
150
+ "applied": global_exists,
151
+ }
152
+ )
153
+ if workspace is not None:
154
+ workspace_path = delegate_config.workspace_config_path(workspace)
155
+ workspace_exists = workspace_path.exists()
156
+ layers.append(
157
+ {
158
+ "name": "workspace",
159
+ "path": str(workspace_path),
160
+ "exists": workspace_exists,
161
+ "applied": workspace_exists,
162
+ }
163
+ )
164
+ explicit = os.environ.get(CONFIG_ENV)
165
+ if explicit:
166
+ explicit_path = Path(explicit).expanduser()
167
+ explicit_exists = explicit_path.exists()
168
+ layers.append(
169
+ {
170
+ "name": CONFIG_ENV,
171
+ "path": str(explicit_path),
172
+ "exists": explicit_exists,
173
+ "applied": explicit_exists,
174
+ }
175
+ )
176
+ return {
177
+ "source": config_source,
178
+ "effectiveConfigPath": str(config_path()),
179
+ "workspace": str(workspace) if workspace is not None else None,
180
+ "layers": layers,
181
+ }
182
+
183
+
184
+ def models_payload(
185
+ config: JsonObject,
186
+ config_source: str,
187
+ workspace: Path | None = None,
188
+ ) -> JsonObject:
189
+ cache = reasoning.load_reasoning_capability_cache(workspace) if workspace is not None else None
190
+ return {
191
+ "ok": True,
192
+ "configSource": config_source,
193
+ "configResolution": config_resolution_payload(config_source, workspace),
194
+ "runtime": runtime_payload(),
195
+ "reasoningAliases": reasoning.build_alias_reasoning_summaries(config, cache),
196
+ "cursor": {
197
+ "defaultModel": config["cursor"]["defaultModel"],
198
+ "argvPrefix": config["cursor"]["argvPrefix"],
199
+ "defaultReasoningEffort": config["cursor"].get("defaultReasoningEffort"),
200
+ "reasoningEffortModels": config["cursor"].get("reasoningEffortModels", {}),
201
+ },
202
+ "droid": {
203
+ "models": config["droid"]["models"],
204
+ "defaultReasoningEffort": config["droid"].get("defaultReasoningEffort"),
205
+ },
206
+ "codex": {
207
+ "binary": config["codex"]["binary"],
208
+ "defaultModel": config["codex"]["defaultModel"],
209
+ "defaultReasoningEffort": config["codex"].get("defaultReasoningEffort"),
210
+ "profile": config["codex"]["profile"],
211
+ },
212
+ "claude": {
213
+ "binary": config["claude"]["binary"],
214
+ "defaultModel": config["claude"]["defaultModel"],
215
+ "defaultReasoningEffort": config["claude"].get("defaultReasoningEffort"),
216
+ "workPermissionMode": config["claude"]["workPermissionMode"],
217
+ "noSessionPersistence": config["claude"]["noSessionPersistence"],
218
+ "bare": config["claude"]["bare"],
219
+ },
220
+ "kimi": {
221
+ "binary": config["kimi"]["binary"],
222
+ "defaultModel": config["kimi"]["defaultModel"],
223
+ "defaultReasoningEffort": config["kimi"].get("defaultReasoningEffort"),
224
+ },
225
+ "grok": {
226
+ "binary": config["grok"]["binary"],
227
+ "defaultModel": config["grok"]["defaultModel"],
228
+ "defaultReasoningEffort": config["grok"].get("defaultReasoningEffort"),
229
+ "workPermissionMode": config["grok"]["workPermissionMode"],
230
+ "safePermissionMode": config["grok"]["safePermissionMode"],
231
+ "safeSandbox": config["grok"]["safeSandbox"],
232
+ "workSandbox": config["grok"]["workSandbox"],
233
+ "disableWebSearch": config["grok"]["disableWebSearch"],
234
+ "noSubagents": config["grok"]["noSubagents"],
235
+ },
236
+ }
237
+
238
+
239
+ def _reasoning_for_alias(reasoning_aliases: JsonObject, engine: str, alias: str) -> JsonObject:
240
+ engine_payload = reasoning_aliases.get(engine)
241
+ if not isinstance(engine_payload, dict):
242
+ return {}
243
+ payload = engine_payload.get(alias)
244
+ return payload if isinstance(payload, dict) else {}
245
+
246
+
247
+ def _summary_reasoning_fields(reasoning_payload: JsonObject) -> JsonObject:
248
+ output: JsonObject = {}
249
+ supported = reasoning_payload.get("supported")
250
+ if isinstance(supported, list) and all(isinstance(item, str) for item in supported):
251
+ output["reasoningEfforts"] = supported
252
+ elif supported is None and reasoning_payload:
253
+ output["reasoningEfforts"] = None
254
+ default = reasoning_payload.get("default")
255
+ if isinstance(default, str):
256
+ output["defaultReasoningEffort"] = default
257
+ config_default = reasoning_payload.get("configDefault")
258
+ if isinstance(config_default, str):
259
+ output["configuredReasoningEffort"] = config_default
260
+ source = reasoning_payload.get("source")
261
+ if isinstance(source, str):
262
+ output["reasoningCapabilitySource"] = source
263
+ routing = reasoning_payload.get("effortModelRouting")
264
+ if isinstance(routing, list):
265
+ routed: list[JsonObject] = []
266
+ for item in routing:
267
+ if not isinstance(item, dict):
268
+ continue
269
+ effort = item.get("effort")
270
+ model = item.get("model")
271
+ if not isinstance(effort, str) or not effort:
272
+ continue
273
+ route: JsonObject = {"effort": effort}
274
+ route["model"] = model if isinstance(model, str) and model else None
275
+ routed.append(route)
276
+ if routed:
277
+ output["reasoningEffortRouting"] = routed
278
+ warning = reasoning_payload.get("warning")
279
+ if isinstance(warning, str):
280
+ output["warnings"] = [warning]
281
+ return output
282
+
283
+
284
+ def models_summary_payload(
285
+ config: JsonObject,
286
+ config_source: str,
287
+ workspace: Path | None = None,
288
+ ) -> JsonObject:
289
+ cache = reasoning.load_reasoning_capability_cache(workspace) if workspace is not None else None
290
+ reasoning_aliases = reasoning.build_alias_reasoning_summaries(config, cache)
291
+ aliases: list[JsonObject] = []
292
+
293
+ for engine in MODEL_SUMMARY_ENGINES:
294
+ section = config.get(engine)
295
+ if not isinstance(section, dict):
296
+ continue
297
+ default_model = section.get("defaultModel")
298
+ entry: JsonObject = {
299
+ "alias": engine,
300
+ "provider": engine,
301
+ "command": f"delegate {engine} {{safe,work,call}}",
302
+ "available": True,
303
+ "safeSupported": True,
304
+ "workSupported": True,
305
+ "defaultModel": default_model
306
+ if isinstance(default_model, str) and default_model
307
+ else None,
308
+ }
309
+ reason_key = (
310
+ default_model if isinstance(default_model, str) and default_model else "(default)"
311
+ )
312
+ entry.update(
313
+ _summary_reasoning_fields(
314
+ _reasoning_for_alias(reasoning_aliases, engine, reason_key),
315
+ )
316
+ )
317
+ aliases.append(entry)
318
+
319
+ droid = config.get("droid")
320
+ if isinstance(droid, dict):
321
+ models = droid.get("models")
322
+ if isinstance(models, dict):
323
+ for alias, model_id in sorted(models.items()):
324
+ if not isinstance(alias, str) or not alias:
325
+ continue
326
+ entry = {
327
+ "alias": alias,
328
+ "provider": "droid",
329
+ "command": f"delegate droid {alias} {{safe,work,call}}",
330
+ "available": isinstance(model_id, str) and bool(model_id),
331
+ "safeSupported": True,
332
+ "workSupported": True,
333
+ "model": model_id if isinstance(model_id, str) else None,
334
+ }
335
+ entry.update(
336
+ _summary_reasoning_fields(
337
+ _reasoning_for_alias(reasoning_aliases, "droid", alias),
338
+ )
339
+ )
340
+ aliases.append(entry)
341
+
342
+ return {
343
+ "ok": True,
344
+ "summary": True,
345
+ "configSource": config_source,
346
+ "version": VERSION,
347
+ "aliases": aliases,
348
+ "counts": {
349
+ "aliases": len(aliases),
350
+ "providers": len({str(item.get("provider")) for item in aliases}),
351
+ },
352
+ "discovery": {
353
+ "fullModels": "delegate --json models",
354
+ "safeSummary": "delegate --json models --summary",
355
+ "reasoningCapabilities": "delegate --json capabilities",
356
+ },
357
+ }
358
+
359
+
360
+ def _policy_field_support_matrix() -> JsonObject:
361
+ codex_supported = {
362
+ "networkAccess": True,
363
+ "webSearch": True,
364
+ "bypassApprovalsAndSandbox": True,
365
+ "bypassHookTrust": True,
366
+ }
367
+ unsupported = {key: False for key in delegate_config.POLICY_MODE_KEYS}
368
+ claude_supported = dict(unsupported)
369
+ claude_supported["bypassApprovalsAndSandbox"] = True
370
+ grok_supported = dict(unsupported)
371
+ grok_supported["webSearch"] = True
372
+ grok_supported["bypassApprovalsAndSandbox"] = True
373
+ return {
374
+ "codex": codex_supported,
375
+ "claude": claude_supported,
376
+ "grok": grok_supported,
377
+ "cursor": unsupported,
378
+ "droid": unsupported,
379
+ "kimi": unsupported,
380
+ }
381
+
382
+
383
+ def _engine_capabilities() -> JsonObject:
384
+ return {engine: {"outputSchema": engine == "codex"} for engine in KNOWN_ENGINES}
385
+
386
+
387
+ def _runtime_policy(config: JsonObject, mode: str, engine: str, bypass_check) -> JsonObject:
388
+ policy = {key: False for key in delegate_config.POLICY_MODE_KEYS}
389
+ if engine == "grok":
390
+ policy = delegate_config.effective_policy(config, engine=engine, mode=mode)
391
+ if mode == MODE_WORK:
392
+ policy = dict(policy)
393
+ policy["bypassApprovalsAndSandbox"] = bypass_check(config, mode)
394
+ return policy
395
+
396
+
397
+ def _claude_runtime_policy(config: JsonObject, mode: str) -> JsonObject:
398
+ return _runtime_policy(config, mode, "claude", _claude_harness_bypass_enabled)
399
+
400
+
401
+ def _grok_runtime_policy(config: JsonObject, mode: str) -> JsonObject:
402
+ return _runtime_policy(config, mode, "grok", _grok_harness_bypass_enabled)
403
+
404
+
405
+ def _codex_describe_argv(
406
+ codex: JsonObject,
407
+ *,
408
+ mode: str,
409
+ workspace: str,
410
+ prompt: str,
411
+ policy: JsonObject,
412
+ ) -> list[str]:
413
+ return build_codex_argv(
414
+ codex,
415
+ mode,
416
+ workspace,
417
+ _resolve_default_model(codex),
418
+ prompt,
419
+ policy,
420
+ workspace_kind="git",
421
+ prompt_transport=PROMPT_TRANSPORT_STDIN,
422
+ )
423
+
424
+
425
+ def _claude_describe_argv(
426
+ config: JsonObject,
427
+ claude: JsonObject,
428
+ *,
429
+ mode: str,
430
+ policy: JsonObject,
431
+ ) -> list[str]:
432
+ return build_claude_argv(
433
+ claude,
434
+ mode,
435
+ _resolve_default_model(claude),
436
+ policy,
437
+ allow_bypass_permissions=_claude_harness_bypass_enabled(config, mode),
438
+ )
439
+
440
+
441
+ def _grok_describe_argv(
442
+ config: JsonObject,
443
+ grok: JsonObject,
444
+ *,
445
+ mode: str,
446
+ workspace: str,
447
+ policy: JsonObject,
448
+ ) -> list[str]:
449
+ argv = build_grok_argv(
450
+ grok,
451
+ mode,
452
+ workspace,
453
+ _resolve_default_model(grok),
454
+ policy,
455
+ allow_bypass_permissions=_grok_harness_bypass_enabled(config, mode),
456
+ )
457
+ return [PROMPT_FILE_DISPLAY if item == PROMPT_FILE_ARG_PLACEHOLDER else item for item in argv]
458
+
459
+
460
+ def describe_payload(
461
+ config: JsonObject,
462
+ config_source: str,
463
+ workspace: Path | None = None,
464
+ ) -> JsonObject:
465
+ codex = config["codex"]
466
+ claude = config["claude"]
467
+ grok = config["grok"]
468
+ codex_safe_policy = delegate_config.effective_policy(config, engine="codex", mode=MODE_SAFE)
469
+ codex_work_policy = delegate_config.effective_policy(config, engine="codex", mode=MODE_WORK)
470
+ claude_safe_policy = _claude_runtime_policy(config, MODE_SAFE)
471
+ claude_work_policy = _claude_runtime_policy(config, MODE_WORK)
472
+ grok_safe_policy = _grok_runtime_policy(config, MODE_SAFE)
473
+ grok_work_policy = _grok_runtime_policy(config, MODE_WORK)
474
+ codex_safe_argv = _codex_describe_argv(
475
+ codex,
476
+ mode=MODE_SAFE,
477
+ workspace="<isolated-workspace>",
478
+ prompt="<codex-safe-prefixed-skill-review-prompt>",
479
+ policy=codex_safe_policy,
480
+ )
481
+ codex_work_argv = _codex_describe_argv(
482
+ codex,
483
+ mode=MODE_WORK,
484
+ workspace="<workspace>",
485
+ prompt="<skill-review-prompt>",
486
+ policy=codex_work_policy,
487
+ )
488
+ claude_safe_argv = _claude_describe_argv(
489
+ config,
490
+ claude,
491
+ mode=MODE_SAFE,
492
+ policy=claude_safe_policy,
493
+ )
494
+ claude_work_argv = _claude_describe_argv(
495
+ config,
496
+ claude,
497
+ mode=MODE_WORK,
498
+ policy=claude_work_policy,
499
+ )
500
+ grok_safe_argv = _grok_describe_argv(
501
+ config,
502
+ grok,
503
+ mode=MODE_SAFE,
504
+ workspace="<isolated-workspace>",
505
+ policy=grok_safe_policy,
506
+ )
507
+ grok_work_argv = _grok_describe_argv(
508
+ config,
509
+ grok,
510
+ mode=MODE_WORK,
511
+ workspace="<workspace>",
512
+ policy=grok_work_policy,
513
+ )
514
+ return {
515
+ "ok": True,
516
+ "summary": False,
517
+ "version": VERSION,
518
+ "runtime": runtime_payload(),
519
+ "configPath": str(config_path()),
520
+ "configSource": config_source,
521
+ "configResolution": config_resolution_payload(config_source, workspace),
522
+ "engines": list(KNOWN_ENGINES),
523
+ "isolationValues": list(delegate_config.VALID_ISOLATION_VALUES),
524
+ "policyProfiles": list(delegate_config.POLICY_PROFILES),
525
+ "policyFieldSupport": _policy_field_support_matrix(),
526
+ "engineCapabilities": _engine_capabilities(),
527
+ "effectivePolicy": {
528
+ "codex": {
529
+ "safe": codex_safe_policy,
530
+ "work": codex_work_policy,
531
+ },
532
+ "claude": {
533
+ "safe": claude_safe_policy,
534
+ "work": claude_work_policy,
535
+ },
536
+ "grok": {
537
+ "safe": grok_safe_policy,
538
+ "work": grok_work_policy,
539
+ },
540
+ },
541
+ "modes": [MODE_SAFE, MODE_WORK, MODE_CALL],
542
+ "promptSources": ["direct", "prompt-file", "stdin"],
543
+ "promptTransports": {
544
+ "cursor": PROMPT_TRANSPORT_ARGV,
545
+ "droid": PROMPT_TRANSPORT_FILE,
546
+ "codex": PROMPT_TRANSPORT_STDIN,
547
+ "kimi": PROMPT_TRANSPORT_ARGV,
548
+ "claude": PROMPT_TRANSPORT_STDIN,
549
+ "grok": PROMPT_TRANSPORT_FILE,
550
+ },
551
+ "globalOptions": _global_options(),
552
+ "launchOptions": _launch_options(),
553
+ "completionReportModes": list(delegate_config.COMPLETION_REPORT_MODES),
554
+ "promptTransforms": [
555
+ "Always prepends mandatory skill review instructions before the operator prompt.",
556
+ "Optionally appends completion-report instructions unless disabled.",
557
+ ],
558
+ "profiles": _profiles_config_payload(config),
559
+ "passThrough": "Opt-in raw child stdout/stderr streaming; incompatible with --json.",
560
+ "cwdResolution": "Git directories resolve to the repository root; non-Git directories are used directly.",
561
+ "isolation": {
562
+ "defaults": config["isolation"],
563
+ "supportedValues": list(delegate_config.VALID_ISOLATION_VALUES),
564
+ "safeNoneAllowed": {
565
+ "cursor": False,
566
+ "droid": False,
567
+ "codex": True,
568
+ "kimi": False,
569
+ "claude": False,
570
+ "grok": False,
571
+ },
572
+ },
573
+ "worktrees": {
574
+ "dataHome": config["worktrees"]["dataHome"],
575
+ "autoPrune": config["worktrees"]["autoPrune"],
576
+ },
577
+ "modeMapping": {
578
+ "cursor": {
579
+ "safe": [
580
+ *config["cursor"]["argvPrefix"],
581
+ "--workspace",
582
+ "<isolated-workspace>",
583
+ "-p",
584
+ "--trust",
585
+ "--model",
586
+ config["cursor"]["defaultModel"],
587
+ "--print",
588
+ "--output-format",
589
+ "stream-json",
590
+ "<read-only-review-prefixed-skill-review-prompt>",
591
+ ],
592
+ "safeNotes": [
593
+ SAFE_WORKSPACE_SYNC_NOTE,
594
+ "No --mode=plan, --mode=ask, --force, or --approve-mcps.",
595
+ "Writes .cursor/cli.json in the isolated workspace (Read(**), read-only shell helpers; no git/find shell).",
596
+ ],
597
+ "work": [
598
+ *config["cursor"]["argvPrefix"],
599
+ "--workspace",
600
+ "<workspace>",
601
+ "-p",
602
+ "--trust",
603
+ "--approve-mcps",
604
+ "--force",
605
+ "--model",
606
+ config["cursor"]["defaultModel"],
607
+ "--print",
608
+ "--output-format",
609
+ "stream-json",
610
+ "<skill-review-prompt>",
611
+ ],
612
+ },
613
+ "droid": {
614
+ "safe": [
615
+ config["droid"]["binary"],
616
+ "exec",
617
+ "--cwd",
618
+ "<isolated-workspace>",
619
+ "--model",
620
+ "<model-id>",
621
+ "--output-format",
622
+ "stream-json",
623
+ "--file",
624
+ DROID_PROMPT_FILE_DISPLAY,
625
+ ],
626
+ "safeNotes": [
627
+ SAFE_WORKSPACE_SYNC_NOTE,
628
+ "No --auto, --use-spec, or --skip-permissions-unsafe in safe mode.",
629
+ "Uses a read-only safety prompt; --isolation none is normalized to auto for Droid safe mode.",
630
+ ],
631
+ "work": [
632
+ config["droid"]["binary"],
633
+ "exec",
634
+ "--cwd",
635
+ "<workspace>",
636
+ "--skip-permissions-unsafe",
637
+ "--model",
638
+ "<model-id>",
639
+ "--output-format",
640
+ "stream-json",
641
+ "--file",
642
+ DROID_PROMPT_FILE_DISPLAY,
643
+ ],
644
+ },
645
+ "codex": {
646
+ "safe": codex_safe_argv,
647
+ "safeNotes": [
648
+ SAFE_WORKSPACE_SYNC_NOTE,
649
+ "Always uses --sandbox read-only; safe sandbox is not configurable in v1.",
650
+ "Non-interactive: --ask-for-approval never.",
651
+ ],
652
+ "work": codex_work_argv,
653
+ "workNotes": [
654
+ "networkAccess enables -c sandbox_workspace_write.network_access=true when workSandbox is workspace-write.",
655
+ "webSearch enables global --search before exec.",
656
+ "profile is config-only (codex.profile); not accepted in run input JSON.",
657
+ ],
658
+ },
659
+ "claude": {
660
+ "safe": claude_safe_argv,
661
+ "safeNotes": [
662
+ SAFE_WORKSPACE_SYNC_NOTE,
663
+ "Uses Claude Code -p with --permission-mode plan, --strict-mcp-config, Read/Grep/Glob, and selected read-only Bash tools.",
664
+ "Prompt is delivered on stdin; dry-run argv and manifests do not contain the prompt.",
665
+ "Delegate does not prove Claude hooks, plugins, or user settings are disabled; keep safe-mode work review-only.",
666
+ ],
667
+ "work": claude_work_argv,
668
+ "workNotes": [
669
+ "Uses claude.workPermissionMode unless policy.harness.claude.work.bypassApprovalsAndSandbox explicitly requests bypassPermissions.",
670
+ "Reasoning effort is emitted as Claude Code --effort, independent of model capability cache.",
671
+ "Delegate sets subprocess cwd; Claude Code receives no workspace argv flag.",
672
+ ],
673
+ },
674
+ "kimi": {
675
+ "safe": [
676
+ config["kimi"]["binary"],
677
+ "--model",
678
+ config["kimi"]["defaultModel"],
679
+ "--output-format",
680
+ "stream-json",
681
+ "--prompt",
682
+ "<kimi-safe-prefixed-skill-review-prompt>",
683
+ ],
684
+ "safeNotes": [
685
+ SAFE_WORKSPACE_SYNC_NOTE,
686
+ "Prompt mode cannot be combined with Kimi --plan; Delegate uses a read-only safety prompt instead.",
687
+ "Kimi prompt mode auto-approves tool actions; the isolated workspace is the effective write boundary and the safety prompt is advisory.",
688
+ "No CLI workspace flag; Delegate sets subprocess cwd.",
689
+ ],
690
+ "work": [
691
+ config["kimi"]["binary"],
692
+ "--model",
693
+ config["kimi"]["defaultModel"],
694
+ "--output-format",
695
+ "stream-json",
696
+ "--prompt",
697
+ "<skill-review-prompt>",
698
+ ],
699
+ "workNotes": [
700
+ "Kimi prompt mode auto-approves tool actions; Delegate does not pass --yolo because Kimi rejects combining it with --prompt.",
701
+ "No CLI workspace flag; Delegate sets subprocess cwd.",
702
+ ],
703
+ },
704
+ "grok": {
705
+ "safe": grok_safe_argv,
706
+ "safeNotes": [
707
+ SAFE_WORKSPACE_SYNC_NOTE,
708
+ "Uses Grok --prompt-file with Delegate temp prompt materialization; dry-run argv shows <prompt file>.",
709
+ "Safe mode uses Delegate isolated copy plus Grok read-only sandbox/permission controls; it does not use Grok plan mode.",
710
+ "Grok safe mode disables web search by default; explicit policy.harness.grok.safe.webSearch=true re-enables network egress, and Delegate safe-mode isolation is filesystem-only.",
711
+ "The isolated workspace is the effective write boundary; Grok sandbox/permission flags are advisory defense-in-depth.",
712
+ ],
713
+ "work": grok_work_argv,
714
+ "workNotes": [
715
+ "Uses grok.workPermissionMode unless policy.harness.grok.work.bypassApprovalsAndSandbox explicitly requests bypassPermissions.",
716
+ "Reasoning effort maps to Grok --effort (low, medium, high, xhigh, max).",
717
+ "Tracked runs use --output-format streaming-json; pass-through uses plain.",
718
+ ],
719
+ },
720
+ },
721
+ "commands": _commands_catalog(),
722
+ "recommendedDiscovery": [
723
+ "delegate --json describe --summary",
724
+ "delegate --json models --summary",
725
+ "delegate --json help <command>",
726
+ ],
727
+ }
728
+
729
+
730
+ def describe_summary_payload(
731
+ config: JsonObject,
732
+ config_source: str,
733
+ workspace: Path | None = None,
734
+ ) -> JsonObject:
735
+ return {
736
+ "ok": True,
737
+ "summary": True,
738
+ "version": VERSION,
739
+ "configSource": config_source,
740
+ "configResolution": config_resolution_payload(config_source, workspace),
741
+ "engines": list(KNOWN_ENGINES),
742
+ "modes": [MODE_SAFE, MODE_WORK, MODE_CALL],
743
+ "isolationValues": list(delegate_config.VALID_ISOLATION_VALUES),
744
+ "profiles": _profiles_config_payload(config),
745
+ "globalOptions": _global_options(),
746
+ "launchOptions": _launch_options(),
747
+ "commands": _commands_catalog(),
748
+ "recommendedDiscovery": [
749
+ "delegate --json describe --summary",
750
+ "delegate --json models --summary",
751
+ "delegate --json help <command>",
752
+ ],
753
+ }
754
+
755
+
756
+ def _emit_models_text(payload: JsonObject, config_source: str, stdout: TextIO) -> None:
757
+ if config_source == "embedded-default":
758
+ print("warning: using embedded default config", file=stdout)
759
+ cursor = payload.get("cursor")
760
+ if isinstance(cursor, dict):
761
+ print(
762
+ "cursor: "
763
+ f"{_text_or_none(cursor.get('defaultModel'))} "
764
+ f"({_text_argv_prefix_label(cursor.get('argvPrefix'))})",
765
+ file=stdout,
766
+ )
767
+ print("droid:", file=stdout)
768
+ droid = payload.get("droid")
769
+ if isinstance(droid, dict):
770
+ models = droid.get("models")
771
+ if isinstance(models, dict):
772
+ for alias, model_id in sorted(models.items()):
773
+ print(f" {alias} -> {_text_or_none(model_id)}", file=stdout)
774
+ codex = payload.get("codex")
775
+ if isinstance(codex, dict):
776
+ profile = codex.get("profile")
777
+ profile_label = _text_or_none(profile)
778
+ print(
779
+ "codex: "
780
+ f"binary={_text_or_none(codex.get('binary'))} "
781
+ f"defaultModel={_text_or_none(codex.get('defaultModel'))} "
782
+ f"profile={profile_label}",
783
+ file=stdout,
784
+ )
785
+ claude = payload.get("claude")
786
+ if isinstance(claude, dict):
787
+ print(
788
+ "claude: "
789
+ f"binary={_text_or_none(claude.get('binary'))} "
790
+ f"defaultModel={_text_or_none(claude.get('defaultModel'))} "
791
+ f"workPermissionMode={claude.get('workPermissionMode')}",
792
+ file=stdout,
793
+ )
794
+ grok = payload.get("grok")
795
+ if isinstance(grok, dict):
796
+ print(
797
+ "grok: "
798
+ f"binary={_text_or_none(grok.get('binary'))} "
799
+ f"defaultModel={_text_or_none(grok.get('defaultModel'))} "
800
+ f"workPermissionMode={grok.get('workPermissionMode')}",
801
+ file=stdout,
802
+ )
803
+ kimi = payload.get("kimi")
804
+ if isinstance(kimi, dict):
805
+ print(
806
+ "kimi: "
807
+ f"binary={_text_or_none(kimi.get('binary'))} "
808
+ f"defaultModel={_text_or_none(kimi.get('defaultModel'))}",
809
+ file=stdout,
810
+ )
811
+ runtime = payload.get("runtime")
812
+ if isinstance(runtime, dict):
813
+ print(
814
+ f"runtime: {_text_or_none(runtime.get('modulePath'))}",
815
+ file=stdout,
816
+ )
817
+
818
+
819
+ def emit_models(
820
+ config: JsonObject,
821
+ config_source: str,
822
+ json_mode: bool,
823
+ stdout: TextIO,
824
+ *,
825
+ workspace: Path | None = None,
826
+ summary: bool = False,
827
+ ) -> int:
828
+ if summary:
829
+ payload = _scrub_discovery_payload(models_summary_payload(config, config_source, workspace))
830
+ if json_mode:
831
+ delegate_rendering.print_json(payload, stdout)
832
+ else:
833
+ aliases = payload.get("aliases")
834
+ if isinstance(aliases, list):
835
+ for item in aliases:
836
+ if not isinstance(item, dict):
837
+ continue
838
+ warnings = item.get("warnings")
839
+ suffix = f" warnings={len(warnings)}" if isinstance(warnings, list) else ""
840
+ print(
841
+ f"{item['provider']}:{item['alias']} safe={item['safeSupported']} work={item['workSupported']}{suffix}",
842
+ file=stdout,
843
+ )
844
+ return EXIT_OK
845
+ payload = _scrub_discovery_payload(models_payload(config, config_source, workspace))
846
+ if json_mode:
847
+ delegate_rendering.print_json(payload, stdout)
848
+ return EXIT_OK
849
+ _emit_models_text(payload, config_source, stdout)
850
+ return EXIT_OK
851
+
852
+
853
+ def emit_describe(
854
+ config: JsonObject,
855
+ config_source: str,
856
+ json_mode: bool,
857
+ stdout: TextIO,
858
+ *,
859
+ workspace: Path | None = None,
860
+ summary: bool = False,
861
+ ) -> int:
862
+ raw_payload = (
863
+ describe_summary_payload(config, config_source, workspace)
864
+ if summary
865
+ else describe_payload(config, config_source, workspace)
866
+ )
867
+ payload = _scrub_discovery_payload(raw_payload)
868
+ if json_mode:
869
+ delegate_rendering.print_json(payload, stdout)
870
+ return EXIT_OK
871
+ if summary:
872
+ print(f"delegate {payload['version']} summary", file=stdout)
873
+ print(f"config: {payload['configSource']}", file=stdout)
874
+ print(f"engines: {', '.join(payload['engines'])}", file=stdout)
875
+ print(f"modes: {', '.join(payload['modes'])}", file=stdout)
876
+ print(f"launch options: {', '.join(payload['launchOptions'])}", file=stdout)
877
+ print("recommended discovery:", file=stdout)
878
+ for command in payload["recommendedDiscovery"]:
879
+ print(f" {command}", file=stdout)
880
+ return EXIT_OK
881
+ print(f"delegate {VERSION}", file=stdout)
882
+ print(f"config: {payload['configPath']} ({payload['configSource']})", file=stdout)
883
+ print(f"runtime: {payload['runtime']['modulePath']}", file=stdout)
884
+ print("engines: cursor, droid, codex, kimi, claude, grok", file=stdout)
885
+ print("modes: safe, work", file=stdout)
886
+ print("prompt sources: direct, --prompt-file, stdin", file=stdout)
887
+ print("global options must appear before the subcommand", file=stdout)
888
+ return EXIT_OK
889
+
890
+
891
+ def emit_command_help(topic: str | None, json_mode: bool, stdout: TextIO) -> int:
892
+ """Render help: overview when topic is empty, else focused help for one command."""
893
+
894
+ if not topic:
895
+ if json_mode:
896
+ delegate_rendering.print_json(command_help.help_index_payload(), stdout)
897
+ else:
898
+ print(command_help.render_overview_text(), file=stdout, end="")
899
+ return EXIT_OK
900
+ spec = command_help.COMMAND_SPECS.get(topic)
901
+ if spec is None:
902
+ raise DelegateError(
903
+ "unknown_help_topic",
904
+ f"Unknown help topic: {topic}. Run delegate help for the command list.",
905
+ )
906
+ if json_mode:
907
+ delegate_rendering.print_json(command_help.command_help_payload(spec), stdout)
908
+ else:
909
+ print(command_help.render_command_help_text(spec), file=stdout, end="")
910
+ return EXIT_OK
911
+
912
+
913
+ def emit_agent_help(stdout: TextIO) -> int:
914
+ print(
915
+ f"""Use delegate for bounded execution tasks only.
916
+
917
+ Good defaults:
918
+ delegate cursor work "Implement the scoped task; report changed files and tests."
919
+ delegate cursor safe "Review this diff for regressions; report findings with file/line/severity."
920
+ delegate droid <alias> safe "Investigate this issue; do not edit."
921
+ delegate droid <alias> work "Implement this bounded change; run the named check."
922
+ delegate codex safe "Review this workspace. Do not edit files."
923
+ delegate codex work "Implement the scoped fix, run the named check, and report changed files."
924
+ delegate claude safe "Review this workspace. Do not edit files."
925
+ delegate claude work "Implement the scoped fix, run the named check, and report changed files."
926
+ delegate grok safe "Review this workspace. Do not edit files."
927
+ delegate grok work "Implement the scoped fix, run the named check, and report changed files."
928
+ delegate kimi safe "Review this repo for regressions; report file/line/severity."
929
+ delegate kimi work "Implement the scoped task; report changed files and tests."
930
+
931
+ Kimi:
932
+ - {SAFE_WORKSPACE_SYNC_NOTE}
933
+ - Model selection uses kimi.defaultModel in config or optional JSON input model; no CLI model alias in v1.
934
+ - Reasoning effort is unsupported for Kimi in v1.
935
+ - No CLI workspace flag; Delegate sets subprocess cwd.
936
+
937
+ Codex:
938
+ - {SAFE_WORKSPACE_SYNC_NOTE}
939
+ - Model selection uses codex.defaultModel in config or optional JSON input model; no CLI model alias in v1.
940
+ - Codex profile (codex.profile) is config-only; run input JSON must not include profile.
941
+
942
+ Claude:
943
+ - Uses Claude Code headless mode: claude -p with prompt delivered on stdin.
944
+ - {SAFE_WORKSPACE_SYNC_NOTE}
945
+ - Claude safe mode runs with --permission-mode plan, --strict-mcp-config,
946
+ Read/Grep/Glob, and selected read-only Bash tools.
947
+ Delegate does not currently prove that Claude Code hooks, plugins, user
948
+ settings, or other non-MCP customization surfaces are disabled.
949
+ - Work mode uses claude.workPermissionMode, or bypassPermissions only when
950
+ Delegate policy explicitly enables policy.harness.claude.work.bypassApprovalsAndSandbox.
951
+ - Reasoning effort maps to Claude Code --effort (low, medium, high, xhigh, max).
952
+
953
+ Grok:
954
+ - Uses Grok Build CLI with --prompt-file; Delegate materializes the effective prompt in a temp file.
955
+ - {SAFE_WORKSPACE_SYNC_NOTE}
956
+ - Safe mode uses Delegate isolated copy plus Grok read-only sandbox/permission controls; not Grok plan mode.
957
+ - Work mode uses grok.workPermissionMode, or bypassPermissions only when
958
+ Delegate policy explicitly enables policy.harness.grok.work.bypassApprovalsAndSandbox.
959
+ - Reasoning effort maps to Grok --effort (low, medium, high, xhigh, max).
960
+ - Tracked runs use streaming-json; pass-through uses plain output.
961
+ - --output-schema is unsupported in v1 because Grok --json-schema forces final json output.
962
+
963
+ Droid modes:
964
+ - {SAFE_WORKSPACE_SYNC_NOTE}
965
+ - Droid safe mode remains read-only: no --auto, --use-spec, or unsafe skip.
966
+ - Uses Factory Droid --skip-permissions-unsafe, not --auto high.
967
+ - Work mode is intentionally no-prompt; use only for bounded tasks in workspaces you trust.
968
+ - --reasoning-effort requires a resolved Droid model alias from droid.models.
969
+
970
+ Cursor safe mode:
971
+ - {SAFE_WORKSPACE_SYNC_NOTE}
972
+ - Uses default Cursor Agent behavior, not plan/ask mode.
973
+ - The child runs in the isolated copy; tracked runs may still write .delegate metadata in the source workspace.
974
+
975
+ Profiles (auth/env switching):
976
+ - A profile selects which credentials/env every spawned harness inherits; the active profile is detected from env (profiles.detectFrom) or set explicitly with --auth-profile NAME before the subcommand.
977
+ - --auth-profile applies to launches, dry-run, run, profiles, and capabilities refresh; it is rejected for run-inspection, worktree, discovery, and the cached capabilities report.
978
+ - delegate profiles (optionally with --json) reports the resolved profile, source, and non-secret env keys; it never mutates config.
979
+ - profiles.definitions.<name>.env holds non-secret pointers only (e.g. CODEX_HOME); secret-shaped keys are rejected at config load, and values must not interpolate secrets via $VAR. Export real credentials in the shell instead.
980
+
981
+ Rules for agents:
982
+ - Keep prompts bounded: task, scope, verification, report format.
983
+ - Delegate always prepends a mandatory skill-review instruction before your prompt.
984
+ - Use --prompt-file or delegate --json run --input-json for long prompts.
985
+ - Run from the target workspace, or pass --cwd before the subcommand.
986
+ - Inside Git, --cwd resolves to the repo root; outside Git, the directory is used directly.
987
+ - Always review diffs after work mode when Git is available; outside Git, manually review changed files.
988
+ - Do not use delegate for production deploys or repository publishing unless the operator explicitly asks.
989
+ - Launch normally; do not pipe delegate launches through tail just to suppress noise.
990
+ - For long tracked foreground runs, add --progress before prompt text to get bounded, redacted stderr heartbeats.
991
+ - After a tracked run, use delegate snapshot/runs/run-output; do not tail launch output or .delegate log files.
992
+ - Default output is bounded; use --pass-through only when raw harness streaming is required.
993
+ - If you intentionally pipe delegate output in a shell script, use set -o pipefail.
994
+
995
+ Run inspection:
996
+ delegate snapshot <alias-or-runId>
997
+ delegate runs --active
998
+ delegate run-output <alias> --completion-report
999
+ delegate run-output <alias> --stderr --tail 100
1000
+
1001
+ Avoid:
1002
+ delegate cursor work --prompt-file task.md 2>&1 | tail -20
1003
+
1004
+ Prefer:
1005
+ delegate cursor work --prompt-file task.md
1006
+ delegate snapshot cursor-1
1007
+ delegate run-output cursor-1 --completion-report
1008
+
1009
+ Discovery:
1010
+ delegate --json models --summary
1011
+ delegate --json describe --summary
1012
+ delegate --json models # full/raw details when needed
1013
+ delegate --json describe # full/raw details when needed
1014
+ delegate agent-help
1015
+ """.rstrip(),
1016
+ file=stdout,
1017
+ )
1018
+ return EXIT_OK