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,242 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from delegate_agent.git_utils import (
|
|
7
|
+
GIT_QUICK_TIMEOUT_SECONDS,
|
|
8
|
+
git_stdout_or_warn,
|
|
9
|
+
rev_parse_verify,
|
|
10
|
+
run_git,
|
|
11
|
+
)
|
|
12
|
+
from delegate_agent.json_types import JsonObject
|
|
13
|
+
|
|
14
|
+
MAX_CHANGED_FILES_REPORTED = 50
|
|
15
|
+
MAX_COMMITS_REPORTED = 20
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _str(value: Any) -> str | None:
|
|
19
|
+
return value if isinstance(value, str) and value else None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _git_stdout(cwd: str, args: list[str], warnings: list[str]) -> str | None:
|
|
23
|
+
return git_stdout_or_warn(
|
|
24
|
+
cwd,
|
|
25
|
+
args,
|
|
26
|
+
warnings=warnings,
|
|
27
|
+
timeout_seconds=GIT_QUICK_TIMEOUT_SECONDS,
|
|
28
|
+
git_runner=run_git,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _parse_porcelain_line(line: str) -> JsonObject:
|
|
33
|
+
path = line[3:] if len(line) > 3 else ""
|
|
34
|
+
entry: JsonObject = {
|
|
35
|
+
"status": line[:2] if len(line) >= 2 else line,
|
|
36
|
+
"path": path,
|
|
37
|
+
}
|
|
38
|
+
if " -> " in path:
|
|
39
|
+
old_path, new_path = path.split(" -> ", 1)
|
|
40
|
+
entry["path"] = new_path
|
|
41
|
+
entry["oldPath"] = old_path
|
|
42
|
+
return entry
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _changed_files(execution_cwd: str, warnings: list[str]) -> tuple[list[JsonObject], int]:
|
|
46
|
+
stdout = _git_stdout(
|
|
47
|
+
execution_cwd,
|
|
48
|
+
["status", "--porcelain=v1", "--untracked-files=normal", "--ignore-submodules=none"],
|
|
49
|
+
warnings,
|
|
50
|
+
)
|
|
51
|
+
if stdout is None:
|
|
52
|
+
return [], 0
|
|
53
|
+
lines = stdout.splitlines()
|
|
54
|
+
return changed_files_from_porcelain_lines(lines)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def changed_files_from_porcelain_lines(
|
|
58
|
+
lines: list[str],
|
|
59
|
+
total: int | None = None,
|
|
60
|
+
) -> tuple[list[JsonObject], int]:
|
|
61
|
+
return (
|
|
62
|
+
[_parse_porcelain_line(line) for line in lines[:MAX_CHANGED_FILES_REPORTED]],
|
|
63
|
+
len(lines) if total is None else total,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_SHORTSTAT_FILES_RE = re.compile(r"(\d+)\s+files?\s+changed")
|
|
68
|
+
_SHORTSTAT_INSERTIONS_RE = re.compile(r"(\d+)\s+insertions?\(\+\)")
|
|
69
|
+
_SHORTSTAT_DELETIONS_RE = re.compile(r"(\d+)\s+deletions?\(-\)")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _parse_shortstat(raw: str) -> JsonObject:
|
|
73
|
+
payload: JsonObject = {"raw": raw}
|
|
74
|
+
files = _SHORTSTAT_FILES_RE.search(raw)
|
|
75
|
+
insertions = _SHORTSTAT_INSERTIONS_RE.search(raw)
|
|
76
|
+
deletions = _SHORTSTAT_DELETIONS_RE.search(raw)
|
|
77
|
+
if files is not None:
|
|
78
|
+
payload["filesChanged"] = int(files.group(1))
|
|
79
|
+
if insertions is not None:
|
|
80
|
+
payload["insertions"] = int(insertions.group(1))
|
|
81
|
+
if deletions is not None:
|
|
82
|
+
payload["deletions"] = int(deletions.group(1))
|
|
83
|
+
return payload
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _diff_shortstat(execution_cwd: str, rev: str, warnings: list[str]) -> JsonObject:
|
|
87
|
+
stdout = _git_stdout(execution_cwd, ["diff", "--shortstat", rev], warnings)
|
|
88
|
+
return _parse_shortstat(stdout or "")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _rev_parse(cwd: str, rev: str, warnings: list[str]) -> str | None:
|
|
92
|
+
return rev_parse_verify(
|
|
93
|
+
cwd,
|
|
94
|
+
rev,
|
|
95
|
+
warnings=warnings,
|
|
96
|
+
timeout_seconds=GIT_QUICK_TIMEOUT_SECONDS,
|
|
97
|
+
git_runner=run_git,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _rev_list_count(cwd: str, rev_range: str, warnings: list[str]) -> int | None:
|
|
102
|
+
stdout = _git_stdout(cwd, ["rev-list", "--count", rev_range], warnings)
|
|
103
|
+
if stdout is None:
|
|
104
|
+
return None
|
|
105
|
+
try:
|
|
106
|
+
return int(stdout.strip())
|
|
107
|
+
except ValueError:
|
|
108
|
+
warnings.append(f"git rev-list returned non-integer count for {rev_range!r}: {stdout}")
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _ahead_behind(cwd: str, left: str, right: str, warnings: list[str]) -> JsonObject | None:
|
|
113
|
+
stdout = _git_stdout(
|
|
114
|
+
cwd, ["rev-list", "--left-right", "--count", f"{left}...{right}"], warnings
|
|
115
|
+
)
|
|
116
|
+
if stdout is None:
|
|
117
|
+
return None
|
|
118
|
+
parts = stdout.split()
|
|
119
|
+
if len(parts) != 2:
|
|
120
|
+
warnings.append(f"git rev-list returned unexpected ahead/behind output: {stdout}")
|
|
121
|
+
return None
|
|
122
|
+
try:
|
|
123
|
+
return {"behind": int(parts[0]), "ahead": int(parts[1])}
|
|
124
|
+
except ValueError:
|
|
125
|
+
warnings.append(f"git rev-list returned non-integer ahead/behind output: {stdout}")
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _commits_created(execution_cwd: str, base: str, warnings: list[str]) -> list[JsonObject] | None:
|
|
130
|
+
stdout = _git_stdout(
|
|
131
|
+
execution_cwd,
|
|
132
|
+
[
|
|
133
|
+
"log",
|
|
134
|
+
f"--max-count={MAX_COMMITS_REPORTED}",
|
|
135
|
+
"--reverse",
|
|
136
|
+
"--format=%H%x01%h%x01%s",
|
|
137
|
+
f"{base}..HEAD",
|
|
138
|
+
],
|
|
139
|
+
warnings,
|
|
140
|
+
)
|
|
141
|
+
if stdout is None:
|
|
142
|
+
return None
|
|
143
|
+
commits: list[JsonObject] = []
|
|
144
|
+
for line in stdout.splitlines():
|
|
145
|
+
parts = line.split("\x01", 2)
|
|
146
|
+
if len(parts) != 3:
|
|
147
|
+
continue
|
|
148
|
+
oid, short_oid, subject = parts
|
|
149
|
+
commits.append({"oid": oid, "shortOid": short_oid, "subject": subject})
|
|
150
|
+
return commits
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def build_work_summary(
|
|
154
|
+
*,
|
|
155
|
+
source_git_root: str | None,
|
|
156
|
+
execution_cwd: str,
|
|
157
|
+
branch: str | None,
|
|
158
|
+
creation_context: JsonObject | None,
|
|
159
|
+
prefetched_changed_files: tuple[list[JsonObject], int] | None = None,
|
|
160
|
+
) -> JsonObject | None:
|
|
161
|
+
"""Return a compact objective summary for a persistent worktree run.
|
|
162
|
+
|
|
163
|
+
The summary is best-effort: unavailable Git metadata is reported in
|
|
164
|
+
``warnings`` instead of making launch finalization fail.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
if not source_git_root or not execution_cwd or not branch:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
warnings: list[str] = []
|
|
171
|
+
creation = creation_context if isinstance(creation_context, dict) else {}
|
|
172
|
+
base = _str(creation.get("sourceHeadOid"))
|
|
173
|
+
|
|
174
|
+
if prefetched_changed_files is None:
|
|
175
|
+
changed_files, changed_total = _changed_files(execution_cwd, warnings)
|
|
176
|
+
else:
|
|
177
|
+
changed_files, changed_total = prefetched_changed_files
|
|
178
|
+
dirty = changed_total > 0
|
|
179
|
+
head_commit = _rev_parse(execution_cwd, "HEAD", warnings)
|
|
180
|
+
source_head = _rev_parse(source_git_root, "HEAD", warnings)
|
|
181
|
+
|
|
182
|
+
commits_count: int | None = None
|
|
183
|
+
commits: list[JsonObject] = []
|
|
184
|
+
commits_fetch_ok = False
|
|
185
|
+
branch_ahead_of_base: JsonObject | None = None
|
|
186
|
+
diff_stat_vs_base: JsonObject | None = None
|
|
187
|
+
if base is not None:
|
|
188
|
+
commits_count = _rev_list_count(execution_cwd, f"{base}..HEAD", warnings)
|
|
189
|
+
behind_base = _rev_list_count(execution_cwd, f"HEAD..{base}", warnings)
|
|
190
|
+
if commits_count is not None and behind_base is not None:
|
|
191
|
+
branch_ahead_of_base = {
|
|
192
|
+
"ahead": commits_count,
|
|
193
|
+
"behind": behind_base,
|
|
194
|
+
"baseOid": base,
|
|
195
|
+
}
|
|
196
|
+
if commits_count is not None:
|
|
197
|
+
commits_result = _commits_created(execution_cwd, base, warnings)
|
|
198
|
+
commits_fetch_ok = commits_result is not None
|
|
199
|
+
commits = commits_result or []
|
|
200
|
+
diff_stat_vs_base = _diff_shortstat(execution_cwd, base, warnings)
|
|
201
|
+
commit_inspection_verified = commits_count is not None
|
|
202
|
+
|
|
203
|
+
branch_ahead_of_source = (
|
|
204
|
+
_ahead_behind(execution_cwd, source_head, "HEAD", warnings) if source_head else None
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
summary: JsonObject = {
|
|
208
|
+
"dirty": dirty,
|
|
209
|
+
"changedFilesCount": changed_total,
|
|
210
|
+
"changedFiles": changed_files,
|
|
211
|
+
"changedFilesTruncated": changed_total > len(changed_files),
|
|
212
|
+
"commitsCreatedCount": commits_count,
|
|
213
|
+
"commitsCreated": commits,
|
|
214
|
+
"commitsCreatedTruncated": (
|
|
215
|
+
commits_count > len(commits)
|
|
216
|
+
if commit_inspection_verified and commits_fetch_ok
|
|
217
|
+
else False
|
|
218
|
+
),
|
|
219
|
+
"commitInspectionStatus": "verified" if commit_inspection_verified else "unverified",
|
|
220
|
+
"baseCommit": base,
|
|
221
|
+
"headCommit": head_commit,
|
|
222
|
+
"sourceHead": source_head,
|
|
223
|
+
"branch": branch,
|
|
224
|
+
"diffStat": _diff_shortstat(execution_cwd, "HEAD", warnings),
|
|
225
|
+
"noChanges": (not dirty and commits_count == 0 if commit_inspection_verified else False),
|
|
226
|
+
}
|
|
227
|
+
if branch_ahead_of_base is not None:
|
|
228
|
+
summary["branchAheadOfBase"] = branch_ahead_of_base
|
|
229
|
+
if branch_ahead_of_source is not None:
|
|
230
|
+
summary["branchAheadOfSource"] = branch_ahead_of_source
|
|
231
|
+
if diff_stat_vs_base is not None:
|
|
232
|
+
summary["diffStatVsBase"] = diff_stat_vs_base
|
|
233
|
+
if warnings:
|
|
234
|
+
summary["warnings"] = warnings
|
|
235
|
+
return summary
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def commits_created_count(summary: JsonObject | None) -> int | None:
|
|
239
|
+
if not isinstance(summary, dict):
|
|
240
|
+
return None
|
|
241
|
+
count = summary.get("commitsCreatedCount")
|
|
242
|
+
return count if isinstance(count, int) and count >= 0 else None
|
delegate_agent/wsl.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
WINDOWS_PATH_RE = re.compile(r"^(?:[A-Za-z]:[\\/]|%[A-Za-z_][A-Za-z0-9_]*%)")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_wsl(env: dict[str, str] | None = None) -> bool:
|
|
11
|
+
env = os.environ if env is None else env
|
|
12
|
+
if env.get("WSL_DISTRO_NAME") or env.get("WSL_INTEROP"):
|
|
13
|
+
return True
|
|
14
|
+
try:
|
|
15
|
+
return "microsoft" in Path("/proc/sys/kernel/osrelease").read_text(encoding="utf-8").lower()
|
|
16
|
+
except OSError:
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_windows_path_text(value: str) -> bool:
|
|
21
|
+
return bool(WINDOWS_PATH_RE.match(value.strip()))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def should_reject_windows_path(value: str) -> bool:
|
|
25
|
+
return is_wsl() and is_windows_path_text(value)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def windows_path_message(field: str, value: str) -> str:
|
|
29
|
+
return (
|
|
30
|
+
f"{field} must be a POSIX/WSL path, got Windows-style path {value!r}. "
|
|
31
|
+
"Inside WSL, convert Windows paths with `wslpath -u` or use `/home/...` "
|
|
32
|
+
"or `/mnt/c/...`."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def drivefs_mount(path: str | Path) -> str | None:
|
|
37
|
+
parts = Path(path).as_posix().split("/")
|
|
38
|
+
if len(parts) >= 3 and parts[1] == "mnt" and len(parts[2]) == 1 and parts[2].isalnum():
|
|
39
|
+
return f"/mnt/{parts[2]}"
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def drivefs_workspace_warning(path: str | Path) -> str | None:
|
|
44
|
+
if not is_wsl():
|
|
45
|
+
return None
|
|
46
|
+
mount = drivefs_mount(path)
|
|
47
|
+
if mount is None:
|
|
48
|
+
return None
|
|
49
|
+
return (
|
|
50
|
+
f"workspace is on Windows-mounted filesystem {mount}; WSL runs are faster "
|
|
51
|
+
"and Delegate's private-mode file permissions are more reliable under "
|
|
52
|
+
"`/home/<user>/...`."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def windows_git_message(git_path: str | None) -> str | None:
|
|
57
|
+
if not is_wsl() or not git_path:
|
|
58
|
+
return None
|
|
59
|
+
path = Path(git_path)
|
|
60
|
+
if path.name.lower() != "git.exe":
|
|
61
|
+
return None
|
|
62
|
+
return (
|
|
63
|
+
f"git resolves to Windows Git at {git_path!r}. Install Git inside WSL "
|
|
64
|
+
"(`sudo apt install git`) or put WSL-native Git before Windows PATH entries."
|
|
65
|
+
)
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: delegate-agent-cli
|
|
3
|
+
Version: 0.11.0
|
|
4
|
+
Summary: A tiny CLI for delegating bounded agent tasks to Cursor, Droid, OpenAI Codex, Claude Code, Grok Build, or Kimi Code runtimes.
|
|
5
|
+
Author: Trey Goff
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/treygoff24/delegate-agent
|
|
8
|
+
Project-URL: Repository, https://github.com/treygoff24/delegate-agent
|
|
9
|
+
Project-URL: Issues, https://github.com/treygoff24/delegate-agent/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/treygoff24/delegate-agent/tree/main/docs
|
|
11
|
+
Project-URL: Security, https://github.com/treygoff24/delegate-agent/security/policy
|
|
12
|
+
Keywords: agents,cli,claude,codex,delegation,cursor,droid,kimi
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Software Development
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: ruff==0.15.15; extra == "dev"
|
|
30
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
31
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<img src="docs/assets/delegate-agent-header.png" alt="Delegate Agent" width="100%">
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
# Delegate Agent
|
|
39
|
+
|
|
40
|
+
Delegate Agent is a small CLI for handing a bounded task to another coding-agent runtime. It normalizes common calls to Cursor Agent, Factory Droid, OpenAI Codex, Claude Code, and Kimi Code so humans or other agents can launch review, investigation, and implementation jobs without remembering each tool's flags.
|
|
41
|
+
|
|
42
|
+
Use it when you want a predictable wrapper around prompts like:
|
|
43
|
+
|
|
44
|
+
- "Review this diff and report risks. Do not edit."
|
|
45
|
+
- "Investigate this failure in an isolated copy."
|
|
46
|
+
- "Implement this scoped change, run the named check, and report changed files."
|
|
47
|
+
|
|
48
|
+
Delegate does **not** commit, push, merge, deploy, publish, or run a background service. It builds the child command, adds safety framing, launches the selected runtime, and records local run metadata for later inspection.
|
|
49
|
+
|
|
50
|
+
Prompt handling is provider-specific: Codex and Claude prompts are delivered to the child
|
|
51
|
+
runtime over stdin; Droid prompts are delivered through a private temporary
|
|
52
|
+
prompt file using Droid's `--file` option; Cursor Agent and Kimi Code currently
|
|
53
|
+
require prompt argv. Delegate redacts Cursor and Kimi prompt argv in dry-run
|
|
54
|
+
output and run manifests, but true process-argv hiding for those harnesses
|
|
55
|
+
depends on the child CLIs exposing stdin or prompt-file transport.
|
|
56
|
+
|
|
57
|
+
## Install from source
|
|
58
|
+
|
|
59
|
+
Delegate requires Python 3.11 or newer. It is currently documented as a GitHub-source install, not a PyPI package.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
python3 -m pip install "delegate-agent @ git+https://github.com/treygoff24/delegate-agent.git"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For local development or a checkout-only smoke test:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/treygoff24/delegate-agent.git
|
|
69
|
+
cd delegate-agent
|
|
70
|
+
python3 -m venv .venv
|
|
71
|
+
. .venv/bin/activate
|
|
72
|
+
python -m pip install -e .
|
|
73
|
+
python3 bin/delegate.py --json describe
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
CI currently validates on Linux with Python 3.11, 3.12, 3.13, and 3.14. Windows support is not claimed until it is covered by tests.
|
|
77
|
+
|
|
78
|
+
## Prerequisites
|
|
79
|
+
|
|
80
|
+
Delegate wraps other CLIs. Install and authenticate only the runtimes you plan to call:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
command -v agent # Cursor Agent CLI (default model: Cursor Composer), used by delegate cursor ...
|
|
84
|
+
command -v droid # Factory Droid CLI, used by delegate droid ...
|
|
85
|
+
command -v codex # OpenAI Codex CLI, used by delegate codex ...
|
|
86
|
+
command -v claude # Claude Code CLI, used by delegate claude ...
|
|
87
|
+
command -v grok # xAI Grok Build CLI, used by delegate grok ...
|
|
88
|
+
command -v kimi # Kimi Code CLI, used by delegate kimi ...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Runtime authentication is owned by each child CLI. Delegate cannot log in for you. Dry-runs and CI tests do not require the real child binaries.
|
|
92
|
+
|
|
93
|
+
Initialize a starter config and replace placeholder Droid model IDs before real Droid runs:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
delegate config init
|
|
97
|
+
$EDITOR ~/.delegate/config.json
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`config init` also writes missing `~/.delegate/config.work.json` and
|
|
101
|
+
`~/.delegate/config.personal.json` profile overlays. For an existing install,
|
|
102
|
+
run `env -u AI_PROFILE delegate config sync-profiles` to materialize any missing
|
|
103
|
+
overlays without overwriting ones you already edited.
|
|
104
|
+
|
|
105
|
+
Inspect what Delegate sees:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
delegate --version # installed version — include this in bug reports
|
|
109
|
+
delegate --json describe --summary
|
|
110
|
+
delegate --json models --summary
|
|
111
|
+
delegate --json describe
|
|
112
|
+
delegate --json models
|
|
113
|
+
delegate --json capabilities
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Discover commands as you go: `delegate <command> --help` prints focused help for any command path, and `delegate --json <command> --help` returns an agent-friendly spec of its usage, arguments, and options. For launch and `dry-run` commands, `--json` and `--isolation` may also appear before inline prompt text begins, for example `delegate codex work --prompt-file task.md --json`. `delegate --json describe` includes a `commands` catalog of the whole surface. `delegate --json capabilities` reports reasoning-effort support without launching a child runtime.
|
|
117
|
+
|
|
118
|
+
From this development checkout, use `python3 bin/delegate.py ...` instead of an installed `delegate` shim.
|
|
119
|
+
|
|
120
|
+
### WSL setup notes
|
|
121
|
+
|
|
122
|
+
Delegate is a POSIX/Linux CLI and works best in WSL when everything is WSL-native:
|
|
123
|
+
|
|
124
|
+
- Install Python, Git, Delegate, and child CLIs inside the WSL distro.
|
|
125
|
+
- Keep projects under `/home/<user>/...`; `/mnt/c/...` works but can be slower and has weaker private-file permission semantics.
|
|
126
|
+
- Pass POSIX paths (`/home/...` or `/mnt/c/...`), not Windows paths like `C:\Users\...`; use `wslpath -u` to convert.
|
|
127
|
+
- If `git` resolves to Windows `git.exe`, Delegate fails with a targeted hint. Install WSL Git, for example `sudo apt install git`.
|
|
128
|
+
|
|
129
|
+
## Quickstart
|
|
130
|
+
|
|
131
|
+
Preview the command without launching a child runtime:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
delegate --json dry-run codex safe "Review this repository. Do not edit files."
|
|
135
|
+
delegate --json dry-run claude safe "Review this repository. Do not edit files."
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Run a read-only review in an isolated throwaway workspace:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
delegate codex safe "Review this repository for correctness risks. Do not edit files."
|
|
142
|
+
delegate claude safe "Review this repository for correctness risks. Do not edit files."
|
|
143
|
+
delegate grok safe "Review this repository for correctness risks. Do not edit files."
|
|
144
|
+
delegate cursor safe "Review the current diff for regressions. Do not edit files."
|
|
145
|
+
delegate kimi safe "Review this repository for regressions. Do not edit files."
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Review uncommitted local changes without committing first or pasting a diff — safe mode mirrors your working tree into the isolated copy:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Edit files locally, then launch safe review as-is:
|
|
152
|
+
delegate codex safe "Review my uncommitted changes for regressions. Do not edit files."
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Run an edit-capable task in a workspace you trust:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
delegate cursor work "Fix the parser bug. Run python3 -m unittest tests.test_delegate_parser. Report changed files."
|
|
159
|
+
delegate claude work "Implement the scoped change and run the named check. Report changed files."
|
|
160
|
+
delegate kimi work "Implement the scoped change and run the named check. Report changed files."
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
For long foreground runs, add `--progress` to emit bounded parent heartbeats to
|
|
164
|
+
stderr while keeping final stdout machine-readable, or set `progress.enabled`
|
|
165
|
+
to `true` in config. Use `--no-progress` to override config for one launch.
|
|
166
|
+
Heartbeat labels are credential-scrubbed before printing but are still
|
|
167
|
+
operational metadata, not raw child output:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
delegate --json claude safe --progress "Review this repository. Do not edit files."
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Run through JSON input for agent callers after copying an example and setting a real `cwd`:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
cp examples/task.codex.json /tmp/delegate-task.json
|
|
177
|
+
$EDITOR /tmp/delegate-task.json
|
|
178
|
+
delegate --json run --input-json /tmp/delegate-task.json
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
For Codex fan-outs that must return machine-parseable records, add `--output-schema` (or JSON `outputSchema`); Delegate suppresses completion-report injection so the schema owns the final message.
|
|
182
|
+
|
|
183
|
+
For a one-hop prompt that should not see the current repo or create a tracked
|
|
184
|
+
run, use stateless `call` mode:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
delegate --json codex call "Summarize this context in three bullets."
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Reasoning effort is provider-aware. Unsupported combinations fail before launch. It changes only the requested model thinking depth/cost/latency; it does not change safe/work/call mode, sandboxing, approvals, or edit capability. Codex/Droid validate effort against model capability metadata, Cursor maps effort to configured model selection, Claude maps to Claude Code `--effort`, and Grok maps to Grok `--effort` (`low`, `medium`, `high`, `xhigh`, `max`). Explicit Codex effort can target the harness default model even when `codex.defaultModel` is unset:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
delegate --json dry-run codex safe --reasoning-effort high "Review this repository. Do not edit files."
|
|
194
|
+
delegate --json dry-run claude safe --reasoning-effort high "Review this repository. Do not edit files."
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Inspect tracked output by alias:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
delegate runs --recent
|
|
201
|
+
delegate snapshot <alias-or-runId>
|
|
202
|
+
delegate run-output <alias-or-runId>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Aliases are always numbered (`codex-1`, `cursor-2`); a **bare harness name
|
|
206
|
+
resolves to the latest matching run**, so `delegate run-output codex` reads the
|
|
207
|
+
newest codex run and `delegate run-output codex:glm` the newest matching
|
|
208
|
+
model. Envelopes echo `requestedHandle`/`resolvedHandle`/`resolutionKind` so you
|
|
209
|
+
can confirm which run you addressed.
|
|
210
|
+
|
|
211
|
+
`run-output` defaults to the best available parent-facing output: a completion
|
|
212
|
+
report when present, a recovered final assistant message when possible, or
|
|
213
|
+
bounded stdout/stderr diagnostics. Use `--completion-report` when you want that
|
|
214
|
+
selector explicitly. Tracked runs also carry a `resultQuality` classification
|
|
215
|
+
(`ok` / `housekeeping_noop` / `empty` / `suspect_short` / `no_assistant_text`),
|
|
216
|
+
and failed or cancelled runs always get a completion report — synthesized when
|
|
217
|
+
the child produced none — so `--completion-report` never dead-ends.
|
|
218
|
+
|
|
219
|
+
Block on background runs instead of polling, and cancel them cleanly:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
delegate wait <alias-or-runId>... [--group NAME] [--timeout SEC] [--completion-report]
|
|
223
|
+
delegate cancel <alias-or-runId>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`wait` uses effective status (a dead child is a terminal failure, not a hang)
|
|
227
|
+
and exits `0` when all waited runs succeeded, `1` when any failed or was
|
|
228
|
+
cancelled, and `124` on timeout. `cancel` signals the run's process group
|
|
229
|
+
(SIGTERM, a short grace period, then SIGKILL) with terminal/stale refusal and a
|
|
230
|
+
start-identity check against PID reuse; a cancelled run reports `cancelled`
|
|
231
|
+
rather than a false success. Tag a batch of launches with `--group NAME` and
|
|
232
|
+
`wait`, `runs`, and the worktree commands can select the whole group at once.
|
|
233
|
+
|
|
234
|
+
## Task design: cluster related work into fewer, richer delegations
|
|
235
|
+
|
|
236
|
+
When splitting a project across delegated runs (or any subagents), fewer workers with clustered, related tasks consistently outperform wide fan-outs of narrow siloed tasks. Modern child runtimes carry very large context windows — the scarce resource is coherence across related tasks, not tokens.
|
|
237
|
+
|
|
238
|
+
- Cluster tasks that share files, schemas, or design trade-offs into one delegation. A single worker that holds the whole entangled cluster can reason about the interactions between its tasks and make globally consistent choices; several siloed workers each make locally reasonable decisions that collide at the seams.
|
|
239
|
+
- Fan out only across genuinely independent units: disjoint file ownership, no shared design decisions, results that do not need to agree with each other.
|
|
240
|
+
- Brief the clustered worker generously. Include the full findings or spec, the decisions already made, and how the tasks relate — context that lets it weigh trade-offs *between* its tasks is the point of clustering.
|
|
241
|
+
|
|
242
|
+
## Safe mode, work mode, and worktree isolation
|
|
243
|
+
|
|
244
|
+
Delegate separates three ideas:
|
|
245
|
+
|
|
246
|
+
| Concept | Meaning |
|
|
247
|
+
| --- | --- |
|
|
248
|
+
| Mode | `safe` is for review/investigation; `work` is edit-capable; `call` is a stateless one-hop model call. |
|
|
249
|
+
| Isolation | The child runtime can run in the source workspace, a temporary copy/worktree, or a persistent Git worktree. |
|
|
250
|
+
| Runtime policy | Extra flags passed to the child runtime, such as Codex `--sandbox read-only`. |
|
|
251
|
+
|
|
252
|
+
Defaults are intentionally conservative for review paths:
|
|
253
|
+
|
|
254
|
+
- `delegate cursor safe`, `delegate codex safe`, `delegate claude safe`, `delegate grok safe`, `delegate droid ALIAS safe`, and `delegate kimi safe` run in an isolated throwaway workspace. Safe mode reviews your **current working tree** — uncommitted tracked edits and untracked, non-ignored files are mirrored into an isolated throwaway copy (only gitignored paths are excluded), so you can review local changes without committing first or pasting a diff.
|
|
255
|
+
- Grok safe mode uses Delegate isolated copy plus Grok read-only sandbox/permission controls (`--sandbox read-only`, `--permission-mode dontAsk` by default). It does not use Grok `plan` mode. Prompts are delivered via Grok `--prompt-file` from a Delegate temp file.
|
|
256
|
+
- Claude safe mode invokes `claude -p` with prompt text on stdin, `--permission-mode plan`, `--strict-mcp-config`, Read/Grep/Glob plus selected read-only Bash tools, and `--no-session-persistence` by default. Delegate does not currently prove that Claude Code hooks, plugins, user settings, or other non-MCP customization surfaces are disabled.
|
|
257
|
+
- `work` mode can edit. By default it runs in the real workspace for backward compatibility.
|
|
258
|
+
|
|
259
|
+
For edit-capable isolation, use a persistent Git worktree:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
delegate --isolation worktree cursor work "Implement the scoped change and run the named check."
|
|
263
|
+
delegate --isolation worktree cursor work --forbid-commit "Implement the scoped change without creating commits."
|
|
264
|
+
delegate worktree list
|
|
265
|
+
delegate worktree show <alias-or-runId>
|
|
266
|
+
delegate worktree remove <alias-or-runId>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Worktree isolation protects the source checkout from ordinary relative-path edits. It is **not** a full security sandbox; the child process can still use its runtime permissions, credentials, network access, and absolute paths according to the environment and runtime policy.
|
|
270
|
+
|
|
271
|
+
Persistent worktree completions and `worktree list/show` include a work summary
|
|
272
|
+
with dirty state, changed file counts, diff stat, and commits created by the
|
|
273
|
+
child where that summary is available. `--forbid-commit` fails the run if
|
|
274
|
+
commits remain ahead of the creation base when the child exits, and it now
|
|
275
|
+
implies `--isolation worktree`.
|
|
276
|
+
|
|
277
|
+
To carry uncommitted local work into the worktree instead of stashing it, add
|
|
278
|
+
`--include-dirty`: it syncs tracked edits and untracked, non-ignored files into
|
|
279
|
+
the worktree through the same snapshot primitives as safe mode, and tears the
|
|
280
|
+
worktree down before launching the child if that sync fails.
|
|
281
|
+
|
|
282
|
+
Safe isolation and `--include-dirty` recreate an untracked symlink only when it is relative, resolves inside the source workspace, and its target is not gitignored; any symlink that fails those checks — an absolute target, an escape out of the tree, or a target that is itself a gitignored secret — is replaced with an inert placeholder file, failing closed on any ambiguity. Delegate reports a warning listing the symlink paths it blocked. In Git repositories with no commits yet, Cursor/Codex/Claude/Droid/Kimi safe isolation falls back to a directory copy because Git cannot create a detached worktree from an unborn `HEAD`.
|
|
283
|
+
|
|
284
|
+
Snapshots and `run-output` redact common credential shapes by default, including authorization headers, bearer/basic tokens, JWT-like strings, and common `token=` / `api_key=` / `password=` values. Use `--no-redact` only when exact output is necessary and safe to display.
|
|
285
|
+
|
|
286
|
+
## Profile-aware auth and env
|
|
287
|
+
|
|
288
|
+
A **profile** selects which credentials and environment every delegated harness inherits, so one session can run under, say, a work account and another under a personal account without editing config between runs. Define profiles under the top-level `profiles` block, then either let Delegate detect the active one from an environment variable (`profiles.detectFrom`) or pin it explicitly with `--auth-profile NAME`:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
delegate --json --auth-profile work profiles # inspect the resolved profile (read-only)
|
|
292
|
+
delegate --auth-profile work codex safe "Review this diff."
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Delegate resolves the profile once per request and injects its env into every spawned child across tracked, pass-through, safe-isolation, and persistent-worktree paths. Profile `env` holds **non-secret routing pointers only** (for example `CODEX_HOME`); secret-shaped keys are rejected at config load, so real credentials stay in your shell or a harness-native key store. See [Configuration](docs/configuration.md#profiles) for the full schema.
|
|
296
|
+
|
|
297
|
+
If `AI_PROFILE=work|personal` points at a missing profile overlay, Delegate
|
|
298
|
+
blocks launch and mutation commands, but lets read-only diagnostics
|
|
299
|
+
(`profiles`, `runs`, `run-output`, `snapshot`, cached `capabilities`,
|
|
300
|
+
`worktree show`, `worktree list`, `describe`, `models`) continue with a
|
|
301
|
+
warning. This guarantee is enforced in the Python CLI itself (`delegate_agent.cli:main`), so it holds no matter how you invoke Delegate: the
|
|
302
|
+
pip console script, `python -m delegate_agent.cli`, `bin/delegate.py`, or a
|
|
303
|
+
shell shim in front of any of those. The tracked launcher shim template,
|
|
304
|
+
`bin/delegate-profile-shim`, applies the same check earlier, before Python
|
|
305
|
+
even starts, as an additional early gate. Temporary bypasses: `env -u
|
|
306
|
+
AI_PROFILE delegate ...` uses the base config, or set
|
|
307
|
+
`DELEGATE_CONFIG=/path/to/config.json`.
|
|
308
|
+
|
|
309
|
+
## Useful docs
|
|
310
|
+
|
|
311
|
+
- [Agent setup](docs/agent-setup.md): human and non-interactive setup flows.
|
|
312
|
+
- [CLI reference](docs/cli-reference.md): commands, exit codes, and JSON contracts.
|
|
313
|
+
- [Configuration](docs/configuration.md): config precedence, sections, and provider-neutral aliases.
|
|
314
|
+
- [Security model](docs/security-model.md): boundaries, limitations, and safe usage.
|
|
315
|
+
- [Worktrees](docs/worktrees.md): persistent-worktree lifecycle and cleanup.
|
|
316
|
+
- [Troubleshooting](docs/troubleshooting.md): common failures and checks.
|
|
317
|
+
- [Contributing](CONTRIBUTING.md) and [Security](SECURITY.md).
|
|
318
|
+
|
|
319
|
+
## Limitations
|
|
320
|
+
|
|
321
|
+
- Delegate is an alpha CLI wrapper. Child runtimes can change their own flags or behavior.
|
|
322
|
+
- Safe mode is a policy and isolation pattern, not a guarantee that a runtime cannot perform side effects outside its execution workspace.
|
|
323
|
+
- Persistent worktrees require a Git repository with a valid `HEAD`; work-mode worktree runs require a clean source checkout.
|
|
324
|
+
- `--pass-through` is incompatible with `--json` and with persistent worktree runs.
|
|
325
|
+
- Delegate stores local run metadata under `.delegate/`; do not commit that directory.
|