agent-control-plane 0.1.6 → 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 +3 -2
- package/references/architecture.md +209 -0
- package/references/commands.md +127 -0
- package/references/control-plane-map.md +120 -0
- package/references/docs-map.md +73 -0
- package/references/release-checklist.md +67 -0
- package/references/repo-map.md +36 -0
- 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
- package/tools/bin/test-smoke.sh +59 -8
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Release Checklist
|
|
2
|
+
|
|
3
|
+
Maintainer checklist for shipping a new public package release of
|
|
4
|
+
`agent-control-plane`.
|
|
5
|
+
|
|
6
|
+
## Pre-Release
|
|
7
|
+
|
|
8
|
+
- confirm `git status` is clean
|
|
9
|
+
- confirm `package.json` version is intentional
|
|
10
|
+
- review `CHANGELOG.md` and prepare release notes from
|
|
11
|
+
`.github/release-template.md`
|
|
12
|
+
- refresh README demo media if the dashboard UI changed:
|
|
13
|
+
`bash tools/bin/render-dashboard-demo-media.sh`
|
|
14
|
+
- review `README.md`, `SECURITY.md`, `CONTRIBUTING.md`, and `CLA.md` for stale
|
|
15
|
+
links or policy text
|
|
16
|
+
- if a public GitHub repo now exists, set or verify the `homepage`,
|
|
17
|
+
`repository`, and `bugs` URLs in `package.json`
|
|
18
|
+
- make sure sponsor links still point to the intended maintainer identity
|
|
19
|
+
- confirm the intended license is still `MIT`
|
|
20
|
+
|
|
21
|
+
## Verification
|
|
22
|
+
|
|
23
|
+
Run the core checks:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bash tools/tests/test-package-public-metadata.sh
|
|
27
|
+
bash tools/tests/test-package-funding-metadata.sh
|
|
28
|
+
bash tools/tests/test-contribution-docs.sh
|
|
29
|
+
bash tools/tests/test-agent-control-plane-npm-cli.sh
|
|
30
|
+
bash tools/bin/test-smoke.sh
|
|
31
|
+
npm pack --dry-run
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Publish
|
|
35
|
+
|
|
36
|
+
Recommended release flow uses npm trusted publishing from GitHub Actions, so you
|
|
37
|
+
do not have to enter an OTP during every local publish.
|
|
38
|
+
|
|
39
|
+
One-time setup:
|
|
40
|
+
|
|
41
|
+
- configure `agent-control-plane` for npm trusted publishing from
|
|
42
|
+
`ducminhnguyen0319/agent-control-plane`
|
|
43
|
+
- verify the publish workflow file is `.github/workflows/publish.yml`
|
|
44
|
+
- keep npm package publishing policy on the stricter setting you want after
|
|
45
|
+
trusted publishing is working
|
|
46
|
+
|
|
47
|
+
Per-release command flow:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm version <patch|minor|major>
|
|
51
|
+
git push origin main --follow-tags
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Then:
|
|
55
|
+
|
|
56
|
+
- watch `.github/workflows/publish.yml` for the npm publish run
|
|
57
|
+
- create or update a GitHub release using `.github/release-template.md`
|
|
58
|
+
- update `CHANGELOG.md` if the final shipped notes differ from the draft
|
|
59
|
+
|
|
60
|
+
## Post-Release
|
|
61
|
+
|
|
62
|
+
- verify `npx agent-control-plane@latest help` works from a clean shell
|
|
63
|
+
- verify the npm package shows provenance for the new version
|
|
64
|
+
- verify `npm fund` shows the expected sponsor link
|
|
65
|
+
- verify the repo README, sponsor button, and package metadata all point to the
|
|
66
|
+
same maintainer identity
|
|
67
|
+
- announce the release where relevant
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Repository Map
|
|
2
|
+
|
|
3
|
+
This file maps the `agent-control-plane` repository itself.
|
|
4
|
+
|
|
5
|
+
## Core Layout
|
|
6
|
+
|
|
7
|
+
- `SKILL.md`
|
|
8
|
+
Shared operating manual for the control plane.
|
|
9
|
+
- `assets/`
|
|
10
|
+
Workflow catalog and static non-profile assets.
|
|
11
|
+
- `bin/`
|
|
12
|
+
Queue, label, and risk scripts shared by installed profiles.
|
|
13
|
+
- `hooks/`
|
|
14
|
+
Heartbeat and reconcile hooks shared by installed profiles.
|
|
15
|
+
- `tools/bin/`
|
|
16
|
+
Runtime wrappers, onboarding helpers, publication utilities, and doctor tools.
|
|
17
|
+
- `tools/templates/`
|
|
18
|
+
Generic fallback prompts used when a profile does not override a template.
|
|
19
|
+
- `tools/tests/`
|
|
20
|
+
Shell regression coverage for control-plane behavior.
|
|
21
|
+
- `references/`
|
|
22
|
+
Control-plane docs, operator commands, and repository maps.
|
|
23
|
+
|
|
24
|
+
## Operator Surfaces
|
|
25
|
+
|
|
26
|
+
- `tools/bin/render-flow-config.sh`
|
|
27
|
+
Effective config viewer for the selected profile.
|
|
28
|
+
- `tools/bin/profile-smoke.sh`
|
|
29
|
+
Installed-profile validation and collision detection.
|
|
30
|
+
- `tools/bin/profile-adopt.sh`
|
|
31
|
+
Local runtime/bootstrap helper for onboarding a profile onto a workstation.
|
|
32
|
+
- `tools/bin/sync-shared-agent-home.sh`
|
|
33
|
+
Publication repair for shared/runtime copies.
|
|
34
|
+
|
|
35
|
+
Installed profiles live outside this repo under
|
|
36
|
+
`~/.agent-runtime/control-plane/profiles/<id>/`.
|
|
@@ -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"
|