agent-control-plane 0.3.0 → 0.6.0
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.
- package/README.md +141 -28
- package/assets/workflow-catalog.json +1 -1
- package/bin/pr-risk.sh +22 -7
- package/bin/sync-pr-labels.sh +1 -1
- package/hooks/heartbeat-hooks.sh +125 -12
- package/hooks/issue-reconcile-hooks.sh +1 -1
- package/hooks/pr-reconcile-hooks.sh +1 -1
- package/npm/bin/agent-control-plane.js +257 -59
- package/package.json +39 -32
- package/tools/bin/debug-session.sh +106 -0
- package/tools/bin/flow-config-lib.sh +1203 -60
- package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
- package/tools/bin/flow-runtime-doctor.sh +5 -1
- package/tools/bin/flow-shell-lib.sh +32 -0
- package/tools/bin/github-core-rate-limit-state.sh +77 -0
- package/tools/bin/github-write-outbox.sh +470 -0
- package/tools/bin/heartbeat-loop-scheduling-lib.sh +7 -7
- package/tools/bin/heartbeat-safe-auto.sh +42 -0
- package/tools/bin/install-project-launchd.sh +17 -2
- package/tools/bin/install-project-systemd.sh +255 -0
- package/tools/bin/project-init.sh +21 -1
- package/tools/bin/project-launchd-bootstrap.sh +5 -1
- package/tools/bin/project-runtimectl.sh +91 -2
- package/tools/bin/project-systemd-bootstrap.sh +74 -0
- package/tools/bin/scaffold-profile.sh +61 -3
- package/tools/bin/uninstall-project-systemd.sh +87 -0
- package/tools/dashboard/app.js +228 -6
- package/tools/dashboard/dashboard_snapshot.py +55 -0
- package/tools/dashboard/issue_queue_state.py +101 -0
- package/tools/dashboard/server.py +123 -1
- package/tools/dashboard/styles.css +526 -455
- package/tools/templates/pr-fix-template.md +3 -1
- package/tools/templates/pr-merge-repair-template.md +2 -1
- package/references/architecture.md +0 -217
- package/references/commands.md +0 -128
- package/references/control-plane-map.md +0 -124
- package/references/docs-map.md +0 -73
- package/references/release-checklist.md +0 -65
- package/references/repo-map.md +0 -36
- package/tools/bin/agent-cleanup-worktree +0 -247
- package/tools/bin/agent-github-update-labels +0 -71
- package/tools/bin/agent-init-worktree +0 -216
- package/tools/bin/agent-project-archive-run +0 -52
- package/tools/bin/agent-project-capture-worker +0 -46
- package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
- package/tools/bin/agent-project-catch-up-merged-prs +0 -194
- package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
- package/tools/bin/agent-project-cleanup-session +0 -513
- package/tools/bin/agent-project-detached-launch +0 -127
- package/tools/bin/agent-project-heartbeat-loop +0 -1029
- package/tools/bin/agent-project-open-issue-worktree +0 -89
- package/tools/bin/agent-project-open-pr-worktree +0 -80
- package/tools/bin/agent-project-publish-issue-pr +0 -465
- package/tools/bin/agent-project-reconcile-issue-session +0 -1398
- package/tools/bin/agent-project-reconcile-pr-session +0 -1230
- package/tools/bin/agent-project-retry-state +0 -147
- package/tools/bin/agent-project-run-claude-session +0 -805
- package/tools/bin/agent-project-run-codex-resilient +0 -955
- package/tools/bin/agent-project-run-codex-session +0 -435
- package/tools/bin/agent-project-run-kilo-session +0 -369
- package/tools/bin/agent-project-run-ollama-session +0 -658
- package/tools/bin/agent-project-run-openclaw-session +0 -1309
- package/tools/bin/agent-project-run-opencode-session +0 -377
- package/tools/bin/agent-project-run-pi-session +0 -479
- package/tools/bin/agent-project-sync-anchor-repo +0 -139
- package/tools/bin/agent-project-worker-status +0 -188
- package/tools/bin/branch-verification-guard.sh +0 -364
- package/tools/bin/capture-worker.sh +0 -18
- package/tools/bin/cleanup-worktree.sh +0 -52
- package/tools/bin/codex-quota +0 -31
- package/tools/bin/create-follow-up-issue.sh +0 -114
- package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
- package/tools/bin/issue-publish-localization-guard.sh +0 -142
- package/tools/bin/issue-publish-scope-guard.sh +0 -242
- package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
- package/tools/bin/issue-resource-class.sh +0 -12
- package/tools/bin/kick-scheduler.sh +0 -75
- package/tools/bin/label-follow-up-issues.sh +0 -14
- package/tools/bin/new-pr-worktree.sh +0 -50
- package/tools/bin/new-worktree.sh +0 -49
- package/tools/bin/pr-risk.sh +0 -12
- package/tools/bin/prepare-worktree.sh +0 -142
- package/tools/bin/provider-cooldown-state.sh +0 -204
- package/tools/bin/publish-issue-worker.sh +0 -31
- package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
- package/tools/bin/reconcile-issue-worker.sh +0 -34
- package/tools/bin/reconcile-pr-worker.sh +0 -34
- package/tools/bin/record-verification.sh +0 -71
- package/tools/bin/render-flow-config.sh +0 -98
- package/tools/bin/resident-issue-controller-lib.sh +0 -448
- package/tools/bin/resident-issue-queue-status.py +0 -35
- package/tools/bin/retry-state.sh +0 -31
- package/tools/bin/reuse-issue-worktree.sh +0 -121
- package/tools/bin/run-codex-bypass.sh +0 -3
- package/tools/bin/run-codex-safe.sh +0 -3
- package/tools/bin/run-codex-task.sh +0 -280
- package/tools/bin/serve-dashboard.sh +0 -5
- package/tools/bin/split-retained-slice.sh +0 -124
- package/tools/bin/start-issue-worker.sh +0 -943
- package/tools/bin/start-pr-fix-worker.sh +0 -491
- package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
- package/tools/bin/start-pr-review-worker.sh +0 -261
- package/tools/bin/start-resident-issue-loop.sh +0 -499
- package/tools/bin/update-github-labels.sh +0 -14
- package/tools/bin/worker-status.sh +0 -19
- package/tools/bin/workflow-catalog.sh +0 -77
|
@@ -1,1309 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
usage() {
|
|
5
|
-
cat <<'EOF'
|
|
6
|
-
Usage:
|
|
7
|
-
agent-project-run-openclaw-session --mode safe|bypass --session <id> --worktree <path> --prompt-file <path> --runs-root <path> --adapter-id <id> --task-kind <kind> --task-id <id> [options]
|
|
8
|
-
|
|
9
|
-
Launch an OpenClaw local agent worker inside tmux for a project adapter and
|
|
10
|
-
persist the standard run artifacts.
|
|
11
|
-
|
|
12
|
-
Options:
|
|
13
|
-
--env-prefix <prefix> Export prefixed runtime/context env vars inside the worker
|
|
14
|
-
--context <KEY=VALUE> Extra metadata written to run.env and exported to the worker
|
|
15
|
-
--collect-file <name> Copy worker artifact file into the host run dir after execution
|
|
16
|
-
--reconcile-command <cmd> Host-side command queued after the worker exits
|
|
17
|
-
--sandbox-subdir <name> Subdir under the worktree for worker artifacts (default: .openclaw-artifacts)
|
|
18
|
-
--openclaw-model <id> Model id for the isolated OpenClaw agent
|
|
19
|
-
--openclaw-thinking <level> OpenClaw thinking level
|
|
20
|
-
--openclaw-timeout-seconds <secs> OpenClaw local-agent timeout
|
|
21
|
-
--openclaw-stall-seconds <secs> Fail when the agent produces no output for too long (0 disables)
|
|
22
|
-
--help Show this help
|
|
23
|
-
EOF
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
mode=""
|
|
27
|
-
session=""
|
|
28
|
-
worktree=""
|
|
29
|
-
prompt_file=""
|
|
30
|
-
runs_root=""
|
|
31
|
-
adapter_id=""
|
|
32
|
-
task_kind=""
|
|
33
|
-
task_id=""
|
|
34
|
-
env_prefix=""
|
|
35
|
-
sandbox_subdir=".openclaw-artifacts"
|
|
36
|
-
reconcile_command=""
|
|
37
|
-
keep_agent="false"
|
|
38
|
-
openclaw_model="${ACP_OPENCLAW_MODEL:-${F_LOSNING_OPENCLAW_MODEL:-openrouter/qwen/qwen3.6-plus-preview:free}}"
|
|
39
|
-
openclaw_thinking="${ACP_OPENCLAW_THINKING:-${F_LOSNING_OPENCLAW_THINKING:-low}}"
|
|
40
|
-
openclaw_timeout_seconds="${ACP_OPENCLAW_TIMEOUT_SECONDS:-${F_LOSNING_OPENCLAW_TIMEOUT_SECONDS:-900}}"
|
|
41
|
-
openclaw_stall_seconds="${ACP_OPENCLAW_STALL_SECONDS:-${F_LOSNING_OPENCLAW_STALL_SECONDS:-180}}"
|
|
42
|
-
openclaw_progress_heartbeat_seconds="${ACP_OPENCLAW_PROGRESS_HEARTBEAT_SECONDS:-${F_LOSNING_OPENCLAW_PROGRESS_HEARTBEAT_SECONDS:-30}}"
|
|
43
|
-
provided_openclaw_agent_id=""
|
|
44
|
-
provided_openclaw_session_id=""
|
|
45
|
-
provided_openclaw_agent_dir=""
|
|
46
|
-
provided_openclaw_state_dir=""
|
|
47
|
-
provided_openclaw_config_path=""
|
|
48
|
-
declare -a context_items=()
|
|
49
|
-
declare -a collect_files=()
|
|
50
|
-
|
|
51
|
-
while [[ $# -gt 0 ]]; do
|
|
52
|
-
case "$1" in
|
|
53
|
-
--mode) mode="${2:-}"; shift 2 ;;
|
|
54
|
-
--session) session="${2:-}"; shift 2 ;;
|
|
55
|
-
--worktree) worktree="${2:-}"; shift 2 ;;
|
|
56
|
-
--prompt-file) prompt_file="${2:-}"; shift 2 ;;
|
|
57
|
-
--runs-root) runs_root="${2:-}"; shift 2 ;;
|
|
58
|
-
--adapter-id) adapter_id="${2:-}"; shift 2 ;;
|
|
59
|
-
--task-kind) task_kind="${2:-}"; shift 2 ;;
|
|
60
|
-
--task-id) task_id="${2:-}"; shift 2 ;;
|
|
61
|
-
--env-prefix) env_prefix="${2:-}"; shift 2 ;;
|
|
62
|
-
--context) context_items+=("${2:-}"); shift 2 ;;
|
|
63
|
-
--collect-file) collect_files+=("${2:-}"); shift 2 ;;
|
|
64
|
-
--reconcile-command) reconcile_command="${2:-}"; shift 2 ;;
|
|
65
|
-
--sandbox-subdir) sandbox_subdir="${2:-}"; shift 2 ;;
|
|
66
|
-
--keep-agent) keep_agent="true"; shift ;;
|
|
67
|
-
--openclaw-model) openclaw_model="${2:-}"; shift 2 ;;
|
|
68
|
-
--openclaw-thinking) openclaw_thinking="${2:-}"; shift 2 ;;
|
|
69
|
-
--openclaw-timeout-seconds) openclaw_timeout_seconds="${2:-}"; shift 2 ;;
|
|
70
|
-
--openclaw-stall-seconds) openclaw_stall_seconds="${2:-}"; shift 2 ;;
|
|
71
|
-
--openclaw-agent-id) provided_openclaw_agent_id="${2:-}"; shift 2 ;;
|
|
72
|
-
--openclaw-session-id) provided_openclaw_session_id="${2:-}"; shift 2 ;;
|
|
73
|
-
--openclaw-agent-dir) provided_openclaw_agent_dir="${2:-}"; shift 2 ;;
|
|
74
|
-
--openclaw-state-dir) provided_openclaw_state_dir="${2:-}"; shift 2 ;;
|
|
75
|
-
--openclaw-config-path) provided_openclaw_config_path="${2:-}"; shift 2 ;;
|
|
76
|
-
--help|-h) usage; exit 0 ;;
|
|
77
|
-
*) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
|
|
78
|
-
esac
|
|
79
|
-
done
|
|
80
|
-
|
|
81
|
-
if [[ -z "$mode" || -z "$session" || -z "$worktree" || -z "$prompt_file" || -z "$runs_root" || -z "$adapter_id" || -z "$task_kind" || -z "$task_id" ]]; then
|
|
82
|
-
usage >&2
|
|
83
|
-
exit 1
|
|
84
|
-
fi
|
|
85
|
-
|
|
86
|
-
case "$mode" in
|
|
87
|
-
safe|bypass) ;;
|
|
88
|
-
*)
|
|
89
|
-
echo "--mode must be safe or bypass" >&2
|
|
90
|
-
exit 1
|
|
91
|
-
;;
|
|
92
|
-
esac
|
|
93
|
-
|
|
94
|
-
case "$openclaw_timeout_seconds" in
|
|
95
|
-
''|*[!0-9]*) echo "--openclaw-timeout-seconds must be numeric" >&2; exit 1 ;;
|
|
96
|
-
esac
|
|
97
|
-
case "$openclaw_stall_seconds" in
|
|
98
|
-
''|*[!0-9]*) echo "--openclaw-stall-seconds must be numeric" >&2; exit 1 ;;
|
|
99
|
-
esac
|
|
100
|
-
case "$openclaw_progress_heartbeat_seconds" in
|
|
101
|
-
''|*[!0-9]*) echo "OpenClaw progress heartbeat seconds must be numeric" >&2; exit 1 ;;
|
|
102
|
-
0) echo "OpenClaw progress heartbeat seconds must be greater than zero" >&2; exit 1 ;;
|
|
103
|
-
esac
|
|
104
|
-
|
|
105
|
-
if ! command -v openclaw >/dev/null 2>&1; then
|
|
106
|
-
echo "unable to resolve a runnable openclaw binary" >&2
|
|
107
|
-
exit 1
|
|
108
|
-
fi
|
|
109
|
-
|
|
110
|
-
artifact_dir="${runs_root}/${session}"
|
|
111
|
-
output_file="${artifact_dir}/${session}.log"
|
|
112
|
-
inner_script="${artifact_dir}/${session}.sh"
|
|
113
|
-
meta_file="${artifact_dir}/run.env"
|
|
114
|
-
result_file="${artifact_dir}/result.env"
|
|
115
|
-
runner_state_file="${artifact_dir}/runner.env"
|
|
116
|
-
sandbox_artifact_dir="${worktree%/}/${sandbox_subdir}"
|
|
117
|
-
sandbox_run_dir="${worktree%/}/${sandbox_subdir}/${session}"
|
|
118
|
-
retained_repo_root="${ACP_RETAINED_REPO_ROOT:-${F_LOSNING_RETAINED_REPO_ROOT:-}}"
|
|
119
|
-
started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
120
|
-
openclaw_bin="$(command -v openclaw)"
|
|
121
|
-
default_openclaw_agent_id="$(
|
|
122
|
-
printf '%s-%s' "$adapter_id" "$session" \
|
|
123
|
-
| tr '[:upper:]' '[:lower:]' \
|
|
124
|
-
| sed -E 's/[^a-z0-9._-]+/-/g; s/^-+//; s/-+$//; s/-+/-/g' \
|
|
125
|
-
| cut -c1-63
|
|
126
|
-
)"
|
|
127
|
-
openclaw_agent_id="${provided_openclaw_agent_id:-${default_openclaw_agent_id}}"
|
|
128
|
-
openclaw_session_id="${provided_openclaw_session_id:-${openclaw_agent_id}}"
|
|
129
|
-
openclaw_state_dir="${provided_openclaw_state_dir:-${artifact_dir}/openclaw-state}"
|
|
130
|
-
openclaw_config_path="${provided_openclaw_config_path:-${artifact_dir}/openclaw-config/openclaw.json}"
|
|
131
|
-
openclaw_agent_dir="${provided_openclaw_agent_dir:-${artifact_dir}/openclaw-agent}"
|
|
132
|
-
|
|
133
|
-
mkdir -p "$artifact_dir"
|
|
134
|
-
mkdir -p "$sandbox_run_dir"
|
|
135
|
-
|
|
136
|
-
if tmux has-session -t "$session" 2>/dev/null; then
|
|
137
|
-
echo "tmux session already exists: $session" >&2
|
|
138
|
-
exit 1
|
|
139
|
-
fi
|
|
140
|
-
|
|
141
|
-
branch_name="$(git -C "$worktree" branch --show-current 2>/dev/null || true)"
|
|
142
|
-
|
|
143
|
-
printf -v session_q '%q' "$session"
|
|
144
|
-
printf -v task_kind_q '%q' "$task_kind"
|
|
145
|
-
printf -v task_id_q '%q' "$task_id"
|
|
146
|
-
printf -v mode_q '%q' "$mode"
|
|
147
|
-
printf -v worktree_q '%q' "$worktree"
|
|
148
|
-
printf -v prompt_q '%q' "$prompt_file"
|
|
149
|
-
printf -v output_q '%q' "$output_file"
|
|
150
|
-
printf -v artifact_dir_q '%q' "$artifact_dir"
|
|
151
|
-
printf -v script_q '%q' "$inner_script"
|
|
152
|
-
printf -v result_q '%q' "$result_file"
|
|
153
|
-
printf -v meta_file_q '%q' "$meta_file"
|
|
154
|
-
printf -v runner_state_q '%q' "$runner_state_file"
|
|
155
|
-
printf -v branch_q '%q' "$branch_name"
|
|
156
|
-
printf -v sandbox_artifact_dir_q '%q' "$sandbox_artifact_dir"
|
|
157
|
-
printf -v sandbox_run_dir_q '%q' "$sandbox_run_dir"
|
|
158
|
-
printf -v retained_repo_root_q '%q' "$retained_repo_root"
|
|
159
|
-
printf -v adapter_id_q '%q' "$adapter_id"
|
|
160
|
-
printf -v started_at_q '%q' "$started_at"
|
|
161
|
-
printf -v openclaw_bin_q '%q' "$openclaw_bin"
|
|
162
|
-
printf -v openclaw_state_dir_q '%q' "$openclaw_state_dir"
|
|
163
|
-
printf -v openclaw_config_path_q '%q' "$openclaw_config_path"
|
|
164
|
-
printf -v openclaw_agent_dir_q '%q' "$openclaw_agent_dir"
|
|
165
|
-
printf -v openclaw_agent_id_q '%q' "$openclaw_agent_id"
|
|
166
|
-
printf -v openclaw_session_id_q '%q' "$openclaw_session_id"
|
|
167
|
-
printf -v openclaw_model_q '%q' "$openclaw_model"
|
|
168
|
-
printf -v openclaw_thinking_q '%q' "$openclaw_thinking"
|
|
169
|
-
printf -v openclaw_timeout_q '%q' "$openclaw_timeout_seconds"
|
|
170
|
-
printf -v openclaw_stall_q '%q' "$openclaw_stall_seconds"
|
|
171
|
-
printf -v openclaw_progress_heartbeat_q '%q' "$openclaw_progress_heartbeat_seconds"
|
|
172
|
-
printf -v keep_agent_q '%q' "$keep_agent"
|
|
173
|
-
|
|
174
|
-
{
|
|
175
|
-
printf 'TASK_KIND=%s\n' "$task_kind_q"
|
|
176
|
-
printf 'TASK_ID=%s\n' "$task_id_q"
|
|
177
|
-
printf 'SESSION=%s\n' "$session_q"
|
|
178
|
-
printf 'MODE=%s\n' "$mode_q"
|
|
179
|
-
printf 'WORKTREE=%s\n' "$worktree_q"
|
|
180
|
-
printf 'PROMPT_FILE=%s\n' "$prompt_q"
|
|
181
|
-
printf 'OUTPUT_FILE=%s\n' "$output_q"
|
|
182
|
-
printf 'SCRIPT=%s\n' "$script_q"
|
|
183
|
-
printf 'BRANCH=%s\n' "$branch_q"
|
|
184
|
-
printf 'RESULT_FILE=%s\n' "$result_q"
|
|
185
|
-
printf 'RUNNER_STATE_FILE=%s\n' "$runner_state_q"
|
|
186
|
-
printf 'SANDBOX_RUN_DIR=%s\n' "$sandbox_run_dir_q"
|
|
187
|
-
printf 'ADAPTER_ID=%s\n' "$adapter_id_q"
|
|
188
|
-
printf 'STARTED_AT=%s\n' "$started_at_q"
|
|
189
|
-
printf 'OPENCLAW_BIN=%s\n' "$openclaw_bin_q"
|
|
190
|
-
printf 'OPENCLAW_STATE_DIR=%s\n' "$openclaw_state_dir_q"
|
|
191
|
-
printf 'OPENCLAW_CONFIG_PATH=%s\n' "$openclaw_config_path_q"
|
|
192
|
-
printf 'OPENCLAW_AGENT_DIR=%s\n' "$openclaw_agent_dir_q"
|
|
193
|
-
printf 'OPENCLAW_AGENT_ID=%s\n' "$openclaw_agent_id_q"
|
|
194
|
-
printf 'OPENCLAW_SESSION_ID=%s\n' "$openclaw_session_id_q"
|
|
195
|
-
printf 'OPENCLAW_MODEL=%s\n' "$openclaw_model_q"
|
|
196
|
-
printf 'OPENCLAW_THINKING=%s\n' "$openclaw_thinking_q"
|
|
197
|
-
printf 'OPENCLAW_TIMEOUT_SECONDS=%s\n' "$openclaw_timeout_q"
|
|
198
|
-
printf 'OPENCLAW_STALL_SECONDS=%s\n' "$openclaw_stall_q"
|
|
199
|
-
printf 'OPENCLAW_PROGRESS_HEARTBEAT_SECONDS=%s\n' "$openclaw_progress_heartbeat_q"
|
|
200
|
-
printf 'OPENCLAW_KEEP_AGENT=%s\n' "$keep_agent_q"
|
|
201
|
-
} >"$meta_file"
|
|
202
|
-
|
|
203
|
-
context_exports=""
|
|
204
|
-
if ((${#context_items[@]} > 0)); then
|
|
205
|
-
for item in "${context_items[@]}"; do
|
|
206
|
-
if [[ "$item" != *=* ]]; then
|
|
207
|
-
echo "--context must use KEY=VALUE syntax: $item" >&2
|
|
208
|
-
exit 1
|
|
209
|
-
fi
|
|
210
|
-
key="${item%%=*}"
|
|
211
|
-
value="${item#*=}"
|
|
212
|
-
if [[ ! "$key" =~ ^[A-Z0-9_]+$ ]]; then
|
|
213
|
-
echo "Invalid context key: $key" >&2
|
|
214
|
-
exit 1
|
|
215
|
-
fi
|
|
216
|
-
printf -v value_q '%q' "$value"
|
|
217
|
-
printf '%s=%s\n' "$key" "$value_q" >>"$meta_file"
|
|
218
|
-
if [[ -n "$env_prefix" ]]; then
|
|
219
|
-
context_exports+="export ${env_prefix}${key}=${value_q}"$'\n'
|
|
220
|
-
fi
|
|
221
|
-
context_exports+="export ACP_${key}=${value_q}"$'\n'
|
|
222
|
-
if [[ "$env_prefix" != "F_LOSNING_" ]]; then
|
|
223
|
-
context_exports+="export F_LOSNING_${key}=${value_q}"$'\n'
|
|
224
|
-
fi
|
|
225
|
-
done
|
|
226
|
-
fi
|
|
227
|
-
|
|
228
|
-
runtime_exports=$(
|
|
229
|
-
cat <<EOF
|
|
230
|
-
export AGENT_PROJECT_SESSION=${session_q}
|
|
231
|
-
export AGENT_PROJECT_RUN_DIR=${sandbox_run_dir_q}
|
|
232
|
-
export AGENT_PROJECT_HOST_RUN_DIR=${artifact_dir_q}
|
|
233
|
-
export AGENT_PROJECT_RESULT_FILE=${sandbox_run_dir_q}/result.env
|
|
234
|
-
export AGENT_PROJECT_OPENCLAW_BIN=${openclaw_bin_q}
|
|
235
|
-
export AGENT_PROJECT_RETAINED_REPO_ROOT=${retained_repo_root_q}
|
|
236
|
-
export ACP_SESSION=${session_q}
|
|
237
|
-
export ACP_RUN_DIR=${sandbox_run_dir_q}
|
|
238
|
-
export ACP_HOST_RUN_DIR=${artifact_dir_q}
|
|
239
|
-
export ACP_RESULT_FILE=${sandbox_run_dir_q}/result.env
|
|
240
|
-
export ACP_OPENCLAW_BIN=${openclaw_bin_q}
|
|
241
|
-
export ACP_OPENCLAW_SESSION_ID=${openclaw_session_id_q}
|
|
242
|
-
export ACP_RETAINED_REPO_ROOT=${retained_repo_root_q}
|
|
243
|
-
export F_LOSNING_SESSION=${session_q}
|
|
244
|
-
export F_LOSNING_RUN_DIR=${sandbox_run_dir_q}
|
|
245
|
-
export F_LOSNING_HOST_RUN_DIR=${artifact_dir_q}
|
|
246
|
-
export F_LOSNING_RESULT_FILE=${sandbox_run_dir_q}/result.env
|
|
247
|
-
export F_LOSNING_OPENCLAW_BIN=${openclaw_bin_q}
|
|
248
|
-
export F_LOSNING_OPENCLAW_SESSION_ID=${openclaw_session_id_q}
|
|
249
|
-
export F_LOSNING_RETAINED_REPO_ROOT=${retained_repo_root_q}
|
|
250
|
-
export OPENCLAW_STATE_DIR=${openclaw_state_dir_q}
|
|
251
|
-
export OPENCLAW_CONFIG_PATH=${openclaw_config_path_q}
|
|
252
|
-
EOF
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
if [[ -n "$env_prefix" ]]; then
|
|
256
|
-
runtime_exports+=$'\n'
|
|
257
|
-
runtime_exports+=$(cat <<EOF
|
|
258
|
-
export ${env_prefix}SESSION=${session_q}
|
|
259
|
-
export ${env_prefix}RUN_DIR=${sandbox_run_dir_q}
|
|
260
|
-
export ${env_prefix}HOST_RUN_DIR=${artifact_dir_q}
|
|
261
|
-
export ${env_prefix}RESULT_FILE=${sandbox_run_dir_q}/result.env
|
|
262
|
-
export ${env_prefix}OPENCLAW_BIN=${openclaw_bin_q}
|
|
263
|
-
export ${env_prefix}OPENCLAW_SESSION_ID=${openclaw_session_id_q}
|
|
264
|
-
EOF
|
|
265
|
-
)
|
|
266
|
-
fi
|
|
267
|
-
|
|
268
|
-
collect_copy_snippet=""
|
|
269
|
-
if ((${#collect_files[@]} > 0)); then
|
|
270
|
-
for artifact_name in "${collect_files[@]}"; do
|
|
271
|
-
if [[ -z "$artifact_name" ]]; then
|
|
272
|
-
continue
|
|
273
|
-
fi
|
|
274
|
-
printf -v artifact_q '%q' "$artifact_name"
|
|
275
|
-
collect_copy_snippet+=$(
|
|
276
|
-
cat <<EOF
|
|
277
|
-
if [[ -f ${sandbox_run_dir_q}/${artifact_q} ]]; then
|
|
278
|
-
cp ${sandbox_run_dir_q}/${artifact_q} ${artifact_dir_q}/${artifact_q}
|
|
279
|
-
fi
|
|
280
|
-
EOF
|
|
281
|
-
)
|
|
282
|
-
collect_copy_snippet+=$'\n'
|
|
283
|
-
done
|
|
284
|
-
fi
|
|
285
|
-
|
|
286
|
-
# Always collect result.env from sandbox to artifact_dir
|
|
287
|
-
collect_copy_snippet+=$(
|
|
288
|
-
cat <<EOF
|
|
289
|
-
if [[ -f ${sandbox_run_dir_q}/result.env ]]; then
|
|
290
|
-
cp ${sandbox_run_dir_q}/result.env ${artifact_dir_q}/result.env
|
|
291
|
-
fi
|
|
292
|
-
EOF
|
|
293
|
-
)
|
|
294
|
-
collect_copy_snippet+=$'\n'
|
|
295
|
-
|
|
296
|
-
reconcile_snippet=""
|
|
297
|
-
if [[ -n "$reconcile_command" ]]; then
|
|
298
|
-
printf -v delayed_reconcile_q '%q' "export ACP_EXPECTED_RUN_STARTED_AT=${started_at_q}; export F_LOSNING_EXPECTED_RUN_STARTED_AT=${started_at_q}; while tmux has-session -t ${session_q} 2>/dev/null; do sleep 1; done; sleep 2; $reconcile_command"
|
|
299
|
-
reconcile_snippet="nohup bash -lc ${delayed_reconcile_q} >> ${output_q} 2>&1 </dev/null &"
|
|
300
|
-
fi
|
|
301
|
-
|
|
302
|
-
cat >"$inner_script" <<EOF
|
|
303
|
-
#!/usr/bin/env bash
|
|
304
|
-
set -euo pipefail
|
|
305
|
-
${runtime_exports}
|
|
306
|
-
${context_exports}cd ${worktree_q}
|
|
307
|
-
|
|
308
|
-
runner_state_file=${runner_state_q}
|
|
309
|
-
output_file=${output_q}
|
|
310
|
-
sandbox_artifact_dir=${sandbox_artifact_dir_q}
|
|
311
|
-
sandbox_run_dir=${sandbox_run_dir_q}
|
|
312
|
-
retained_repo_root=${retained_repo_root_q}
|
|
313
|
-
artifact_dir=${artifact_dir_q}
|
|
314
|
-
run_dir=${artifact_dir_q}
|
|
315
|
-
task_kind=${task_kind_q}
|
|
316
|
-
worktree=${worktree_q}
|
|
317
|
-
prompt_file_path=${prompt_q}
|
|
318
|
-
openclaw_state_dir=${openclaw_state_dir_q}
|
|
319
|
-
openclaw_config_path=${openclaw_config_path_q}
|
|
320
|
-
openclaw_agent_dir=${openclaw_agent_dir_q}
|
|
321
|
-
openclaw_agent_id=${openclaw_agent_id_q}
|
|
322
|
-
openclaw_session_id=${openclaw_session_id_q}
|
|
323
|
-
openclaw_model=${openclaw_model_q}
|
|
324
|
-
openclaw_bin=${openclaw_bin_q}
|
|
325
|
-
openclaw_timeout=${openclaw_timeout_q}
|
|
326
|
-
openclaw_stall_seconds=${openclaw_stall_q}
|
|
327
|
-
openclaw_progress_heartbeat_seconds=${openclaw_progress_heartbeat_q}
|
|
328
|
-
openclaw_thinking=${openclaw_thinking_q}
|
|
329
|
-
keep_agent=${keep_agent_q}
|
|
330
|
-
openclaw_add_log="\${sandbox_run_dir}/openclaw-agents-add.log"
|
|
331
|
-
|
|
332
|
-
write_state() {
|
|
333
|
-
local runner_state="\${1:?runner state required}"
|
|
334
|
-
local last_exit_code="\${2:-}"
|
|
335
|
-
local failure_reason="\${3:-}"
|
|
336
|
-
local updated_at tmp_file
|
|
337
|
-
|
|
338
|
-
updated_at="\$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
339
|
-
tmp_file="\${runner_state_file}.tmp.\$\$"
|
|
340
|
-
{
|
|
341
|
-
printf 'RUNNER_STATE=%q\n' "\${runner_state}"
|
|
342
|
-
printf 'THREAD_ID=%q\n' "\${openclaw_session_id}"
|
|
343
|
-
printf 'ATTEMPT=1\n'
|
|
344
|
-
printf 'RESUME_COUNT=0\n'
|
|
345
|
-
printf 'LAST_EXIT_CODE=%q\n' "\${last_exit_code}"
|
|
346
|
-
printf 'LAST_FAILURE_REASON=%q\n' "\${failure_reason}"
|
|
347
|
-
printf 'LAST_TRIGGER_REASON=%q\n' ''
|
|
348
|
-
printf 'AUTH_WAIT_STARTED_AT=%q\n' ''
|
|
349
|
-
printf 'LAST_AUTH_FINGERPRINT=%q\n' ''
|
|
350
|
-
printf 'UPDATED_AT=%q\n' "\${updated_at}"
|
|
351
|
-
} >"\${tmp_file}"
|
|
352
|
-
mv "\${tmp_file}" "\${runner_state_file}"
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
record_final_git_state() {
|
|
356
|
-
local final_head final_branch tmp_file
|
|
357
|
-
|
|
358
|
-
final_head="\$(git -C ${worktree_q} rev-parse HEAD 2>/dev/null || true)"
|
|
359
|
-
final_branch="\$(git -C ${worktree_q} branch --show-current 2>/dev/null || true)"
|
|
360
|
-
tmp_file=${meta_file_q}.tmp.final.$$
|
|
361
|
-
grep -vE '^(FINAL_HEAD|FINAL_BRANCH)=' ${meta_file_q} >"\${tmp_file}" 2>/dev/null || true
|
|
362
|
-
{
|
|
363
|
-
printf 'FINAL_HEAD=%q\n' "\${final_head}"
|
|
364
|
-
printf 'FINAL_BRANCH=%q\n' "\${final_branch}"
|
|
365
|
-
} >>"\${tmp_file}"
|
|
366
|
-
mv "\${tmp_file}" ${meta_file_q}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
ensure_openclaw_workspace_excludes() {
|
|
370
|
-
local exclude_file line
|
|
371
|
-
if ! git -C ${worktree_q} rev-parse --git-dir >/dev/null 2>&1; then
|
|
372
|
-
return 0
|
|
373
|
-
fi
|
|
374
|
-
exclude_file="\$(git -C ${worktree_q} config --worktree --get core.excludesFile 2>/dev/null || true)"
|
|
375
|
-
if [[ -z "\${exclude_file}" ]]; then
|
|
376
|
-
exclude_file="\${sandbox_artifact_dir}/git-exclude"
|
|
377
|
-
git -C ${worktree_q} config extensions.worktreeConfig true >/dev/null 2>&1 || true
|
|
378
|
-
git -C ${worktree_q} config --worktree core.excludesFile "\${exclude_file}"
|
|
379
|
-
fi
|
|
380
|
-
|
|
381
|
-
mkdir -p "\$(dirname "\${exclude_file}")"
|
|
382
|
-
touch "\${exclude_file}"
|
|
383
|
-
while IFS= read -r line; do
|
|
384
|
-
[[ -n "\${line}" ]] || continue
|
|
385
|
-
if ! grep -Fqx "\${line}" "\${exclude_file}" 2>/dev/null; then
|
|
386
|
-
printf '%s\n' "\${line}" >>"\${exclude_file}"
|
|
387
|
-
fi
|
|
388
|
-
done <<'PATTERNS'
|
|
389
|
-
.openclaw-artifacts
|
|
390
|
-
.openclaw
|
|
391
|
-
SOUL.md
|
|
392
|
-
TOOLS.md
|
|
393
|
-
IDENTITY.md
|
|
394
|
-
USER.md
|
|
395
|
-
HEARTBEAT.md
|
|
396
|
-
BOOTSTRAP.md
|
|
397
|
-
AGENTS.md
|
|
398
|
-
.agent-session.env
|
|
399
|
-
\$ACP_RUN_DIR
|
|
400
|
-
\$AGENT_PROJECT_RUN_DIR
|
|
401
|
-
\$F_LOSNING_RUN_DIR
|
|
402
|
-
\$ACP_HOST_RUN_DIR
|
|
403
|
-
\$AGENT_PROJECT_HOST_RUN_DIR
|
|
404
|
-
\$F_LOSNING_HOST_RUN_DIR
|
|
405
|
-
\$ACP_RESULT_FILE
|
|
406
|
-
\$AGENT_PROJECT_RESULT_FILE
|
|
407
|
-
\$F_LOSNING_RESULT_FILE
|
|
408
|
-
PATTERNS
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
install_pre_commit_scope_hook() {
|
|
412
|
-
local hooks_dir="\$(git -C ${worktree_q} rev-parse --git-path hooks 2>/dev/null || true)"
|
|
413
|
-
if [[ -z "\${hooks_dir}" ]]; then
|
|
414
|
-
hooks_dir="\$(git -C ${worktree_q} config --get core.hooksPath 2>/dev/null || true)"
|
|
415
|
-
fi
|
|
416
|
-
if [[ -z "\${hooks_dir}" ]]; then
|
|
417
|
-
hooks_dir="${worktree_q}/.git-hooks"
|
|
418
|
-
fi
|
|
419
|
-
# Resolve relative path against worktree root
|
|
420
|
-
if [[ "\${hooks_dir}" != /* ]]; then
|
|
421
|
-
hooks_dir="${worktree_q}/\${hooks_dir}"
|
|
422
|
-
fi
|
|
423
|
-
mkdir -p "\${hooks_dir}"
|
|
424
|
-
cat > "\${hooks_dir}/pre-commit" <<'HOOK_EOF'
|
|
425
|
-
#!/usr/bin/env bash
|
|
426
|
-
# Pre-commit scope guard: reject commits that touch too many product surfaces
|
|
427
|
-
set -euo pipefail
|
|
428
|
-
|
|
429
|
-
changed_files="\$(
|
|
430
|
-
git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true
|
|
431
|
-
)"
|
|
432
|
-
|
|
433
|
-
if [[ -z "\${changed_files}" ]]; then
|
|
434
|
-
exit 0
|
|
435
|
-
fi
|
|
436
|
-
|
|
437
|
-
# Count non-test product files by surface
|
|
438
|
-
api_count=0
|
|
439
|
-
web_count=0
|
|
440
|
-
mobile_count=0
|
|
441
|
-
package_count=0
|
|
442
|
-
doc_count=0
|
|
443
|
-
other_count=0
|
|
444
|
-
|
|
445
|
-
while IFS= read -r file; do
|
|
446
|
-
[[ -n "\${file}" ]] || continue
|
|
447
|
-
case "\${file}" in
|
|
448
|
-
apps/api/*) api_count=\$((api_count + 1)) ;;
|
|
449
|
-
apps/web/*) web_count=\$((web_count + 1)) ;;
|
|
450
|
-
apps/mobile/*) mobile_count=\$((mobile_count + 1)) ;;
|
|
451
|
-
packages/*) package_count=\$((package_count + 1)) ;;
|
|
452
|
-
openspec/*|docs/*|*.md) doc_count=\$((doc_count + 1)) ;;
|
|
453
|
-
*) other_count=\$((other_count + 1)) ;;
|
|
454
|
-
esac
|
|
455
|
-
done <<< "\${changed_files}"
|
|
456
|
-
|
|
457
|
-
# Count how many product surfaces are touched (excluding docs-only)
|
|
458
|
-
surfaces=0
|
|
459
|
-
[[ \$api_count -gt 0 ]] && surfaces=\$((surfaces + 1))
|
|
460
|
-
[[ \$web_count -gt 0 ]] && surfaces=\$((surfaces + 1))
|
|
461
|
-
[[ \$mobile_count -gt 0 ]] && surfaces=\$((surfaces + 1))
|
|
462
|
-
|
|
463
|
-
# If touching 3+ product surfaces, warn but allow (scope guard at publish is stricter)
|
|
464
|
-
if [[ \$surfaces -ge 3 ]]; then
|
|
465
|
-
echo "[pre-commit scope warning] This commit touches \$surfaces product surfaces (api=\$api_count web=\$web_count mobile=\$mobile_count). Consider splitting into focused commits." >&2
|
|
466
|
-
fi
|
|
467
|
-
|
|
468
|
-
# Hard block: more than 20 non-doc product files in one commit
|
|
469
|
-
total_product=\$((api_count + web_count + mobile_count + package_count + other_count))
|
|
470
|
-
if [[ \$total_product -gt 20 ]]; then
|
|
471
|
-
echo "[pre-commit scope BLOCK] This commit touches \$total_product product files across \$surfaces surfaces. Split into smaller focused commits." >&2
|
|
472
|
-
exit 1
|
|
473
|
-
fi
|
|
474
|
-
|
|
475
|
-
# Hard block: more than 8 mobile product files in one commit
|
|
476
|
-
if [[ \$mobile_count -gt 8 ]]; then
|
|
477
|
-
echo "[pre-commit scope BLOCK] This commit touches \$mobile_product mobile product files. Keep mobile changes to one focused route family (max 8 files)." >&2
|
|
478
|
-
exit 1
|
|
479
|
-
fi
|
|
480
|
-
|
|
481
|
-
exit 0
|
|
482
|
-
HOOK_EOF
|
|
483
|
-
chmod +x "\${hooks_dir}/pre-commit"
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
ensure_runtime_artifact_alias() {
|
|
487
|
-
local literal_name="\${1:?literal name required}"
|
|
488
|
-
local target_path="\${2:?target path required}"
|
|
489
|
-
local alias_path="\${worktree}/\${literal_name}"
|
|
490
|
-
local current_target=""
|
|
491
|
-
|
|
492
|
-
if [[ -L "\${alias_path}" ]]; then
|
|
493
|
-
current_target="\$(readlink "\${alias_path}" 2>/dev/null || true)"
|
|
494
|
-
if [[ "\${current_target}" == "\${target_path}" ]]; then
|
|
495
|
-
return 0
|
|
496
|
-
fi
|
|
497
|
-
rm -f "\${alias_path}"
|
|
498
|
-
elif [[ -e "\${alias_path}" ]]; then
|
|
499
|
-
printf '[openclaw] runtime alias path already exists, leaving untouched: %s\n' "\${alias_path}" >>"\${output_file}" 2>/dev/null || true
|
|
500
|
-
return 0
|
|
501
|
-
fi
|
|
502
|
-
|
|
503
|
-
ln -s "\${target_path}" "\${alias_path}"
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
ensure_runtime_artifact_aliases() {
|
|
507
|
-
ensure_runtime_artifact_alias '\$ACP_RUN_DIR' "\${sandbox_run_dir}"
|
|
508
|
-
ensure_runtime_artifact_alias '\$AGENT_PROJECT_RUN_DIR' "\${sandbox_run_dir}"
|
|
509
|
-
ensure_runtime_artifact_alias '\$F_LOSNING_RUN_DIR' "\${sandbox_run_dir}"
|
|
510
|
-
ensure_runtime_artifact_alias '\$ACP_HOST_RUN_DIR' "\${artifact_dir}"
|
|
511
|
-
ensure_runtime_artifact_alias '\$AGENT_PROJECT_HOST_RUN_DIR' "\${artifact_dir}"
|
|
512
|
-
ensure_runtime_artifact_alias '\$F_LOSNING_HOST_RUN_DIR' "\${artifact_dir}"
|
|
513
|
-
ensure_runtime_artifact_alias '\$ACP_RESULT_FILE' "\${sandbox_run_dir}/result.env"
|
|
514
|
-
ensure_runtime_artifact_alias '\$AGENT_PROJECT_RESULT_FILE' "\${sandbox_run_dir}/result.env"
|
|
515
|
-
ensure_runtime_artifact_alias '\$F_LOSNING_RESULT_FILE' "\${sandbox_run_dir}/result.env"
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
cleanup_runtime_artifact_aliases() {
|
|
519
|
-
local literal_name alias_path
|
|
520
|
-
for literal_name in \
|
|
521
|
-
'\$ACP_RUN_DIR' \
|
|
522
|
-
'\$AGENT_PROJECT_RUN_DIR' \
|
|
523
|
-
'\$F_LOSNING_RUN_DIR' \
|
|
524
|
-
'\$ACP_HOST_RUN_DIR' \
|
|
525
|
-
'\$AGENT_PROJECT_HOST_RUN_DIR' \
|
|
526
|
-
'\$F_LOSNING_HOST_RUN_DIR' \
|
|
527
|
-
'\$ACP_RESULT_FILE' \
|
|
528
|
-
'\$AGENT_PROJECT_RESULT_FILE' \
|
|
529
|
-
'\$F_LOSNING_RESULT_FILE'
|
|
530
|
-
do
|
|
531
|
-
alias_path="\${worktree}/\${literal_name}"
|
|
532
|
-
if [[ -L "\${alias_path}" ]]; then
|
|
533
|
-
rm -f "\${alias_path}"
|
|
534
|
-
fi
|
|
535
|
-
done
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
recover_literal_runtime_artifacts() {
|
|
539
|
-
local literal_name literal_path actual_result_file recovered="no"
|
|
540
|
-
|
|
541
|
-
actual_result_file="\${sandbox_run_dir}/result.env"
|
|
542
|
-
for literal_name in '\$ACP_RESULT_FILE' '\$AGENT_PROJECT_RESULT_FILE' '\$F_LOSNING_RESULT_FILE'; do
|
|
543
|
-
literal_path="\${worktree}/\${literal_name}"
|
|
544
|
-
if [[ -f "\${literal_path}" && ! -L "\${literal_path}" ]]; then
|
|
545
|
-
cp "\${literal_path}" "\${actual_result_file}" 2>/dev/null || true
|
|
546
|
-
cp "\${literal_path}" "\${run_dir}/result.env" 2>/dev/null || true
|
|
547
|
-
rm -f "\${literal_path}" 2>/dev/null || true
|
|
548
|
-
printf '[openclaw] recovered literal result artifact: %s\n' "\${literal_path}" >>"\${output_file}" 2>/dev/null || true
|
|
549
|
-
recovered="yes"
|
|
550
|
-
break
|
|
551
|
-
fi
|
|
552
|
-
done
|
|
553
|
-
|
|
554
|
-
for literal_name in '\$ACP_RUN_DIR' '\$AGENT_PROJECT_RUN_DIR' '\$F_LOSNING_RUN_DIR'; do
|
|
555
|
-
literal_path="\${worktree}/\${literal_name}"
|
|
556
|
-
if [[ -d "\${literal_path}" && ! -L "\${literal_path}" ]]; then
|
|
557
|
-
for artifact_name in result.env verification.jsonl issue-comment.md pr-comment.md; do
|
|
558
|
-
if [[ -f "\${literal_path}/\${artifact_name}" ]]; then
|
|
559
|
-
cp "\${literal_path}/\${artifact_name}" "\${sandbox_run_dir}/\${artifact_name}" 2>/dev/null || true
|
|
560
|
-
cp "\${literal_path}/\${artifact_name}" "\${artifact_dir}/\${artifact_name}" 2>/dev/null || true
|
|
561
|
-
recovered="yes"
|
|
562
|
-
fi
|
|
563
|
-
done
|
|
564
|
-
rm -rf "\${literal_path}" 2>/dev/null || true
|
|
565
|
-
printf '[openclaw] recovered literal run-dir artifact tree: %s\n' "\${literal_path}" >>"\${output_file}" 2>/dev/null || true
|
|
566
|
-
fi
|
|
567
|
-
done
|
|
568
|
-
|
|
569
|
-
[[ "\${recovered}" == "yes" ]] && return 0
|
|
570
|
-
return 0
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
recover_retained_repo_artifact_leaks() {
|
|
574
|
-
local retained_worktree_root=""
|
|
575
|
-
local leaked_run_dir=""
|
|
576
|
-
local worktree_name=""
|
|
577
|
-
local session_name=""
|
|
578
|
-
local artifact_name=""
|
|
579
|
-
local recovered="no"
|
|
580
|
-
|
|
581
|
-
[[ -n "\${retained_repo_root}" ]] || return 0
|
|
582
|
-
worktree_name="\$(basename "\${worktree}")"
|
|
583
|
-
session_name="\${AGENT_PROJECT_SESSION:-}"
|
|
584
|
-
[[ -n "\${session_name}" ]] || return 0
|
|
585
|
-
retained_worktree_root="\${retained_repo_root%/}/worktrees"
|
|
586
|
-
leaked_run_dir="\${retained_worktree_root}/\${worktree_name}/.openclaw-artifacts/\${session_name}"
|
|
587
|
-
|
|
588
|
-
if [[ ! -d "\${leaked_run_dir}" || "\${leaked_run_dir}" == "\${sandbox_run_dir}" ]]; then
|
|
589
|
-
return 0
|
|
590
|
-
fi
|
|
591
|
-
|
|
592
|
-
for artifact_name in result.env verification.jsonl issue-comment.md pr-comment.md; do
|
|
593
|
-
if [[ -f "\${leaked_run_dir}/\${artifact_name}" ]]; then
|
|
594
|
-
cp "\${leaked_run_dir}/\${artifact_name}" "\${sandbox_run_dir}/\${artifact_name}" 2>/dev/null || true
|
|
595
|
-
cp "\${leaked_run_dir}/\${artifact_name}" "\${artifact_dir}/\${artifact_name}" 2>/dev/null || true
|
|
596
|
-
recovered="yes"
|
|
597
|
-
fi
|
|
598
|
-
done
|
|
599
|
-
|
|
600
|
-
rm -rf "\${leaked_run_dir}" 2>/dev/null || true
|
|
601
|
-
rmdir "\${retained_worktree_root}/\${worktree_name}/.openclaw-artifacts" 2>/dev/null || true
|
|
602
|
-
rmdir "\${retained_worktree_root}/\${worktree_name}" 2>/dev/null || true
|
|
603
|
-
rmdir "\${retained_worktree_root}" 2>/dev/null || true
|
|
604
|
-
|
|
605
|
-
if [[ "\${recovered}" == "yes" ]]; then
|
|
606
|
-
printf '[openclaw] recovered retained-repo artifact leak: %s\n' "\${leaked_run_dir}" >>"\${output_file}" 2>/dev/null || true
|
|
607
|
-
fi
|
|
608
|
-
|
|
609
|
-
return 0
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
reset_sandbox_run_dir() {
|
|
613
|
-
mkdir -p "\${sandbox_run_dir}"
|
|
614
|
-
find "\${sandbox_run_dir}" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
classify_failure_reason() {
|
|
618
|
-
if grep -Eiq 'Config was last written by a newer OpenClaw' "\${output_file}" 2>/dev/null; then
|
|
619
|
-
printf 'openclaw-version-mismatch\n'
|
|
620
|
-
return 0
|
|
621
|
-
fi
|
|
622
|
-
if grep -Eiq 'invalid api key|authentication failed|unauthorized|provider api key|login required|please authenticate|api_key_invalid' "\${output_file}" 2>/dev/null; then
|
|
623
|
-
printf 'auth-failure\n'
|
|
624
|
-
return 0
|
|
625
|
-
fi
|
|
626
|
-
if grep -Eiq 'rate limit exceeded|quota exceeded|usage limit|insufficient credits|payment required|too many requests|429' "\${output_file}" 2>/dev/null; then
|
|
627
|
-
printf 'provider-quota-limit\n'
|
|
628
|
-
return 0
|
|
629
|
-
fi
|
|
630
|
-
if grep -Eiq 'model not found|model .* not available|unsupported model|invalid model' "\${output_file}" 2>/dev/null; then
|
|
631
|
-
printf 'model-unavailable\n'
|
|
632
|
-
return 0
|
|
633
|
-
fi
|
|
634
|
-
if grep -Eiq 'context length exceeded|token limit|maximum context|too many tokens' "\${output_file}" 2>/dev/null; then
|
|
635
|
-
printf 'context-length-exceeded\n'
|
|
636
|
-
return 0
|
|
637
|
-
fi
|
|
638
|
-
if grep -Eiq 'stale-run no-agent-output-before-stall-threshold|no-agent-output-before-stall-threshold' "\${output_file}" 2>/dev/null; then
|
|
639
|
-
printf 'no-agent-output-before-stall-threshold\n'
|
|
640
|
-
return 0
|
|
641
|
-
fi
|
|
642
|
-
if grep -Eiq 'stale-run no-agent-progress-before-stall-threshold|no-agent-progress-before-stall-threshold' "\${output_file}" 2>/dev/null; then
|
|
643
|
-
printf 'no-agent-progress-before-stall-threshold\n'
|
|
644
|
-
return 0
|
|
645
|
-
fi
|
|
646
|
-
if grep -Eiq 'timeout|timed out|ETIMEDOUT|ECONNREFUSED' "\${output_file}" 2>/dev/null; then
|
|
647
|
-
printf 'timeout\n'
|
|
648
|
-
return 0
|
|
649
|
-
fi
|
|
650
|
-
printf 'openclaw-exit-failed\n'
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
infer_result_from_output() {
|
|
654
|
-
local result_file_path="\${sandbox_run_dir}/result.env"
|
|
655
|
-
local verification_file="\${sandbox_run_dir}/verification.jsonl"
|
|
656
|
-
# Host-side result file (always writable, never inside worktree)
|
|
657
|
-
local host_result_file="\${run_dir}/result.env"
|
|
658
|
-
local recovered_contract=""
|
|
659
|
-
local write_result=''
|
|
660
|
-
|
|
661
|
-
write_result() {
|
|
662
|
-
printf '%b' "\$1" > "\${result_file_path}" 2>/dev/null || true
|
|
663
|
-
printf '%b' "\$1" > "\${host_result_file}" 2>/dev/null || true
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
recover_result_contract_from_output() {
|
|
667
|
-
python3 - "\${output_file}" <<'PY'
|
|
668
|
-
import re
|
|
669
|
-
import sys
|
|
670
|
-
|
|
671
|
-
log_path = sys.argv[1]
|
|
672
|
-
try:
|
|
673
|
-
raw = open(log_path, "r", encoding="utf-8", errors="replace").read()
|
|
674
|
-
except Exception:
|
|
675
|
-
raise SystemExit(1)
|
|
676
|
-
|
|
677
|
-
matches = re.findall(r"Result file written:\s*([^\r\n]+)", raw, flags=re.IGNORECASE)
|
|
678
|
-
if not matches:
|
|
679
|
-
raise SystemExit(1)
|
|
680
|
-
|
|
681
|
-
line = matches[-1]
|
|
682
|
-
fields = {}
|
|
683
|
-
for key in ("OUTCOME", "ACTION", "DETAIL", "ISSUE_ID"):
|
|
684
|
-
match = re.search(rf"{key}=([A-Za-z0-9._/-]+)", line)
|
|
685
|
-
if match:
|
|
686
|
-
fields[key] = match.group(1).strip()
|
|
687
|
-
|
|
688
|
-
if "OUTCOME" not in fields or "ACTION" not in fields:
|
|
689
|
-
raise SystemExit(1)
|
|
690
|
-
|
|
691
|
-
for key in ("OUTCOME", "ACTION", "DETAIL", "ISSUE_ID"):
|
|
692
|
-
value = fields.get(key)
|
|
693
|
-
if value:
|
|
694
|
-
print(f"{key}={value}")
|
|
695
|
-
PY
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
# If sandbox result.env already exists, trust the agent's contract.
|
|
699
|
-
# When the agent wrote OUTCOME=implemented but verification.jsonl is missing,
|
|
700
|
-
# keep the contract intact — the host reconcile will attempt verification
|
|
701
|
-
# recovery (extract_issue_host_recovery_commands) before publish, and that
|
|
702
|
-
# path can block if recovery also fails. Overriding to blocked here would
|
|
703
|
-
# skip reconcile's host-side recovery entirely and always produce a false
|
|
704
|
-
# block when the tool sandbox didn't inherit env vars for record-verification.
|
|
705
|
-
if [[ -f "\${result_file_path}" ]]; then
|
|
706
|
-
if grep -q 'OUTCOME=implemented' "\${result_file_path}" 2>/dev/null && [[ ! -f "\${verification_file}" ]]; then
|
|
707
|
-
printf '[infer] WARN: agent wrote OUTCOME=implemented but verification.jsonl is missing — deferring to host reconcile recovery\n' >> "\${output_file}" 2>/dev/null || true
|
|
708
|
-
fi
|
|
709
|
-
return 0
|
|
710
|
-
fi
|
|
711
|
-
|
|
712
|
-
if grep -Fq '[tools] exec failed: Provide a command to start.' "\${output_file}" 2>/dev/null; then
|
|
713
|
-
write_result 'OUTCOME=blocked\nACTION=host-comment-blocker\nDETAIL=worker-tool-exec-empty-command\n'
|
|
714
|
-
return 0
|
|
715
|
-
fi
|
|
716
|
-
|
|
717
|
-
recovered_contract="\$(recover_result_contract_from_output 2>/dev/null || true)"
|
|
718
|
-
if [[ -n "\${recovered_contract}" ]]; then
|
|
719
|
-
write_result "\${recovered_contract}"$'\n'
|
|
720
|
-
return 0
|
|
721
|
-
fi
|
|
722
|
-
|
|
723
|
-
# Check if there are actual code changes (not just artifact files or docs)
|
|
724
|
-
local has_product_changes="no"
|
|
725
|
-
if git -C ${worktree_q} diff --name-only HEAD 2>/dev/null | grep -qvE '\.openclaw-artifacts/|\.openclaw/|\.agent-session\.env$|\.md$' 2>/dev/null; then
|
|
726
|
-
has_product_changes="yes"
|
|
727
|
-
fi
|
|
728
|
-
if git -C ${worktree_q} diff --cached --name-only 2>/dev/null | grep -qvE '\.openclaw-artifacts/|\.openclaw/|\.agent-session\.env$|\.md$' 2>/dev/null; then
|
|
729
|
-
has_product_changes="yes"
|
|
730
|
-
fi
|
|
731
|
-
if git -C ${worktree_q} diff --name-only origin/main..HEAD 2>/dev/null | grep -qvE '\.openclaw-artifacts/|\.openclaw/|\.agent-session\.env$|\.md$' 2>/dev/null; then
|
|
732
|
-
has_product_changes="yes"
|
|
733
|
-
fi
|
|
734
|
-
|
|
735
|
-
# If no product changes and output suggests nothing to do, report no-changes
|
|
736
|
-
if [[ "\${has_product_changes}" == "no" ]] && grep -Eiq 'already done|no changes needed|up to date|nothing to do|no changes' "\${output_file}" 2>/dev/null; then
|
|
737
|
-
write_result 'OUTCOME=reported\nACTION=host-comment-scheduled-report\nDETAIL=no-changes-needed\n'
|
|
738
|
-
return 0
|
|
739
|
-
fi
|
|
740
|
-
|
|
741
|
-
# If there are product changes AND verification.jsonl exists with pass entries, allow implemented
|
|
742
|
-
if [[ "\${has_product_changes}" == "yes" && -f "\${verification_file}" ]] && grep -q '"status":"pass"' "\${verification_file}" 2>/dev/null; then
|
|
743
|
-
write_result 'OUTCOME=implemented\nACTION=host-publish-issue-pr\n'
|
|
744
|
-
return 0
|
|
745
|
-
fi
|
|
746
|
-
|
|
747
|
-
# If there are product changes but NO verification.jsonl, still mark as
|
|
748
|
-
# implemented and let the host reconcile attempt verification recovery.
|
|
749
|
-
# Blocking here would prevent host-side recovery from ever running.
|
|
750
|
-
if [[ "\${has_product_changes}" == "yes" && ! -f "\${verification_file}" ]]; then
|
|
751
|
-
printf '[infer] product changes detected without verification.jsonl — marking implemented for host recovery\n' >> "\${output_file}" 2>/dev/null || true
|
|
752
|
-
write_result 'OUTCOME=implemented\nACTION=host-publish-issue-pr\n'
|
|
753
|
-
return 0
|
|
754
|
-
fi
|
|
755
|
-
|
|
756
|
-
# If output explicitly indicates the agent decided it is blocked.
|
|
757
|
-
# Use narrow patterns to avoid false positives from prompt context echoed in logs
|
|
758
|
-
# (e.g. "Prior Blocker Context" sections or issue comments mentioning "blocked").
|
|
759
|
-
if grep -Eiq '^(OUTCOME=blocked|I am blocked|This issue is blocked|Cannot proceed with implementation|Unable to complete the task)' "\${output_file}" 2>/dev/null; then
|
|
760
|
-
write_result 'OUTCOME=blocked\nACTION=host-comment-blocker\n'
|
|
761
|
-
return 0
|
|
762
|
-
fi
|
|
763
|
-
|
|
764
|
-
# If output suggests implemented, mark as implemented and let reconcile verify
|
|
765
|
-
if grep -Eiq 'created PR|opened PR|PR #|pull request|implemented' "\${output_file}" 2>/dev/null; then
|
|
766
|
-
write_result 'OUTCOME=implemented\nACTION=host-publish-issue-pr\n'
|
|
767
|
-
return 0
|
|
768
|
-
fi
|
|
769
|
-
|
|
770
|
-
# Default fallback: block (safe default)
|
|
771
|
-
write_result 'OUTCOME=blocked\nACTION=host-comment-blocker\n'
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
synthesize_comment_artifact_from_output() {
|
|
775
|
-
local target_file=""
|
|
776
|
-
local result_file_path="\${sandbox_run_dir}/result.env"
|
|
777
|
-
|
|
778
|
-
if [[ ! -f "\${result_file_path}" ]] || ! grep -Eq '^ACTION=host-comment-' "\${result_file_path}" 2>/dev/null; then
|
|
779
|
-
return 0
|
|
780
|
-
fi
|
|
781
|
-
|
|
782
|
-
case "\${task_kind}" in
|
|
783
|
-
issue|task)
|
|
784
|
-
target_file="\${sandbox_run_dir}/issue-comment.md"
|
|
785
|
-
;;
|
|
786
|
-
pr)
|
|
787
|
-
target_file="\${sandbox_run_dir}/pr-comment.md"
|
|
788
|
-
;;
|
|
789
|
-
*)
|
|
790
|
-
return 0
|
|
791
|
-
;;
|
|
792
|
-
esac
|
|
793
|
-
|
|
794
|
-
[[ -n "\${target_file}" ]] || return 0
|
|
795
|
-
[[ ! -f "\${target_file}" ]] || return 0
|
|
796
|
-
|
|
797
|
-
python3 - "\${output_file}" "\${target_file}" <<'PY2'
|
|
798
|
-
import json
|
|
799
|
-
import os
|
|
800
|
-
import sys
|
|
801
|
-
|
|
802
|
-
log_path, target_path = sys.argv[1:3]
|
|
803
|
-
|
|
804
|
-
try:
|
|
805
|
-
raw = open(log_path, 'r', encoding='utf-8', errors='replace').read()
|
|
806
|
-
except Exception:
|
|
807
|
-
raise SystemExit(0)
|
|
808
|
-
|
|
809
|
-
decoder = json.JSONDecoder()
|
|
810
|
-
message = ''
|
|
811
|
-
idx = 0
|
|
812
|
-
while idx < len(raw):
|
|
813
|
-
start = raw.find('{', idx)
|
|
814
|
-
if start == -1:
|
|
815
|
-
break
|
|
816
|
-
try:
|
|
817
|
-
payload, end = decoder.raw_decode(raw, start)
|
|
818
|
-
except Exception:
|
|
819
|
-
idx = start + 1
|
|
820
|
-
continue
|
|
821
|
-
idx = end
|
|
822
|
-
if not isinstance(payload, dict):
|
|
823
|
-
continue
|
|
824
|
-
payloads = payload.get('payloads')
|
|
825
|
-
if not isinstance(payloads, list):
|
|
826
|
-
continue
|
|
827
|
-
parts = []
|
|
828
|
-
for item in payloads:
|
|
829
|
-
if not isinstance(item, dict):
|
|
830
|
-
continue
|
|
831
|
-
value = item.get('text')
|
|
832
|
-
if isinstance(value, str) and value.strip():
|
|
833
|
-
parts.append(value.rstrip())
|
|
834
|
-
if parts:
|
|
835
|
-
message = '\n\n'.join(parts).strip()
|
|
836
|
-
|
|
837
|
-
if not message:
|
|
838
|
-
raise SystemExit(0)
|
|
839
|
-
|
|
840
|
-
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
841
|
-
with open(target_path, 'w', encoding='utf-8') as handle:
|
|
842
|
-
handle.write(message)
|
|
843
|
-
handle.write('\n')
|
|
844
|
-
PY2
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
cleanup_agent() {
|
|
848
|
-
# --force required for non-interactive (tmux) sessions, otherwise delete waits for confirmation
|
|
849
|
-
"\${openclaw_bin}" agents delete "\${openclaw_agent_id}" --json --force >/dev/null 2>&1 || true
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
openclaw_agent_exists() {
|
|
853
|
-
local agents_json=""
|
|
854
|
-
|
|
855
|
-
agents_json="\$("\${openclaw_bin}" agents list --json 2>/dev/null || true)"
|
|
856
|
-
[[ -n "\${agents_json}" ]] || return 1
|
|
857
|
-
|
|
858
|
-
OPENCLAW_AGENTS_JSON="\${agents_json}" python3 - "\${openclaw_agent_id}" <<'PY'
|
|
859
|
-
import json
|
|
860
|
-
import os
|
|
861
|
-
import sys
|
|
862
|
-
|
|
863
|
-
agent_id = sys.argv[1]
|
|
864
|
-
|
|
865
|
-
try:
|
|
866
|
-
payload = json.loads(os.environ.get("OPENCLAW_AGENTS_JSON", ""))
|
|
867
|
-
except Exception:
|
|
868
|
-
raise SystemExit(1)
|
|
869
|
-
|
|
870
|
-
agents = payload if isinstance(payload, list) else payload.get("agents", [])
|
|
871
|
-
for agent in agents:
|
|
872
|
-
if not isinstance(agent, dict):
|
|
873
|
-
continue
|
|
874
|
-
if str(agent.get("id", "")) == agent_id or str(agent.get("name", "")) == agent_id:
|
|
875
|
-
raise SystemExit(0)
|
|
876
|
-
|
|
877
|
-
raise SystemExit(1)
|
|
878
|
-
PY
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
sync_openclaw_agent_config() {
|
|
882
|
-
mkdir -p "\$(dirname "\${openclaw_config_path}")"
|
|
883
|
-
|
|
884
|
-
python3 - "\${openclaw_config_path}" "\${openclaw_agent_id}" "\${worktree}" "\${openclaw_agent_dir}" "\${openclaw_model}" <<'PY'
|
|
885
|
-
import json
|
|
886
|
-
import os
|
|
887
|
-
import sys
|
|
888
|
-
|
|
889
|
-
config_path, agent_id, workspace, agent_dir, model = sys.argv[1:6]
|
|
890
|
-
|
|
891
|
-
payload = {}
|
|
892
|
-
if os.path.exists(config_path):
|
|
893
|
-
try:
|
|
894
|
-
with open(config_path, "r", encoding="utf-8") as handle:
|
|
895
|
-
loaded = json.load(handle)
|
|
896
|
-
if isinstance(loaded, dict):
|
|
897
|
-
payload = loaded
|
|
898
|
-
except Exception:
|
|
899
|
-
payload = {}
|
|
900
|
-
|
|
901
|
-
agents = payload.get("agents")
|
|
902
|
-
if not isinstance(agents, dict):
|
|
903
|
-
agents = {}
|
|
904
|
-
payload["agents"] = agents
|
|
905
|
-
|
|
906
|
-
agent_list = agents.get("list")
|
|
907
|
-
if not isinstance(agent_list, list):
|
|
908
|
-
agent_list = []
|
|
909
|
-
agents["list"] = agent_list
|
|
910
|
-
|
|
911
|
-
entry = None
|
|
912
|
-
for candidate in agent_list:
|
|
913
|
-
if not isinstance(candidate, dict):
|
|
914
|
-
continue
|
|
915
|
-
candidate_id = str(candidate.get("id", ""))
|
|
916
|
-
candidate_name = str(candidate.get("name", ""))
|
|
917
|
-
if candidate_id == agent_id or candidate_name == agent_id:
|
|
918
|
-
entry = candidate
|
|
919
|
-
break
|
|
920
|
-
|
|
921
|
-
if entry is None:
|
|
922
|
-
entry = {"id": agent_id}
|
|
923
|
-
agent_list.append(entry)
|
|
924
|
-
|
|
925
|
-
entry["id"] = agent_id
|
|
926
|
-
entry["name"] = agent_id
|
|
927
|
-
entry["workspace"] = workspace
|
|
928
|
-
entry["agentDir"] = agent_dir
|
|
929
|
-
if model:
|
|
930
|
-
entry["model"] = model
|
|
931
|
-
|
|
932
|
-
tmp_path = f"{config_path}.tmp.{os.getpid()}"
|
|
933
|
-
with open(tmp_path, "w", encoding="utf-8") as handle:
|
|
934
|
-
json.dump(payload, handle, indent=2)
|
|
935
|
-
handle.write("\n")
|
|
936
|
-
os.replace(tmp_path, config_path)
|
|
937
|
-
PY
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
run_openclaw_agent_command() {
|
|
941
|
-
python3 - "\${output_file}" "\${runner_state_file}" "\${openclaw_timeout}" "\${openclaw_stall_seconds}" "\${openclaw_progress_heartbeat_seconds}" "\${openclaw_bin}" "\${openclaw_agent_id}" "\${openclaw_session_id}" "\${openclaw_thinking}" "\${prompt_file_path}" <<'PY'
|
|
942
|
-
import os
|
|
943
|
-
import re
|
|
944
|
-
import selectors
|
|
945
|
-
import signal
|
|
946
|
-
import subprocess
|
|
947
|
-
import sys
|
|
948
|
-
import time
|
|
949
|
-
|
|
950
|
-
output_path, runner_state_path, timeout_seconds_raw, stall_seconds_raw, heartbeat_seconds_raw, openclaw_bin, agent_id, session_id, thinking, prompt_path = sys.argv[1:11]
|
|
951
|
-
|
|
952
|
-
with open(prompt_path, "r", encoding="utf-8") as handle:
|
|
953
|
-
prompt = handle.read()
|
|
954
|
-
|
|
955
|
-
cmd = [
|
|
956
|
-
openclaw_bin,
|
|
957
|
-
"agent",
|
|
958
|
-
"--agent",
|
|
959
|
-
agent_id,
|
|
960
|
-
"--session-id",
|
|
961
|
-
session_id,
|
|
962
|
-
"--local",
|
|
963
|
-
"--json",
|
|
964
|
-
"--timeout",
|
|
965
|
-
timeout_seconds_raw,
|
|
966
|
-
"--thinking",
|
|
967
|
-
thinking,
|
|
968
|
-
"--message",
|
|
969
|
-
prompt,
|
|
970
|
-
]
|
|
971
|
-
|
|
972
|
-
timeout_seconds = float(timeout_seconds_raw)
|
|
973
|
-
stall_seconds = float(stall_seconds_raw)
|
|
974
|
-
heartbeat_seconds = max(float(heartbeat_seconds_raw), 1.0)
|
|
975
|
-
hard_deadline = time.monotonic() + timeout_seconds + 15.0
|
|
976
|
-
started_at = time.monotonic()
|
|
977
|
-
next_heartbeat = time.monotonic() + heartbeat_seconds
|
|
978
|
-
seen_agent_progress = False
|
|
979
|
-
last_agent_progress_at = started_at
|
|
980
|
-
last_progress_source = "none"
|
|
981
|
-
terminal_patterns = [
|
|
982
|
-
re.compile(r"Config was last written by a newer OpenClaw", re.I),
|
|
983
|
-
re.compile(r"invalid api key|authentication failed|unauthorized|provider api key|login required|please authenticate|api_key_invalid", re.I),
|
|
984
|
-
re.compile(r"rate limit exceeded|quota exceeded|usage limit|insufficient credits|payment required|too many requests|429", re.I),
|
|
985
|
-
re.compile(r"model not found|model .* not available|unsupported model|invalid model", re.I),
|
|
986
|
-
re.compile(r"context length exceeded|token limit|maximum context|too many tokens", re.I),
|
|
987
|
-
]
|
|
988
|
-
|
|
989
|
-
proc = None
|
|
990
|
-
sel = selectors.DefaultSelector()
|
|
991
|
-
matched_terminal_error = False
|
|
992
|
-
tail = ""
|
|
993
|
-
openclaw_state_dir = os.environ.get("OPENCLAW_STATE_DIR", "")
|
|
994
|
-
sandbox_run_dir = (
|
|
995
|
-
os.environ.get("ACP_RUN_DIR")
|
|
996
|
-
or os.environ.get("AGENT_PROJECT_RUN_DIR")
|
|
997
|
-
or os.environ.get("F_LOSNING_RUN_DIR")
|
|
998
|
-
or ""
|
|
999
|
-
)
|
|
1000
|
-
host_managed_prefixes = tuple(
|
|
1001
|
-
prefix
|
|
1002
|
-
for prefix in (
|
|
1003
|
-
os.path.realpath(runner_state_path) if runner_state_path else "",
|
|
1004
|
-
os.path.realpath(output_path) if output_path else "",
|
|
1005
|
-
)
|
|
1006
|
-
if prefix
|
|
1007
|
-
)
|
|
1008
|
-
|
|
1009
|
-
def write_running_heartbeat() -> None:
|
|
1010
|
-
updated_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
1011
|
-
tmp_path = f"{runner_state_path}.tmp.{os.getpid()}"
|
|
1012
|
-
with open(tmp_path, "w", encoding="utf-8") as handle:
|
|
1013
|
-
handle.write("RUNNER_STATE=running\n")
|
|
1014
|
-
handle.write(f"THREAD_ID={sh_quote(session_id)}\n")
|
|
1015
|
-
handle.write("ATTEMPT=1\n")
|
|
1016
|
-
handle.write("RESUME_COUNT=0\n")
|
|
1017
|
-
handle.write("LAST_EXIT_CODE=''\n")
|
|
1018
|
-
handle.write("LAST_FAILURE_REASON=''\n")
|
|
1019
|
-
handle.write("LAST_TRIGGER_REASON=''\n")
|
|
1020
|
-
handle.write("AUTH_WAIT_STARTED_AT=''\n")
|
|
1021
|
-
handle.write("LAST_AUTH_FINGERPRINT=''\n")
|
|
1022
|
-
handle.write(f"UPDATED_AT={sh_quote(updated_at)}\n")
|
|
1023
|
-
os.replace(tmp_path, runner_state_path)
|
|
1024
|
-
|
|
1025
|
-
def sh_quote(value: str) -> str:
|
|
1026
|
-
return "'" + value.replace("'", "'\"'\"'") + "'"
|
|
1027
|
-
|
|
1028
|
-
def terminate_process_group(process: subprocess.Popen) -> None:
|
|
1029
|
-
try:
|
|
1030
|
-
os.killpg(process.pid, signal.SIGTERM)
|
|
1031
|
-
except ProcessLookupError:
|
|
1032
|
-
return
|
|
1033
|
-
deadline = time.monotonic() + 2.0
|
|
1034
|
-
while time.monotonic() < deadline:
|
|
1035
|
-
if process.poll() is not None:
|
|
1036
|
-
return
|
|
1037
|
-
time.sleep(0.1)
|
|
1038
|
-
try:
|
|
1039
|
-
os.killpg(process.pid, signal.SIGKILL)
|
|
1040
|
-
except ProcessLookupError:
|
|
1041
|
-
return
|
|
1042
|
-
|
|
1043
|
-
def progress_signature() -> tuple[tuple[str, int, int], ...]:
|
|
1044
|
-
entries: list[tuple[str, int, int]] = []
|
|
1045
|
-
|
|
1046
|
-
def add_file(path: str) -> None:
|
|
1047
|
-
real_path = ""
|
|
1048
|
-
if not path:
|
|
1049
|
-
return
|
|
1050
|
-
try:
|
|
1051
|
-
stat_result = os.stat(path)
|
|
1052
|
-
except OSError:
|
|
1053
|
-
return
|
|
1054
|
-
if not os.path.isfile(path):
|
|
1055
|
-
return
|
|
1056
|
-
real_path = os.path.realpath(path)
|
|
1057
|
-
for prefix in host_managed_prefixes:
|
|
1058
|
-
if real_path == prefix or real_path.startswith(f"{prefix}.tmp."):
|
|
1059
|
-
return
|
|
1060
|
-
entries.append((real_path, stat_result.st_mtime_ns, stat_result.st_size))
|
|
1061
|
-
|
|
1062
|
-
if sandbox_run_dir:
|
|
1063
|
-
try:
|
|
1064
|
-
for name in os.listdir(sandbox_run_dir):
|
|
1065
|
-
add_file(os.path.join(sandbox_run_dir, name))
|
|
1066
|
-
except OSError:
|
|
1067
|
-
pass
|
|
1068
|
-
|
|
1069
|
-
if openclaw_state_dir:
|
|
1070
|
-
sessions_dir = os.path.join(openclaw_state_dir, "agents", agent_id, "sessions")
|
|
1071
|
-
add_file(os.path.join(sessions_dir, "sessions.json"))
|
|
1072
|
-
try:
|
|
1073
|
-
for name in os.listdir(sessions_dir):
|
|
1074
|
-
if name.endswith(".jsonl") and not name.endswith(".lock"):
|
|
1075
|
-
add_file(os.path.join(sessions_dir, name))
|
|
1076
|
-
except OSError:
|
|
1077
|
-
pass
|
|
1078
|
-
|
|
1079
|
-
entries.sort()
|
|
1080
|
-
return tuple(entries)
|
|
1081
|
-
|
|
1082
|
-
last_progress_signature = progress_signature()
|
|
1083
|
-
|
|
1084
|
-
with open(output_path, "ab", buffering=0) as log_handle:
|
|
1085
|
-
proc = subprocess.Popen(
|
|
1086
|
-
cmd,
|
|
1087
|
-
stdin=subprocess.PIPE,
|
|
1088
|
-
stdout=subprocess.PIPE,
|
|
1089
|
-
stderr=subprocess.STDOUT,
|
|
1090
|
-
start_new_session=True,
|
|
1091
|
-
)
|
|
1092
|
-
if proc.stdin is not None:
|
|
1093
|
-
proc.stdin.write(prompt.encode("utf-8"))
|
|
1094
|
-
proc.stdin.close()
|
|
1095
|
-
|
|
1096
|
-
if proc.stdout is None:
|
|
1097
|
-
raise SystemExit(1)
|
|
1098
|
-
|
|
1099
|
-
sel.register(proc.stdout, selectors.EVENT_READ)
|
|
1100
|
-
|
|
1101
|
-
while True:
|
|
1102
|
-
if time.monotonic() >= hard_deadline:
|
|
1103
|
-
terminate_process_group(proc)
|
|
1104
|
-
break
|
|
1105
|
-
|
|
1106
|
-
current_progress_signature = progress_signature()
|
|
1107
|
-
if current_progress_signature != last_progress_signature:
|
|
1108
|
-
last_progress_signature = current_progress_signature
|
|
1109
|
-
seen_agent_progress = True
|
|
1110
|
-
last_agent_progress_at = time.monotonic()
|
|
1111
|
-
last_progress_source = "session-state"
|
|
1112
|
-
next_heartbeat = time.monotonic() + heartbeat_seconds
|
|
1113
|
-
|
|
1114
|
-
events = sel.select(timeout=0.2)
|
|
1115
|
-
if not events:
|
|
1116
|
-
if proc.poll() is None and not seen_agent_progress and stall_seconds > 0 and (time.monotonic() - started_at) >= stall_seconds:
|
|
1117
|
-
elapsed = int(time.monotonic() - started_at)
|
|
1118
|
-
write_running_heartbeat()
|
|
1119
|
-
log_handle.write(f"[openclaw] stale-run no-agent-output-before-stall-threshold elapsed={elapsed}s\n".encode("utf-8"))
|
|
1120
|
-
terminate_process_group(proc)
|
|
1121
|
-
break
|
|
1122
|
-
if proc.poll() is None and seen_agent_progress and stall_seconds > 0 and (time.monotonic() - last_agent_progress_at) >= stall_seconds:
|
|
1123
|
-
elapsed = int(time.monotonic() - started_at)
|
|
1124
|
-
idle_for = int(time.monotonic() - last_agent_progress_at)
|
|
1125
|
-
write_running_heartbeat()
|
|
1126
|
-
log_handle.write(f"[openclaw] stale-run no-agent-progress-before-stall-threshold elapsed={elapsed}s idle={idle_for}s\n".encode("utf-8"))
|
|
1127
|
-
terminate_process_group(proc)
|
|
1128
|
-
break
|
|
1129
|
-
if proc.poll() is None and not seen_agent_progress and time.monotonic() >= next_heartbeat:
|
|
1130
|
-
elapsed = int(time.monotonic() - started_at)
|
|
1131
|
-
write_running_heartbeat()
|
|
1132
|
-
log_handle.write(f"[openclaw] heartbeat waiting-for-agent-output elapsed={elapsed}s\n".encode("utf-8"))
|
|
1133
|
-
next_heartbeat = time.monotonic() + heartbeat_seconds
|
|
1134
|
-
if proc.poll() is None and seen_agent_progress and time.monotonic() >= next_heartbeat:
|
|
1135
|
-
elapsed = int(time.monotonic() - started_at)
|
|
1136
|
-
idle_for = int(time.monotonic() - last_agent_progress_at)
|
|
1137
|
-
write_running_heartbeat()
|
|
1138
|
-
log_handle.write(
|
|
1139
|
-
f"[openclaw] heartbeat progress source={last_progress_source} elapsed={elapsed}s idle={idle_for}s\n".encode("utf-8")
|
|
1140
|
-
)
|
|
1141
|
-
next_heartbeat = time.monotonic() + heartbeat_seconds
|
|
1142
|
-
if proc.poll() is not None:
|
|
1143
|
-
break
|
|
1144
|
-
continue
|
|
1145
|
-
|
|
1146
|
-
for key, _ in events:
|
|
1147
|
-
chunk = os.read(key.fd, 4096)
|
|
1148
|
-
if not chunk:
|
|
1149
|
-
continue
|
|
1150
|
-
|
|
1151
|
-
log_handle.write(chunk)
|
|
1152
|
-
text = chunk.decode("utf-8", errors="replace")
|
|
1153
|
-
tail = (tail + text)[-8192:]
|
|
1154
|
-
next_heartbeat = time.monotonic() + heartbeat_seconds
|
|
1155
|
-
seen_agent_progress = True
|
|
1156
|
-
last_agent_progress_at = time.monotonic()
|
|
1157
|
-
last_progress_source = "stdout"
|
|
1158
|
-
|
|
1159
|
-
if not matched_terminal_error and any(pattern.search(tail) for pattern in terminal_patterns):
|
|
1160
|
-
matched_terminal_error = True
|
|
1161
|
-
terminate_process_group(proc)
|
|
1162
|
-
|
|
1163
|
-
if proc.poll() is not None:
|
|
1164
|
-
break
|
|
1165
|
-
|
|
1166
|
-
while True:
|
|
1167
|
-
if proc.stdout is None:
|
|
1168
|
-
break
|
|
1169
|
-
chunk = os.read(proc.stdout.fileno(), 4096)
|
|
1170
|
-
if not chunk:
|
|
1171
|
-
break
|
|
1172
|
-
log_handle.write(chunk)
|
|
1173
|
-
|
|
1174
|
-
return_code = proc.wait()
|
|
1175
|
-
if matched_terminal_error and return_code == 0:
|
|
1176
|
-
raise SystemExit(1)
|
|
1177
|
-
raise SystemExit(return_code)
|
|
1178
|
-
PY
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
ensure_openclaw_auth_profiles() {
|
|
1182
|
-
if [[ ! -s "\${openclaw_agent_dir}/auth-profiles.json" && -n "\${OPENROUTER_API_KEY:-}" ]]; then
|
|
1183
|
-
mkdir -p "\$(dirname "\${openclaw_agent_dir}/auth-profiles.json" 2>/dev/null || true)"
|
|
1184
|
-
cat > "\${openclaw_agent_dir}/auth-profiles.json" <<AUTH_EOF
|
|
1185
|
-
{
|
|
1186
|
-
"version": 1,
|
|
1187
|
-
"profiles": {
|
|
1188
|
-
"openrouter:default": {
|
|
1189
|
-
"type": "api_key",
|
|
1190
|
-
"provider": "openrouter",
|
|
1191
|
-
"keyRef": {
|
|
1192
|
-
"source": "direct",
|
|
1193
|
-
"provider": "default",
|
|
1194
|
-
"id": "OPENROUTER_API_KEY",
|
|
1195
|
-
"secret": "\${OPENROUTER_API_KEY}"
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
AUTH_EOF
|
|
1201
|
-
fi
|
|
1202
|
-
|
|
1203
|
-
if [[ ! -s "\${openclaw_agent_dir}/auth-profiles.json" && -z "\${OPENROUTER_API_KEY:-}" && -f "\${openclaw_agent_dir}/../auth-profiles.json" ]]; then
|
|
1204
|
-
cp "\${openclaw_agent_dir}/../auth-profiles.json" "\${openclaw_agent_dir}/auth-profiles.json" 2>/dev/null || true
|
|
1205
|
-
fi
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
reset_openclaw_resident_state() {
|
|
1209
|
-
rm -rf "\${openclaw_state_dir}" "\${openclaw_agent_dir}" "\$(dirname "\${openclaw_config_path}")"
|
|
1210
|
-
mkdir -p "\${openclaw_state_dir}" "\$(dirname "\${openclaw_config_path}")" "\${openclaw_agent_dir}"
|
|
1211
|
-
ensure_openclaw_auth_profiles
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
mkdir -p "\${sandbox_run_dir}" "\${artifact_dir}" "\${openclaw_state_dir}" "\$(dirname "\${openclaw_config_path}")" "\${openclaw_agent_dir}"
|
|
1215
|
-
reset_sandbox_run_dir
|
|
1216
|
-
ensure_openclaw_workspace_excludes
|
|
1217
|
-
install_pre_commit_scope_hook
|
|
1218
|
-
ensure_runtime_artifact_aliases
|
|
1219
|
-
trap cleanup_runtime_artifact_aliases EXIT
|
|
1220
|
-
write_state running "" ""
|
|
1221
|
-
|
|
1222
|
-
ensure_openclaw_auth_profiles
|
|
1223
|
-
|
|
1224
|
-
# Export API key as env var for --local mode (required alongside auth-profiles.json)
|
|
1225
|
-
export OPENROUTER_API_KEY="\${OPENROUTER_API_KEY:-}"
|
|
1226
|
-
|
|
1227
|
-
prompt_content="\$(cat ${prompt_q})"
|
|
1228
|
-
set +e
|
|
1229
|
-
status=0
|
|
1230
|
-
failure_reason=""
|
|
1231
|
-
resident_reset_attempted="no"
|
|
1232
|
-
while true; do
|
|
1233
|
-
status=0
|
|
1234
|
-
if [[ "\${keep_agent}" != "true" ]] || ! openclaw_agent_exists; then
|
|
1235
|
-
: >"\${openclaw_add_log}"
|
|
1236
|
-
if "\${openclaw_bin}" agents add "\${openclaw_agent_id}" \
|
|
1237
|
-
--model "\${openclaw_model}" \\
|
|
1238
|
-
--workspace ${worktree_q} \\
|
|
1239
|
-
--agent-dir "\${openclaw_agent_dir}" \\
|
|
1240
|
-
--non-interactive --json >"\${openclaw_add_log}" 2>&1; then
|
|
1241
|
-
status=0
|
|
1242
|
-
else
|
|
1243
|
-
status=\$?
|
|
1244
|
-
if grep -Eiq 'already exists' "\${openclaw_add_log}" 2>/dev/null; then
|
|
1245
|
-
printf '[openclaw] reusing existing agent after add race: %s\n' "\${openclaw_agent_id}" >>"\${output_file}" 2>/dev/null || true
|
|
1246
|
-
status=0
|
|
1247
|
-
fi
|
|
1248
|
-
fi
|
|
1249
|
-
cat "\${openclaw_add_log}" >>"\${output_file}" 2>/dev/null || true
|
|
1250
|
-
fi
|
|
1251
|
-
if [[ "\${status}" -eq 0 ]]; then
|
|
1252
|
-
if sync_openclaw_agent_config; then
|
|
1253
|
-
:
|
|
1254
|
-
else
|
|
1255
|
-
status=1
|
|
1256
|
-
failure_reason="openclaw-config-sync-failed"
|
|
1257
|
-
printf '[openclaw] failed to sync agent config for %s\n' "\${openclaw_agent_id}" >>"\${output_file}" 2>/dev/null || true
|
|
1258
|
-
fi
|
|
1259
|
-
fi
|
|
1260
|
-
if [[ "\${status}" -eq 0 ]]; then
|
|
1261
|
-
run_openclaw_agent_command
|
|
1262
|
-
status=\$?
|
|
1263
|
-
fi
|
|
1264
|
-
if [[ "\${status}" -eq 0 ]]; then
|
|
1265
|
-
break
|
|
1266
|
-
fi
|
|
1267
|
-
|
|
1268
|
-
failure_reason="\$(classify_failure_reason)"
|
|
1269
|
-
if [[ "\${keep_agent}" == "true" && "\${resident_reset_attempted}" != "yes" && "\${failure_reason}" == "openclaw-version-mismatch" ]]; then
|
|
1270
|
-
resident_reset_attempted="yes"
|
|
1271
|
-
reset_openclaw_resident_state
|
|
1272
|
-
continue
|
|
1273
|
-
fi
|
|
1274
|
-
break
|
|
1275
|
-
done
|
|
1276
|
-
recover_literal_runtime_artifacts
|
|
1277
|
-
recover_retained_repo_artifact_leaks
|
|
1278
|
-
infer_result_from_output
|
|
1279
|
-
synthesize_comment_artifact_from_output
|
|
1280
|
-
if [[ "\${status}" -eq 0 ]]; then
|
|
1281
|
-
write_state succeeded "0" ""
|
|
1282
|
-
else
|
|
1283
|
-
if [[ -z "\${failure_reason}" ]]; then
|
|
1284
|
-
failure_reason="\$(classify_failure_reason)"
|
|
1285
|
-
fi
|
|
1286
|
-
write_state failed "\${status}" "\${failure_reason}"
|
|
1287
|
-
fi
|
|
1288
|
-
record_final_git_state
|
|
1289
|
-
if [[ -f ${sandbox_run_dir_q}/result.env ]]; then
|
|
1290
|
-
cp ${sandbox_run_dir_q}/result.env ${result_q}
|
|
1291
|
-
fi
|
|
1292
|
-
if [[ "\${keep_agent}" != "true" ]]; then
|
|
1293
|
-
cleanup_agent
|
|
1294
|
-
fi
|
|
1295
|
-
${collect_copy_snippet}${reconcile_snippet}
|
|
1296
|
-
printf '\n__CODEX_EXIT__:%s\n' "\${status}" | tee -a ${output_q}
|
|
1297
|
-
exit "\${status}"
|
|
1298
|
-
EOF
|
|
1299
|
-
|
|
1300
|
-
chmod +x "$inner_script"
|
|
1301
|
-
tmux new-session -d -s "$session" "$inner_script"
|
|
1302
|
-
|
|
1303
|
-
printf 'SESSION=%s\n' "$session"
|
|
1304
|
-
printf 'TASK_KIND=%s\n' "$task_kind"
|
|
1305
|
-
printf 'TASK_ID=%s\n' "$task_id"
|
|
1306
|
-
printf 'WORKTREE=%s\n' "$worktree"
|
|
1307
|
-
printf 'OUTPUT=%s\n' "$output_file"
|
|
1308
|
-
printf 'SCRIPT=%s\n' "$inner_script"
|
|
1309
|
-
printf 'META=%s\n' "$meta_file"
|