agent-control-plane 0.4.9 → 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.
Files changed (80) hide show
  1. package/README.md +72 -9
  2. package/npm/bin/agent-control-plane.js +1 -1
  3. package/package.json +39 -33
  4. package/tools/bin/debug-session.sh +106 -0
  5. package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
  6. package/tools/bin/flow-runtime-doctor.sh +5 -1
  7. package/tools/bin/install-project-systemd.sh +255 -0
  8. package/tools/bin/project-runtimectl.sh +45 -0
  9. package/tools/bin/project-systemd-bootstrap.sh +74 -0
  10. package/tools/bin/uninstall-project-systemd.sh +87 -0
  11. package/tools/dashboard/app.js +198 -5
  12. package/tools/dashboard/issue_queue_state.py +101 -0
  13. package/tools/dashboard/server.py +123 -1
  14. package/tools/dashboard/styles.css +526 -455
  15. package/tools/bin/agent-cleanup-worktree +0 -247
  16. package/tools/bin/agent-github-update-labels +0 -105
  17. package/tools/bin/agent-init-worktree +0 -216
  18. package/tools/bin/agent-project-archive-run +0 -52
  19. package/tools/bin/agent-project-capture-worker +0 -46
  20. package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
  21. package/tools/bin/agent-project-catch-up-merged-prs +0 -195
  22. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
  23. package/tools/bin/agent-project-cleanup-session +0 -513
  24. package/tools/bin/agent-project-detached-launch +0 -127
  25. package/tools/bin/agent-project-heartbeat-loop +0 -1029
  26. package/tools/bin/agent-project-open-issue-worktree +0 -89
  27. package/tools/bin/agent-project-open-pr-worktree +0 -80
  28. package/tools/bin/agent-project-publish-issue-pr +0 -468
  29. package/tools/bin/agent-project-reconcile-issue-session +0 -1409
  30. package/tools/bin/agent-project-reconcile-pr-session +0 -1288
  31. package/tools/bin/agent-project-retry-state +0 -158
  32. package/tools/bin/agent-project-run-claude-session +0 -805
  33. package/tools/bin/agent-project-run-codex-resilient +0 -963
  34. package/tools/bin/agent-project-run-codex-session +0 -435
  35. package/tools/bin/agent-project-run-kilo-session +0 -369
  36. package/tools/bin/agent-project-run-ollama-session +0 -658
  37. package/tools/bin/agent-project-run-openclaw-session +0 -1309
  38. package/tools/bin/agent-project-run-opencode-session +0 -377
  39. package/tools/bin/agent-project-run-pi-session +0 -479
  40. package/tools/bin/agent-project-sync-anchor-repo +0 -139
  41. package/tools/bin/agent-project-sync-source-repo-main +0 -163
  42. package/tools/bin/agent-project-worker-status +0 -188
  43. package/tools/bin/branch-verification-guard.sh +0 -364
  44. package/tools/bin/capture-worker.sh +0 -18
  45. package/tools/bin/cleanup-worktree.sh +0 -52
  46. package/tools/bin/codex-quota +0 -31
  47. package/tools/bin/create-follow-up-issue.sh +0 -114
  48. package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
  49. package/tools/bin/issue-publish-localization-guard.sh +0 -142
  50. package/tools/bin/issue-publish-scope-guard.sh +0 -242
  51. package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
  52. package/tools/bin/issue-resource-class.sh +0 -12
  53. package/tools/bin/kick-scheduler.sh +0 -75
  54. package/tools/bin/label-follow-up-issues.sh +0 -14
  55. package/tools/bin/new-pr-worktree.sh +0 -50
  56. package/tools/bin/new-worktree.sh +0 -49
  57. package/tools/bin/pr-risk.sh +0 -12
  58. package/tools/bin/prepare-worktree.sh +0 -142
  59. package/tools/bin/provider-cooldown-state.sh +0 -204
  60. package/tools/bin/publish-issue-worker.sh +0 -31
  61. package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
  62. package/tools/bin/reconcile-issue-worker.sh +0 -34
  63. package/tools/bin/reconcile-pr-worker.sh +0 -34
  64. package/tools/bin/record-verification.sh +0 -71
  65. package/tools/bin/render-flow-config.sh +0 -98
  66. package/tools/bin/resident-issue-controller-lib.sh +0 -448
  67. package/tools/bin/retry-state.sh +0 -31
  68. package/tools/bin/reuse-issue-worktree.sh +0 -121
  69. package/tools/bin/run-codex-bypass.sh +0 -3
  70. package/tools/bin/run-codex-safe.sh +0 -3
  71. package/tools/bin/run-codex-task.sh +0 -280
  72. package/tools/bin/serve-dashboard.sh +0 -5
  73. package/tools/bin/start-issue-worker.sh +0 -943
  74. package/tools/bin/start-pr-fix-worker.sh +0 -528
  75. package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
  76. package/tools/bin/start-pr-review-worker.sh +0 -261
  77. package/tools/bin/start-resident-issue-loop.sh +0 -499
  78. package/tools/bin/update-github-labels.sh +0 -14
  79. package/tools/bin/worker-status.sh +0 -19
  80. package/tools/bin/workflow-catalog.sh +0 -77
