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.
- package/README.md +141 -28
- package/assets/workflow-catalog.json +1 -1
- package/bin/pr-risk.sh +22 -7
- package/bin/sync-pr-labels.sh +1 -1
- package/hooks/heartbeat-hooks.sh +125 -12
- package/hooks/issue-reconcile-hooks.sh +1 -1
- package/hooks/pr-reconcile-hooks.sh +1 -1
- package/npm/bin/agent-control-plane.js +257 -59
- package/package.json +39 -32
- package/tools/bin/debug-session.sh +106 -0
- package/tools/bin/flow-config-lib.sh +1203 -60
- package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
- package/tools/bin/flow-runtime-doctor.sh +5 -1
- package/tools/bin/flow-shell-lib.sh +32 -0
- package/tools/bin/github-core-rate-limit-state.sh +77 -0
- package/tools/bin/github-write-outbox.sh +470 -0
- package/tools/bin/heartbeat-loop-scheduling-lib.sh +7 -7
- package/tools/bin/heartbeat-safe-auto.sh +42 -0
- package/tools/bin/install-project-launchd.sh +17 -2
- package/tools/bin/install-project-systemd.sh +255 -0
- package/tools/bin/project-init.sh +21 -1
- package/tools/bin/project-launchd-bootstrap.sh +5 -1
- package/tools/bin/project-runtimectl.sh +91 -2
- package/tools/bin/project-systemd-bootstrap.sh +74 -0
- package/tools/bin/scaffold-profile.sh +61 -3
- package/tools/bin/uninstall-project-systemd.sh +87 -0
- package/tools/dashboard/app.js +228 -6
- package/tools/dashboard/dashboard_snapshot.py +55 -0
- package/tools/dashboard/issue_queue_state.py +101 -0
- package/tools/dashboard/server.py +123 -1
- package/tools/dashboard/styles.css +526 -455
- package/tools/templates/pr-fix-template.md +3 -1
- package/tools/templates/pr-merge-repair-template.md +2 -1
- package/references/architecture.md +0 -217
- package/references/commands.md +0 -128
- package/references/control-plane-map.md +0 -124
- package/references/docs-map.md +0 -73
- package/references/release-checklist.md +0 -65
- package/references/repo-map.md +0 -36
- package/tools/bin/agent-cleanup-worktree +0 -247
- package/tools/bin/agent-github-update-labels +0 -71
- package/tools/bin/agent-init-worktree +0 -216
- package/tools/bin/agent-project-archive-run +0 -52
- package/tools/bin/agent-project-capture-worker +0 -46
- package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
- package/tools/bin/agent-project-catch-up-merged-prs +0 -194
- package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
- package/tools/bin/agent-project-cleanup-session +0 -513
- package/tools/bin/agent-project-detached-launch +0 -127
- package/tools/bin/agent-project-heartbeat-loop +0 -1029
- package/tools/bin/agent-project-open-issue-worktree +0 -89
- package/tools/bin/agent-project-open-pr-worktree +0 -80
- package/tools/bin/agent-project-publish-issue-pr +0 -465
- package/tools/bin/agent-project-reconcile-issue-session +0 -1398
- package/tools/bin/agent-project-reconcile-pr-session +0 -1230
- package/tools/bin/agent-project-retry-state +0 -147
- package/tools/bin/agent-project-run-claude-session +0 -805
- package/tools/bin/agent-project-run-codex-resilient +0 -955
- package/tools/bin/agent-project-run-codex-session +0 -435
- package/tools/bin/agent-project-run-kilo-session +0 -369
- package/tools/bin/agent-project-run-ollama-session +0 -658
- package/tools/bin/agent-project-run-openclaw-session +0 -1309
- package/tools/bin/agent-project-run-opencode-session +0 -377
- package/tools/bin/agent-project-run-pi-session +0 -479
- package/tools/bin/agent-project-sync-anchor-repo +0 -139
- package/tools/bin/agent-project-worker-status +0 -188
- package/tools/bin/branch-verification-guard.sh +0 -364
- package/tools/bin/capture-worker.sh +0 -18
- package/tools/bin/cleanup-worktree.sh +0 -52
- package/tools/bin/codex-quota +0 -31
- package/tools/bin/create-follow-up-issue.sh +0 -114
- package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
- package/tools/bin/issue-publish-localization-guard.sh +0 -142
- package/tools/bin/issue-publish-scope-guard.sh +0 -242
- package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
- package/tools/bin/issue-resource-class.sh +0 -12
- package/tools/bin/kick-scheduler.sh +0 -75
- package/tools/bin/label-follow-up-issues.sh +0 -14
- package/tools/bin/new-pr-worktree.sh +0 -50
- package/tools/bin/new-worktree.sh +0 -49
- package/tools/bin/pr-risk.sh +0 -12
- package/tools/bin/prepare-worktree.sh +0 -142
- package/tools/bin/provider-cooldown-state.sh +0 -204
- package/tools/bin/publish-issue-worker.sh +0 -31
- package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
- package/tools/bin/reconcile-issue-worker.sh +0 -34
- package/tools/bin/reconcile-pr-worker.sh +0 -34
- package/tools/bin/record-verification.sh +0 -71
- package/tools/bin/render-flow-config.sh +0 -98
- package/tools/bin/resident-issue-controller-lib.sh +0 -448
- package/tools/bin/resident-issue-queue-status.py +0 -35
- package/tools/bin/retry-state.sh +0 -31
- package/tools/bin/reuse-issue-worktree.sh +0 -121
- package/tools/bin/run-codex-bypass.sh +0 -3
- package/tools/bin/run-codex-safe.sh +0 -3
- package/tools/bin/run-codex-task.sh +0 -280
- package/tools/bin/serve-dashboard.sh +0 -5
- package/tools/bin/split-retained-slice.sh +0 -124
- package/tools/bin/start-issue-worker.sh +0 -943
- package/tools/bin/start-pr-fix-worker.sh +0 -491
- package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
- package/tools/bin/start-pr-review-worker.sh +0 -261
- package/tools/bin/start-resident-issue-loop.sh +0 -499
- package/tools/bin/update-github-labels.sh +0 -14
- package/tools/bin/worker-status.sh +0 -19
- 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"
|