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,654 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shlex
|
|
4
|
+
import subprocess # nosec B404 - persistent worktree cleanup intentionally runs fixed git argv with shell=False.
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass, replace
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Protocol, TextIO
|
|
9
|
+
|
|
10
|
+
from delegate_agent import harness_events, profiles, retention, run_registry, safe_workspace
|
|
11
|
+
from delegate_agent import runner as delegate_runner
|
|
12
|
+
from delegate_agent.argv_utils import replace_workspace_arg_in_argv
|
|
13
|
+
from delegate_agent.git_utils import (
|
|
14
|
+
GIT_MUTATION_TIMEOUT_SECONDS,
|
|
15
|
+
GIT_QUICK_TIMEOUT_SECONDS,
|
|
16
|
+
capture_git_metadata,
|
|
17
|
+
)
|
|
18
|
+
from delegate_agent.git_utils import run_git as _run_git
|
|
19
|
+
from delegate_agent.isolation import (
|
|
20
|
+
IsolationContext,
|
|
21
|
+
IsolationExecutionError,
|
|
22
|
+
branch_label,
|
|
23
|
+
compute_repo_fingerprint_from_common_dir,
|
|
24
|
+
create_persistent_worktree,
|
|
25
|
+
plan_branch_name,
|
|
26
|
+
plan_worktree_path,
|
|
27
|
+
prepend_persistent_worktree_context,
|
|
28
|
+
require_clean_source,
|
|
29
|
+
require_valid_head,
|
|
30
|
+
short_run_id,
|
|
31
|
+
worktrees_data_home,
|
|
32
|
+
)
|
|
33
|
+
from delegate_agent.json_types import JsonObject
|
|
34
|
+
from delegate_agent.prompt_transport import (
|
|
35
|
+
PROMPT_FILE_ARG_PLACEHOLDER,
|
|
36
|
+
PROMPT_TRANSPORT_FILE,
|
|
37
|
+
PROMPT_TRANSPORT_STDIN,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PersistentWorktreeError(Exception):
|
|
42
|
+
"""User-facing error raised by the persistent worktree execution boundary."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, error: str, message: str) -> None:
|
|
45
|
+
super().__init__(message)
|
|
46
|
+
self.error = error
|
|
47
|
+
self.message = message
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PersistentExecutionRequest(Protocol):
|
|
51
|
+
engine: str
|
|
52
|
+
mode: str
|
|
53
|
+
workspace: str
|
|
54
|
+
prompt: str
|
|
55
|
+
argv: list[str]
|
|
56
|
+
model: str | None
|
|
57
|
+
model_alias: str | None
|
|
58
|
+
workspace_kind: str
|
|
59
|
+
isolation_context: IsolationContext | None
|
|
60
|
+
warnings: tuple[str, ...]
|
|
61
|
+
reasoning_effort: str | None
|
|
62
|
+
reasoning_effort_source: str | None
|
|
63
|
+
reasoning_capability_source: str | None
|
|
64
|
+
reasoning_transport: str | None
|
|
65
|
+
progress: bool
|
|
66
|
+
forbid_commit: bool
|
|
67
|
+
include_dirty: bool
|
|
68
|
+
stdin_text: str | None
|
|
69
|
+
prompt_file_text: str | None
|
|
70
|
+
prompt_transport: str
|
|
71
|
+
display_argv: list[str] | None
|
|
72
|
+
env_overrides: dict[str, str] | None
|
|
73
|
+
group: str | None
|
|
74
|
+
auth_profile: str | None
|
|
75
|
+
fallback_auth_profile: str | None
|
|
76
|
+
profile_resolution: profiles.ProfileResolution
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class SourceWorkspace(Protocol):
|
|
80
|
+
path: str
|
|
81
|
+
kind: str
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
BinaryValidator = Callable[[list[str], str], None]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class PersistentWorktreeExecution:
|
|
89
|
+
request: PersistentExecutionRequest
|
|
90
|
+
json_mode: bool
|
|
91
|
+
config: JsonObject
|
|
92
|
+
pass_through: bool
|
|
93
|
+
completion_report_mode: str
|
|
94
|
+
source_workspace: SourceWorkspace
|
|
95
|
+
stdout: TextIO
|
|
96
|
+
stderr: TextIO
|
|
97
|
+
binary_validator: BinaryValidator
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class PersistentWorktreePreflight:
|
|
102
|
+
iso_ctx: IsolationContext
|
|
103
|
+
source_git_root: str
|
|
104
|
+
base_oid: str
|
|
105
|
+
source_git_common_dir: str | None
|
|
106
|
+
source_head_oid: str
|
|
107
|
+
source_head_ref: str | None
|
|
108
|
+
source_branch: str | None
|
|
109
|
+
registry_root: Path
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class PersistentWorktreeRegistration:
|
|
114
|
+
run_id: str
|
|
115
|
+
alias: str
|
|
116
|
+
run_path: Path
|
|
117
|
+
branch: str
|
|
118
|
+
worktree_path: str
|
|
119
|
+
creation_context: JsonObject
|
|
120
|
+
pre_ctx: delegate_runner.RunContext
|
|
121
|
+
synced_files: int = 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class ExecutionWorkspaceRequest:
|
|
126
|
+
argv: list[str]
|
|
127
|
+
display_argv: list[str]
|
|
128
|
+
stdin_text: str | None
|
|
129
|
+
prompt_file_text: str | None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def execute_persistent_worktree(
|
|
133
|
+
execution: PersistentWorktreeExecution,
|
|
134
|
+
) -> tuple[int, JsonObject | None]:
|
|
135
|
+
"""Launch a work-mode child in a preserved Delegate-managed git worktree."""
|
|
136
|
+
preflight = _validate_persistent_worktree_request(execution)
|
|
137
|
+
registration = _register_persistent_worktree_run(execution, preflight)
|
|
138
|
+
_create_persistent_worktree_or_record_failure(execution, preflight, registration)
|
|
139
|
+
return _launch_child_in_persistent_worktree(execution, preflight, registration)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _validate_persistent_worktree_request(
|
|
143
|
+
execution: PersistentWorktreeExecution,
|
|
144
|
+
) -> PersistentWorktreePreflight:
|
|
145
|
+
request = execution.request
|
|
146
|
+
iso_ctx = request.isolation_context
|
|
147
|
+
if iso_ctx is None:
|
|
148
|
+
raise PersistentWorktreeError(
|
|
149
|
+
"missing_isolation_context",
|
|
150
|
+
"Persistent worktree execution requires an isolation context.",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if request.workspace_kind != "git":
|
|
154
|
+
raise PersistentWorktreeError(
|
|
155
|
+
"worktree_requires_git",
|
|
156
|
+
"--isolation worktree requires a Git workspace.",
|
|
157
|
+
)
|
|
158
|
+
source_git_root = request.workspace
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
base_oid = require_valid_head(source_git_root)
|
|
162
|
+
except IsolationExecutionError as exc:
|
|
163
|
+
raise PersistentWorktreeError(exc.error, exc.message) from exc
|
|
164
|
+
(
|
|
165
|
+
_current_git_root,
|
|
166
|
+
current_git_common_dir,
|
|
167
|
+
_current_head_oid,
|
|
168
|
+
current_head_ref,
|
|
169
|
+
current_branch,
|
|
170
|
+
) = capture_git_metadata(source_git_root)
|
|
171
|
+
|
|
172
|
+
if execution.pass_through:
|
|
173
|
+
raise PersistentWorktreeError(
|
|
174
|
+
"pass_through_with_persistent_isolation",
|
|
175
|
+
"--pass-through is not supported with persistent worktree runs (work mode + effective worktree isolation).",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if not request.include_dirty:
|
|
179
|
+
try:
|
|
180
|
+
require_clean_source(source_git_root)
|
|
181
|
+
except IsolationExecutionError as exc:
|
|
182
|
+
raise PersistentWorktreeError(exc.error, exc.message) from exc
|
|
183
|
+
|
|
184
|
+
execution.binary_validator(request.argv, request.engine)
|
|
185
|
+
|
|
186
|
+
if request.engine == "codex":
|
|
187
|
+
profiles.preflight_codex_request(request, execution.config.get("codex", {}))
|
|
188
|
+
|
|
189
|
+
registry_root = run_registry.ensure_registry(
|
|
190
|
+
Path(execution.source_workspace.path),
|
|
191
|
+
workspace_kind=execution.source_workspace.kind,
|
|
192
|
+
)
|
|
193
|
+
retention.run_retention_pass(registry_root, execution.config)
|
|
194
|
+
|
|
195
|
+
return PersistentWorktreePreflight(
|
|
196
|
+
iso_ctx=iso_ctx,
|
|
197
|
+
source_git_root=source_git_root,
|
|
198
|
+
base_oid=base_oid,
|
|
199
|
+
source_git_common_dir=current_git_common_dir or iso_ctx.source_git_common_dir,
|
|
200
|
+
source_head_oid=base_oid,
|
|
201
|
+
source_head_ref=current_head_ref,
|
|
202
|
+
source_branch=current_branch,
|
|
203
|
+
registry_root=registry_root,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _build_persistent_worktree_run_context(
|
|
208
|
+
execution: PersistentWorktreeExecution,
|
|
209
|
+
preflight: PersistentWorktreePreflight,
|
|
210
|
+
*,
|
|
211
|
+
run_id: str,
|
|
212
|
+
alias: str,
|
|
213
|
+
branch: str,
|
|
214
|
+
worktree_path: str,
|
|
215
|
+
creation_context: JsonObject,
|
|
216
|
+
) -> delegate_runner.RunContext:
|
|
217
|
+
request = execution.request
|
|
218
|
+
iso_ctx = preflight.iso_ctx
|
|
219
|
+
dirty_warnings = creation_context.get("includeDirtyWarnings")
|
|
220
|
+
merged_warnings = (
|
|
221
|
+
(*request.warnings, *[w for w in dirty_warnings if isinstance(w, str)])
|
|
222
|
+
if isinstance(dirty_warnings, list)
|
|
223
|
+
else request.warnings
|
|
224
|
+
)
|
|
225
|
+
return delegate_runner.RunContext(
|
|
226
|
+
registry_root=preflight.registry_root,
|
|
227
|
+
run_id=run_id,
|
|
228
|
+
alias=alias,
|
|
229
|
+
harness=request.engine,
|
|
230
|
+
engine=request.engine,
|
|
231
|
+
mode=request.mode,
|
|
232
|
+
model=request.model,
|
|
233
|
+
source_cwd=execution.source_workspace.path,
|
|
234
|
+
execution_cwd=worktree_path,
|
|
235
|
+
workspace_kind=execution.source_workspace.kind,
|
|
236
|
+
isolated_workspace=True,
|
|
237
|
+
started_at=run_registry.utc_now_iso(),
|
|
238
|
+
model_alias=request.model_alias,
|
|
239
|
+
model_resolved=request.model,
|
|
240
|
+
creation_context=creation_context,
|
|
241
|
+
source_git_root=iso_ctx.source_git_root or preflight.source_git_root,
|
|
242
|
+
isolation_mode=iso_ctx.isolation_mode,
|
|
243
|
+
effective_isolation=iso_ctx.effective_isolation,
|
|
244
|
+
isolation_lifecycle=iso_ctx.isolation_lifecycle,
|
|
245
|
+
preserved_workspace=iso_ctx.preserved_workspace,
|
|
246
|
+
branch=branch,
|
|
247
|
+
worktree_status="present",
|
|
248
|
+
warnings=merged_warnings,
|
|
249
|
+
reasoning_effort=request.reasoning_effort,
|
|
250
|
+
reasoning_effort_source=request.reasoning_effort_source,
|
|
251
|
+
reasoning_capability_source=request.reasoning_capability_source,
|
|
252
|
+
reasoning_transport=request.reasoning_transport,
|
|
253
|
+
prompt_transport=request.prompt_transport,
|
|
254
|
+
forbid_commit=request.forbid_commit,
|
|
255
|
+
progress_initial_delay_sec=request.progress_initial_delay_sec,
|
|
256
|
+
progress_interval_sec=request.progress_interval_sec,
|
|
257
|
+
env_overrides=dict(request.env_overrides or {}),
|
|
258
|
+
fallback_env_overrides=dict(
|
|
259
|
+
profiles.codex_fallback_env_overrides(request.profile_resolution) or {}
|
|
260
|
+
),
|
|
261
|
+
auth_profile=request.auth_profile,
|
|
262
|
+
fallback_auth_profile=request.fallback_auth_profile,
|
|
263
|
+
include_dirty=request.include_dirty,
|
|
264
|
+
synced_files=int(creation_context.get("syncedFiles") or 0),
|
|
265
|
+
group=request.group,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _register_persistent_worktree_run(
|
|
270
|
+
execution: PersistentWorktreeExecution,
|
|
271
|
+
preflight: PersistentWorktreePreflight,
|
|
272
|
+
) -> PersistentWorktreeRegistration:
|
|
273
|
+
request = execution.request
|
|
274
|
+
label = branch_label(request.engine, request.model_alias)
|
|
275
|
+
|
|
276
|
+
run_id, alias = run_registry.register_run(
|
|
277
|
+
preflight.registry_root,
|
|
278
|
+
harness=request.engine,
|
|
279
|
+
metadata={
|
|
280
|
+
"mode": request.mode,
|
|
281
|
+
"model": request.model,
|
|
282
|
+
"modelAlias": request.model_alias,
|
|
283
|
+
"modelResolved": request.model,
|
|
284
|
+
"cwd": execution.source_workspace.path,
|
|
285
|
+
"group": request.group,
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
short_id = short_run_id(run_id)
|
|
290
|
+
branch = plan_branch_name(label, short_id)
|
|
291
|
+
data_home = worktrees_data_home(execution.config)
|
|
292
|
+
|
|
293
|
+
source_git_common_dir = preflight.source_git_common_dir
|
|
294
|
+
if source_git_common_dir is None:
|
|
295
|
+
raise PersistentWorktreeError(
|
|
296
|
+
"worktree_requires_git",
|
|
297
|
+
"--isolation worktree could not determine the Git common directory.",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
fingerprint = compute_repo_fingerprint_from_common_dir(source_git_common_dir)
|
|
301
|
+
worktree_path = str(plan_worktree_path(data_home, fingerprint, label, short_id))
|
|
302
|
+
|
|
303
|
+
creation_context: JsonObject = {
|
|
304
|
+
"sourceHeadOid": preflight.source_head_oid,
|
|
305
|
+
"sourceHeadRef": preflight.source_head_ref,
|
|
306
|
+
"sourceBranch": preflight.source_branch,
|
|
307
|
+
"sourceGitCommonDir": source_git_common_dir,
|
|
308
|
+
"branch": branch,
|
|
309
|
+
"plannedBranch": branch,
|
|
310
|
+
"plannedExecutionCwd": worktree_path,
|
|
311
|
+
"label": label,
|
|
312
|
+
"shortRunId": short_id,
|
|
313
|
+
"includeDirty": request.include_dirty,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
pre_ctx = _build_persistent_worktree_run_context(
|
|
317
|
+
execution,
|
|
318
|
+
preflight,
|
|
319
|
+
run_id=run_id,
|
|
320
|
+
alias=alias,
|
|
321
|
+
branch=branch,
|
|
322
|
+
worktree_path=worktree_path,
|
|
323
|
+
creation_context=creation_context,
|
|
324
|
+
)
|
|
325
|
+
run_path = run_registry.run_directory(preflight.registry_root, run_id)
|
|
326
|
+
run_path.mkdir(parents=True, exist_ok=True)
|
|
327
|
+
delegate_runner.write_manifest(
|
|
328
|
+
run_path,
|
|
329
|
+
delegate_runner.build_manifest(pre_ctx, _public_argv(request)),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
delegate_runner.write_state(
|
|
333
|
+
run_path,
|
|
334
|
+
delegate_runner.build_state(
|
|
335
|
+
pre_ctx,
|
|
336
|
+
status="creating_isolation",
|
|
337
|
+
extra={"plannedBranch": branch, "plannedExecutionCwd": worktree_path},
|
|
338
|
+
),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return PersistentWorktreeRegistration(
|
|
342
|
+
run_id=run_id,
|
|
343
|
+
alias=alias,
|
|
344
|
+
run_path=run_path,
|
|
345
|
+
branch=branch,
|
|
346
|
+
worktree_path=worktree_path,
|
|
347
|
+
creation_context=creation_context,
|
|
348
|
+
pre_ctx=pre_ctx,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _record_persistent_worktree_failure(
|
|
353
|
+
registration: PersistentWorktreeRegistration,
|
|
354
|
+
*,
|
|
355
|
+
error: str,
|
|
356
|
+
message: str,
|
|
357
|
+
) -> None:
|
|
358
|
+
failed_state = delegate_runner.build_state(
|
|
359
|
+
registration.pre_ctx,
|
|
360
|
+
status="failed",
|
|
361
|
+
extra={
|
|
362
|
+
"error": error,
|
|
363
|
+
"message": message,
|
|
364
|
+
"plannedBranch": registration.branch,
|
|
365
|
+
"plannedExecutionCwd": registration.worktree_path,
|
|
366
|
+
},
|
|
367
|
+
)
|
|
368
|
+
delegate_runner.write_state(registration.run_path, failed_state)
|
|
369
|
+
|
|
370
|
+
failed_snapshot = delegate_runner.build_snapshot(
|
|
371
|
+
registration.pre_ctx,
|
|
372
|
+
accumulator=harness_events.StreamAccumulator(harness=registration.pre_ctx.harness),
|
|
373
|
+
)
|
|
374
|
+
failed_snapshot["ok"] = False
|
|
375
|
+
failed_snapshot["error"] = error
|
|
376
|
+
failed_snapshot["message"] = message
|
|
377
|
+
failed_snapshot["status"] = "failed"
|
|
378
|
+
failed_snapshot["plannedBranch"] = registration.branch
|
|
379
|
+
failed_snapshot["plannedExecutionCwd"] = registration.worktree_path
|
|
380
|
+
for key in ("executionCwd", "worktreeStatus", "worktreeCleanupCommands", "branch"):
|
|
381
|
+
failed_snapshot.pop(key, None)
|
|
382
|
+
delegate_runner.write_snapshot(registration.run_path, failed_snapshot)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _create_persistent_worktree_or_record_failure(
|
|
386
|
+
execution: PersistentWorktreeExecution,
|
|
387
|
+
preflight: PersistentWorktreePreflight,
|
|
388
|
+
registration: PersistentWorktreeRegistration,
|
|
389
|
+
) -> None:
|
|
390
|
+
try:
|
|
391
|
+
create_persistent_worktree(
|
|
392
|
+
preflight.source_git_root,
|
|
393
|
+
registration.branch,
|
|
394
|
+
registration.worktree_path,
|
|
395
|
+
preflight.base_oid,
|
|
396
|
+
)
|
|
397
|
+
if execution.request.include_dirty:
|
|
398
|
+
synced_files, warnings = safe_workspace.sync_git_dirty_snapshot(
|
|
399
|
+
preflight.source_git_root,
|
|
400
|
+
registration.worktree_path,
|
|
401
|
+
)
|
|
402
|
+
registration.creation_context["syncedFiles"] = synced_files
|
|
403
|
+
registration.creation_context["includeDirtyWarnings"] = list(warnings)
|
|
404
|
+
if warnings:
|
|
405
|
+
registration.pre_ctx = replace(
|
|
406
|
+
registration.pre_ctx,
|
|
407
|
+
warnings=(*registration.pre_ctx.warnings, *warnings),
|
|
408
|
+
synced_files=synced_files,
|
|
409
|
+
)
|
|
410
|
+
else:
|
|
411
|
+
registration.pre_ctx = replace(
|
|
412
|
+
registration.pre_ctx,
|
|
413
|
+
synced_files=synced_files,
|
|
414
|
+
)
|
|
415
|
+
delegate_runner.write_manifest(
|
|
416
|
+
registration.run_path,
|
|
417
|
+
delegate_runner.build_manifest(
|
|
418
|
+
registration.pre_ctx,
|
|
419
|
+
_public_argv(execution.request),
|
|
420
|
+
),
|
|
421
|
+
)
|
|
422
|
+
except IsolationExecutionError as exc:
|
|
423
|
+
_record_persistent_worktree_failure(
|
|
424
|
+
registration,
|
|
425
|
+
error=exc.error,
|
|
426
|
+
message=exc.message,
|
|
427
|
+
)
|
|
428
|
+
if exc.error != "branch_collision":
|
|
429
|
+
_cleanup_partial_worktree(
|
|
430
|
+
preflight.source_git_root,
|
|
431
|
+
registration.worktree_path,
|
|
432
|
+
registration.branch,
|
|
433
|
+
registration.run_path,
|
|
434
|
+
stderr=execution.stderr,
|
|
435
|
+
remove_branch=True,
|
|
436
|
+
)
|
|
437
|
+
raise PersistentWorktreeError(exc.error, exc.message) from exc
|
|
438
|
+
except Exception as exc:
|
|
439
|
+
error = getattr(exc, "error", "worktree_setup_failed")
|
|
440
|
+
message = getattr(exc, "message", str(exc))
|
|
441
|
+
_record_persistent_worktree_failure(
|
|
442
|
+
registration,
|
|
443
|
+
error=str(error),
|
|
444
|
+
message=str(message),
|
|
445
|
+
)
|
|
446
|
+
_cleanup_partial_worktree(
|
|
447
|
+
preflight.source_git_root,
|
|
448
|
+
registration.worktree_path,
|
|
449
|
+
registration.branch,
|
|
450
|
+
registration.run_path,
|
|
451
|
+
stderr=execution.stderr,
|
|
452
|
+
remove_branch=True,
|
|
453
|
+
)
|
|
454
|
+
raise PersistentWorktreeError(str(error), str(message)) from exc
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _request_for_execution_workspace(
|
|
458
|
+
request: PersistentExecutionRequest,
|
|
459
|
+
execution_workspace: str,
|
|
460
|
+
) -> ExecutionWorkspaceRequest:
|
|
461
|
+
execution_argv = replace_workspace_arg_in_argv(
|
|
462
|
+
request.engine,
|
|
463
|
+
request.argv,
|
|
464
|
+
execution_workspace,
|
|
465
|
+
)
|
|
466
|
+
execution_stdin_text = request.stdin_text
|
|
467
|
+
execution_prompt_file_text = request.prompt_file_text
|
|
468
|
+
execution_prompt = _persistent_prompt(request.prompt, forbid_commit=request.forbid_commit)
|
|
469
|
+
if request.prompt_transport == PROMPT_TRANSPORT_STDIN:
|
|
470
|
+
execution_stdin_text = _persistent_prompt(
|
|
471
|
+
execution_stdin_text or request.prompt,
|
|
472
|
+
forbid_commit=request.forbid_commit,
|
|
473
|
+
)
|
|
474
|
+
elif request.prompt_transport == PROMPT_TRANSPORT_FILE:
|
|
475
|
+
execution_prompt_file_text = _persistent_prompt(
|
|
476
|
+
execution_prompt_file_text or request.prompt,
|
|
477
|
+
forbid_commit=request.forbid_commit,
|
|
478
|
+
)
|
|
479
|
+
else:
|
|
480
|
+
execution_argv[-1] = execution_prompt
|
|
481
|
+
execution_display_argv = replace_workspace_arg_in_argv(
|
|
482
|
+
request.engine,
|
|
483
|
+
_public_argv(request),
|
|
484
|
+
execution_workspace,
|
|
485
|
+
)
|
|
486
|
+
return ExecutionWorkspaceRequest(
|
|
487
|
+
argv=execution_argv,
|
|
488
|
+
display_argv=execution_display_argv,
|
|
489
|
+
stdin_text=execution_stdin_text,
|
|
490
|
+
prompt_file_text=execution_prompt_file_text,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _persistent_prompt(prompt: str, *, forbid_commit: bool) -> str:
|
|
495
|
+
prompt = prepend_persistent_worktree_context(prompt)
|
|
496
|
+
if not forbid_commit:
|
|
497
|
+
return prompt
|
|
498
|
+
return (
|
|
499
|
+
"Delegate commit policy: --forbid-commit is active for this run. "
|
|
500
|
+
"Do not run `git commit` or create commits. Leave file changes uncommitted; "
|
|
501
|
+
"Delegate will mark the run failed if commits remain ahead of the creation base "
|
|
502
|
+
"when the child exits.\n\n"
|
|
503
|
+
f"{prompt}"
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _launch_child_in_persistent_worktree(
|
|
508
|
+
execution: PersistentWorktreeExecution,
|
|
509
|
+
preflight: PersistentWorktreePreflight,
|
|
510
|
+
registration: PersistentWorktreeRegistration,
|
|
511
|
+
) -> tuple[int, JsonObject | None]:
|
|
512
|
+
request = execution.request
|
|
513
|
+
try:
|
|
514
|
+
execution_request = _request_for_execution_workspace(
|
|
515
|
+
request,
|
|
516
|
+
registration.worktree_path,
|
|
517
|
+
)
|
|
518
|
+
execution.binary_validator(execution_request.argv, request.engine)
|
|
519
|
+
|
|
520
|
+
exec_ctx = _build_persistent_worktree_run_context(
|
|
521
|
+
execution,
|
|
522
|
+
preflight,
|
|
523
|
+
run_id=registration.run_id,
|
|
524
|
+
alias=registration.alias,
|
|
525
|
+
branch=registration.branch,
|
|
526
|
+
worktree_path=registration.worktree_path,
|
|
527
|
+
creation_context=registration.creation_context,
|
|
528
|
+
)
|
|
529
|
+
delegate_runner.write_manifest(
|
|
530
|
+
registration.run_path,
|
|
531
|
+
delegate_runner.build_manifest(exec_ctx, execution_request.display_argv),
|
|
532
|
+
)
|
|
533
|
+
exit_code, payload = delegate_runner.execute_tracked(
|
|
534
|
+
execution_request.argv,
|
|
535
|
+
registration.worktree_path,
|
|
536
|
+
exec_ctx,
|
|
537
|
+
json_mode=execution.json_mode,
|
|
538
|
+
stdout=execution.stdout,
|
|
539
|
+
stderr=execution.stderr,
|
|
540
|
+
completion_report_mode=execution.completion_report_mode,
|
|
541
|
+
stdin_text=execution_request.stdin_text,
|
|
542
|
+
prompt_file_text=execution_request.prompt_file_text,
|
|
543
|
+
prompt_file_placeholder=PROMPT_FILE_ARG_PLACEHOLDER,
|
|
544
|
+
manifest_argv=execution_request.display_argv,
|
|
545
|
+
progress=request.progress,
|
|
546
|
+
progress_initial_delay_sec=request.progress_initial_delay_sec,
|
|
547
|
+
progress_interval_sec=request.progress_interval_sec,
|
|
548
|
+
)
|
|
549
|
+
except Exception as exc:
|
|
550
|
+
error_msg = str(exc)
|
|
551
|
+
error_code = getattr(exc, "error", "execution_failed")
|
|
552
|
+
# _record_persistent_worktree_failure writes both the failed state and
|
|
553
|
+
# the failed snapshot in one consolidated pass; do not write_state here
|
|
554
|
+
# first (that would be a redundant double write of the same failed
|
|
555
|
+
# status). Behavior is identical, with a single state write.
|
|
556
|
+
_record_persistent_worktree_failure(
|
|
557
|
+
registration,
|
|
558
|
+
error=str(error_code),
|
|
559
|
+
message=error_msg,
|
|
560
|
+
)
|
|
561
|
+
raise PersistentWorktreeError(error_code, error_msg) from exc
|
|
562
|
+
|
|
563
|
+
run_registry.set_worktree_status(
|
|
564
|
+
preflight.registry_root,
|
|
565
|
+
registration.run_id,
|
|
566
|
+
"present",
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
return exit_code, payload
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _public_argv(request: PersistentExecutionRequest) -> list[str]:
|
|
573
|
+
return list(request.display_argv if request.display_argv is not None else request.argv)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _cleanup_partial_worktree(
|
|
577
|
+
source_git_root: str,
|
|
578
|
+
worktree_path: str,
|
|
579
|
+
branch: str,
|
|
580
|
+
run_path: Path,
|
|
581
|
+
*,
|
|
582
|
+
stderr: TextIO,
|
|
583
|
+
remove_branch: bool = True,
|
|
584
|
+
) -> None:
|
|
585
|
+
path = Path(worktree_path)
|
|
586
|
+
cleanup_failed = False
|
|
587
|
+
if path.exists() or path.is_symlink():
|
|
588
|
+
try:
|
|
589
|
+
result = _run_git(
|
|
590
|
+
source_git_root,
|
|
591
|
+
["worktree", "remove", "--force", worktree_path],
|
|
592
|
+
timeout_seconds=GIT_MUTATION_TIMEOUT_SECONDS,
|
|
593
|
+
)
|
|
594
|
+
if result.returncode != 0:
|
|
595
|
+
cleanup_failed = True
|
|
596
|
+
except (OSError, subprocess.SubprocessError):
|
|
597
|
+
cleanup_failed = True
|
|
598
|
+
if remove_branch:
|
|
599
|
+
try:
|
|
600
|
+
result = _run_git(
|
|
601
|
+
source_git_root,
|
|
602
|
+
["branch", "-D", branch],
|
|
603
|
+
timeout_seconds=GIT_MUTATION_TIMEOUT_SECONDS,
|
|
604
|
+
)
|
|
605
|
+
if result.returncode != 0 and _branch_delete_still_needs_cleanup(
|
|
606
|
+
source_git_root,
|
|
607
|
+
branch,
|
|
608
|
+
):
|
|
609
|
+
cleanup_failed = True
|
|
610
|
+
except (OSError, subprocess.SubprocessError):
|
|
611
|
+
cleanup_failed = True
|
|
612
|
+
if cleanup_failed:
|
|
613
|
+
commands = [
|
|
614
|
+
shlex.join(
|
|
615
|
+
["git", "-C", source_git_root, "worktree", "remove", "--force", worktree_path]
|
|
616
|
+
)
|
|
617
|
+
]
|
|
618
|
+
if remove_branch:
|
|
619
|
+
commands.append(shlex.join(["git", "-C", source_git_root, "branch", "-D", branch]))
|
|
620
|
+
manual = " && ".join(commands)
|
|
621
|
+
snapshot_path = run_path / run_registry.SNAPSHOT_FILE
|
|
622
|
+
metadata_warning: str | None = None
|
|
623
|
+
if snapshot_path.exists():
|
|
624
|
+
try:
|
|
625
|
+
existing = run_registry.read_json_object(snapshot_path)
|
|
626
|
+
if existing is not None:
|
|
627
|
+
existing["cleanupFailed"] = True
|
|
628
|
+
existing["manualCleanup"] = manual
|
|
629
|
+
run_registry.write_json_atomic(snapshot_path, existing)
|
|
630
|
+
except (OSError, ValueError) as exc:
|
|
631
|
+
metadata_warning = (
|
|
632
|
+
"warning: partial worktree cleanup failed, and Delegate could not "
|
|
633
|
+
f"record cleanup metadata in {snapshot_path}: {exc}"
|
|
634
|
+
)
|
|
635
|
+
if metadata_warning is not None:
|
|
636
|
+
print(metadata_warning, file=stderr)
|
|
637
|
+
print(
|
|
638
|
+
f"warning: partial worktree cleanup failed; manual cleanup required: {manual}",
|
|
639
|
+
file=stderr,
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _branch_delete_still_needs_cleanup(source_git_root: str, branch: str) -> bool:
|
|
644
|
+
try:
|
|
645
|
+
result = _run_git(
|
|
646
|
+
source_git_root,
|
|
647
|
+
["show-ref", "--verify", "--quiet", f"refs/heads/{branch}"],
|
|
648
|
+
timeout_seconds=GIT_QUICK_TIMEOUT_SECONDS,
|
|
649
|
+
)
|
|
650
|
+
except (OSError, subprocess.SubprocessError):
|
|
651
|
+
return True
|
|
652
|
+
if result.returncode == 0:
|
|
653
|
+
return True
|
|
654
|
+
return result.returncode != 1
|