agent-control-plane 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +141 -28
  2. package/assets/workflow-catalog.json +1 -1
  3. package/bin/pr-risk.sh +22 -7
  4. package/bin/sync-pr-labels.sh +1 -1
  5. package/hooks/heartbeat-hooks.sh +125 -12
  6. package/hooks/issue-reconcile-hooks.sh +1 -1
  7. package/hooks/pr-reconcile-hooks.sh +1 -1
  8. package/npm/bin/agent-control-plane.js +257 -59
  9. package/package.json +39 -32
  10. package/tools/bin/debug-session.sh +106 -0
  11. package/tools/bin/flow-config-lib.sh +1203 -60
  12. package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
  13. package/tools/bin/flow-runtime-doctor.sh +5 -1
  14. package/tools/bin/flow-shell-lib.sh +32 -0
  15. package/tools/bin/github-core-rate-limit-state.sh +77 -0
  16. package/tools/bin/github-write-outbox.sh +470 -0
  17. package/tools/bin/heartbeat-loop-scheduling-lib.sh +7 -7
  18. package/tools/bin/heartbeat-safe-auto.sh +42 -0
  19. package/tools/bin/install-project-launchd.sh +17 -2
  20. package/tools/bin/install-project-systemd.sh +255 -0
  21. package/tools/bin/project-init.sh +21 -1
  22. package/tools/bin/project-launchd-bootstrap.sh +5 -1
  23. package/tools/bin/project-runtimectl.sh +91 -2
  24. package/tools/bin/project-systemd-bootstrap.sh +74 -0
  25. package/tools/bin/scaffold-profile.sh +61 -3
  26. package/tools/bin/uninstall-project-systemd.sh +87 -0
  27. package/tools/dashboard/app.js +228 -6
  28. package/tools/dashboard/dashboard_snapshot.py +55 -0
  29. package/tools/dashboard/issue_queue_state.py +101 -0
  30. package/tools/dashboard/server.py +123 -1
  31. package/tools/dashboard/styles.css +526 -455
  32. package/tools/templates/pr-fix-template.md +3 -1
  33. package/tools/templates/pr-merge-repair-template.md +2 -1
  34. package/references/architecture.md +0 -217
  35. package/references/commands.md +0 -128
  36. package/references/control-plane-map.md +0 -124
  37. package/references/docs-map.md +0 -73
  38. package/references/release-checklist.md +0 -65
  39. package/references/repo-map.md +0 -36
  40. package/tools/bin/agent-cleanup-worktree +0 -247
  41. package/tools/bin/agent-github-update-labels +0 -71
  42. package/tools/bin/agent-init-worktree +0 -216
  43. package/tools/bin/agent-project-archive-run +0 -52
  44. package/tools/bin/agent-project-capture-worker +0 -46
  45. package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
  46. package/tools/bin/agent-project-catch-up-merged-prs +0 -194
  47. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
  48. package/tools/bin/agent-project-cleanup-session +0 -513
  49. package/tools/bin/agent-project-detached-launch +0 -127
  50. package/tools/bin/agent-project-heartbeat-loop +0 -1029
  51. package/tools/bin/agent-project-open-issue-worktree +0 -89
  52. package/tools/bin/agent-project-open-pr-worktree +0 -80
  53. package/tools/bin/agent-project-publish-issue-pr +0 -465
  54. package/tools/bin/agent-project-reconcile-issue-session +0 -1398
  55. package/tools/bin/agent-project-reconcile-pr-session +0 -1230
  56. package/tools/bin/agent-project-retry-state +0 -147
  57. package/tools/bin/agent-project-run-claude-session +0 -805
  58. package/tools/bin/agent-project-run-codex-resilient +0 -955
  59. package/tools/bin/agent-project-run-codex-session +0 -435
  60. package/tools/bin/agent-project-run-kilo-session +0 -369
  61. package/tools/bin/agent-project-run-ollama-session +0 -658
  62. package/tools/bin/agent-project-run-openclaw-session +0 -1309
  63. package/tools/bin/agent-project-run-opencode-session +0 -377
  64. package/tools/bin/agent-project-run-pi-session +0 -479
  65. package/tools/bin/agent-project-sync-anchor-repo +0 -139
  66. package/tools/bin/agent-project-worker-status +0 -188
  67. package/tools/bin/branch-verification-guard.sh +0 -364
  68. package/tools/bin/capture-worker.sh +0 -18
  69. package/tools/bin/cleanup-worktree.sh +0 -52
  70. package/tools/bin/codex-quota +0 -31
  71. package/tools/bin/create-follow-up-issue.sh +0 -114
  72. package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
  73. package/tools/bin/issue-publish-localization-guard.sh +0 -142
  74. package/tools/bin/issue-publish-scope-guard.sh +0 -242
  75. package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
  76. package/tools/bin/issue-resource-class.sh +0 -12
  77. package/tools/bin/kick-scheduler.sh +0 -75
  78. package/tools/bin/label-follow-up-issues.sh +0 -14
  79. package/tools/bin/new-pr-worktree.sh +0 -50
  80. package/tools/bin/new-worktree.sh +0 -49
  81. package/tools/bin/pr-risk.sh +0 -12
  82. package/tools/bin/prepare-worktree.sh +0 -142
  83. package/tools/bin/provider-cooldown-state.sh +0 -204
  84. package/tools/bin/publish-issue-worker.sh +0 -31
  85. package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
  86. package/tools/bin/reconcile-issue-worker.sh +0 -34
  87. package/tools/bin/reconcile-pr-worker.sh +0 -34
  88. package/tools/bin/record-verification.sh +0 -71
  89. package/tools/bin/render-flow-config.sh +0 -98
  90. package/tools/bin/resident-issue-controller-lib.sh +0 -448
  91. package/tools/bin/resident-issue-queue-status.py +0 -35
  92. package/tools/bin/retry-state.sh +0 -31
  93. package/tools/bin/reuse-issue-worktree.sh +0 -121
  94. package/tools/bin/run-codex-bypass.sh +0 -3
  95. package/tools/bin/run-codex-safe.sh +0 -3
  96. package/tools/bin/run-codex-task.sh +0 -280
  97. package/tools/bin/serve-dashboard.sh +0 -5
  98. package/tools/bin/split-retained-slice.sh +0 -124
  99. package/tools/bin/start-issue-worker.sh +0 -943
  100. package/tools/bin/start-pr-fix-worker.sh +0 -491
  101. package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
  102. package/tools/bin/start-pr-review-worker.sh +0 -261
  103. package/tools/bin/start-resident-issue-loop.sh +0 -499
  104. package/tools/bin/update-github-labels.sh +0 -14
  105. package/tools/bin/worker-status.sh +0 -19
  106. 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"