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,116 @@
1
+ """Fail-closed AI_PROFILE guard for the Python CLI entrypoint.
2
+
3
+ ``bin/delegate-profile-shim`` is a *template* users may copy in front of the
4
+ installed ``delegate`` command. It is not the only way to reach
5
+ ``delegate_agent.cli:main`` — the pip console script, ``python -m
6
+ delegate_agent.cli``, and ``bin/delegate.py`` all call it directly, with no
7
+ shell shim in front. This module re-implements the shim's fail-closed check
8
+ inside the Python CLI itself so the guarantee holds regardless of entrypoint:
9
+ a recognized ``AI_PROFILE`` (``work``/``personal``) with a missing overlay
10
+ config must never silently fall through to the base account for a command
11
+ that launches a child harness or mutates state. See docs/security-model.md.
12
+
13
+ When the shim already validated and exported ``DELEGATE_CONFIG``, this guard
14
+ does not re-fire: its trigger condition requires ``DELEGATE_CONFIG`` to be
15
+ unset, so a successful shim run and a direct Python invocation compose
16
+ without double warnings or a redundant check.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ from pathlib import Path
23
+ from typing import TextIO
24
+
25
+ from delegate_agent import config as delegate_config
26
+ from delegate_agent.errors import DelegateError
27
+ from delegate_agent.request_models import ParsedCommand
28
+
29
+ # Mirrors bin/delegate-profile-shim's `_delegate_is_readonly_request` allow-list.
30
+ # Kept as the real subcommand names the parser produces (not positional argv
31
+ # guessing) so the two layers can only drift if someone edits one and forgets
32
+ # the other, not because of divergent classification logic.
33
+ READ_ONLY_SUBCOMMANDS = frozenset(
34
+ {
35
+ "help",
36
+ "version",
37
+ "agent-help",
38
+ "models",
39
+ "describe",
40
+ "profiles",
41
+ "snapshot",
42
+ "runs",
43
+ "run-output",
44
+ }
45
+ )
46
+ READ_ONLY_WORKTREE_ACTIONS = frozenset({"show", "list"})
47
+
48
+
49
+ def is_read_only_command(parsed: ParsedCommand) -> bool:
50
+ """Classify a parsed command as read-only diagnostic vs launch/mutation.
51
+
52
+ ``capabilities`` is read-only except ``capabilities refresh`` (which can
53
+ spawn a child probe process). ``worktree`` is read-only only for
54
+ ``show``/``list``; ``remove``/``prune``/``gc`` mutate the worktree store.
55
+ ``config``, ``dry-run``, ``wait``, ``cancel``, and every engine/``run``
56
+ launch remain classified as mutation/launch (conservative by design).
57
+ """
58
+ subcommand = parsed.subcommand
59
+ if subcommand in READ_ONLY_SUBCOMMANDS:
60
+ return True
61
+ if subcommand == "capabilities":
62
+ return parsed.capabilities is not None and not parsed.capabilities.refresh
63
+ if subcommand == "worktree":
64
+ return parsed.worktree is not None and parsed.worktree.action in READ_ONLY_WORKTREE_ACTIONS
65
+ return False
66
+
67
+
68
+ def _fix_message(profile: str, config_path: Path) -> str:
69
+ return (
70
+ f"delegate: AI_PROFILE={profile} but {config_path} is missing/unreadable; "
71
+ "refusing to run a launch or mutation command on the wrong account.\n"
72
+ "Fix: create the profile config from config.example.json, or run "
73
+ "'env -u AI_PROFILE delegate config sync-profiles' to materialize missing profile overlays.\n"
74
+ "Temporary bypasses: 'env -u AI_PROFILE delegate ...' uses the base config (work Claude, "
75
+ "ambient Codex), or set 'DELEGATE_CONFIG=/path/to/config.json delegate ...'."
76
+ )
77
+
78
+
79
+ def enforce_profile_guard(parsed: ParsedCommand, *, stderr: TextIO) -> None:
80
+ """Fail closed when AI_PROFILE names a profile whose overlay config is missing.
81
+
82
+ No-ops (silently) when: DELEGATE_CONFIG is already set (shim precedence,
83
+ or the user set it directly), or AI_PROFILE is unset/empty. Warns and
84
+ proceeds for an unrecognized non-empty AI_PROFILE, or for a read-only
85
+ command with a missing overlay. Raises DelegateError (fail closed) for a
86
+ launch/mutation command with a missing or unreadable overlay.
87
+ """
88
+ if os.environ.get(delegate_config.CONFIG_ENV):
89
+ return
90
+ profile = os.environ.get("AI_PROFILE", "")
91
+ if not profile:
92
+ return
93
+ if profile not in delegate_config.PROFILE_CONFIG_NAMES:
94
+ print(
95
+ f"delegate: AI_PROFILE={profile!r} is not a recognized profile (work|personal); "
96
+ "running on the base account",
97
+ file=stderr,
98
+ )
99
+ return
100
+
101
+ config_path = delegate_config.profile_config_path(
102
+ delegate_config.default_config_path(), profile
103
+ )
104
+ if config_path.is_file() and os.access(config_path, os.R_OK):
105
+ return
106
+
107
+ if is_read_only_command(parsed):
108
+ print(
109
+ f"delegate: warning: AI_PROFILE={profile} but {config_path} is missing/unreadable; "
110
+ f"continuing because '{parsed.subcommand}' is read-only. Launch and mutation commands "
111
+ "remain blocked until the profile config exists.",
112
+ file=stderr,
113
+ )
114
+ return
115
+
116
+ raise DelegateError("profile_config_missing", _fix_message(profile, config_path))
@@ -0,0 +1,389 @@
1
+ """Profile selection, environment injection, and Codex auth helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import subprocess
8
+ from collections.abc import Mapping
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+
12
+ from delegate_agent import redaction, wsl
13
+ from delegate_agent.errors import DelegateError
14
+ from delegate_agent.git_utils import GIT_QUICK_TIMEOUT_SECONDS
15
+ from delegate_agent.git_utils import run_git as _run_git
16
+ from delegate_agent.harness_events import StreamAccumulator
17
+ from delegate_agent.json_types import JsonObject
18
+
19
+ STDERR_TAIL_LIMIT = 8_000
20
+
21
+ _RATE_LIMIT_PATTERN = re.compile(r"rate limit", re.IGNORECASE)
22
+
23
+ _USAGE_LIMIT_PATTERNS = (
24
+ re.compile(r"usage limit", re.IGNORECASE),
25
+ re.compile(r"insufficient_quota", re.IGNORECASE),
26
+ re.compile(r"exceeded your current quota", re.IGNORECASE),
27
+ _RATE_LIMIT_PATTERN,
28
+ )
29
+
30
+ _ACCOUNT_QUOTA_CONTEXT = re.compile(
31
+ r"(quota|usage|billing|subscription|account|credit)",
32
+ re.IGNORECASE,
33
+ )
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class ProfileResolution:
38
+ name: str | None
39
+ source: str | None
40
+ warnings: tuple[str, ...] = ()
41
+ env: dict[str, str] = field(default_factory=dict)
42
+ codex_home: str | None = None
43
+ codex_fallback_home: str | None = None
44
+
45
+
46
+ def empty_profile_resolution() -> ProfileResolution:
47
+ return ProfileResolution(name=None, source=None)
48
+
49
+
50
+ def profiles_section(config: JsonObject) -> JsonObject:
51
+ section = config.get("profiles")
52
+ return section if isinstance(section, dict) else {}
53
+
54
+
55
+ def profile_definitions(config: JsonObject) -> dict[str, JsonObject]:
56
+ definitions = profiles_section(config).get("definitions")
57
+ if not isinstance(definitions, dict):
58
+ return {}
59
+ return {
60
+ name.strip(): entry
61
+ for name, entry in definitions.items()
62
+ if isinstance(name, str) and name.strip() and isinstance(entry, dict)
63
+ }
64
+
65
+
66
+ def _expanded_env_value(value: str) -> str:
67
+ return os.path.expanduser(os.path.expandvars(value.strip()))
68
+
69
+
70
+ def _unknown_profile_error(name: str, definitions: Mapping[str, JsonObject]) -> DelegateError:
71
+ defined = sorted(definitions)
72
+ if defined:
73
+ hint = f"Defined profiles: {', '.join(defined)}."
74
+ else:
75
+ hint = "No profiles are defined; add a profiles.definitions entry to config."
76
+ return DelegateError(
77
+ "unknown_profile",
78
+ f"Unknown auth profile: {name}. {hint} Run delegate profiles to inspect the active profile.",
79
+ )
80
+
81
+
82
+ def profile_env(config: JsonObject, name: str) -> dict[str, str]:
83
+ definitions = profile_definitions(config)
84
+ profile = definitions.get(name)
85
+ if profile is None:
86
+ raise _unknown_profile_error(name, definitions)
87
+ env = profile.get("env")
88
+ if not isinstance(env, dict):
89
+ return {}
90
+ return {
91
+ key: _expanded_env_value(value)
92
+ for key, value in env.items()
93
+ if isinstance(key, str) and isinstance(value, str)
94
+ }
95
+
96
+
97
+ def codex_section(config: JsonObject) -> JsonObject:
98
+ section = config.get("codex")
99
+ return section if isinstance(section, dict) else {}
100
+
101
+
102
+ def codex_fallback_profile(config: JsonObject) -> str | None:
103
+ fallback = codex_section(config).get("fallbackProfile")
104
+ if isinstance(fallback, str) and fallback.strip():
105
+ return fallback.strip()
106
+ return None
107
+
108
+
109
+ def _warn_once(warnings: list[str], warning: str) -> None:
110
+ if warning not in warnings:
111
+ warnings.append(warning)
112
+
113
+
114
+ def _resolve_default_profile(
115
+ config: JsonObject, definitions: Mapping[str, JsonObject]
116
+ ) -> str | None:
117
+ default = profiles_section(config).get("default")
118
+ if isinstance(default, str) and default in definitions:
119
+ return default
120
+ return None
121
+
122
+
123
+ def resolve_active_profile(
124
+ config: JsonObject,
125
+ env: Mapping[str, str | None],
126
+ cli_override: str | None = None,
127
+ ) -> ProfileResolution:
128
+ definitions = profile_definitions(config)
129
+ warnings: list[str] = []
130
+ selected: str | None = None
131
+ source: str | None = None
132
+
133
+ if cli_override is not None:
134
+ selected = cli_override.strip()
135
+ if not selected or selected not in definitions:
136
+ raise _unknown_profile_error(cli_override, definitions)
137
+ source = "flag"
138
+ elif not definitions:
139
+ return empty_profile_resolution()
140
+ else:
141
+ detect_from = profiles_section(config).get("detectFrom")
142
+ for var_name in detect_from if isinstance(detect_from, list) else []:
143
+ if not isinstance(var_name, str):
144
+ continue
145
+ raw = env.get(var_name)
146
+ value = raw.strip() if isinstance(raw, str) else ""
147
+ if not value:
148
+ continue
149
+ if value not in definitions:
150
+ safe_value = redaction.redact_mapping_value(var_name, value)
151
+ _warn_once(
152
+ warnings,
153
+ f"profile detect var {var_name}={safe_value} is not defined; ignoring it.",
154
+ )
155
+ continue
156
+ if selected is None:
157
+ selected = value
158
+ source = var_name
159
+ continue
160
+ if value != selected:
161
+ _warn_once(
162
+ warnings,
163
+ f"profile mismatch ({source}={selected} vs {var_name}={value}); using {selected}.",
164
+ )
165
+ if selected is None:
166
+ selected = _resolve_default_profile(config, definitions)
167
+ if selected is not None:
168
+ source = "default"
169
+
170
+ active_env = profile_env(config, selected) if selected is not None else {}
171
+ fallback_profile = codex_fallback_profile(config)
172
+ fallback_home = None
173
+ if fallback_profile is not None:
174
+ fallback_home = profile_env(config, fallback_profile).get("CODEX_HOME")
175
+ return ProfileResolution(
176
+ name=selected,
177
+ source=source,
178
+ warnings=tuple(warnings),
179
+ env=active_env,
180
+ codex_home=active_env.get("CODEX_HOME"),
181
+ codex_fallback_home=fallback_home,
182
+ )
183
+
184
+
185
+ def child_environment(
186
+ base: dict[str, str] | None = None, overrides: dict[str, str] | None = None
187
+ ) -> dict[str, str]:
188
+ env = dict(os.environ)
189
+ if base:
190
+ env.update(base)
191
+ if overrides:
192
+ env.update(overrides)
193
+ return env
194
+
195
+
196
+ def _auth_file_readable(path: Path) -> bool:
197
+ try:
198
+ return path.is_file() and os.access(path, os.R_OK)
199
+ except OSError:
200
+ return False
201
+
202
+
203
+ def preflight_codex_home(codex_home: str, *, codex: JsonObject) -> None:
204
+ if wsl.should_reject_windows_path(codex_home):
205
+ raise DelegateError("windows_path", wsl.windows_path_message("CODEX_HOME", codex_home))
206
+ home = Path(_expanded_env_value(codex_home))
207
+ if not home.is_absolute():
208
+ raise DelegateError(
209
+ "codex_home_not_absolute",
210
+ f"Codex auth preflight failed: CODEX_HOME must be absolute after expansion, "
211
+ f"got {codex_home!r} -> {home}. A relative path resolves against different cwds "
212
+ "for preflight vs the spawned child and can bill the wrong account.",
213
+ )
214
+ auth_json = home / "auth.json"
215
+ if not _auth_file_readable(auth_json):
216
+ raise DelegateError(
217
+ "codex_auth_unavailable",
218
+ f"Codex auth preflight failed: unreadable auth.json at {auth_json}.",
219
+ )
220
+ profile_overlay = codex.get("profile")
221
+ if isinstance(profile_overlay, str) and profile_overlay.strip():
222
+ config_toml = home / "config.toml"
223
+ if not _auth_file_readable(config_toml):
224
+ raise DelegateError(
225
+ "codex_auth_unavailable",
226
+ f"Codex auth preflight failed: unreadable config.toml at {config_toml}.",
227
+ )
228
+
229
+
230
+ def _request_resolution(request: object) -> ProfileResolution:
231
+ resolution = getattr(request, "profile_resolution", None)
232
+ return resolution if resolution is not None else empty_profile_resolution()
233
+
234
+
235
+ def preflight_codex_request(request: object, codex_config: JsonObject) -> None:
236
+ resolution = _request_resolution(request)
237
+ if resolution.name is not None and resolution.codex_home is None:
238
+ raise DelegateError(
239
+ "profile_missing_codex_home",
240
+ f"Profile {resolution.name!r} is active for a Codex Run but does not define CODEX_HOME. "
241
+ f"Add it under profiles.definitions.{resolution.name}.env.CODEX_HOME.",
242
+ )
243
+ if resolution.codex_home is None:
244
+ return
245
+ preflight_codex_home(resolution.codex_home, codex=codex_config)
246
+ fallback_home = resolution.codex_fallback_home
247
+ if fallback_home is not None and not codex_homes_same_account(
248
+ resolution.codex_home, fallback_home
249
+ ):
250
+ preflight_codex_home(fallback_home, codex=codex_config)
251
+
252
+
253
+ def _codex_auth_file(home: str) -> Path:
254
+ return Path(_expanded_env_value(home), "auth.json")
255
+
256
+
257
+ def codex_homes_same_account(primary_home: str, fallback_home: str) -> bool:
258
+ primary_auth = _codex_auth_file(primary_home)
259
+ fallback_auth = _codex_auth_file(fallback_home)
260
+ try:
261
+ return primary_auth.resolve() == fallback_auth.resolve()
262
+ except (OSError, RuntimeError):
263
+ # Symlink loop or unresolvable path: compare without re-resolving (which would
264
+ # raise again) — fall back to a normalized non-resolving path comparison.
265
+ return os.path.normpath(primary_auth) == os.path.normpath(fallback_auth)
266
+
267
+
268
+ def codex_fallback_env_overrides(resolution: ProfileResolution) -> dict[str, str] | None:
269
+ if resolution.codex_home is None or resolution.codex_fallback_home is None:
270
+ return None
271
+ if codex_homes_same_account(resolution.codex_home, resolution.codex_fallback_home):
272
+ return None
273
+ return {**resolution.env, "CODEX_HOME": resolution.codex_fallback_home}
274
+
275
+
276
+ def classify_codex_usage_limit(stderr_text: str) -> bool:
277
+ if not stderr_text.strip():
278
+ return False
279
+ for pattern in _USAGE_LIMIT_PATTERNS:
280
+ if not pattern.search(stderr_text):
281
+ continue
282
+ if pattern is _RATE_LIMIT_PATTERN and not _ACCOUNT_QUOTA_CONTEXT.search(stderr_text):
283
+ continue
284
+ return True
285
+ return False
286
+
287
+
288
+ def read_bounded_stderr_tail(stderr_log: Path, *, limit: int = STDERR_TAIL_LIMIT) -> str:
289
+ if not stderr_log.exists():
290
+ return ""
291
+ if limit <= 0:
292
+ return ""
293
+ with stderr_log.open("rb") as handle:
294
+ handle.seek(0, os.SEEK_END)
295
+ handle.seek(max(handle.tell() - limit, 0), os.SEEK_SET)
296
+ text = handle.read(limit).decode("utf-8", errors="replace")
297
+ return redaction.redact_string(text)
298
+
299
+
300
+ def accumulator_had_tool_events(accumulator: StreamAccumulator) -> bool:
301
+ return any(event.kind in {"tool.started", "tool.completed"} for event in accumulator.events)
302
+
303
+
304
+ def capture_workspace_porcelain(cwd: str) -> str | None:
305
+ try:
306
+ result = _run_git(
307
+ cwd,
308
+ ["status", "--porcelain=v1", "--untracked-files=normal", "--ignore-submodules=none"],
309
+ timeout_seconds=GIT_QUICK_TIMEOUT_SECONDS,
310
+ )
311
+ except (FileNotFoundError, subprocess.SubprocessError):
312
+ return None
313
+ if result.returncode != 0:
314
+ return None
315
+ return result.stdout
316
+
317
+
318
+ def capture_head_oid(cwd: str) -> str | None:
319
+ try:
320
+ result = _run_git(
321
+ cwd,
322
+ ["rev-parse", "--verify", "HEAD"],
323
+ timeout_seconds=GIT_QUICK_TIMEOUT_SECONDS,
324
+ )
325
+ except (FileNotFoundError, subprocess.SubprocessError):
326
+ return None
327
+ if result.returncode != 0:
328
+ return None
329
+ head = result.stdout.strip()
330
+ return head or None
331
+
332
+
333
+ @dataclass(frozen=True)
334
+ class WorkspaceBaseline:
335
+ porcelain: str
336
+ head: str
337
+
338
+
339
+ def capture_workspace_baseline(cwd: str) -> WorkspaceBaseline | None:
340
+ porcelain = capture_workspace_porcelain(cwd)
341
+ if porcelain is None:
342
+ return None
343
+ head = capture_head_oid(cwd)
344
+ if head is None:
345
+ return None
346
+ return WorkspaceBaseline(porcelain=porcelain, head=head)
347
+
348
+
349
+ def workspace_is_clean(porcelain: str | None) -> bool:
350
+ if porcelain is None:
351
+ return False
352
+ return porcelain.strip() == ""
353
+
354
+
355
+ def workspace_baseline_unchanged(cwd: str, baseline: WorkspaceBaseline | None) -> bool:
356
+ if baseline is None:
357
+ return False
358
+ current = capture_workspace_baseline(cwd)
359
+ if current is None:
360
+ return False
361
+ return current.porcelain == baseline.porcelain and current.head == baseline.head
362
+
363
+
364
+ def work_mode_safe_for_codex_fallback(cwd: str, baseline: WorkspaceBaseline | None) -> bool:
365
+ if baseline is None:
366
+ return False
367
+ if not workspace_is_clean(baseline.porcelain):
368
+ return False
369
+ return workspace_baseline_unchanged(cwd, baseline)
370
+
371
+
372
+ def codex_auth_fallback_metadata(
373
+ *,
374
+ reason: str,
375
+ primary_auth_profile: str | None,
376
+ fallback_auth_profile: str | None,
377
+ primary_exit_code: int,
378
+ fallback_exit_code: int,
379
+ primary_stderr_tail: str,
380
+ ) -> JsonObject:
381
+ return {
382
+ "triggered": True,
383
+ "reason": reason,
384
+ "primaryAuthProfile": primary_auth_profile,
385
+ "fallbackProfile": fallback_auth_profile,
386
+ "primaryExitCode": primary_exit_code,
387
+ "fallbackExitCode": fallback_exit_code,
388
+ "primaryStderrTail": primary_stderr_tail,
389
+ }
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ SKILL_REVIEW_PREFIX = """## Delegate sub-agent skill review requirement
4
+
5
+ Before doing the task, review the full list of skills available in your current agent environment. Load/read and apply any skill instructions that are relevant to the task, workspace, tools, code quality, verification, or final deliverable. If no skill is relevant, proceed normally after explicitly deciding that. This requirement is mandatory for every Delegate Agent run; do not skip it just because the parent prompt did not mention skills.
6
+
7
+ Respect the current Delegate run mode. In safe/read-only mode, skill instructions may guide analysis, review, or recommendations, but must not override the read-only requirement.
8
+
9
+ """
10
+
11
+ COMPLETION_REPORT_SUFFIX = """
12
+
13
+ ## Delegate completion report requirement
14
+
15
+ When you finish, include a concise completion report for the parent agent before
16
+ any operator-requested final payload:
17
+
18
+ - Status: completed / blocked / failed
19
+ - What you did or found
20
+ - Files changed or reviewed
21
+ - Verification run and result
22
+ - Remaining risks or follow-ups
23
+
24
+ Keep it concise. Do not include raw logs unless explicitly relevant. If the
25
+ operator requested an exact final payload such as bare JSON, put that payload
26
+ last after the report, without wrapping it in the report.
27
+ """
28
+
29
+
30
+ def prepend_skill_review_instructions(prompt: str) -> str:
31
+ if prompt.startswith(SKILL_REVIEW_PREFIX):
32
+ return prompt
33
+ return SKILL_REVIEW_PREFIX + prompt
34
+
35
+
36
+ def append_completion_report_instructions(prompt: str) -> str:
37
+ if prompt.rstrip().endswith(COMPLETION_REPORT_SUFFIX.strip()):
38
+ return prompt
39
+ return prompt + COMPLETION_REPORT_SUFFIX
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ CURSOR_PROMPT_REDACTION = "<prompt redacted: cursor argv transport>"
4
+ KIMI_PROMPT_REDACTION = "<prompt redacted: kimi argv transport>"
5
+ PROMPT_FILE_ARG_PLACEHOLDER = "<delegate-prompt-file>"
6
+ PROMPT_FILE_DISPLAY = "<prompt file>"
7
+ DROID_PROMPT_FILE_ARG_PLACEHOLDER = PROMPT_FILE_ARG_PLACEHOLDER
8
+ DROID_PROMPT_FILE_DISPLAY = PROMPT_FILE_DISPLAY
9
+
10
+ PROMPT_TRANSPORT_ARGV = "argv"
11
+ PROMPT_TRANSPORT_FILE = "file"
12
+ PROMPT_TRANSPORT_STDIN = "stdin"