agent-control-plane 0.1.7 → 0.1.9
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/bin/pr-risk.sh +54 -10
- package/hooks/heartbeat-hooks.sh +70 -6
- package/hooks/issue-reconcile-hooks.sh +9 -2
- package/hooks/pr-reconcile-hooks.sh +11 -4
- package/package.json +2 -2
- package/tools/bin/agent-project-cleanup-session +10 -2
- package/tools/bin/agent-project-detached-launch +22 -2
- package/tools/bin/agent-project-heartbeat-loop +29 -2
- package/tools/bin/agent-project-reconcile-issue-session +109 -3
- package/tools/bin/agent-project-reconcile-pr-session +104 -13
- package/tools/bin/agent-project-run-claude-session +193 -55
- package/tools/bin/agent-project-run-codex-session +1 -1
- package/tools/bin/agent-project-run-openclaw-session +200 -7
- package/tools/bin/agent-project-sync-anchor-repo +13 -2
- package/tools/bin/agent-project-worker-status +19 -14
- package/tools/bin/flow-shell-lib.sh +13 -7
- package/tools/bin/prepare-worktree.sh +3 -1
- package/tools/bin/provider-cooldown-state.sh +1 -1
- package/tools/bin/render-flow-config.sh +16 -1
- package/tools/bin/run-codex-task.sh +3 -3
- package/tools/bin/scaffold-profile.sh +4 -4
- package/tools/bin/start-issue-worker.sh +42 -10
- package/tools/dashboard/app.js +20 -2
- package/tools/dashboard/dashboard_snapshot.py +45 -0
|
@@ -168,6 +168,9 @@ fi
|
|
|
168
168
|
result_outcome=""
|
|
169
169
|
result_action=""
|
|
170
170
|
result_issue_id="${ISSUE_ID:-}"
|
|
171
|
+
result_detail=""
|
|
172
|
+
run_started_at="${STARTED_AT:-}"
|
|
173
|
+
expected_run_started_at="${ACP_EXPECTED_RUN_STARTED_AT:-${F_LOSNING_EXPECTED_RUN_STARTED_AT:-}}"
|
|
171
174
|
host_blocker_file="${run_dir}/host-blocker.md"
|
|
172
175
|
prompt_file="${run_dir}/prompt.md"
|
|
173
176
|
pr_comment_file="${run_dir}/pr-comment.md"
|
|
@@ -184,9 +187,18 @@ if [[ -f "$result_file_candidate" ]]; then
|
|
|
184
187
|
set +a
|
|
185
188
|
result_outcome="${OUTCOME:-}"
|
|
186
189
|
result_action="${ACTION:-}"
|
|
190
|
+
result_detail="${DETAIL:-}"
|
|
187
191
|
result_issue_id="${ISSUE_ID:-${result_issue_id}}"
|
|
188
192
|
fi
|
|
189
193
|
|
|
194
|
+
if [[ -n "${expected_run_started_at}" && "${expected_run_started_at}" != "${run_started_at}" ]]; then
|
|
195
|
+
printf 'STATUS=STALE-RUN-SKIPPED\n'
|
|
196
|
+
printf 'SESSION=%s\n' "$session"
|
|
197
|
+
printf 'EXPECTED_STARTED_AT=%s\n' "${expected_run_started_at}"
|
|
198
|
+
printf 'ACTUAL_STARTED_AT=%s\n' "${run_started_at}"
|
|
199
|
+
exit 0
|
|
200
|
+
fi
|
|
201
|
+
|
|
190
202
|
pr_schedule_retry() { :; }
|
|
191
203
|
pr_clear_retry() { :; }
|
|
192
204
|
pr_cleanup_linked_issue_session() { :; }
|
|
@@ -225,6 +237,8 @@ clear_provider_quota_cooldown() {
|
|
|
225
237
|
"${provider_cooldown_script}" clear >/dev/null || true
|
|
226
238
|
}
|
|
227
239
|
|
|
240
|
+
blocked_runtime_reason=""
|
|
241
|
+
|
|
228
242
|
owner="${repo_slug%%/*}"
|
|
229
243
|
repo="${repo_slug#*/}"
|
|
230
244
|
pr_view_json="$(flow_github_pr_view_json "$repo_slug" "$pr_number")"
|
|
@@ -339,6 +353,11 @@ normalize_pr_result_contract() {
|
|
|
339
353
|
host-comment-pr-blocker)
|
|
340
354
|
return 0
|
|
341
355
|
;;
|
|
356
|
+
host-comment-blocker)
|
|
357
|
+
result_action="host-comment-pr-blocker"
|
|
358
|
+
pr_result_contract_note="normalized-legacy-blocked-action"
|
|
359
|
+
return 0
|
|
360
|
+
;;
|
|
342
361
|
requested-changes-or-blocked)
|
|
343
362
|
result_action="host-comment-pr-blocker"
|
|
344
363
|
pr_result_contract_note="normalized-legacy-blocked-action"
|
|
@@ -358,8 +377,15 @@ normalize_pr_result_contract() {
|
|
|
358
377
|
}
|
|
359
378
|
|
|
360
379
|
mark_reconciled() {
|
|
380
|
+
local reconciled_at tmp_file
|
|
361
381
|
if [[ -d "$run_dir" ]]; then
|
|
362
|
-
|
|
382
|
+
reconciled_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
383
|
+
tmp_file="${run_dir}/reconciled.ok.tmp.$$"
|
|
384
|
+
{
|
|
385
|
+
printf 'STARTED_AT=%s\n' "${run_started_at}"
|
|
386
|
+
printf 'RECONCILED_AT=%s\n' "${reconciled_at}"
|
|
387
|
+
} >"${tmp_file}"
|
|
388
|
+
mv "${tmp_file}" "${run_dir}/reconciled.ok"
|
|
363
389
|
fi
|
|
364
390
|
}
|
|
365
391
|
|
|
@@ -393,6 +419,20 @@ blocked_result_indicates_local_bind_failure() {
|
|
|
393
419
|
return 1
|
|
394
420
|
}
|
|
395
421
|
|
|
422
|
+
classify_pr_blocked_runtime_reason() {
|
|
423
|
+
if [[ "${result_detail:-}" == "worker-tool-exec-empty-command" ]]; then
|
|
424
|
+
printf 'worker-tool-exec-empty-command\n'
|
|
425
|
+
return 0
|
|
426
|
+
fi
|
|
427
|
+
|
|
428
|
+
if [[ -f "$session_log_file" ]] && grep -Fq '[tools] exec failed: Provide a command to start.' "$session_log_file"; then
|
|
429
|
+
printf 'worker-tool-exec-empty-command\n'
|
|
430
|
+
return 0
|
|
431
|
+
fi
|
|
432
|
+
|
|
433
|
+
return 1
|
|
434
|
+
}
|
|
435
|
+
|
|
396
436
|
extract_preapproved_host_recovery_commands() {
|
|
397
437
|
[[ -f "$prompt_file" ]] || return 0
|
|
398
438
|
sed -n 's/^.*loopback retry command: `\(.*\)`$/\1/p' "$prompt_file"
|
|
@@ -700,18 +740,53 @@ merge_state_prepared() {
|
|
|
700
740
|
git -C "$pr_worktree" rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1
|
|
701
741
|
}
|
|
702
742
|
|
|
743
|
+
current_github_login() {
|
|
744
|
+
flow_export_github_cli_auth_env "${repo_slug}"
|
|
745
|
+
gh api user --jq '.login // ""' 2>/dev/null || true
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
pr_author_login() {
|
|
749
|
+
flow_export_github_cli_auth_env "${repo_slug}"
|
|
750
|
+
gh pr view "${pr_number}" -R "${repo_slug}" --json author --jq '.author.login // ""' 2>/dev/null || true
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
pr_is_self_authored_for_current_actor() {
|
|
754
|
+
local actor_login=""
|
|
755
|
+
local author_login=""
|
|
756
|
+
|
|
757
|
+
actor_login="$(current_github_login)"
|
|
758
|
+
author_login="$(pr_author_login)"
|
|
759
|
+
[[ -n "${actor_login}" && -n "${author_login}" && "${actor_login}" == "${author_login}" ]]
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
pr_remote_head_oid() {
|
|
763
|
+
flow_export_github_cli_auth_env "${repo_slug}"
|
|
764
|
+
gh pr view "${pr_number}" -R "${repo_slug}" --json headRefOid --jq '.headRefOid // ""' 2>/dev/null || true
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
pr_remote_already_has_final_head() {
|
|
768
|
+
local final_head="${FINAL_HEAD:-}"
|
|
769
|
+
local remote_head=""
|
|
770
|
+
|
|
771
|
+
[[ -n "${final_head}" ]] || return 1
|
|
772
|
+
remote_head="$(pr_remote_head_oid)"
|
|
773
|
+
[[ -n "${remote_head}" && "${remote_head}" == "${final_head}" ]]
|
|
774
|
+
}
|
|
775
|
+
|
|
703
776
|
approve_and_merge() {
|
|
704
777
|
local approve_output
|
|
705
|
-
if !
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
778
|
+
if ! pr_is_self_authored_for_current_actor; then
|
|
779
|
+
if ! approve_output="$(
|
|
780
|
+
flow_github_api_repo "${repo_slug}" "pulls/${pr_number}/reviews" \
|
|
781
|
+
--method POST \
|
|
782
|
+
-f event=APPROVE \
|
|
783
|
+
-f body="Automated final review passed. Safe low-risk scope, green checks, and host-side merge approved." \
|
|
784
|
+
2>&1
|
|
785
|
+
)"; then
|
|
786
|
+
if ! grep -q "Can not approve your own pull request" <<<"$approve_output"; then
|
|
787
|
+
printf '%s\n' "$approve_output" >&2
|
|
788
|
+
return 1
|
|
789
|
+
fi
|
|
715
790
|
fi
|
|
716
791
|
fi
|
|
717
792
|
|
|
@@ -753,7 +828,14 @@ handle_linked_issue_merge_cleanup() {
|
|
|
753
828
|
|
|
754
829
|
handle_updated_branch_result() {
|
|
755
830
|
if [[ -z "$pr_worktree" || ! -d "$pr_worktree" ]]; then
|
|
756
|
-
if
|
|
831
|
+
if pr_remote_already_has_final_head; then
|
|
832
|
+
post_pr_comment_if_present
|
|
833
|
+
require_transition "pr_clear_retry" pr_clear_retry
|
|
834
|
+
require_transition "pr_after_updated_branch" pr_after_updated_branch "$pr_number"
|
|
835
|
+
cleanup_pr_session
|
|
836
|
+
result_action="${result_action:-host-push-pr-branch}"
|
|
837
|
+
notify_pr_reconciled
|
|
838
|
+
elif pr_comment_already_posted; then
|
|
757
839
|
require_transition "pr_clear_retry" pr_clear_retry
|
|
758
840
|
require_transition "pr_after_updated_branch" pr_after_updated_branch "$pr_number"
|
|
759
841
|
cleanup_pr_session
|
|
@@ -968,7 +1050,16 @@ elif [[ "$status" == "SUCCEEDED" && "$result_outcome" == "no-change-needed" ]];
|
|
|
968
1050
|
fi
|
|
969
1051
|
fi
|
|
970
1052
|
elif [[ "$status" == "SUCCEEDED" && "$result_outcome" == "blocked" ]]; then
|
|
971
|
-
|
|
1053
|
+
blocked_runtime_reason="$(classify_pr_blocked_runtime_reason || true)"
|
|
1054
|
+
if [[ -n "${blocked_runtime_reason:-}" ]]; then
|
|
1055
|
+
status="FAILED"
|
|
1056
|
+
failure_reason="${blocked_runtime_reason}"
|
|
1057
|
+
require_transition "pr_schedule_retry" pr_schedule_retry "$failure_reason"
|
|
1058
|
+
require_transition "pr_after_failed" pr_after_failed "$pr_number"
|
|
1059
|
+
cleanup_pr_session
|
|
1060
|
+
result_action="queued-pr-retry"
|
|
1061
|
+
notify_pr_reconciled
|
|
1062
|
+
elif attempt_blocked_pr_host_verification_recovery; then
|
|
972
1063
|
handle_updated_branch_result
|
|
973
1064
|
else
|
|
974
1065
|
post_pr_comment_if_present
|
|
@@ -11,12 +11,13 @@ persist the standard run artifacts.
|
|
|
11
11
|
|
|
12
12
|
Options:
|
|
13
13
|
--claude-model <name> Claude model alias or full name
|
|
14
|
-
--claude-permission-mode <mode> Claude permission mode (e.g.
|
|
14
|
+
--claude-permission-mode <mode> Claude permission mode (e.g. acceptEdits, bypassPermissions)
|
|
15
15
|
--claude-effort <level> Claude effort level (low, medium, high, max)
|
|
16
16
|
--claude-timeout-seconds <secs> Claude command timeout (default: 900)
|
|
17
17
|
--claude-max-attempts <count> Retry transient failures this many times (default: 3)
|
|
18
18
|
--claude-retry-backoff-seconds <s>
|
|
19
19
|
Sleep between transient retries (default: 30)
|
|
20
|
+
--claude-allowed-tools <spec> Allowed Claude tools for headless runs
|
|
20
21
|
--env-prefix <prefix> Export prefixed runtime/context env vars inside the worker
|
|
21
22
|
--context <KEY=VALUE> Extra metadata written to run.env and exported to the worker
|
|
22
23
|
--collect-file <name> Copy sandbox artifact file into the host run dir after execution
|
|
@@ -35,11 +36,12 @@ adapter_id=""
|
|
|
35
36
|
task_kind=""
|
|
36
37
|
task_id=""
|
|
37
38
|
claude_model="${ACP_CLAUDE_MODEL:-${F_LOSNING_CLAUDE_MODEL:-sonnet}}"
|
|
38
|
-
claude_permission_mode="${ACP_CLAUDE_PERMISSION_MODE:-${F_LOSNING_CLAUDE_PERMISSION_MODE:-
|
|
39
|
+
claude_permission_mode="${ACP_CLAUDE_PERMISSION_MODE:-${F_LOSNING_CLAUDE_PERMISSION_MODE:-acceptEdits}}"
|
|
39
40
|
claude_effort="${ACP_CLAUDE_EFFORT:-${F_LOSNING_CLAUDE_EFFORT:-medium}}"
|
|
40
41
|
claude_timeout_seconds="${ACP_CLAUDE_TIMEOUT_SECONDS:-${F_LOSNING_CLAUDE_TIMEOUT_SECONDS:-900}}"
|
|
41
42
|
claude_max_attempts="${ACP_CLAUDE_MAX_ATTEMPTS:-${F_LOSNING_CLAUDE_MAX_ATTEMPTS:-3}}"
|
|
42
43
|
claude_retry_backoff_seconds="${ACP_CLAUDE_RETRY_BACKOFF_SECONDS:-${F_LOSNING_CLAUDE_RETRY_BACKOFF_SECONDS:-30}}"
|
|
44
|
+
claude_allowed_tools="${ACP_CLAUDE_ALLOWED_TOOLS:-${F_LOSNING_CLAUDE_ALLOWED_TOOLS:-Bash(*),Read,Grep,Glob,LS,Edit,Write,MultiEdit}}"
|
|
43
45
|
env_prefix=""
|
|
44
46
|
sandbox_subdir=".openclaw-artifacts"
|
|
45
47
|
reconcile_command=""
|
|
@@ -59,6 +61,24 @@ resolve_claude_bin() {
|
|
|
59
61
|
return 0
|
|
60
62
|
fi
|
|
61
63
|
|
|
64
|
+
# Well-known install locations for Claude Code CLI.
|
|
65
|
+
# Detached supervisors and LaunchAgents run with a minimal PATH that
|
|
66
|
+
# does not include user-local directories, so command -v alone is not
|
|
67
|
+
# enough. Try the common locations explicitly.
|
|
68
|
+
local -a fallback_paths=(
|
|
69
|
+
"${HOME}/.local/bin/claude"
|
|
70
|
+
"${HOME}/.claude/local/bin/claude"
|
|
71
|
+
"/usr/local/bin/claude"
|
|
72
|
+
"/opt/homebrew/bin/claude"
|
|
73
|
+
)
|
|
74
|
+
local p
|
|
75
|
+
for p in "${fallback_paths[@]}"; do
|
|
76
|
+
if [[ -x "${p}" ]]; then
|
|
77
|
+
printf '%s\n' "${p}"
|
|
78
|
+
return 0
|
|
79
|
+
fi
|
|
80
|
+
done
|
|
81
|
+
|
|
62
82
|
return 1
|
|
63
83
|
}
|
|
64
84
|
|
|
@@ -94,6 +114,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
94
114
|
--claude-timeout-seconds) claude_timeout_seconds="${2:-}"; shift 2 ;;
|
|
95
115
|
--claude-max-attempts) claude_max_attempts="${2:-}"; shift 2 ;;
|
|
96
116
|
--claude-retry-backoff-seconds) claude_retry_backoff_seconds="${2:-}"; shift 2 ;;
|
|
117
|
+
--claude-allowed-tools) claude_allowed_tools="${2:-}"; shift 2 ;;
|
|
97
118
|
--env-prefix) env_prefix="${2:-}"; shift 2 ;;
|
|
98
119
|
--context) context_items+=("${2:-}"); shift 2 ;;
|
|
99
120
|
--collect-file) collect_files+=("${2:-}"); shift 2 ;;
|
|
@@ -156,11 +177,31 @@ meta_file="${artifact_dir}/run.env"
|
|
|
156
177
|
result_file="${artifact_dir}/result.env"
|
|
157
178
|
runner_state_file="${artifact_dir}/runner.env"
|
|
158
179
|
sandbox_run_dir="${worktree%/}/${sandbox_subdir}/${session}"
|
|
180
|
+
claude_settings_file="${artifact_dir}/claude-headless-settings.json"
|
|
181
|
+
claude_mcp_config_file="${artifact_dir}/claude-headless-mcp.json"
|
|
182
|
+
claude_debug_file="${artifact_dir}/claude-debug.log"
|
|
159
183
|
started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
160
184
|
|
|
161
185
|
mkdir -p "$artifact_dir"
|
|
162
186
|
mkdir -p "$sandbox_run_dir"
|
|
163
187
|
|
|
188
|
+
effective_claude_permission_mode="${claude_permission_mode}"
|
|
189
|
+
if [[ "${effective_claude_permission_mode}" == "dontAsk" ]]; then
|
|
190
|
+
effective_claude_permission_mode="acceptEdits"
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
cat >"$claude_settings_file" <<'EOF'
|
|
194
|
+
{
|
|
195
|
+
"disableAllHooks": true
|
|
196
|
+
}
|
|
197
|
+
EOF
|
|
198
|
+
|
|
199
|
+
cat >"$claude_mcp_config_file" <<'EOF'
|
|
200
|
+
{
|
|
201
|
+
"mcpServers": {}
|
|
202
|
+
}
|
|
203
|
+
EOF
|
|
204
|
+
|
|
164
205
|
if tmux has-session -t "$session" 2>/dev/null; then
|
|
165
206
|
echo "tmux session already exists: $session" >&2
|
|
166
207
|
exit 1
|
|
@@ -187,10 +228,15 @@ printf -v started_at_q '%q' "$started_at"
|
|
|
187
228
|
printf -v claude_bin_q '%q' "$claude_bin"
|
|
188
229
|
printf -v claude_model_q '%q' "$claude_model"
|
|
189
230
|
printf -v claude_permission_mode_q '%q' "$claude_permission_mode"
|
|
231
|
+
printf -v claude_effective_permission_mode_q '%q' "$effective_claude_permission_mode"
|
|
190
232
|
printf -v claude_effort_q '%q' "$claude_effort"
|
|
191
233
|
printf -v claude_timeout_q '%q' "$claude_timeout_seconds"
|
|
192
234
|
printf -v claude_max_attempts_q '%q' "$claude_max_attempts"
|
|
193
235
|
printf -v claude_retry_backoff_q '%q' "$claude_retry_backoff_seconds"
|
|
236
|
+
printf -v claude_allowed_tools_q '%q' "$claude_allowed_tools"
|
|
237
|
+
printf -v claude_settings_file_q '%q' "$claude_settings_file"
|
|
238
|
+
printf -v claude_mcp_config_file_q '%q' "$claude_mcp_config_file"
|
|
239
|
+
printf -v claude_debug_file_q '%q' "$claude_debug_file"
|
|
194
240
|
printf -v python_bin_q '%q' "$python_bin"
|
|
195
241
|
printf -v sandbox_subdir_q '%q' "$sandbox_subdir"
|
|
196
242
|
printf -v claude_thread_id_q '%q' "claude-print-${session}"
|
|
@@ -213,10 +259,15 @@ printf -v claude_thread_id_q '%q' "claude-print-${session}"
|
|
|
213
259
|
printf 'CLAUDE_BIN=%s\n' "$claude_bin_q"
|
|
214
260
|
printf 'CLAUDE_MODEL=%s\n' "$claude_model_q"
|
|
215
261
|
printf 'CLAUDE_PERMISSION_MODE=%s\n' "$claude_permission_mode_q"
|
|
262
|
+
printf 'CLAUDE_EFFECTIVE_PERMISSION_MODE=%s\n' "$claude_effective_permission_mode_q"
|
|
216
263
|
printf 'CLAUDE_EFFORT=%s\n' "$claude_effort_q"
|
|
217
264
|
printf 'CLAUDE_TIMEOUT_SECONDS=%s\n' "$claude_timeout_q"
|
|
218
265
|
printf 'CLAUDE_MAX_ATTEMPTS=%s\n' "$claude_max_attempts_q"
|
|
219
266
|
printf 'CLAUDE_RETRY_BACKOFF_SECONDS=%s\n' "$claude_retry_backoff_q"
|
|
267
|
+
printf 'CLAUDE_ALLOWED_TOOLS=%s\n' "$claude_allowed_tools_q"
|
|
268
|
+
printf 'CLAUDE_SETTINGS_FILE=%s\n' "$claude_settings_file_q"
|
|
269
|
+
printf 'CLAUDE_MCP_CONFIG_FILE=%s\n' "$claude_mcp_config_file_q"
|
|
270
|
+
printf 'CLAUDE_DEBUG_FILE=%s\n' "$claude_debug_file_q"
|
|
220
271
|
printf 'PYTHON_BIN=%s\n' "$python_bin_q"
|
|
221
272
|
} >"$meta_file"
|
|
222
273
|
|
|
@@ -315,7 +366,7 @@ fi
|
|
|
315
366
|
|
|
316
367
|
reconcile_snippet=""
|
|
317
368
|
if [[ -n "$reconcile_command" ]]; then
|
|
318
|
-
printf -v delayed_reconcile_q '%q' "sleep 2; $reconcile_command"
|
|
369
|
+
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"
|
|
319
370
|
reconcile_snippet="nohup bash -lc ${delayed_reconcile_q} >> ${output_q} 2>&1 </dev/null &"
|
|
320
371
|
fi
|
|
321
372
|
|
|
@@ -334,14 +385,20 @@ host_result_file=${result_q}
|
|
|
334
385
|
claude_bin=${claude_bin_q}
|
|
335
386
|
claude_model=${claude_model_q}
|
|
336
387
|
claude_permission_mode=${claude_permission_mode_q}
|
|
388
|
+
claude_effective_permission_mode=${claude_effective_permission_mode_q}
|
|
337
389
|
claude_effort=${claude_effort_q}
|
|
338
390
|
claude_timeout_seconds=${claude_timeout_q}
|
|
339
391
|
claude_max_attempts=${claude_max_attempts_q}
|
|
340
392
|
claude_retry_backoff_seconds=${claude_retry_backoff_q}
|
|
393
|
+
claude_allowed_tools=${claude_allowed_tools_q}
|
|
394
|
+
claude_settings_file=${claude_settings_file_q}
|
|
395
|
+
claude_mcp_config_file=${claude_mcp_config_file_q}
|
|
396
|
+
claude_debug_file=${claude_debug_file_q}
|
|
341
397
|
python_bin=${python_bin_q}
|
|
342
398
|
worktree_root=${worktree_q}
|
|
343
399
|
sandbox_subdir=${sandbox_subdir_q}
|
|
344
400
|
prompt_file=${prompt_q}
|
|
401
|
+
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
|
|
345
402
|
|
|
346
403
|
write_state() {
|
|
347
404
|
local runner_state="\${1:?runner state required}"
|
|
@@ -370,48 +427,119 @@ write_state() {
|
|
|
370
427
|
|
|
371
428
|
run_with_timeout() {
|
|
372
429
|
local timeout_seconds="\${1:?timeout seconds required}"
|
|
430
|
+
local stdin_file="\${2:?stdin file required}"
|
|
431
|
+
shift
|
|
373
432
|
shift
|
|
374
433
|
|
|
375
|
-
"\${python_bin}" - "\${timeout_seconds}" "\$@" <<'PY'
|
|
434
|
+
"\${python_bin}" - "\${timeout_seconds}" "\${stdin_file}" "\$@" <<'PY'
|
|
435
|
+
import errno
|
|
436
|
+
import fcntl
|
|
376
437
|
import os
|
|
438
|
+
import selectors
|
|
377
439
|
import signal
|
|
378
440
|
import subprocess
|
|
379
441
|
import sys
|
|
442
|
+
import time
|
|
380
443
|
|
|
381
444
|
timeout_seconds = float(sys.argv[1])
|
|
382
|
-
|
|
445
|
+
stdin_path = sys.argv[2]
|
|
446
|
+
argv = sys.argv[3:]
|
|
383
447
|
|
|
384
448
|
if not argv:
|
|
385
449
|
sys.exit(64)
|
|
386
450
|
|
|
387
|
-
|
|
451
|
+
stdin_handle = open(stdin_path, "rb")
|
|
452
|
+
proc = subprocess.Popen(
|
|
453
|
+
argv,
|
|
454
|
+
start_new_session=True,
|
|
455
|
+
stdin=stdin_handle,
|
|
456
|
+
stdout=subprocess.PIPE,
|
|
457
|
+
stderr=subprocess.PIPE,
|
|
458
|
+
)
|
|
388
459
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
460
|
+
for stream in (proc.stdout, proc.stderr):
|
|
461
|
+
if stream is None:
|
|
462
|
+
continue
|
|
463
|
+
flags = fcntl.fcntl(stream.fileno(), fcntl.F_GETFL)
|
|
464
|
+
fcntl.fcntl(stream.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
|
465
|
+
|
|
466
|
+
selector = selectors.DefaultSelector()
|
|
467
|
+
if proc.stdout is not None:
|
|
468
|
+
selector.register(proc.stdout, selectors.EVENT_READ, sys.stdout.buffer)
|
|
469
|
+
if proc.stderr is not None:
|
|
470
|
+
selector.register(proc.stderr, selectors.EVENT_READ, sys.stderr.buffer)
|
|
471
|
+
|
|
472
|
+
def terminate_process_group(sig):
|
|
392
473
|
try:
|
|
393
|
-
os.killpg(proc.pid,
|
|
474
|
+
os.killpg(proc.pid, sig)
|
|
394
475
|
except ProcessLookupError:
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
476
|
+
return
|
|
477
|
+
|
|
478
|
+
def drain_streams(wait_seconds):
|
|
479
|
+
events = selector.select(wait_seconds)
|
|
480
|
+
for key, _ in events:
|
|
399
481
|
try:
|
|
400
|
-
|
|
401
|
-
except
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
482
|
+
chunk = key.fileobj.read()
|
|
483
|
+
except BlockingIOError:
|
|
484
|
+
continue
|
|
485
|
+
except OSError as exc:
|
|
486
|
+
if exc.errno == errno.EAGAIN:
|
|
487
|
+
continue
|
|
488
|
+
raise
|
|
489
|
+
if not chunk:
|
|
490
|
+
selector.unregister(key.fileobj)
|
|
491
|
+
continue
|
|
492
|
+
key.data.write(chunk)
|
|
493
|
+
key.data.flush()
|
|
494
|
+
|
|
495
|
+
def handle_parent_signal(signum, _frame):
|
|
496
|
+
terminate_process_group(signal.SIGTERM)
|
|
497
|
+
deadline = time.monotonic() + 2.0
|
|
498
|
+
while proc.poll() is None and time.monotonic() < deadline:
|
|
499
|
+
drain_streams(0.1)
|
|
500
|
+
if proc.poll() is None:
|
|
501
|
+
terminate_process_group(signal.SIGKILL)
|
|
502
|
+
while selector.get_map():
|
|
503
|
+
drain_streams(0)
|
|
504
|
+
sys.exit(128 + signum)
|
|
505
|
+
|
|
506
|
+
for signum in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
|
|
507
|
+
signal.signal(signum, handle_parent_signal)
|
|
508
|
+
|
|
509
|
+
deadline = time.monotonic() + timeout_seconds
|
|
510
|
+
grace_deadline = None
|
|
511
|
+
timed_out = False
|
|
409
512
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
513
|
+
try:
|
|
514
|
+
while True:
|
|
515
|
+
now = time.monotonic()
|
|
516
|
+
if not timed_out and now >= deadline:
|
|
517
|
+
timed_out = True
|
|
518
|
+
grace_deadline = now + 2.0
|
|
519
|
+
terminate_process_group(signal.SIGTERM)
|
|
520
|
+
elif timed_out and grace_deadline is not None and proc.poll() is None and now >= grace_deadline:
|
|
521
|
+
grace_deadline = None
|
|
522
|
+
terminate_process_group(signal.SIGKILL)
|
|
523
|
+
|
|
524
|
+
wait_seconds = 0.1
|
|
525
|
+
if not timed_out:
|
|
526
|
+
wait_seconds = max(0.0, min(0.1, deadline - now))
|
|
527
|
+
elif grace_deadline is not None:
|
|
528
|
+
wait_seconds = max(0.0, min(0.1, grace_deadline - now))
|
|
529
|
+
|
|
530
|
+
drain_streams(wait_seconds)
|
|
531
|
+
|
|
532
|
+
if proc.poll() is not None and not selector.get_map():
|
|
533
|
+
break
|
|
534
|
+
finally:
|
|
535
|
+
while selector.get_map():
|
|
536
|
+
drain_streams(0)
|
|
537
|
+
|
|
538
|
+
if timed_out and proc.returncode is None:
|
|
539
|
+
sys.exit(124)
|
|
540
|
+
if timed_out:
|
|
541
|
+
sys.exit(124)
|
|
542
|
+
sys.exit(proc.wait())
|
|
415
543
|
PY
|
|
416
544
|
}
|
|
417
545
|
|
|
@@ -528,28 +656,30 @@ HOOK_EOF
|
|
|
528
656
|
}
|
|
529
657
|
|
|
530
658
|
classify_failure_reason() {
|
|
531
|
-
local log_file="
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
659
|
+
local log_file=""
|
|
660
|
+
for log_file in "\$@"; do
|
|
661
|
+
[[ -n "\${log_file}" && -f "\${log_file}" ]] || continue
|
|
662
|
+
if grep -Eiq 'authentication|unauthorized|login required|invalid api key|api key' "\${log_file}" 2>/dev/null; then
|
|
663
|
+
printf 'auth-failure\n'
|
|
664
|
+
return 0
|
|
665
|
+
fi
|
|
666
|
+
if grep -Eiq 'rate limit|quota exceeded|insufficient credits|payment required|429' "\${log_file}" 2>/dev/null; then
|
|
667
|
+
printf 'provider-quota-limit\n'
|
|
668
|
+
return 0
|
|
669
|
+
fi
|
|
670
|
+
if grep -Eiq 'model .* not available|unsupported model|invalid model|model not found' "\${log_file}" 2>/dev/null; then
|
|
671
|
+
printf 'model-unavailable\n'
|
|
672
|
+
return 0
|
|
673
|
+
fi
|
|
674
|
+
if grep -Eiq 'connection reset|connection error|network error|temporarily unavailable|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN' "\${log_file}" 2>/dev/null; then
|
|
675
|
+
printf 'network-connection\n'
|
|
676
|
+
return 0
|
|
677
|
+
fi
|
|
678
|
+
if grep -Eiq 'timeout|timed out|ETIMEDOUT' "\${log_file}" 2>/dev/null; then
|
|
679
|
+
printf 'timeout\n'
|
|
680
|
+
return 0
|
|
681
|
+
fi
|
|
682
|
+
done
|
|
553
683
|
printf 'claude-exit-failed\n'
|
|
554
684
|
}
|
|
555
685
|
|
|
@@ -580,17 +710,23 @@ reset_sandbox_run_dir
|
|
|
580
710
|
ensure_workspace_excludes
|
|
581
711
|
install_pre_commit_scope_hook
|
|
582
712
|
|
|
583
|
-
prompt_payload="\$(cat "\${prompt_file}")"
|
|
584
713
|
claude_args=(
|
|
585
714
|
-p
|
|
586
715
|
--output-format text
|
|
716
|
+
--verbose
|
|
717
|
+
--debug-file "\${claude_debug_file}"
|
|
587
718
|
--no-session-persistence
|
|
588
|
-
--permission-mode "\${
|
|
719
|
+
--permission-mode "\${claude_effective_permission_mode}"
|
|
720
|
+
--allowed-tools "\${claude_allowed_tools}"
|
|
721
|
+
--disable-slash-commands
|
|
722
|
+
--strict-mcp-config
|
|
723
|
+
--mcp-config "\${claude_mcp_config_file}"
|
|
724
|
+
--settings "\${claude_settings_file}"
|
|
589
725
|
--model "\${claude_model}"
|
|
590
726
|
--effort "\${claude_effort}"
|
|
591
727
|
--add-dir ${worktree_q}
|
|
592
728
|
)
|
|
593
|
-
if [[ "\${
|
|
729
|
+
if [[ "\${claude_effective_permission_mode}" == "bypassPermissions" ]]; then
|
|
594
730
|
claude_args+=(--allow-dangerously-skip-permissions)
|
|
595
731
|
fi
|
|
596
732
|
|
|
@@ -602,7 +738,7 @@ while (( attempt <= claude_max_attempts )); do
|
|
|
602
738
|
attempt_log_file="\${artifact_dir}/claude-attempt-\${attempt}.log"
|
|
603
739
|
write_state running '' '' "\${attempt}" "\$((attempt - 1))"
|
|
604
740
|
printf '\n[claude-attempt] %s/%s\n' "\${attempt}" "\${claude_max_attempts}" | tee -a "\${output_file}" >/dev/null
|
|
605
|
-
run_with_timeout "\${claude_timeout_seconds}" "\${
|
|
741
|
+
run_with_timeout "\${claude_timeout_seconds}" "\${prompt_file}" "\${claude_bin}" "\${claude_args[@]}" >"\${attempt_log_file}" 2>&1
|
|
606
742
|
status=\$?
|
|
607
743
|
cat "\${attempt_log_file}" >>"\${output_file}"
|
|
608
744
|
if [[ "\${status}" -eq 0 ]]; then
|
|
@@ -612,7 +748,7 @@ while (( attempt <= claude_max_attempts )); do
|
|
|
612
748
|
if [[ "\${status}" -eq 124 ]]; then
|
|
613
749
|
failure_reason="timeout"
|
|
614
750
|
else
|
|
615
|
-
failure_reason="\$(classify_failure_reason "\${attempt_log_file}")"
|
|
751
|
+
failure_reason="\$(classify_failure_reason "\${attempt_log_file}" "\${claude_debug_file}")"
|
|
616
752
|
fi
|
|
617
753
|
if (( attempt >= claude_max_attempts )) || ! is_retryable_failure_reason "\${failure_reason}"; then
|
|
618
754
|
break
|
|
@@ -629,6 +765,8 @@ if [[ -f "\${result_file_path}" ]]; then
|
|
|
629
765
|
else
|
|
630
766
|
if [[ "\${status}" -eq 0 ]]; then
|
|
631
767
|
write_result_fallback "missing-result-contract"
|
|
768
|
+
elif [[ "\${status}" -ne 124 && -n "\${failure_reason}" && "\${failure_reason}" != "claude-exit-failed" ]]; then
|
|
769
|
+
write_result_fallback "\${failure_reason}"
|
|
632
770
|
else
|
|
633
771
|
write_result_fallback "worker-exit-\${status}"
|
|
634
772
|
fi
|
|
@@ -256,7 +256,7 @@ fi
|
|
|
256
256
|
|
|
257
257
|
reconcile_snippet=""
|
|
258
258
|
if [[ -n "$reconcile_command" ]]; then
|
|
259
|
-
printf -v delayed_reconcile_q '%q' "sleep 2; $reconcile_command"
|
|
259
|
+
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"
|
|
260
260
|
reconcile_snippet="nohup bash -lc ${delayed_reconcile_q} >> ${output_q} 2>&1 </dev/null &"
|
|
261
261
|
fi
|
|
262
262
|
|