@@ -1,805 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- usage() {
5
- cat <<'EOF'
6
- Usage:
7
- agent-project-run-claude-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 a Claude Code worker session inside tmux for a project adapter and
10
- persist the standard run artifacts.
11
-
12
- Options:
13
- --claude-model <name> Claude model alias or full name
14
- --claude-permission-mode <mode> Claude permission mode (e.g. acceptEdits, bypassPermissions)
15
- --claude-effort <level> Claude effort level (low, medium, high, max)
16
- --claude-timeout-seconds <secs> Claude command timeout (default: 900)
17
- --claude-max-attempts <count> Retry transient failures this many times (default: 3)
18
- --claude-retry-backoff-seconds <s>
19
- Sleep between transient retries (default: 30)
20
- --claude-allowed-tools <spec> Allowed Claude tools for headless runs
21
- --env-prefix <prefix> Export prefixed runtime/context env vars inside the worker
22
- --context <KEY=VALUE> Extra metadata written to run.env and exported to the worker
23
- --collect-file <name> Copy sandbox artifact file into the host run dir after execution
24
- --reconcile-command <cmd> Host-side command queued after the worker exits
25
- --sandbox-subdir <name> Subdir under the worktree for worker artifacts (default: .openclaw-artifacts)
26
- --help Show this help
27
- EOF
28
- }
29
-
30
- mode=""
31
- session=""
32
- worktree=""
33
- prompt_file=""
34
- runs_root=""
35
- adapter_id=""
36
- task_kind=""
37
- task_id=""
38
- claude_model="${ACP_CLAUDE_MODEL:-${F_LOSNING_CLAUDE_MODEL:-sonnet}}"
39
- claude_permission_mode="${ACP_CLAUDE_PERMISSION_MODE:-${F_LOSNING_CLAUDE_PERMISSION_MODE:-acceptEdits}}"
40
- claude_effort="${ACP_CLAUDE_EFFORT:-${F_LOSNING_CLAUDE_EFFORT:-medium}}"
41
- claude_timeout_seconds="${ACP_CLAUDE_TIMEOUT_SECONDS:-${F_LOSNING_CLAUDE_TIMEOUT_SECONDS:-900}}"
42
- claude_max_attempts="${ACP_CLAUDE_MAX_ATTEMPTS:-${F_LOSNING_CLAUDE_MAX_ATTEMPTS:-3}}"
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}}"
45
- env_prefix=""
46
- sandbox_subdir=".openclaw-artifacts"
47
- reconcile_command=""
48
- declare -a context_items=()
49
- declare -a collect_files=()
50
-
51
- resolve_claude_bin() {
52
- local configured_bin="${CLAUDE_BIN:-${ACP_CLAUDE_BIN:-${F_LOSNING_CLAUDE_BIN:-}}}"
53
-
54
- if [[ -n "${configured_bin}" && -x "${configured_bin}" ]]; then
55
- printf '%s\n' "${configured_bin}"
56
- return 0
57
- fi
58
-
59
- if command -v claude >/dev/null 2>&1; then
60
- command -v claude
61
- return 0
62
- fi
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
-
82
- return 1
83
- }
84
-
85
- resolve_python_bin() {
86
- if command -v python3 >/dev/null 2>&1; then
87
- command -v python3
88
- return 0
89
- fi
90
- if [[ -x /opt/homebrew/bin/python3 ]]; then
91
- printf '%s\n' "/opt/homebrew/bin/python3"
92
- return 0
93
- fi
94
- if command -v python >/dev/null 2>&1; then
95
- command -v python
96
- return 0
97
- fi
98
- return 1
99
- }
100
-
101
- while [[ $# -gt 0 ]]; do
102
- case "$1" in
103
- --mode) mode="${2:-}"; shift 2 ;;
104
- --session) session="${2:-}"; shift 2 ;;
105
- --worktree) worktree="${2:-}"; shift 2 ;;
106
- --prompt-file) prompt_file="${2:-}"; shift 2 ;;
107
- --runs-root) runs_root="${2:-}"; shift 2 ;;
108
- --adapter-id) adapter_id="${2:-}"; shift 2 ;;
109
- --task-kind) task_kind="${2:-}"; shift 2 ;;
110
- --task-id) task_id="${2:-}"; shift 2 ;;
111
- --claude-model) claude_model="${2:-}"; shift 2 ;;
112
- --claude-permission-mode) claude_permission_mode="${2:-}"; shift 2 ;;
113
- --claude-effort) claude_effort="${2:-}"; shift 2 ;;
114
- --claude-timeout-seconds) claude_timeout_seconds="${2:-}"; shift 2 ;;
115
- --claude-max-attempts) claude_max_attempts="${2:-}"; shift 2 ;;
116
- --claude-retry-backoff-seconds) claude_retry_backoff_seconds="${2:-}"; shift 2 ;;
117
- --claude-allowed-tools) claude_allowed_tools="${2:-}"; shift 2 ;;
118
- --env-prefix) env_prefix="${2:-}"; shift 2 ;;
119
- --context) context_items+=("${2:-}"); shift 2 ;;
120
- --collect-file) collect_files+=("${2:-}"); shift 2 ;;
121
- --reconcile-command) reconcile_command="${2:-}"; shift 2 ;;
122
- --sandbox-subdir) sandbox_subdir="${2:-}"; shift 2 ;;
123
- --help|-h) usage; exit 0 ;;
124
- *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
125
- esac
126
- done
127
-
128
- if [[ -z "$mode" || -z "$session" || -z "$worktree" || -z "$prompt_file" || -z "$runs_root" || -z "$adapter_id" || -z "$task_kind" || -z "$task_id" ]]; then
129
- usage >&2
130
- exit 1
131
- fi
132
-
133
- case "$mode" in
134
- safe|bypass) ;;
135
- *)
136
- echo "--mode must be safe or bypass" >&2
137
- exit 1
138
- ;;
139
- esac
140
-
141
- case "$claude_effort" in
142
- low|medium|high|max) ;;
143
- *)
144
- echo "--claude-effort must be one of: low, medium, high, max" >&2
145
- exit 1
146
- ;;
147
- esac
148
-
149
- case "$claude_timeout_seconds" in
150
- ''|*[!0-9]*|0) echo "--claude-timeout-seconds must be a positive integer" >&2; exit 1 ;;
151
- esac
152
-
153
- case "$claude_max_attempts" in
154
- ''|*[!0-9]*|0) echo "--claude-max-attempts must be a positive integer" >&2; exit 1 ;;
155
- esac
156
-
157
- case "$claude_retry_backoff_seconds" in
158
- ''|*[!0-9]*) echo "--claude-retry-backoff-seconds must be numeric" >&2; exit 1 ;;
159
- esac
160
-
161
- claude_bin="$(resolve_claude_bin || true)"
162
- if [[ -z "${claude_bin}" || ! -x "${claude_bin}" ]]; then
163
- echo "unable to resolve a runnable claude binary" >&2
164
- exit 1
165
- fi
166
-
167
- python_bin="$(resolve_python_bin || true)"
168
- if [[ -z "${python_bin}" || ! -x "${python_bin}" ]]; then
169
- echo "unable to resolve a runnable python interpreter for claude timeout control" >&2
170
- exit 1
171
- fi
172
-
173
- artifact_dir="${runs_root}/${session}"
174
- output_file="${artifact_dir}/${session}.log"
175
- inner_script="${artifact_dir}/${session}.sh"
176
- meta_file="${artifact_dir}/run.env"
177
- result_file="${artifact_dir}/result.env"
178
- runner_state_file="${artifact_dir}/runner.env"
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"
183
- started_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
184
-
185
- mkdir -p "$artifact_dir"
186
- mkdir -p "$sandbox_run_dir"
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
-
205
- if tmux has-session -t "$session" 2>/dev/null; then
206
- echo "tmux session already exists: $session" >&2
207
- exit 1
208
- fi
209
-
210
- branch_name="$(git -C "$worktree" branch --show-current 2>/dev/null || true)"
211
-
212
- printf -v session_q '%q' "$session"
213
- printf -v task_kind_q '%q' "$task_kind"
214
- printf -v task_id_q '%q' "$task_id"
215
- printf -v mode_q '%q' "$mode"
216
- printf -v worktree_q '%q' "$worktree"
217
- printf -v prompt_q '%q' "$prompt_file"
218
- printf -v output_q '%q' "$output_file"
219
- printf -v artifact_dir_q '%q' "$artifact_dir"
220
- printf -v script_q '%q' "$inner_script"
221
- printf -v result_q '%q' "$result_file"
222
- printf -v meta_file_q '%q' "$meta_file"
223
- printf -v runner_state_q '%q' "$runner_state_file"
224
- printf -v branch_q '%q' "$branch_name"
225
- printf -v sandbox_run_dir_q '%q' "$sandbox_run_dir"
226
- printf -v adapter_id_q '%q' "$adapter_id"
227
- printf -v started_at_q '%q' "$started_at"
228
- printf -v claude_bin_q '%q' "$claude_bin"
229
- printf -v claude_model_q '%q' "$claude_model"
230
- printf -v claude_permission_mode_q '%q' "$claude_permission_mode"
231
- printf -v claude_effective_permission_mode_q '%q' "$effective_claude_permission_mode"
232
- printf -v claude_effort_q '%q' "$claude_effort"
233
- printf -v claude_timeout_q '%q' "$claude_timeout_seconds"
234
- printf -v claude_max_attempts_q '%q' "$claude_max_attempts"
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"
240
- printf -v python_bin_q '%q' "$python_bin"
241
- printf -v sandbox_subdir_q '%q' "$sandbox_subdir"
242
- printf -v claude_thread_id_q '%q' "claude-print-${session}"
243
-
244
- {
245
- printf 'TASK_KIND=%s\n' "$task_kind_q"
246
- printf 'TASK_ID=%s\n' "$task_id_q"
247
- printf 'SESSION=%s\n' "$session_q"
248
- printf 'MODE=%s\n' "$mode_q"
249
- printf 'WORKTREE=%s\n' "$worktree_q"
250
- printf 'PROMPT_FILE=%s\n' "$prompt_q"
251
- printf 'OUTPUT_FILE=%s\n' "$output_q"
252
- printf 'SCRIPT=%s\n' "$script_q"
253
- printf 'BRANCH=%s\n' "$branch_q"
254
- printf 'RESULT_FILE=%s\n' "$result_q"
255
- printf 'RUNNER_STATE_FILE=%s\n' "$runner_state_q"
256
- printf 'SANDBOX_RUN_DIR=%s\n' "$sandbox_run_dir_q"
257
- printf 'ADAPTER_ID=%s\n' "$adapter_id_q"
258
- printf 'STARTED_AT=%s\n' "$started_at_q"
259
- printf 'CLAUDE_BIN=%s\n' "$claude_bin_q"
260
- printf 'CLAUDE_MODEL=%s\n' "$claude_model_q"
261
- printf 'CLAUDE_PERMISSION_MODE=%s\n' "$claude_permission_mode_q"
262
- printf 'CLAUDE_EFFECTIVE_PERMISSION_MODE=%s\n' "$claude_effective_permission_mode_q"
263
- printf 'CLAUDE_EFFORT=%s\n' "$claude_effort_q"
264
- printf 'CLAUDE_TIMEOUT_SECONDS=%s\n' "$claude_timeout_q"
265
- printf 'CLAUDE_MAX_ATTEMPTS=%s\n' "$claude_max_attempts_q"
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"
271
- printf 'PYTHON_BIN=%s\n' "$python_bin_q"
272
- } >"$meta_file"
273
-
274
- context_exports=""
275
- if ((${#context_items[@]} > 0)); then
276
- for item in "${context_items[@]}"; do
277
- if [[ "$item" != *=* ]]; then
278
- echo "--context must use KEY=VALUE syntax: $item" >&2
279
- exit 1
280
- fi
281
- key="${item%%=*}"
282
- value="${item#*=}"
283
- if [[ ! "$key" =~ ^[A-Z0-9_]+$ ]]; then
284
- echo "Invalid context key: $key" >&2
285
- exit 1
286
- fi
287
- printf -v value_q '%q' "$value"
288
- printf '%s=%s\n' "$key" "$value_q" >>"$meta_file"
289
- if [[ -n "$env_prefix" ]]; then
290
- context_exports+="export ${env_prefix}${key}=${value_q}"$'\n'
291
- fi
292
- context_exports+="export ACP_${key}=${value_q}"$'\n'
293
- if [[ "$env_prefix" != "F_LOSNING_" ]]; then
294
- context_exports+="export F_LOSNING_${key}=${value_q}"$'\n'
295
- fi
296
- done
297
- fi
298
-
299
- runtime_exports=$(
300
- cat <<EOF
301
- export AGENT_PROJECT_SESSION=${session_q}
302
- export AGENT_PROJECT_RUN_DIR=${sandbox_run_dir_q}
303
- export AGENT_PROJECT_HOST_RUN_DIR=${artifact_dir_q}
304
- export AGENT_PROJECT_RESULT_FILE=${sandbox_run_dir_q}/result.env
305
- export AGENT_PROJECT_CLAUDE_BIN=${claude_bin_q}
306
- export ACP_SESSION=${session_q}
307
- export ACP_RUN_DIR=${sandbox_run_dir_q}
308
- export ACP_HOST_RUN_DIR=${artifact_dir_q}
309
- export ACP_RESULT_FILE=${sandbox_run_dir_q}/result.env
310
- export ACP_CLAUDE_BIN=${claude_bin_q}
311
- export ACP_CLAUDE_MODEL=${claude_model_q}
312
- export ACP_CLAUDE_PERMISSION_MODE=${claude_permission_mode_q}
313
- export ACP_CLAUDE_EFFORT=${claude_effort_q}
314
- export ACP_CLAUDE_TIMEOUT_SECONDS=${claude_timeout_q}
315
- export ACP_CLAUDE_MAX_ATTEMPTS=${claude_max_attempts_q}
316
- export ACP_CLAUDE_RETRY_BACKOFF_SECONDS=${claude_retry_backoff_q}
317
- export F_LOSNING_SESSION=${session_q}
318
- export F_LOSNING_RUN_DIR=${sandbox_run_dir_q}
319
- export F_LOSNING_HOST_RUN_DIR=${artifact_dir_q}
320
- export F_LOSNING_RESULT_FILE=${sandbox_run_dir_q}/result.env
321
- export F_LOSNING_CLAUDE_BIN=${claude_bin_q}
322
- export F_LOSNING_CLAUDE_MODEL=${claude_model_q}
323
- export F_LOSNING_CLAUDE_PERMISSION_MODE=${claude_permission_mode_q}
324
- export F_LOSNING_CLAUDE_EFFORT=${claude_effort_q}
325
- export F_LOSNING_CLAUDE_TIMEOUT_SECONDS=${claude_timeout_q}
326
- export F_LOSNING_CLAUDE_MAX_ATTEMPTS=${claude_max_attempts_q}
327
- export F_LOSNING_CLAUDE_RETRY_BACKOFF_SECONDS=${claude_retry_backoff_q}
328
- EOF
329
- )
330
-
331
- if [[ -n "$env_prefix" ]]; then
332
- runtime_exports+=$'\n'
333
- runtime_exports+=$(cat <<EOF
334
- export ${env_prefix}SESSION=${session_q}
335
- export ${env_prefix}RUN_DIR=${sandbox_run_dir_q}
336
- export ${env_prefix}HOST_RUN_DIR=${artifact_dir_q}
337
- export ${env_prefix}RESULT_FILE=${sandbox_run_dir_q}/result.env
338
- export ${env_prefix}CLAUDE_BIN=${claude_bin_q}
339
- export ${env_prefix}CLAUDE_MODEL=${claude_model_q}
340
- export ${env_prefix}CLAUDE_PERMISSION_MODE=${claude_permission_mode_q}
341
- export ${env_prefix}CLAUDE_EFFORT=${claude_effort_q}
342
- export ${env_prefix}CLAUDE_TIMEOUT_SECONDS=${claude_timeout_q}
343
- export ${env_prefix}CLAUDE_MAX_ATTEMPTS=${claude_max_attempts_q}
344
- export ${env_prefix}CLAUDE_RETRY_BACKOFF_SECONDS=${claude_retry_backoff_q}
345
- EOF
346
- )
347
- fi
348
-
349
- collect_copy_snippet=""
350
- if ((${#collect_files[@]} > 0)); then
351
- for artifact_name in "${collect_files[@]}"; do
352
- if [[ -z "$artifact_name" ]]; then
353
- continue
354
- fi
355
- printf -v artifact_q '%q' "$artifact_name"
356
- collect_copy_snippet+=$(
357
- cat <<EOF
358
- if [[ -f ${sandbox_run_dir_q}/${artifact_q} ]]; then
359
- cp ${sandbox_run_dir_q}/${artifact_q} ${artifact_dir_q}/${artifact_q}
360
- fi
361
- EOF
362
- )
363
- collect_copy_snippet+=$'\n'
364
- done
365
- fi
366
-
367
- # Always collect result.env from sandbox to artifact_dir
368
- collect_copy_snippet+=$(
369
- cat <<EOF
370
- if [[ -f ${sandbox_run_dir_q}/result.env ]]; then
371
- cp ${sandbox_run_dir_q}/result.env ${artifact_dir_q}/result.env
372
- fi
373
- EOF
374
- )
375
- collect_copy_snippet+=$'\n'
376
-
377
- reconcile_snippet=""
378
- if [[ -n "$reconcile_command" ]]; then
379
- 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"
380
- reconcile_snippet="nohup bash -lc ${delayed_reconcile_q} >> ${output_q} 2>&1 </dev/null &"
381
- fi
382
-
383
- cat >"$inner_script" <<EOF
384
- #!/usr/bin/env bash
385
- set -euo pipefail
386
- ${runtime_exports}
387
- ${context_exports}cd ${worktree_q}
388
-
389
- runner_state_file=${runner_state_q}
390
- output_file=${output_q}
391
- sandbox_run_dir=${sandbox_run_dir_q}
392
- artifact_dir=${artifact_dir_q}
393
- result_file_path=${sandbox_run_dir_q}/result.env
394
- host_result_file=${result_q}
395
- claude_bin=${claude_bin_q}
396
- claude_model=${claude_model_q}
397
- claude_permission_mode=${claude_permission_mode_q}
398
- claude_effective_permission_mode=${claude_effective_permission_mode_q}
399
- claude_effort=${claude_effort_q}
400
- claude_timeout_seconds=${claude_timeout_q}
401
- claude_max_attempts=${claude_max_attempts_q}
402
- claude_retry_backoff_seconds=${claude_retry_backoff_q}
403
- claude_allowed_tools=${claude_allowed_tools_q}
404
- claude_settings_file=${claude_settings_file_q}
405
- claude_mcp_config_file=${claude_mcp_config_file_q}
406
- claude_debug_file=${claude_debug_file_q}
407
- python_bin=${python_bin_q}
408
- worktree_root=${worktree_q}
409
- sandbox_subdir=${sandbox_subdir_q}
410
- prompt_file=${prompt_q}
411
- export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
412
-
413
- write_state() {
414
- local runner_state="\${1:?runner state required}"
415
- local last_exit_code="\${2:-}"
416
- local failure_reason="\${3:-}"
417
- local attempt="\${4:-1}"
418
- local resume_count="\${5:-0}"
419
- local updated_at tmp_file
420
-
421
- updated_at="\$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
422
- tmp_file="\${runner_state_file}.tmp.\$\$"
423
- {
424
- printf 'RUNNER_STATE=%q\n' "\${runner_state}"
425
- printf 'THREAD_ID=%q\n' ${claude_thread_id_q}
426
- printf 'ATTEMPT=%q\n' "\${attempt}"
427
- printf 'RESUME_COUNT=%q\n' "\${resume_count}"
428
- printf 'LAST_EXIT_CODE=%q\n' "\${last_exit_code}"
429
- printf 'LAST_FAILURE_REASON=%q\n' "\${failure_reason}"
430
- printf 'LAST_TRIGGER_REASON=%q\n' ''
431
- printf 'AUTH_WAIT_STARTED_AT=%q\n' ''
432
- printf 'LAST_AUTH_FINGERPRINT=%q\n' ''
433
- printf 'UPDATED_AT=%q\n' "\${updated_at}"
434
- } >"\${tmp_file}"
435
- mv "\${tmp_file}" "\${runner_state_file}"
436
- }
437
-
438
- run_with_timeout() {
439
- local timeout_seconds="\${1:?timeout seconds required}"
440
- local stdin_file="\${2:?stdin file required}"
441
- shift
442
- shift
443
-
444
- "\${python_bin}" - "\${timeout_seconds}" "\${stdin_file}" "\$@" <<'PY'
445
- import errno
446
- import fcntl
447
- import os
448
- import selectors
449
- import signal
450
- import subprocess
451
- import sys
452
- import time
453
-
454
- timeout_seconds = float(sys.argv[1])
455
- stdin_path = sys.argv[2]
456
- argv = sys.argv[3:]
457
-
458
- if not argv:
459
- sys.exit(64)
460
-
461
- stdin_handle = open(stdin_path, "rb")
462
- proc = subprocess.Popen(
463
- argv,
464
- start_new_session=True,
465
- stdin=stdin_handle,
466
- stdout=subprocess.PIPE,
467
- stderr=subprocess.PIPE,
468
- )
469
-
470
- for stream in (proc.stdout, proc.stderr):
471
- if stream is None:
472
- continue
473
- flags = fcntl.fcntl(stream.fileno(), fcntl.F_GETFL)
474
- fcntl.fcntl(stream.fileno(), fcntl.F_SETFL, flags | os.O_NONBLOCK)
475
-
476
- selector = selectors.DefaultSelector()
477
- if proc.stdout is not None:
478
- selector.register(proc.stdout, selectors.EVENT_READ, sys.stdout.buffer)
479
- if proc.stderr is not None:
480
- selector.register(proc.stderr, selectors.EVENT_READ, sys.stderr.buffer)
481
-
482
- def terminate_process_group(sig):
483
- try:
484
- os.killpg(proc.pid, sig)
485
- except ProcessLookupError:
486
- return
487
-
488
- def drain_streams(wait_seconds):
489
- events = selector.select(wait_seconds)
490
- for key, _ in events:
491
- try:
492
- chunk = key.fileobj.read()
493
- except BlockingIOError:
494
- continue
495
- except OSError as exc:
496
- if exc.errno == errno.EAGAIN:
497
- continue
498
- raise
499
- if not chunk:
500
- selector.unregister(key.fileobj)
501
- continue
502
- key.data.write(chunk)
503
- key.data.flush()
504
-
505
- def handle_parent_signal(signum, _frame):
506
- terminate_process_group(signal.SIGTERM)
507
- deadline = time.monotonic() + 2.0
508
- while proc.poll() is None and time.monotonic() < deadline:
509
- drain_streams(0.1)
510
- if proc.poll() is None:
511
- terminate_process_group(signal.SIGKILL)
512
- while selector.get_map():
513
- drain_streams(0)
514
- sys.exit(128 + signum)
515
-
516
- for signum in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
517
- signal.signal(signum, handle_parent_signal)
518
-
519
- deadline = time.monotonic() + timeout_seconds
520
- grace_deadline = None
521
- timed_out = False
522
-
523
- try:
524
- while True:
525
- now = time.monotonic()
526
- if not timed_out and now >= deadline:
527
- timed_out = True
528
- grace_deadline = now + 2.0
529
- terminate_process_group(signal.SIGTERM)
530
- elif timed_out and grace_deadline is not None and proc.poll() is None and now >= grace_deadline:
531
- grace_deadline = None
532
- terminate_process_group(signal.SIGKILL)
533
-
534
- wait_seconds = 0.1
535
- if not timed_out:
536
- wait_seconds = max(0.0, min(0.1, deadline - now))
537
- elif grace_deadline is not None:
538
- wait_seconds = max(0.0, min(0.1, grace_deadline - now))
539
-
540
- drain_streams(wait_seconds)
541
-
542
- if proc.poll() is not None and not selector.get_map():
543
- break
544
- finally:
545
- while selector.get_map():
546
- drain_streams(0)
547
-
548
- if timed_out and proc.returncode is None:
549
- sys.exit(124)
550
- if timed_out:
551
- sys.exit(124)
552
- sys.exit(proc.wait())
553
- PY
554
- }
555
-
556
- record_final_git_state() {
557
- local final_head final_branch tmp_file
558
-
559
- final_head="\$(git -C ${worktree_q} rev-parse HEAD 2>/dev/null || true)"
560
- final_branch="\$(git -C ${worktree_q} branch --show-current 2>/dev/null || true)"
561
- tmp_file=${meta_file_q}.tmp.final.\$\$
562
- grep -vE '^(FINAL_HEAD|FINAL_BRANCH)=' ${meta_file_q} >"\${tmp_file}" 2>/dev/null || true
563
- {
564
- printf 'FINAL_HEAD=%q\n' "\${final_head}"
565
- printf 'FINAL_BRANCH=%q\n' "\${final_branch}"
566
- } >>"\${tmp_file}"
567
- mv "\${tmp_file}" ${meta_file_q}
568
- }
569
-
570
- ensure_workspace_excludes() {
571
- local exclude_file line sandbox_pattern
572
- exclude_file="\$(git -C ${worktree_q} config --worktree --get core.excludesFile 2>/dev/null || true)"
573
- if [[ -z "\${exclude_file}" ]]; then
574
- exclude_file="\${sandbox_run_dir}/git-exclude"
575
- git -C ${worktree_q} config extensions.worktreeConfig true >/dev/null 2>&1 || true
576
- git -C ${worktree_q} config --worktree core.excludesFile "\${exclude_file}"
577
- fi
578
-
579
- mkdir -p "\$(dirname "\${exclude_file}")"
580
- touch "\${exclude_file}"
581
- sandbox_pattern="\${sandbox_subdir#./}"
582
- sandbox_pattern="\${sandbox_pattern#/}"
583
- while IFS= read -r line; do
584
- [[ -n "\${line}" ]] || continue
585
- if ! grep -Fqx "\${line}" "\${exclude_file}" 2>/dev/null; then
586
- printf '%s\n' "\${line}" >>"\${exclude_file}"
587
- fi
588
- done <<PATTERNS
589
- \${sandbox_pattern}
590
- SOUL.md
591
- TOOLS.md
592
- IDENTITY.md
593
- USER.md
594
- HEARTBEAT.md
595
- BOOTSTRAP.md
596
- .agent-session.env
597
- PATTERNS
598
- }
599
-
600
- install_pre_commit_scope_hook() {
601
- local hooks_dir="\$(git -C ${worktree_q} rev-parse --git-path hooks 2>/dev/null || true)"
602
- if [[ -z "\${hooks_dir}" ]]; then
603
- hooks_dir="\$(git -C ${worktree_q} config --get core.hooksPath 2>/dev/null || true)"
604
- fi
605
- if [[ -z "\${hooks_dir}" ]]; then
606
- hooks_dir="${worktree_q}/.git-hooks"
607
- fi
608
- if [[ "\${hooks_dir}" != /* ]]; then
609
- hooks_dir="${worktree_q}/\${hooks_dir}"
610
- fi
611
- mkdir -p "\${hooks_dir}"
612
- cat >"\${hooks_dir}/pre-commit" <<'HOOK_EOF'
613
- #!/usr/bin/env bash
614
- set -euo pipefail
615
-
616
- changed_files="\$(
617
- git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true
618
- )"
619
-
620
- if [[ -z "\${changed_files}" ]]; then
621
- exit 0
622
- fi
623
-
624
- api_count=0
625
- web_count=0
626
- mobile_count=0
627
- package_count=0
628
- doc_count=0
629
- other_count=0
630
-
631
- while IFS= read -r file; do
632
- [[ -n "\${file}" ]] || continue
633
- case "\${file}" in
634
- apps/api/*) api_count=\$((api_count + 1)) ;;
635
- apps/web/*) web_count=\$((web_count + 1)) ;;
636
- apps/mobile/*) mobile_count=\$((mobile_count + 1)) ;;
637
- packages/*) package_count=\$((package_count + 1)) ;;
638
- openspec/*|docs/*|*.md) doc_count=\$((doc_count + 1)) ;;
639
- *) other_count=\$((other_count + 1)) ;;
640
- esac
641
- done <<< "\${changed_files}"
642
-
643
- surfaces=0
644
- [[ \$api_count -gt 0 ]] && surfaces=\$((surfaces + 1))
645
- [[ \$web_count -gt 0 ]] && surfaces=\$((surfaces + 1))
646
- [[ \$mobile_count -gt 0 ]] && surfaces=\$((surfaces + 1))
647
-
648
- if [[ \$surfaces -ge 3 ]]; then
649
- 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
650
- fi
651
-
652
- total_product=\$((api_count + web_count + mobile_count + package_count + other_count))
653
- if [[ \$total_product -gt 20 ]]; then
654
- echo "[pre-commit scope BLOCK] This commit touches \$total_product product files across \$surfaces surfaces. Split into smaller focused commits." >&2
655
- exit 1
656
- fi
657
-
658
- if [[ \$mobile_count -gt 8 ]]; then
659
- echo "[pre-commit scope BLOCK] This commit touches \$mobile_count mobile product files. Keep mobile changes to one focused route family (max 8 files)." >&2
660
- exit 1
661
- fi
662
-
663
- exit 0
664
- HOOK_EOF
665
- chmod +x "\${hooks_dir}/pre-commit"
666
- }
667
-
668
- classify_failure_reason() {
669
- local log_file=""
670
- for log_file in "\$@"; do
671
- [[ -n "\${log_file}" && -f "\${log_file}" ]] || continue
672
- if grep -Eiq 'authentication|unauthorized|login required|invalid api key|api key' "\${log_file}" 2>/dev/null; then
673
- printf 'auth-failure\n'
674
- return 0
675
- fi
676
- if grep -Eiq 'rate limit|quota exceeded|insufficient credits|payment required|429' "\${log_file}" 2>/dev/null; then
677
- printf 'provider-quota-limit\n'
678
- return 0
679
- fi
680
- if grep -Eiq 'model .* not available|unsupported model|invalid model|model not found' "\${log_file}" 2>/dev/null; then
681
- printf 'model-unavailable\n'
682
- return 0
683
- fi
684
- if grep -Eiq 'connection reset|connection error|network error|temporarily unavailable|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN' "\${log_file}" 2>/dev/null; then
685
- printf 'network-connection\n'
686
- return 0
687
- fi
688
- if grep -Eiq 'timeout|timed out|ETIMEDOUT' "\${log_file}" 2>/dev/null; then
689
- printf 'timeout\n'
690
- return 0
691
- fi
692
- done
693
- printf 'claude-exit-failed\n'
694
- }
695
-
696
- is_retryable_failure_reason() {
697
- case "\${1:-}" in
698
- network-connection|timeout) return 0 ;;
699
- *) return 1 ;;
700
- esac
701
- }
702
-
703
- write_result_fallback() {
704
- local detail="\${1:-missing-result-contract}"
705
- cat >"\${host_result_file}" <<RESULT
706
- OUTCOME=blocked
707
- ACTION=host-comment-blocker
708
- DETAIL=\${detail}
709
- RESULT
710
- cp "\${host_result_file}" "\${result_file_path}" 2>/dev/null || true
711
- }
712
-
713
- reset_sandbox_run_dir() {
714
- mkdir -p "\${sandbox_run_dir}"
715
- find "\${sandbox_run_dir}" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
716
- }
717
-
718
- mkdir -p "\${sandbox_run_dir}" "\${artifact_dir}"
719
- reset_sandbox_run_dir
720
- ensure_workspace_excludes
721
- install_pre_commit_scope_hook
722
-
723
- claude_args=(
724
- -p
725
- --output-format text
726
- --verbose
727
- --debug-file "\${claude_debug_file}"
728
- --no-session-persistence
729
- --permission-mode "\${claude_effective_permission_mode}"
730
- --allowed-tools "\${claude_allowed_tools}"
731
- --disable-slash-commands
732
- --strict-mcp-config
733
- --mcp-config "\${claude_mcp_config_file}"
734
- --settings "\${claude_settings_file}"
735
- --model "\${claude_model}"
736
- --effort "\${claude_effort}"
737
- --add-dir ${worktree_q}
738
- )
739
- if [[ "\${claude_effective_permission_mode}" == "bypassPermissions" ]]; then
740
- claude_args+=(--allow-dangerously-skip-permissions)
741
- fi
742
-
743
- status=1
744
- attempt=1
745
- failure_reason=""
746
- set +e
747
- while (( attempt <= claude_max_attempts )); do
748
- attempt_log_file="\${artifact_dir}/claude-attempt-\${attempt}.log"
749
- write_state running '' '' "\${attempt}" "\$((attempt - 1))"
750
- printf '\n[claude-attempt] %s/%s\n' "\${attempt}" "\${claude_max_attempts}" | tee -a "\${output_file}" >/dev/null
751
- run_with_timeout "\${claude_timeout_seconds}" "\${prompt_file}" "\${claude_bin}" "\${claude_args[@]}" >"\${attempt_log_file}" 2>&1
752
- status=\$?
753
- cat "\${attempt_log_file}" >>"\${output_file}"
754
- if [[ "\${status}" -eq 0 ]]; then
755
- failure_reason=""
756
- break
757
- fi
758
- if [[ "\${status}" -eq 124 ]]; then
759
- failure_reason="timeout"
760
- else
761
- failure_reason="\$(classify_failure_reason "\${attempt_log_file}" "\${claude_debug_file}")"
762
- fi
763
- if (( attempt >= claude_max_attempts )) || ! is_retryable_failure_reason "\${failure_reason}"; then
764
- break
765
- fi
766
- printf '[claude-retry] reason=%s backoff=%ss\n' "\${failure_reason}" "\${claude_retry_backoff_seconds}" | tee -a "\${output_file}" >/dev/null
767
- sleep "\${claude_retry_backoff_seconds}"
768
- attempt=\$((attempt + 1))
769
- done
770
- set -e
771
-
772
- record_final_git_state
773
- if [[ -f "\${result_file_path}" ]]; then
774
- cp "\${result_file_path}" "\${host_result_file}"
775
- else
776
- if [[ "\${status}" -eq 0 ]]; then
777
- write_result_fallback "missing-result-contract"
778
- elif [[ "\${status}" -ne 124 && -n "\${failure_reason}" && "\${failure_reason}" != "claude-exit-failed" ]]; then
779
- write_result_fallback "\${failure_reason}"
780
- else
781
- write_result_fallback "worker-exit-\${status}"
782
- fi
783
- fi
784
-
785
- ${collect_copy_snippet}
786
- if [[ "\${status}" -eq 0 ]]; then
787
- write_state succeeded "\${status}" '' "\${attempt}" "\$((attempt - 1))"
788
- else
789
- write_state failed "\${status}" "\${failure_reason}" "\${attempt}" "\$((attempt - 1))"
790
- fi
791
- ${reconcile_snippet}
792
- printf '\n__CODEX_EXIT__:%s\n' "\${status}" | tee -a "\${output_file}"
793
- exit "\${status}"
794
- EOF
795
-
796
- chmod +x "$inner_script"
797
- tmux new-session -d -s "$session" "$inner_script"
798
-
799
- printf 'SESSION=%s\n' "$session"
800
- printf 'TASK_KIND=%s\n' "$task_kind"
801
- printf 'TASK_ID=%s\n' "$task_id"
802
- printf 'WORKTREE=%s\n' "$worktree"
803
- printf 'OUTPUT=%s\n' "$output_file"
804
- printf 'SCRIPT=%s\n' "$inner_script"
805
- printf 'META=%s\n' "$meta_file"