agent-control-plane 0.1.7 → 0.1.8
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/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-detached-launch +22 -2
- package/tools/bin/agent-project-reconcile-issue-session +50 -1
- package/tools/bin/agent-project-run-claude-session +174 -54
- package/tools/bin/run-codex-task.sh +1 -1
- package/tools/bin/scaffold-profile.sh +2 -2
|
@@ -7,6 +7,7 @@ source "${HOOK_SCRIPT_DIR}/../tools/bin/flow-config-lib.sh"
|
|
|
7
7
|
|
|
8
8
|
FLOW_SKILL_DIR="$(cd "${HOOK_SCRIPT_DIR}/.." && pwd)"
|
|
9
9
|
CONFIG_YAML="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
|
|
10
|
+
PROFILE_ID="$(flow_resolve_adapter_id "${CONFIG_YAML}")"
|
|
10
11
|
ADAPTER_BIN_DIR="${FLOW_SKILL_DIR}/bin"
|
|
11
12
|
FLOW_TOOLS_DIR="${FLOW_SKILL_DIR}/tools/bin"
|
|
12
13
|
REPO_SLUG="$(flow_resolve_repo_slug "${CONFIG_YAML}")"
|
|
@@ -15,6 +16,12 @@ STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
|
|
|
15
16
|
RUNS_ROOT="$(flow_resolve_runs_root "${CONFIG_YAML}")"
|
|
16
17
|
BLOCKED_RECOVERY_STATE_DIR="${STATE_ROOT}/blocked-recovery-issues"
|
|
17
18
|
|
|
19
|
+
issue_kick_scheduler() {
|
|
20
|
+
ACP_PROJECT_ID="${PROFILE_ID}" \
|
|
21
|
+
AGENT_PROJECT_ID="${PROFILE_ID}" \
|
|
22
|
+
"${FLOW_TOOLS_DIR}/kick-scheduler.sh" "${1:-2}" >/dev/null || true
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
issue_clear_blocked_recovery_state() {
|
|
19
26
|
rm -f "${BLOCKED_RECOVERY_STATE_DIR}/${ISSUE_ID}.env" 2>/dev/null || true
|
|
20
27
|
}
|
|
@@ -194,7 +201,7 @@ issue_after_pr_created() {
|
|
|
194
201
|
if [[ "$(jq -r '.eligibleForAutoMerge' <<<"$risk_json")" == "true" ]]; then
|
|
195
202
|
bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "${REPO_SLUG}" --number "$pr_number" --add agent-automerge >/dev/null || true
|
|
196
203
|
fi
|
|
197
|
-
|
|
204
|
+
issue_kick_scheduler 5
|
|
198
205
|
}
|
|
199
206
|
|
|
200
207
|
issue_after_reconciled() {
|
|
@@ -213,5 +220,5 @@ issue_after_reconciled() {
|
|
|
213
220
|
esac
|
|
214
221
|
fi
|
|
215
222
|
|
|
216
|
-
|
|
223
|
+
issue_kick_scheduler 2
|
|
217
224
|
}
|
|
@@ -7,6 +7,7 @@ source "${HOOK_SCRIPT_DIR}/../tools/bin/flow-config-lib.sh"
|
|
|
7
7
|
|
|
8
8
|
FLOW_SKILL_DIR="$(cd "${HOOK_SCRIPT_DIR}/.." && pwd)"
|
|
9
9
|
CONFIG_YAML="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
|
|
10
|
+
PROFILE_ID="$(flow_resolve_adapter_id "${CONFIG_YAML}")"
|
|
10
11
|
ADAPTER_BIN_DIR="${FLOW_SKILL_DIR}/bin"
|
|
11
12
|
FLOW_TOOLS_DIR="${FLOW_SKILL_DIR}/tools/bin"
|
|
12
13
|
RUNS_ROOT="$(flow_resolve_runs_root "${CONFIG_YAML}")"
|
|
@@ -19,6 +20,12 @@ ISSUE_SESSION_PREFIX="$(flow_resolve_issue_session_prefix "${CONFIG_YAML}")"
|
|
|
19
20
|
PR_WORKTREE_BRANCH_PREFIX="$(flow_resolve_pr_worktree_branch_prefix "${CONFIG_YAML}")"
|
|
20
21
|
PR_LANE_OVERRIDE_DIR="${STATE_ROOT}/pr-lane-overrides"
|
|
21
22
|
|
|
23
|
+
pr_kick_scheduler() {
|
|
24
|
+
ACP_PROJECT_ID="${PROFILE_ID}" \
|
|
25
|
+
AGENT_PROJECT_ID="${PROFILE_ID}" \
|
|
26
|
+
"${FLOW_TOOLS_DIR}/kick-scheduler.sh" "${1:-2}" >/dev/null || true
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
pr_best_effort_update_labels() {
|
|
23
30
|
bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" "$@" >/dev/null 2>&1 || true
|
|
24
31
|
}
|
|
@@ -131,14 +138,14 @@ pr_after_merged() {
|
|
|
131
138
|
pr_clear_lane_override "$pr_number"
|
|
132
139
|
pr_best_effort_update_labels --repo-slug "${REPO_SLUG}" --number "$pr_number" --remove agent-running --remove agent-automerge --remove agent-repair-queued --remove agent-fix-needed --remove agent-manual-fix-override --remove agent-ci-refresh --remove agent-ci-bypassed --remove agent-double-check-1/2 --remove agent-double-check-2/2 --remove agent-human-review --remove agent-human-approved --remove agent-blocked --remove agent-handoff --remove agent-exclusive
|
|
133
140
|
pr_refresh_linked_issue_checklist "$pr_number"
|
|
134
|
-
|
|
141
|
+
pr_kick_scheduler 5
|
|
135
142
|
}
|
|
136
143
|
|
|
137
144
|
pr_after_closed() {
|
|
138
145
|
local pr_number="${1:?pr number required}"
|
|
139
146
|
pr_clear_lane_override "$pr_number"
|
|
140
147
|
pr_best_effort_update_labels --repo-slug "${REPO_SLUG}" --number "$pr_number" --remove agent-running --remove agent-automerge --remove agent-repair-queued --remove agent-fix-needed --remove agent-manual-fix-override --remove agent-ci-refresh --remove agent-ci-bypassed --remove agent-double-check-1/2 --remove agent-double-check-2/2 --remove agent-human-review --remove agent-human-approved --remove agent-blocked --remove agent-handoff --remove agent-exclusive
|
|
141
|
-
|
|
148
|
+
pr_kick_scheduler 5
|
|
142
149
|
}
|
|
143
150
|
|
|
144
151
|
pr_automerge_allowed() {
|
|
@@ -189,7 +196,7 @@ pr_after_double_check_advanced() {
|
|
|
189
196
|
pr_set_lane_override "$pr_number" "double-check-${next_stage}"
|
|
190
197
|
pr_best_effort_update_labels --repo-slug "${REPO_SLUG}" --number "$pr_number" --remove agent-running --remove agent-automerge --remove agent-repair-queued --remove agent-fix-needed --remove agent-manual-fix-override --remove agent-ci-refresh --remove agent-human-review --remove agent-human-approved --remove agent-double-check-1/2 --remove agent-double-check-2/2 --add "$next_label"
|
|
191
198
|
pr_best_effort_sync_pr_labels "$pr_number"
|
|
192
|
-
|
|
199
|
+
pr_kick_scheduler 5
|
|
193
200
|
}
|
|
194
201
|
|
|
195
202
|
pr_after_updated_branch() {
|
|
@@ -221,5 +228,5 @@ pr_after_failed() {
|
|
|
221
228
|
}
|
|
222
229
|
|
|
223
230
|
pr_after_reconciled() {
|
|
224
|
-
|
|
231
|
+
pr_kick_scheduler 2
|
|
225
232
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-control-plane",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Help a repo keep GitHub-driven coding agents running reliably without constant human babysitting",
|
|
5
5
|
"homepage": "https://github.com/ducminhnguyen0319/agent-control-plane",
|
|
6
6
|
"bugs": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"scripts": {
|
|
41
41
|
"doctor": "node ./npm/bin/agent-control-plane.js doctor",
|
|
42
42
|
"smoke": "node ./npm/bin/agent-control-plane.js smoke",
|
|
43
|
-
"test": "bash tools/tests/test-agent-control-plane-npm-cli.sh && bash tools/tests/test-profile-adopt-skip-anchor-sync-creates-agent-repo-root.sh && bash tools/tests/test-vendored-codex-quota-claude-oauth-only.sh && bash tools/tests/test-package-smoke-command.sh"
|
|
43
|
+
"test": "bash tools/tests/test-agent-control-plane-npm-cli.sh && bash tools/tests/test-agent-project-detached-launch-stable-cwd.sh && bash tools/tests/test-agent-project-claude-session-wrapper-reaps-child-on-term.sh && bash tools/tests/test-agent-project-claude-session-wrapper-does-not-retry-provider-quota.sh && bash tools/tests/test-agent-project-reconcile-issue-provider-quota-schedules-provider-cooldown.sh && bash tools/tests/test-pr-reconcile-hooks-refreshes-recurring-issue-checklist.sh && bash tools/tests/test-issue-reconcile-hooks-kick-scheduler-uses-profile.sh && bash tools/tests/test-profile-adopt-skip-anchor-sync-creates-agent-repo-root.sh && bash tools/tests/test-vendored-codex-quota-claude-oauth-only.sh && bash tools/tests/test-package-smoke-command.sh"
|
|
44
44
|
},
|
|
45
45
|
"keywords": [
|
|
46
46
|
"agents",
|
|
@@ -6,6 +6,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
6
6
|
source "${SCRIPT_DIR}/flow-config-lib.sh"
|
|
7
7
|
CONFIG_YAML="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
|
|
8
8
|
STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
|
|
9
|
+
AGENT_REPO_ROOT="$(flow_resolve_agent_repo_root "${CONFIG_YAML}")"
|
|
10
|
+
REPO_ROOT="$(flow_resolve_repo_root "${CONFIG_YAML}")"
|
|
9
11
|
|
|
10
12
|
usage() {
|
|
11
13
|
cat <<'EOF'
|
|
@@ -66,6 +68,21 @@ if [[ -n "${pending_key}" ]]; then
|
|
|
66
68
|
pending_file="${pending_dir}/${pending_key}.pid"
|
|
67
69
|
fi
|
|
68
70
|
|
|
71
|
+
launch_cwd="${ACP_LAUNCH_CWD:-${F_LOSNING_LAUNCH_CWD:-}}"
|
|
72
|
+
if [[ -z "${launch_cwd}" ]]; then
|
|
73
|
+
for candidate in "${AGENT_REPO_ROOT}" "${REPO_ROOT}" "${STATE_ROOT}" "${HOME:-}" "/"; do
|
|
74
|
+
if [[ -n "${candidate}" && -d "${candidate}" ]]; then
|
|
75
|
+
launch_cwd="${candidate}"
|
|
76
|
+
break
|
|
77
|
+
fi
|
|
78
|
+
done
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
if [[ -z "${launch_cwd}" || ! -d "${launch_cwd}" ]]; then
|
|
82
|
+
echo "could not determine a stable working directory for detached launch" >&2
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
85
|
+
|
|
69
86
|
python_bin="${PYTHON_BIN:-$(command -v python3 || true)}"
|
|
70
87
|
if [[ -z "${python_bin}" ]]; then
|
|
71
88
|
echo "python3 is required for detached launch" >&2
|
|
@@ -73,14 +90,15 @@ if [[ -z "${python_bin}" ]]; then
|
|
|
73
90
|
fi
|
|
74
91
|
|
|
75
92
|
launch_pid="$(
|
|
76
|
-
"${python_bin}" - "${log_file}" "${pending_file}" "$@" <<'PY'
|
|
93
|
+
"${python_bin}" - "${log_file}" "${pending_file}" "${launch_cwd}" "$@" <<'PY'
|
|
77
94
|
import subprocess
|
|
78
95
|
import sys
|
|
79
96
|
from pathlib import Path
|
|
80
97
|
|
|
81
98
|
log_file = Path(sys.argv[1])
|
|
82
99
|
pending_file = sys.argv[2]
|
|
83
|
-
|
|
100
|
+
launch_cwd = sys.argv[3]
|
|
101
|
+
argv = sys.argv[4:]
|
|
84
102
|
|
|
85
103
|
with log_file.open("ab", buffering=0) as log_handle:
|
|
86
104
|
proc = subprocess.Popen(
|
|
@@ -88,6 +106,7 @@ with log_file.open("ab", buffering=0) as log_handle:
|
|
|
88
106
|
stdin=subprocess.DEVNULL,
|
|
89
107
|
stdout=log_handle,
|
|
90
108
|
stderr=subprocess.STDOUT,
|
|
109
|
+
cwd=launch_cwd,
|
|
91
110
|
start_new_session=True,
|
|
92
111
|
)
|
|
93
112
|
|
|
@@ -102,6 +121,7 @@ printf 'LAUNCH_MODE=detached\n'
|
|
|
102
121
|
printf 'LAUNCH_NAME=%s\n' "${launch_name}"
|
|
103
122
|
printf 'LAUNCH_PID=%s\n' "${launch_pid}"
|
|
104
123
|
printf 'LAUNCH_LOG=%s\n' "${log_file}"
|
|
124
|
+
printf 'LAUNCH_CWD=%s\n' "${launch_cwd}"
|
|
105
125
|
if [[ -n "${pending_file}" ]]; then
|
|
106
126
|
printf 'LAUNCH_PENDING_FILE=%s\n' "${pending_file}"
|
|
107
127
|
fi
|
|
@@ -794,6 +794,41 @@ ${publish_out}
|
|
|
794
794
|
EOF
|
|
795
795
|
}
|
|
796
796
|
|
|
797
|
+
build_issue_runtime_blocker_comment() {
|
|
798
|
+
local runtime_reason="${1:-worker-exit-failed}"
|
|
799
|
+
local worker_name="${CODING_WORKER:-worker}"
|
|
800
|
+
|
|
801
|
+
case "${runtime_reason}" in
|
|
802
|
+
provider-quota-limit)
|
|
803
|
+
cat <<EOF
|
|
804
|
+
# Blocker: Provider quota is currently exhausted
|
|
805
|
+
|
|
806
|
+
This recurring run stopped before implementation because the configured ${worker_name} account hit a provider-side rate limit.
|
|
807
|
+
|
|
808
|
+
Why it was blocked:
|
|
809
|
+
- the worker reached Anthropic's current request limit for this account
|
|
810
|
+
- ACP recorded the quota hit and will retry after the configured cooldown instead of looping indefinitely
|
|
811
|
+
|
|
812
|
+
Next step:
|
|
813
|
+
- wait for the current quota window to reset, or switch this profile to another available provider/account
|
|
814
|
+
EOF
|
|
815
|
+
return 0
|
|
816
|
+
;;
|
|
817
|
+
esac
|
|
818
|
+
|
|
819
|
+
cat <<EOF
|
|
820
|
+
# Blocker: Worker session failed before publish
|
|
821
|
+
|
|
822
|
+
The worker exited before ACP could publish or reconcile a result for this cycle.
|
|
823
|
+
|
|
824
|
+
Failure reason:
|
|
825
|
+
- \`${runtime_reason}\`
|
|
826
|
+
|
|
827
|
+
Next step:
|
|
828
|
+
- inspect the run logs for this session and re-queue once the underlying worker issue is resolved
|
|
829
|
+
EOF
|
|
830
|
+
}
|
|
831
|
+
|
|
797
832
|
extract_recovery_worktree_from_publish_output() {
|
|
798
833
|
local publish_out="${1:-}"
|
|
799
834
|
awk -F= '/^RECOVERY_WORKTREE=/{print $2}' <<<"$publish_out" | tail -n 1
|
|
@@ -1106,9 +1141,17 @@ case "$status" in
|
|
|
1106
1141
|
failure_reason="${failure_reason:-worker-exit-failed}"
|
|
1107
1142
|
schedule_provider_quota_cooldown "${failure_reason}"
|
|
1108
1143
|
normalize_issue_runner_state "failed" "${LAST_EXIT_CODE:-}" "${failure_reason}"
|
|
1144
|
+
if [[ "${result_outcome:-}" == "blocked" && "${result_action:-}" == "host-comment-blocker" ]]; then
|
|
1145
|
+
if [[ ! -s "${run_dir}/issue-comment.md" ]]; then
|
|
1146
|
+
write_issue_comment_artifact "$(build_issue_runtime_blocker_comment "${failure_reason}")" || true
|
|
1147
|
+
fi
|
|
1148
|
+
post_issue_comment_if_present
|
|
1149
|
+
issue_set_reconcile_summary "$status" "$result_outcome" "$result_action" "$failure_reason"
|
|
1150
|
+
else
|
|
1151
|
+
issue_set_reconcile_summary "$status" "" "" "$failure_reason"
|
|
1152
|
+
fi
|
|
1109
1153
|
require_transition "issue_schedule_retry" issue_schedule_retry "${failure_reason}"
|
|
1110
1154
|
require_transition "issue_mark_ready" issue_mark_ready
|
|
1111
|
-
issue_set_reconcile_summary "$status" "" "" "$failure_reason"
|
|
1112
1155
|
cleanup_issue_session
|
|
1113
1156
|
notify_issue_reconciled
|
|
1114
1157
|
;;
|
|
@@ -1120,6 +1163,12 @@ mark_reconciled
|
|
|
1120
1163
|
printf 'STATUS=%s\n' "$status"
|
|
1121
1164
|
printf 'ISSUE_ID=%s\n' "$issue_id"
|
|
1122
1165
|
printf 'PR_NUMBER=%s\n' "$pr_number"
|
|
1166
|
+
if [[ -n "${issue_summary_outcome:-}" ]]; then
|
|
1167
|
+
printf 'OUTCOME=%s\n' "${issue_summary_outcome}"
|
|
1168
|
+
fi
|
|
1169
|
+
if [[ -n "${issue_summary_action:-}" ]]; then
|
|
1170
|
+
printf 'ACTION=%s\n' "${issue_summary_action}"
|
|
1171
|
+
fi
|
|
1123
1172
|
if [[ -n "$failure_reason" ]]; then
|
|
1124
1173
|
printf 'FAILURE_REASON=%s\n' "$failure_reason"
|
|
1125
1174
|
fi
|
|
@@ -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=""
|
|
@@ -94,6 +96,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
94
96
|
--claude-timeout-seconds) claude_timeout_seconds="${2:-}"; shift 2 ;;
|
|
95
97
|
--claude-max-attempts) claude_max_attempts="${2:-}"; shift 2 ;;
|
|
96
98
|
--claude-retry-backoff-seconds) claude_retry_backoff_seconds="${2:-}"; shift 2 ;;
|
|
99
|
+
--claude-allowed-tools) claude_allowed_tools="${2:-}"; shift 2 ;;
|
|
97
100
|
--env-prefix) env_prefix="${2:-}"; shift 2 ;;
|
|
98
101
|
--context) context_items+=("${2:-}"); shift 2 ;;
|
|
99
102
|
--collect-file) collect_files+=("${2:-}"); shift 2 ;;
|
|
@@ -156,11 +159,31 @@ meta_file="${artifact_dir}/run.env"
|
|
|
156
159
|
result_file="${artifact_dir}/result.env"
|
|
157
160
|
runner_state_file="${artifact_dir}/runner.env"
|
|
158
161
|
sandbox_run_dir="${worktree%/}/${sandbox_subdir}/${session}"
|
|
162
|
+
claude_settings_file="${artifact_dir}/claude-headless-settings.json"
|
|
163
|
+
claude_mcp_config_file="${artifact_dir}/claude-headless-mcp.json"
|
|
164
|
+
claude_debug_file="${artifact_dir}/claude-debug.log"
|
|
159
165
|
started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
160
166
|
|
|
161
167
|
mkdir -p "$artifact_dir"
|
|
162
168
|
mkdir -p "$sandbox_run_dir"
|
|
163
169
|
|
|
170
|
+
effective_claude_permission_mode="${claude_permission_mode}"
|
|
171
|
+
if [[ "${effective_claude_permission_mode}" == "dontAsk" ]]; then
|
|
172
|
+
effective_claude_permission_mode="acceptEdits"
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
cat >"$claude_settings_file" <<'EOF'
|
|
176
|
+
{
|
|
177
|
+
"disableAllHooks": true
|
|
178
|
+
}
|
|
179
|
+
EOF
|
|
180
|
+
|
|
181
|
+
cat >"$claude_mcp_config_file" <<'EOF'
|
|
182
|
+
{
|
|
183
|
+
"mcpServers": {}
|
|
184
|
+
}
|
|
185
|
+
EOF
|
|
186
|
+
|
|
164
187
|
if tmux has-session -t "$session" 2>/dev/null; then
|
|
165
188
|
echo "tmux session already exists: $session" >&2
|
|
166
189
|
exit 1
|
|
@@ -187,10 +210,15 @@ printf -v started_at_q '%q' "$started_at"
|
|
|
187
210
|
printf -v claude_bin_q '%q' "$claude_bin"
|
|
188
211
|
printf -v claude_model_q '%q' "$claude_model"
|
|
189
212
|
printf -v claude_permission_mode_q '%q' "$claude_permission_mode"
|
|
213
|
+
printf -v claude_effective_permission_mode_q '%q' "$effective_claude_permission_mode"
|
|
190
214
|
printf -v claude_effort_q '%q' "$claude_effort"
|
|
191
215
|
printf -v claude_timeout_q '%q' "$claude_timeout_seconds"
|
|
192
216
|
printf -v claude_max_attempts_q '%q' "$claude_max_attempts"
|
|
193
217
|
printf -v claude_retry_backoff_q '%q' "$claude_retry_backoff_seconds"
|
|
218
|
+
printf -v claude_allowed_tools_q '%q' "$claude_allowed_tools"
|
|
219
|
+
printf -v claude_settings_file_q '%q' "$claude_settings_file"
|
|
220
|
+
printf -v claude_mcp_config_file_q '%q' "$claude_mcp_config_file"
|
|
221
|
+
printf -v claude_debug_file_q '%q' "$claude_debug_file"
|
|
194
222
|
printf -v python_bin_q '%q' "$python_bin"
|
|
195
223
|
printf -v sandbox_subdir_q '%q' "$sandbox_subdir"
|
|
196
224
|
printf -v claude_thread_id_q '%q' "claude-print-${session}"
|
|
@@ -213,10 +241,15 @@ printf -v claude_thread_id_q '%q' "claude-print-${session}"
|
|
|
213
241
|
printf 'CLAUDE_BIN=%s\n' "$claude_bin_q"
|
|
214
242
|
printf 'CLAUDE_MODEL=%s\n' "$claude_model_q"
|
|
215
243
|
printf 'CLAUDE_PERMISSION_MODE=%s\n' "$claude_permission_mode_q"
|
|
244
|
+
printf 'CLAUDE_EFFECTIVE_PERMISSION_MODE=%s\n' "$claude_effective_permission_mode_q"
|
|
216
245
|
printf 'CLAUDE_EFFORT=%s\n' "$claude_effort_q"
|
|
217
246
|
printf 'CLAUDE_TIMEOUT_SECONDS=%s\n' "$claude_timeout_q"
|
|
218
247
|
printf 'CLAUDE_MAX_ATTEMPTS=%s\n' "$claude_max_attempts_q"
|
|
219
248
|
printf 'CLAUDE_RETRY_BACKOFF_SECONDS=%s\n' "$claude_retry_backoff_q"
|
|
249
|
+
printf 'CLAUDE_ALLOWED_TOOLS=%s\n' "$claude_allowed_tools_q"
|
|
250
|
+
printf 'CLAUDE_SETTINGS_FILE=%s\n' "$claude_settings_file_q"
|
|
251
|
+
printf 'CLAUDE_MCP_CONFIG_FILE=%s\n' "$claude_mcp_config_file_q"
|
|
252
|
+
printf 'CLAUDE_DEBUG_FILE=%s\n' "$claude_debug_file_q"
|
|
220
253
|
printf 'PYTHON_BIN=%s\n' "$python_bin_q"
|
|
221
254
|
} >"$meta_file"
|
|
222
255
|
|
|
@@ -334,14 +367,20 @@ host_result_file=${result_q}
|
|
|
334
367
|
claude_bin=${claude_bin_q}
|
|
335
368
|
claude_model=${claude_model_q}
|
|
336
369
|
claude_permission_mode=${claude_permission_mode_q}
|
|
370
|
+
claude_effective_permission_mode=${claude_effective_permission_mode_q}
|
|
337
371
|
claude_effort=${claude_effort_q}
|
|
338
372
|
claude_timeout_seconds=${claude_timeout_q}
|
|
339
373
|
claude_max_attempts=${claude_max_attempts_q}
|
|
340
374
|
claude_retry_backoff_seconds=${claude_retry_backoff_q}
|
|
375
|
+
claude_allowed_tools=${claude_allowed_tools_q}
|
|
376
|
+
claude_settings_file=${claude_settings_file_q}
|
|
377
|
+
claude_mcp_config_file=${claude_mcp_config_file_q}
|
|
378
|
+
claude_debug_file=${claude_debug_file_q}
|
|
341
379
|
python_bin=${python_bin_q}
|
|
342
380
|
worktree_root=${worktree_q}
|
|
343
381
|
sandbox_subdir=${sandbox_subdir_q}
|
|
344
382
|
prompt_file=${prompt_q}
|
|
383
|
+
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
|
|
345
384
|
|
|
346
385
|
write_state() {
|
|
347
386
|
local runner_state="\${1:?runner state required}"
|
|
@@ -370,48 +409,119 @@ write_state() {
|
|
|
370
409
|
|
|
371
410
|
run_with_timeout() {
|
|
372
411
|
local timeout_seconds="\${1:?timeout seconds required}"
|
|
412
|
+
local stdin_file="\${2:?stdin file required}"
|
|
413
|
+
shift
|
|
373
414
|
shift
|
|
374
415
|
|
|
375
|
-
"\${python_bin}" - "\${timeout_seconds}" "\$@" <<'PY'
|
|
416
|
+
"\${python_bin}" - "\${timeout_seconds}" "\${stdin_file}" "\$@" <<'PY'
|
|
417
|
+
import errno
|
|
418
|
+
import fcntl
|
|
376
419
|
import os
|
|
420
|
+
import selectors
|
|
377
421
|
import signal
|
|
378
422
|
import subprocess
|
|
379
423
|
import sys
|
|
424
|
+
import time
|
|
380
425
|
|
|
381
426
|
timeout_seconds = float(sys.argv[1])
|
|
382
|
-
|
|
427
|
+
stdin_path = sys.argv[2]
|
|
428
|
+
argv = sys.argv[3:]
|
|
383
429
|
|
|
384
430
|
if not argv:
|
|
385
431
|
sys.exit(64)
|
|
386
432
|
|
|
387
|
-
|
|
433
|
+
stdin_handle = open(stdin_path, "rb")
|
|
434
|
+
proc = subprocess.Popen(
|
|
435
|
+
argv,
|
|
436
|
+
start_new_session=True,
|
|
437
|
+
stdin=stdin_handle,
|
|
438
|
+
stdout=subprocess.PIPE,
|
|
439
|
+
stderr=subprocess.PIPE,
|
|
440
|
+
)
|
|
388
441
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
442
|
+
for stream in (proc.stdout, proc.stderr):
|
|
443
|
+
if stream is None:
|
|
444
|
+
continue
|
|
445
|
+
flags = fcntl.fcntl(stream.fileno(), fcntl.F_GETFL)
|
|
446
|
+
fcntl.fcntl(stream.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
|
447
|
+
|
|
448
|
+
selector = selectors.DefaultSelector()
|
|
449
|
+
if proc.stdout is not None:
|
|
450
|
+
selector.register(proc.stdout, selectors.EVENT_READ, sys.stdout.buffer)
|
|
451
|
+
if proc.stderr is not None:
|
|
452
|
+
selector.register(proc.stderr, selectors.EVENT_READ, sys.stderr.buffer)
|
|
453
|
+
|
|
454
|
+
def terminate_process_group(sig):
|
|
392
455
|
try:
|
|
393
|
-
os.killpg(proc.pid,
|
|
456
|
+
os.killpg(proc.pid, sig)
|
|
394
457
|
except ProcessLookupError:
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
def drain_streams(wait_seconds):
|
|
461
|
+
events = selector.select(wait_seconds)
|
|
462
|
+
for key, _ in events:
|
|
399
463
|
try:
|
|
400
|
-
|
|
401
|
-
except
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
464
|
+
chunk = key.fileobj.read()
|
|
465
|
+
except BlockingIOError:
|
|
466
|
+
continue
|
|
467
|
+
except OSError as exc:
|
|
468
|
+
if exc.errno == errno.EAGAIN:
|
|
469
|
+
continue
|
|
470
|
+
raise
|
|
471
|
+
if not chunk:
|
|
472
|
+
selector.unregister(key.fileobj)
|
|
473
|
+
continue
|
|
474
|
+
key.data.write(chunk)
|
|
475
|
+
key.data.flush()
|
|
476
|
+
|
|
477
|
+
def handle_parent_signal(signum, _frame):
|
|
478
|
+
terminate_process_group(signal.SIGTERM)
|
|
479
|
+
deadline = time.monotonic() + 2.0
|
|
480
|
+
while proc.poll() is None and time.monotonic() < deadline:
|
|
481
|
+
drain_streams(0.1)
|
|
482
|
+
if proc.poll() is None:
|
|
483
|
+
terminate_process_group(signal.SIGKILL)
|
|
484
|
+
while selector.get_map():
|
|
485
|
+
drain_streams(0)
|
|
486
|
+
sys.exit(128 + signum)
|
|
487
|
+
|
|
488
|
+
for signum in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
|
|
489
|
+
signal.signal(signum, handle_parent_signal)
|
|
490
|
+
|
|
491
|
+
deadline = time.monotonic() + timeout_seconds
|
|
492
|
+
grace_deadline = None
|
|
493
|
+
timed_out = False
|
|
409
494
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
495
|
+
try:
|
|
496
|
+
while True:
|
|
497
|
+
now = time.monotonic()
|
|
498
|
+
if not timed_out and now >= deadline:
|
|
499
|
+
timed_out = True
|
|
500
|
+
grace_deadline = now + 2.0
|
|
501
|
+
terminate_process_group(signal.SIGTERM)
|
|
502
|
+
elif timed_out and grace_deadline is not None and proc.poll() is None and now >= grace_deadline:
|
|
503
|
+
grace_deadline = None
|
|
504
|
+
terminate_process_group(signal.SIGKILL)
|
|
505
|
+
|
|
506
|
+
wait_seconds = 0.1
|
|
507
|
+
if not timed_out:
|
|
508
|
+
wait_seconds = max(0.0, min(0.1, deadline - now))
|
|
509
|
+
elif grace_deadline is not None:
|
|
510
|
+
wait_seconds = max(0.0, min(0.1, grace_deadline - now))
|
|
511
|
+
|
|
512
|
+
drain_streams(wait_seconds)
|
|
513
|
+
|
|
514
|
+
if proc.poll() is not None and not selector.get_map():
|
|
515
|
+
break
|
|
516
|
+
finally:
|
|
517
|
+
while selector.get_map():
|
|
518
|
+
drain_streams(0)
|
|
519
|
+
|
|
520
|
+
if timed_out and proc.returncode is None:
|
|
521
|
+
sys.exit(124)
|
|
522
|
+
if timed_out:
|
|
523
|
+
sys.exit(124)
|
|
524
|
+
sys.exit(proc.wait())
|
|
415
525
|
PY
|
|
416
526
|
}
|
|
417
527
|
|
|
@@ -528,28 +638,30 @@ HOOK_EOF
|
|
|
528
638
|
}
|
|
529
639
|
|
|
530
640
|
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
|
-
|
|
641
|
+
local log_file=""
|
|
642
|
+
for log_file in "\$@"; do
|
|
643
|
+
[[ -n "\${log_file}" && -f "\${log_file}" ]] || continue
|
|
644
|
+
if grep -Eiq 'authentication|unauthorized|login required|invalid api key|api key' "\${log_file}" 2>/dev/null; then
|
|
645
|
+
printf 'auth-failure\n'
|
|
646
|
+
return 0
|
|
647
|
+
fi
|
|
648
|
+
if grep -Eiq 'rate limit|quota exceeded|insufficient credits|payment required|429' "\${log_file}" 2>/dev/null; then
|
|
649
|
+
printf 'provider-quota-limit\n'
|
|
650
|
+
return 0
|
|
651
|
+
fi
|
|
652
|
+
if grep -Eiq 'model .* not available|unsupported model|invalid model|model not found' "\${log_file}" 2>/dev/null; then
|
|
653
|
+
printf 'model-unavailable\n'
|
|
654
|
+
return 0
|
|
655
|
+
fi
|
|
656
|
+
if grep -Eiq 'connection reset|connection error|network error|temporarily unavailable|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN' "\${log_file}" 2>/dev/null; then
|
|
657
|
+
printf 'network-connection\n'
|
|
658
|
+
return 0
|
|
659
|
+
fi
|
|
660
|
+
if grep -Eiq 'timeout|timed out|ETIMEDOUT' "\${log_file}" 2>/dev/null; then
|
|
661
|
+
printf 'timeout\n'
|
|
662
|
+
return 0
|
|
663
|
+
fi
|
|
664
|
+
done
|
|
553
665
|
printf 'claude-exit-failed\n'
|
|
554
666
|
}
|
|
555
667
|
|
|
@@ -580,17 +692,23 @@ reset_sandbox_run_dir
|
|
|
580
692
|
ensure_workspace_excludes
|
|
581
693
|
install_pre_commit_scope_hook
|
|
582
694
|
|
|
583
|
-
prompt_payload="\$(cat "\${prompt_file}")"
|
|
584
695
|
claude_args=(
|
|
585
696
|
-p
|
|
586
697
|
--output-format text
|
|
698
|
+
--verbose
|
|
699
|
+
--debug-file "\${claude_debug_file}"
|
|
587
700
|
--no-session-persistence
|
|
588
|
-
--permission-mode "\${
|
|
701
|
+
--permission-mode "\${claude_effective_permission_mode}"
|
|
702
|
+
--allowed-tools "\${claude_allowed_tools}"
|
|
703
|
+
--disable-slash-commands
|
|
704
|
+
--strict-mcp-config
|
|
705
|
+
--mcp-config "\${claude_mcp_config_file}"
|
|
706
|
+
--settings "\${claude_settings_file}"
|
|
589
707
|
--model "\${claude_model}"
|
|
590
708
|
--effort "\${claude_effort}"
|
|
591
709
|
--add-dir ${worktree_q}
|
|
592
710
|
)
|
|
593
|
-
if [[ "\${
|
|
711
|
+
if [[ "\${claude_effective_permission_mode}" == "bypassPermissions" ]]; then
|
|
594
712
|
claude_args+=(--allow-dangerously-skip-permissions)
|
|
595
713
|
fi
|
|
596
714
|
|
|
@@ -602,7 +720,7 @@ while (( attempt <= claude_max_attempts )); do
|
|
|
602
720
|
attempt_log_file="\${artifact_dir}/claude-attempt-\${attempt}.log"
|
|
603
721
|
write_state running '' '' "\${attempt}" "\$((attempt - 1))"
|
|
604
722
|
printf '\n[claude-attempt] %s/%s\n' "\${attempt}" "\${claude_max_attempts}" | tee -a "\${output_file}" >/dev/null
|
|
605
|
-
run_with_timeout "\${claude_timeout_seconds}" "\${
|
|
723
|
+
run_with_timeout "\${claude_timeout_seconds}" "\${prompt_file}" "\${claude_bin}" "\${claude_args[@]}" >"\${attempt_log_file}" 2>&1
|
|
606
724
|
status=\$?
|
|
607
725
|
cat "\${attempt_log_file}" >>"\${output_file}"
|
|
608
726
|
if [[ "\${status}" -eq 0 ]]; then
|
|
@@ -612,7 +730,7 @@ while (( attempt <= claude_max_attempts )); do
|
|
|
612
730
|
if [[ "\${status}" -eq 124 ]]; then
|
|
613
731
|
failure_reason="timeout"
|
|
614
732
|
else
|
|
615
|
-
failure_reason="\$(classify_failure_reason "\${attempt_log_file}")"
|
|
733
|
+
failure_reason="\$(classify_failure_reason "\${attempt_log_file}" "\${claude_debug_file}")"
|
|
616
734
|
fi
|
|
617
735
|
if (( attempt >= claude_max_attempts )) || ! is_retryable_failure_reason "\${failure_reason}"; then
|
|
618
736
|
break
|
|
@@ -629,6 +747,8 @@ if [[ -f "\${result_file_path}" ]]; then
|
|
|
629
747
|
else
|
|
630
748
|
if [[ "\${status}" -eq 0 ]]; then
|
|
631
749
|
write_result_fallback "missing-result-contract"
|
|
750
|
+
elif [[ "\${status}" -ne 124 && -n "\${failure_reason}" && "\${failure_reason}" != "claude-exit-failed" ]]; then
|
|
751
|
+
write_result_fallback "\${failure_reason}"
|
|
632
752
|
else
|
|
633
753
|
write_result_fallback "worker-exit-\${status}"
|
|
634
754
|
fi
|
|
@@ -41,7 +41,7 @@ CODEX_PROFILE_BYPASS="${ACP_CODEX_PROFILE_BYPASS:-${F_LOSNING_CODEX_PROFILE_BYPA
|
|
|
41
41
|
if [[ "$MODE" == "bypass" ]]; then
|
|
42
42
|
CLAUDE_PERMISSION_MODE_DEFAULT="bypassPermissions"
|
|
43
43
|
else
|
|
44
|
-
CLAUDE_PERMISSION_MODE_DEFAULT="
|
|
44
|
+
CLAUDE_PERMISSION_MODE_DEFAULT="acceptEdits"
|
|
45
45
|
fi
|
|
46
46
|
CLAUDE_MODEL="${ACP_CLAUDE_MODEL:-${F_LOSNING_CLAUDE_MODEL:-sonnet}}"
|
|
47
47
|
CLAUDE_PERMISSION_MODE="${ACP_CLAUDE_PERMISSION_MODE:-${F_LOSNING_CLAUDE_PERMISSION_MODE:-${CLAUDE_PERMISSION_MODE_DEFAULT}}}"
|
|
@@ -25,7 +25,7 @@ Options:
|
|
|
25
25
|
--coding-worker <codex|openclaw|claude>
|
|
26
26
|
Default coding backend (default: openclaw)
|
|
27
27
|
--claude-model <model> Claude model alias or full name
|
|
28
|
-
--claude-permission-mode <mode> Claude permission mode (default:
|
|
28
|
+
--claude-permission-mode <mode> Claude permission mode (default: acceptEdits)
|
|
29
29
|
--claude-effort <level> Claude effort level (default: medium)
|
|
30
30
|
--claude-timeout-seconds <secs> Claude timeout (default: 900)
|
|
31
31
|
--claude-max-attempts <count> Claude retry attempts (default: 3)
|
|
@@ -49,7 +49,7 @@ retained_repo_root=""
|
|
|
49
49
|
vscode_workspace_file=""
|
|
50
50
|
coding_worker="openclaw"
|
|
51
51
|
claude_model="sonnet"
|
|
52
|
-
claude_permission_mode="
|
|
52
|
+
claude_permission_mode="acceptEdits"
|
|
53
53
|
claude_effort="medium"
|
|
54
54
|
claude_timeout_seconds="900"
|
|
55
55
|
claude_max_attempts="3"
|