agent-control-plane 0.2.0 → 0.4.9
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 +69 -19
- 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 +296 -61
- package/package.json +11 -7
- package/tools/bin/agent-github-update-labels +36 -2
- package/tools/bin/agent-project-catch-up-merged-prs +4 -2
- package/tools/bin/agent-project-cleanup-session +49 -5
- package/tools/bin/agent-project-heartbeat-loop +119 -1471
- package/tools/bin/agent-project-publish-issue-pr +6 -3
- package/tools/bin/agent-project-reconcile-issue-session +78 -106
- package/tools/bin/agent-project-reconcile-pr-session +166 -143
- package/tools/bin/agent-project-retry-state +18 -7
- package/tools/bin/agent-project-run-claude-session +10 -0
- package/tools/bin/agent-project-run-codex-resilient +99 -14
- package/tools/bin/agent-project-run-codex-session +16 -5
- package/tools/bin/agent-project-run-kilo-session +10 -0
- package/tools/bin/agent-project-run-openclaw-session +10 -0
- package/tools/bin/agent-project-run-opencode-session +10 -0
- package/tools/bin/agent-project-sync-source-repo-main +163 -0
- package/tools/bin/agent-project-worker-status +10 -7
- package/tools/bin/cleanup-worktree.sh +6 -1
- package/tools/bin/flow-config-lib.sh +1257 -34
- package/tools/bin/flow-resident-worker-lib.sh +119 -1
- package/tools/bin/flow-shell-lib.sh +56 -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-cache-lib.sh +164 -0
- package/tools/bin/heartbeat-loop-counting-lib.sh +306 -0
- package/tools/bin/heartbeat-loop-pr-strategy-lib.sh +199 -0
- package/tools/bin/heartbeat-loop-scheduling-lib.sh +506 -0
- package/tools/bin/heartbeat-loop-worker-lib.sh +319 -0
- package/tools/bin/heartbeat-recovery-preflight.sh +12 -1
- package/tools/bin/heartbeat-safe-auto.sh +56 -3
- package/tools/bin/install-project-launchd.sh +17 -2
- package/tools/bin/project-init.sh +21 -1
- package/tools/bin/project-launchd-bootstrap.sh +16 -9
- package/tools/bin/project-runtimectl.sh +46 -2
- package/tools/bin/reconcile-bootstrap-lib.sh +113 -0
- package/tools/bin/resident-issue-controller-lib.sh +448 -0
- package/tools/bin/scaffold-profile.sh +61 -3
- package/tools/bin/start-pr-fix-worker.sh +47 -10
- package/tools/bin/start-resident-issue-loop.sh +28 -439
- package/tools/dashboard/app.js +37 -1
- package/tools/dashboard/dashboard_snapshot.py +65 -26
- package/tools/templates/pr-fix-template.md +3 -1
- package/tools/templates/pr-merge-repair-template.md +2 -1
- package/SKILL.md +0 -149
- 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/split-retained-slice.sh +0 -124
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
# shellcheck source=/dev/null
|
|
6
|
+
source "${SCRIPT_DIR}/flow-shell-lib.sh"
|
|
7
|
+
|
|
4
8
|
usage() {
|
|
5
9
|
cat <<'EOF'
|
|
6
10
|
Usage:
|
|
7
|
-
agent-project-retry-state --state-root <path> --kind issue|pr|provider --item-id <id> --action get|schedule|clear [--reason <text>] [--cooldowns <csv>]
|
|
11
|
+
agent-project-retry-state --state-root <path> --kind issue|pr|provider|github --item-id <id> --action get|schedule|clear [--reason <text>] [--cooldowns <csv>] [--next-at-epoch <unix-seconds>]
|
|
8
12
|
|
|
9
13
|
Generic retry/cooldown state manager for project adapters.
|
|
10
14
|
|
|
@@ -12,6 +16,7 @@ Examples:
|
|
|
12
16
|
agent-project-retry-state --state-root /tmp/state --kind issue --item-id 123 --action get
|
|
13
17
|
agent-project-retry-state --state-root /tmp/state --kind pr --item-id 77 --action schedule --reason worker-exit-failed
|
|
14
18
|
agent-project-retry-state --state-root /tmp/state --kind provider --item-id openclaw-stepfun-free --action schedule --reason provider-quota-limit
|
|
19
|
+
agent-project-retry-state --state-root /tmp/state --kind github --item-id core-api --action schedule --reason github-api-rate-limit --next-at-epoch 4102444800
|
|
15
20
|
EOF
|
|
16
21
|
}
|
|
17
22
|
|
|
@@ -21,6 +26,7 @@ item_id=""
|
|
|
21
26
|
action=""
|
|
22
27
|
reason=""
|
|
23
28
|
cooldowns_csv="${AGENT_PROJECT_RETRY_COOLDOWNS:-300,900,1800,3600}"
|
|
29
|
+
next_at_epoch_override=""
|
|
24
30
|
|
|
25
31
|
while [[ $# -gt 0 ]]; do
|
|
26
32
|
case "$1" in
|
|
@@ -30,6 +36,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
30
36
|
--action) action="${2:-}"; shift 2 ;;
|
|
31
37
|
--reason) reason="${2:-}"; shift 2 ;;
|
|
32
38
|
--cooldowns) cooldowns_csv="${2:-}"; shift 2 ;;
|
|
39
|
+
--next-at-epoch) next_at_epoch_override="${2:-}"; shift 2 ;;
|
|
33
40
|
--help|-h) usage; exit 0 ;;
|
|
34
41
|
*) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
|
|
35
42
|
esac
|
|
@@ -41,8 +48,8 @@ if [[ -z "$state_root" || -z "$kind" || -z "$item_id" || -z "$action" ]]; then
|
|
|
41
48
|
fi
|
|
42
49
|
|
|
43
50
|
case "$kind" in
|
|
44
|
-
issue|pr|provider) ;;
|
|
45
|
-
*) echo "--kind must be issue, pr, or
|
|
51
|
+
issue|pr|provider|github) ;;
|
|
52
|
+
*) echo "--kind must be issue, pr, provider, or github" >&2; exit 1 ;;
|
|
46
53
|
esac
|
|
47
54
|
|
|
48
55
|
case "$action" in
|
|
@@ -98,7 +105,7 @@ write_state() {
|
|
|
98
105
|
|
|
99
106
|
new_updated_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
100
107
|
if [[ "$new_next_epoch" != "0" ]]; then
|
|
101
|
-
new_next_at="$(
|
|
108
|
+
new_next_at="$(flow_format_epoch_utc "$new_next_epoch")"
|
|
102
109
|
fi
|
|
103
110
|
|
|
104
111
|
cat >"$state_file" <<EOF
|
|
@@ -115,10 +122,14 @@ case "$action" in
|
|
|
115
122
|
;;
|
|
116
123
|
schedule)
|
|
117
124
|
attempts=$((attempts + 1))
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
if [[ "${next_at_epoch_override}" =~ ^[0-9]+$ ]] && (( next_at_epoch_override > now_epoch )); then
|
|
126
|
+
next_attempt_epoch="${next_at_epoch_override}"
|
|
127
|
+
else
|
|
128
|
+
cooldown_seconds="$(cooldown_seconds_for_attempt "$attempts")"
|
|
129
|
+
next_attempt_epoch=$((now_epoch + cooldown_seconds))
|
|
130
|
+
fi
|
|
120
131
|
write_state "$attempts" "$next_attempt_epoch" "$reason"
|
|
121
|
-
next_attempt_at="$(
|
|
132
|
+
next_attempt_at="$(flow_format_epoch_utc "$next_attempt_epoch")"
|
|
122
133
|
last_reason="$reason"
|
|
123
134
|
updated_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
124
135
|
;;
|
|
@@ -364,6 +364,16 @@ EOF
|
|
|
364
364
|
done
|
|
365
365
|
fi
|
|
366
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
|
+
|
|
367
377
|
reconcile_snippet=""
|
|
368
378
|
if [[ -n "$reconcile_command" ]]; then
|
|
369
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"
|
|
@@ -38,6 +38,23 @@ auth_refresh_poll_seconds="${ACP_CODEX_AUTH_REFRESH_POLL_SECONDS:-${F_LOSNING_CO
|
|
|
38
38
|
max_quota_autoswitch_attempts="${ACP_CODEX_MAX_AUTOSWITCH_ATTEMPTS:-${F_LOSNING_CODEX_MAX_AUTOSWITCH_ATTEMPTS:-1}}"
|
|
39
39
|
codex_progress_heartbeat_seconds="${ACP_CODEX_PROGRESS_HEARTBEAT_SECONDS:-${F_LOSNING_CODEX_PROGRESS_HEARTBEAT_SECONDS:-30}}"
|
|
40
40
|
codex_stall_seconds="${ACP_CODEX_STALL_SECONDS:-${F_LOSNING_CODEX_STALL_SECONDS:-300}}"
|
|
41
|
+
python_bin=""
|
|
42
|
+
|
|
43
|
+
resolve_python_bin() {
|
|
44
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
45
|
+
command -v python3
|
|
46
|
+
return 0
|
|
47
|
+
fi
|
|
48
|
+
if [[ -x /opt/homebrew/bin/python3 ]]; then
|
|
49
|
+
printf '%s\n' "/opt/homebrew/bin/python3"
|
|
50
|
+
return 0
|
|
51
|
+
fi
|
|
52
|
+
if command -v python >/dev/null 2>&1; then
|
|
53
|
+
command -v python
|
|
54
|
+
return 0
|
|
55
|
+
fi
|
|
56
|
+
return 1
|
|
57
|
+
}
|
|
41
58
|
|
|
42
59
|
while [[ $# -gt 0 ]]; do
|
|
43
60
|
case "$1" in
|
|
@@ -92,6 +109,12 @@ case "$codex_stall_seconds" in
|
|
|
92
109
|
''|*[!0-9]*) echo "ACP_CODEX_STALL_SECONDS must be numeric" >&2; exit 1 ;;
|
|
93
110
|
esac
|
|
94
111
|
|
|
112
|
+
python_bin="$(resolve_python_bin || true)"
|
|
113
|
+
if [[ -z "$python_bin" || ! -x "$python_bin" ]]; then
|
|
114
|
+
echo "unable to resolve a runnable python interpreter for codex supervision" >&2
|
|
115
|
+
exit 1
|
|
116
|
+
fi
|
|
117
|
+
|
|
95
118
|
FLOW_SKILL_DIR="$(resolve_flow_skill_dir "${BASH_SOURCE[0]}")"
|
|
96
119
|
state_file="${host_run_dir}/runner.env"
|
|
97
120
|
auth_file="${HOME}/.codex/auth.json"
|
|
@@ -113,6 +136,15 @@ config_yaml="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
|
|
|
113
136
|
issue_session_prefix="$(flow_resolve_issue_session_prefix "${config_yaml}")"
|
|
114
137
|
pr_session_prefix="$(flow_resolve_pr_session_prefix "${config_yaml}")"
|
|
115
138
|
|
|
139
|
+
# Keep npm-backed verification steps isolated from any broken user-global cache state.
|
|
140
|
+
npm_cache_dir="${NPM_CONFIG_CACHE:-${npm_config_cache:-}}"
|
|
141
|
+
if [[ -z "${npm_cache_dir}" ]]; then
|
|
142
|
+
npm_cache_dir="${ACP_NPM_CACHE_DIR:-${F_LOSNING_NPM_CACHE_DIR:-${HOME}/.agent-runtime/npm-cache}}"
|
|
143
|
+
fi
|
|
144
|
+
export NPM_CONFIG_CACHE="${npm_cache_dir}"
|
|
145
|
+
export npm_config_cache="${npm_cache_dir}"
|
|
146
|
+
mkdir -p "${npm_cache_dir}" 2>/dev/null || true
|
|
147
|
+
|
|
116
148
|
thread_id=""
|
|
117
149
|
attempt=0
|
|
118
150
|
resume_count=0
|
|
@@ -177,7 +209,7 @@ run_with_timeout() {
|
|
|
177
209
|
local timeout_seconds="${1:?timeout seconds required}"
|
|
178
210
|
shift
|
|
179
211
|
|
|
180
|
-
|
|
212
|
+
"$python_bin" - "$timeout_seconds" "$@" <<'PY'
|
|
181
213
|
import os
|
|
182
214
|
import signal
|
|
183
215
|
import subprocess
|
|
@@ -220,6 +252,60 @@ sys.exit(proc.returncode)
|
|
|
220
252
|
PY
|
|
221
253
|
}
|
|
222
254
|
|
|
255
|
+
stat_file_size() {
|
|
256
|
+
local path="${1:?path required}"
|
|
257
|
+
local value=""
|
|
258
|
+
|
|
259
|
+
value="$(stat -f %z "$path" 2>/dev/null || true)"
|
|
260
|
+
if [[ "$value" =~ ^[0-9]+$ ]]; then
|
|
261
|
+
printf '%s\n' "$value"
|
|
262
|
+
return 0
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
value="$(stat -c %s "$path" 2>/dev/null || true)"
|
|
266
|
+
if [[ "$value" =~ ^[0-9]+$ ]]; then
|
|
267
|
+
printf '%s\n' "$value"
|
|
268
|
+
return 0
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
"$python_bin" - "$path" <<'PY'
|
|
272
|
+
import os
|
|
273
|
+
import sys
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
print(os.path.getsize(sys.argv[1]))
|
|
277
|
+
except OSError:
|
|
278
|
+
print("0")
|
|
279
|
+
PY
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
stat_file_mtime() {
|
|
283
|
+
local path="${1:?path required}"
|
|
284
|
+
local value=""
|
|
285
|
+
|
|
286
|
+
value="$(stat -f %m "$path" 2>/dev/null || true)"
|
|
287
|
+
if [[ "$value" =~ ^[0-9]+$ ]]; then
|
|
288
|
+
printf '%s\n' "$value"
|
|
289
|
+
return 0
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
value="$(stat -c %Y "$path" 2>/dev/null || true)"
|
|
293
|
+
if [[ "$value" =~ ^[0-9]+$ ]]; then
|
|
294
|
+
printf '%s\n' "$value"
|
|
295
|
+
return 0
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
"$python_bin" - "$path" <<'PY'
|
|
299
|
+
import os
|
|
300
|
+
import sys
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
print(int(os.path.getmtime(sys.argv[1])))
|
|
304
|
+
except OSError:
|
|
305
|
+
print("0")
|
|
306
|
+
PY
|
|
307
|
+
}
|
|
308
|
+
|
|
223
309
|
auth_fingerprint() {
|
|
224
310
|
if [[ ! -f "$auth_file" ]]; then
|
|
225
311
|
printf 'missing\n'
|
|
@@ -227,8 +313,8 @@ auth_fingerprint() {
|
|
|
227
313
|
fi
|
|
228
314
|
|
|
229
315
|
local mtime size sha
|
|
230
|
-
mtime="$(
|
|
231
|
-
size="$(
|
|
316
|
+
mtime="$(stat_file_mtime "$auth_file" 2>/dev/null || printf '0')"
|
|
317
|
+
size="$(stat_file_size "$auth_file" 2>/dev/null || printf '0')"
|
|
232
318
|
sha="$(shasum -a 256 "$auth_file" | awk '{print $1}')"
|
|
233
319
|
printf '%s:%s:%s\n' "$mtime" "$size" "$sha"
|
|
234
320
|
}
|
|
@@ -256,8 +342,8 @@ quota_switch_signature() {
|
|
|
256
342
|
fi
|
|
257
343
|
|
|
258
344
|
local mtime size sha
|
|
259
|
-
mtime="$(
|
|
260
|
-
size="$(
|
|
345
|
+
mtime="$(stat_file_mtime "$quota_switch_state_file" 2>/dev/null || printf '0')"
|
|
346
|
+
size="$(stat_file_size "$quota_switch_state_file" 2>/dev/null || printf '0')"
|
|
261
347
|
sha="$(shasum -a 256 "$quota_switch_state_file" | awk '{print $1}')"
|
|
262
348
|
printf '%s:%s:%s\n' "$mtime" "$size" "$sha"
|
|
263
349
|
}
|
|
@@ -390,7 +476,7 @@ run_quota_autoswitch() {
|
|
|
390
476
|
new_output_since() {
|
|
391
477
|
local start_size="${1:?start size required}"
|
|
392
478
|
local file_size
|
|
393
|
-
file_size="$(
|
|
479
|
+
file_size="$(stat_file_size "$output_file" 2>/dev/null || printf '0')"
|
|
394
480
|
if (( file_size <= start_size )); then
|
|
395
481
|
return 0
|
|
396
482
|
fi
|
|
@@ -456,7 +542,7 @@ stream_codex_exec() {
|
|
|
456
542
|
local progress_file=""
|
|
457
543
|
local line=""
|
|
458
544
|
|
|
459
|
-
last_attempt_start_size="$(
|
|
545
|
+
last_attempt_start_size="$(stat_file_size "$output_file" 2>/dev/null || printf '0')"
|
|
460
546
|
last_attempt_started_epoch="$(date +%s)"
|
|
461
547
|
progress_file="${host_run_dir}/.codex-progress.$$"
|
|
462
548
|
rm -f "$progress_file"
|
|
@@ -514,7 +600,7 @@ stream_codex_exec() {
|
|
|
514
600
|
break
|
|
515
601
|
fi
|
|
516
602
|
else
|
|
517
|
-
last_progress_epoch="$(
|
|
603
|
+
last_progress_epoch="$(stat_file_mtime "$progress_file" 2>/dev/null || printf '0')"
|
|
518
604
|
if [[ -n "$last_progress_epoch" && "$last_progress_epoch" != "0" ]]; then
|
|
519
605
|
idle_for=$((now - last_progress_epoch))
|
|
520
606
|
if (( idle_for >= codex_stall_seconds )); then
|
|
@@ -546,17 +632,16 @@ stream_codex_exec() {
|
|
|
546
632
|
rm -f "$stream_fifo"
|
|
547
633
|
rm -f "$progress_file"
|
|
548
634
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
fi
|
|
635
|
+
set +e
|
|
636
|
+
wait "$producer_pid" 2>/dev/null
|
|
637
|
+
last_exit_code="$?"
|
|
638
|
+
set -e
|
|
554
639
|
|
|
555
640
|
update_thread_id_from_output "$last_attempt_start_size"
|
|
556
641
|
}
|
|
557
642
|
|
|
558
643
|
extract_thread_id() {
|
|
559
|
-
|
|
644
|
+
"$python_bin" -c '
|
|
560
645
|
import json
|
|
561
646
|
import sys
|
|
562
647
|
|
|
@@ -39,10 +39,16 @@ declare -a context_items=()
|
|
|
39
39
|
declare -a collect_files=()
|
|
40
40
|
|
|
41
41
|
resolve_codex_bin() {
|
|
42
|
-
local configured_bin="${CODEX_BIN:-}"
|
|
42
|
+
local configured_bin="${CODEX_BIN:-${ACP_CODEX_BIN:-${F_LOSNING_CODEX_BIN:-}}}"
|
|
43
43
|
local best_version=""
|
|
44
44
|
local best_bin=""
|
|
45
45
|
local candidate version_line version
|
|
46
|
+
local -a fallback_paths=(
|
|
47
|
+
"${HOME}/.local/bin/codex"
|
|
48
|
+
"${HOME}/.codex/local/bin/codex"
|
|
49
|
+
"/usr/local/bin/codex"
|
|
50
|
+
"/opt/homebrew/bin/codex"
|
|
51
|
+
)
|
|
46
52
|
|
|
47
53
|
if [[ -n "$configured_bin" && -x "$configured_bin" ]]; then
|
|
48
54
|
printf '%s\n' "$configured_bin"
|
|
@@ -50,11 +56,16 @@ resolve_codex_bin() {
|
|
|
50
56
|
fi
|
|
51
57
|
|
|
52
58
|
if command -v codex >/dev/null 2>&1; then
|
|
53
|
-
|
|
59
|
+
command -v codex
|
|
60
|
+
return 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
for candidate in "${fallback_paths[@]}"; do
|
|
54
64
|
if [[ -x "$candidate" ]]; then
|
|
55
|
-
|
|
65
|
+
printf '%s\n' "$candidate"
|
|
66
|
+
return 0
|
|
56
67
|
fi
|
|
57
|
-
|
|
68
|
+
done
|
|
58
69
|
|
|
59
70
|
if [[ -d "${HOME:-}/.nvm/versions/node" ]]; then
|
|
60
71
|
while IFS= read -r candidate; do
|
|
@@ -347,7 +358,7 @@ find_logged_artifact_path() {
|
|
|
347
358
|
if [[ "\$(basename "\${candidate}")" == "\${artifact_name}" && -f "\${candidate}" ]]; then
|
|
348
359
|
printf '%s\n' "\${candidate}"
|
|
349
360
|
fi
|
|
350
|
-
done < <(grep -oE '/
|
|
361
|
+
done < <(grep -oE '/[^[:space:])"]+' ${output_q} 2>/dev/null || true)
|
|
351
362
|
}
|
|
352
363
|
recover_logged_artifact() {
|
|
353
364
|
local artifact_name="\${1:?artifact name required}"
|
|
@@ -233,6 +233,16 @@ EOF
|
|
|
233
233
|
done
|
|
234
234
|
fi
|
|
235
235
|
|
|
236
|
+
# Always collect result.env from sandbox to artifact_dir
|
|
237
|
+
collect_copy_snippet+=$(
|
|
238
|
+
cat <<EOF
|
|
239
|
+
if [[ -f ${sandbox_run_dir_q}/result.env ]]; then
|
|
240
|
+
cp ${sandbox_run_dir_q}/result.env ${artifact_dir_q}/result.env
|
|
241
|
+
fi
|
|
242
|
+
EOF
|
|
243
|
+
)
|
|
244
|
+
collect_copy_snippet+=$'\n'
|
|
245
|
+
|
|
236
246
|
reconcile_snippet=""
|
|
237
247
|
if [[ -n "$reconcile_command" ]]; then
|
|
238
248
|
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"
|
|
@@ -283,6 +283,16 @@ EOF
|
|
|
283
283
|
done
|
|
284
284
|
fi
|
|
285
285
|
|
|
286
|
+
# Always collect result.env from sandbox to artifact_dir
|
|
287
|
+
collect_copy_snippet+=$(
|
|
288
|
+
cat <<EOF
|
|
289
|
+
if [[ -f ${sandbox_run_dir_q}/result.env ]]; then
|
|
290
|
+
cp ${sandbox_run_dir_q}/result.env ${artifact_dir_q}/result.env
|
|
291
|
+
fi
|
|
292
|
+
EOF
|
|
293
|
+
)
|
|
294
|
+
collect_copy_snippet+=$'\n'
|
|
295
|
+
|
|
286
296
|
reconcile_snippet=""
|
|
287
297
|
if [[ -n "$reconcile_command" ]]; then
|
|
288
298
|
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"
|
|
@@ -240,6 +240,16 @@ EOF
|
|
|
240
240
|
done
|
|
241
241
|
fi
|
|
242
242
|
|
|
243
|
+
# Always collect result.env from sandbox to artifact_dir
|
|
244
|
+
collect_copy_snippet+=$(
|
|
245
|
+
cat <<EOF
|
|
246
|
+
if [[ -f ${sandbox_run_dir_q}/result.env ]]; then
|
|
247
|
+
cp ${sandbox_run_dir_q}/result.env ${artifact_dir_q}/result.env
|
|
248
|
+
fi
|
|
249
|
+
EOF
|
|
250
|
+
)
|
|
251
|
+
collect_copy_snippet+=$'\n'
|
|
252
|
+
|
|
243
253
|
reconcile_snippet=""
|
|
244
254
|
if [[ -n "$reconcile_command" ]]; then
|
|
245
255
|
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"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
# shellcheck source=/dev/null
|
|
6
|
+
source "${SCRIPT_DIR}/flow-config-lib.sh"
|
|
7
|
+
|
|
8
|
+
CONFIG_YAML="${ACP_SOURCE_REPO_SYNC_CONFIG_YAML:-$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")}"
|
|
9
|
+
REPO_SLUG="$(flow_resolve_repo_slug "${CONFIG_YAML}")"
|
|
10
|
+
SOURCE_REPO_ROOT="$(flow_resolve_source_repo_root "${CONFIG_YAML}")"
|
|
11
|
+
DEFAULT_BRANCH="$(flow_resolve_default_branch "${CONFIG_YAML}")"
|
|
12
|
+
STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
|
|
13
|
+
SYNC_STATE_FILE="${STATE_ROOT}/source-repo-main-sync.env"
|
|
14
|
+
FORGE_PROVIDER="$(flow_forge_provider)"
|
|
15
|
+
REMOTE_OVERRIDE="${ACP_SOURCE_SYNC_REMOTE:-${F_LOSNING_SOURCE_SYNC_REMOTE:-}}"
|
|
16
|
+
|
|
17
|
+
write_state() {
|
|
18
|
+
local status="${1:-}"
|
|
19
|
+
local remote_name="${2:-}"
|
|
20
|
+
local remote_sha="${3:-}"
|
|
21
|
+
local local_sha="${4:-}"
|
|
22
|
+
local detail="${5:-}"
|
|
23
|
+
|
|
24
|
+
mkdir -p "$(dirname "${SYNC_STATE_FILE}")"
|
|
25
|
+
{
|
|
26
|
+
printf 'STATUS=%s\n' "${status}"
|
|
27
|
+
printf 'UPDATED_AT=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
28
|
+
printf 'SOURCE_REPO_ROOT=%s\n' "${SOURCE_REPO_ROOT}"
|
|
29
|
+
printf 'DEFAULT_BRANCH=%s\n' "${DEFAULT_BRANCH}"
|
|
30
|
+
printf 'REMOTE_NAME=%s\n' "${remote_name}"
|
|
31
|
+
printf 'REMOTE_SHA=%s\n' "${remote_sha}"
|
|
32
|
+
printf 'LOCAL_SHA=%s\n' "${local_sha}"
|
|
33
|
+
printf 'DETAIL=%s\n' "${detail}"
|
|
34
|
+
} >"${SYNC_STATE_FILE}"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
git_ref_sha() {
|
|
38
|
+
local repo_root="${1:?repo root required}"
|
|
39
|
+
local ref_name="${2:?ref required}"
|
|
40
|
+
git -C "${repo_root}" rev-parse --verify --quiet "${ref_name}" 2>/dev/null || true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
git_has_remote() {
|
|
44
|
+
local repo_root="${1:?repo root required}"
|
|
45
|
+
local remote_name="${2:?remote required}"
|
|
46
|
+
git -C "${repo_root}" remote get-url "${remote_name}" >/dev/null 2>&1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
discover_remote_name() {
|
|
50
|
+
local remote_name=""
|
|
51
|
+
|
|
52
|
+
if [[ -n "${REMOTE_OVERRIDE}" ]] && git_has_remote "${SOURCE_REPO_ROOT}" "${REMOTE_OVERRIDE}"; then
|
|
53
|
+
printf '%s\n' "${REMOTE_OVERRIDE}"
|
|
54
|
+
return 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
case "${FORGE_PROVIDER}" in
|
|
58
|
+
gitea)
|
|
59
|
+
if git_has_remote "${SOURCE_REPO_ROOT}" "gitea"; then
|
|
60
|
+
printf 'gitea\n'
|
|
61
|
+
return 0
|
|
62
|
+
fi
|
|
63
|
+
;;
|
|
64
|
+
github)
|
|
65
|
+
if git_has_remote "${SOURCE_REPO_ROOT}" "origin"; then
|
|
66
|
+
printf 'origin\n'
|
|
67
|
+
return 0
|
|
68
|
+
fi
|
|
69
|
+
;;
|
|
70
|
+
esac
|
|
71
|
+
|
|
72
|
+
while IFS= read -r remote_name; do
|
|
73
|
+
[[ -n "${remote_name}" ]] || continue
|
|
74
|
+
if [[ "$(flow_git_remote_repo_slug "${SOURCE_REPO_ROOT}" "${remote_name}" 2>/dev/null || true)" == "${REPO_SLUG}" ]]; then
|
|
75
|
+
printf '%s\n' "${remote_name}"
|
|
76
|
+
return 0
|
|
77
|
+
fi
|
|
78
|
+
done < <(git -C "${SOURCE_REPO_ROOT}" remote)
|
|
79
|
+
|
|
80
|
+
return 1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if [[ -z "${SOURCE_REPO_ROOT}" ]]; then
|
|
84
|
+
write_state "skipped" "" "" "" "source-repo-root-unset"
|
|
85
|
+
printf 'SOURCE_REPO_SYNC_STATUS=skipped\nSOURCE_REPO_SYNC_REASON=source-repo-root-unset\n'
|
|
86
|
+
exit 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
if [[ ! -d "${SOURCE_REPO_ROOT}/.git" && ! -f "${SOURCE_REPO_ROOT}/.git" ]]; then
|
|
90
|
+
write_state "skipped" "" "" "" "source-repo-not-git"
|
|
91
|
+
printf 'SOURCE_REPO_SYNC_STATUS=skipped\nSOURCE_REPO_SYNC_REASON=source-repo-not-git\nSOURCE_REPO_ROOT=%s\n' "${SOURCE_REPO_ROOT}"
|
|
92
|
+
exit 0
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
REMOTE_NAME="$(discover_remote_name || true)"
|
|
96
|
+
if [[ -z "${REMOTE_NAME}" ]]; then
|
|
97
|
+
write_state "skipped" "" "" "" "remote-not-found"
|
|
98
|
+
printf 'SOURCE_REPO_SYNC_STATUS=skipped\nSOURCE_REPO_SYNC_REASON=remote-not-found\nSOURCE_REPO_ROOT=%s\n' "${SOURCE_REPO_ROOT}"
|
|
99
|
+
exit 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
if ! git -C "${SOURCE_REPO_ROOT}" fetch "${REMOTE_NAME}" "+refs/heads/${DEFAULT_BRANCH}:refs/remotes/${REMOTE_NAME}/${DEFAULT_BRANCH}" --prune >/dev/null 2>&1; then
|
|
103
|
+
write_state "failed" "${REMOTE_NAME}" "" "" "fetch-failed"
|
|
104
|
+
printf 'SOURCE_REPO_SYNC_STATUS=failed\nSOURCE_REPO_SYNC_REASON=fetch-failed\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\n' "${SOURCE_REPO_ROOT}" "${REMOTE_NAME}"
|
|
105
|
+
exit 1
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
remote_sha="$(git_ref_sha "${SOURCE_REPO_ROOT}" "refs/remotes/${REMOTE_NAME}/${DEFAULT_BRANCH}")"
|
|
109
|
+
local_branch_sha="$(git_ref_sha "${SOURCE_REPO_ROOT}" "refs/heads/${DEFAULT_BRANCH}")"
|
|
110
|
+
current_branch="$(git -C "${SOURCE_REPO_ROOT}" symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
|
|
111
|
+
|
|
112
|
+
if [[ -z "${remote_sha}" || -z "${local_branch_sha}" ]]; then
|
|
113
|
+
write_state "failed" "${REMOTE_NAME}" "${remote_sha}" "${local_branch_sha}" "missing-branch-ref"
|
|
114
|
+
printf 'SOURCE_REPO_SYNC_STATUS=failed\nSOURCE_REPO_SYNC_REASON=missing-branch-ref\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\n' "${SOURCE_REPO_ROOT}" "${REMOTE_NAME}"
|
|
115
|
+
exit 1
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
if [[ "${remote_sha}" == "${local_branch_sha}" ]]; then
|
|
119
|
+
write_state "unchanged" "${REMOTE_NAME}" "${remote_sha}" "${local_branch_sha}" "already-current"
|
|
120
|
+
printf 'SOURCE_REPO_SYNC_STATUS=unchanged\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\nSOURCE_REPO_SYNC_SHA=%s\n' "${SOURCE_REPO_ROOT}" "${REMOTE_NAME}" "${remote_sha}"
|
|
121
|
+
exit 0
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
if [[ "${current_branch}" == "${DEFAULT_BRANCH}" ]]; then
|
|
125
|
+
if [[ -n "$(git -C "${SOURCE_REPO_ROOT}" status --porcelain 2>/dev/null || true)" ]]; then
|
|
126
|
+
write_state "blocked" "${REMOTE_NAME}" "${remote_sha}" "${local_branch_sha}" "working-tree-dirty"
|
|
127
|
+
printf 'SOURCE_REPO_SYNC_STATUS=blocked\nSOURCE_REPO_SYNC_REASON=working-tree-dirty\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\nLOCAL_SHA=%s\nREMOTE_SHA=%s\n' \
|
|
128
|
+
"${SOURCE_REPO_ROOT}" "${REMOTE_NAME}" "${local_branch_sha}" "${remote_sha}"
|
|
129
|
+
exit 0
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
if git -C "${SOURCE_REPO_ROOT}" merge-base --is-ancestor "${local_branch_sha}" "${remote_sha}" >/dev/null 2>&1; then
|
|
133
|
+
git -C "${SOURCE_REPO_ROOT}" merge --ff-only "refs/remotes/${REMOTE_NAME}/${DEFAULT_BRANCH}" >/dev/null
|
|
134
|
+
updated_sha="$(git_ref_sha "${SOURCE_REPO_ROOT}" "refs/heads/${DEFAULT_BRANCH}")"
|
|
135
|
+
write_state "updated" "${REMOTE_NAME}" "${remote_sha}" "${updated_sha}" "fast-forward-checked-out-branch"
|
|
136
|
+
printf 'SOURCE_REPO_SYNC_STATUS=updated\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\nSOURCE_REPO_SYNC_SHA=%s\n' "${SOURCE_REPO_ROOT}" "${REMOTE_NAME}" "${updated_sha}"
|
|
137
|
+
exit 0
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
if git -C "${SOURCE_REPO_ROOT}" merge --no-edit "refs/remotes/${REMOTE_NAME}/${DEFAULT_BRANCH}" >/dev/null 2>&1; then
|
|
141
|
+
updated_sha="$(git_ref_sha "${SOURCE_REPO_ROOT}" "refs/heads/${DEFAULT_BRANCH}")"
|
|
142
|
+
write_state "updated" "${REMOTE_NAME}" "${remote_sha}" "${updated_sha}" "merge-checked-out-branch"
|
|
143
|
+
printf 'SOURCE_REPO_SYNC_STATUS=updated\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\nSOURCE_REPO_SYNC_SHA=%s\n' "${SOURCE_REPO_ROOT}" "${REMOTE_NAME}" "${updated_sha}"
|
|
144
|
+
exit 0
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
git -C "${SOURCE_REPO_ROOT}" merge --abort >/dev/null 2>&1 || true
|
|
148
|
+
write_state "blocked" "${REMOTE_NAME}" "${remote_sha}" "${local_branch_sha}" "merge-conflict"
|
|
149
|
+
printf 'SOURCE_REPO_SYNC_STATUS=blocked\nSOURCE_REPO_SYNC_REASON=merge-conflict\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\nLOCAL_SHA=%s\nREMOTE_SHA=%s\n' \
|
|
150
|
+
"${SOURCE_REPO_ROOT}" "${REMOTE_NAME}" "${local_branch_sha}" "${remote_sha}"
|
|
151
|
+
exit 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
if ! git -C "${SOURCE_REPO_ROOT}" merge-base --is-ancestor "${local_branch_sha}" "${remote_sha}" >/dev/null 2>&1; then
|
|
155
|
+
write_state "blocked" "${REMOTE_NAME}" "${remote_sha}" "${local_branch_sha}" "local-main-diverged"
|
|
156
|
+
printf 'SOURCE_REPO_SYNC_STATUS=blocked\nSOURCE_REPO_SYNC_REASON=local-main-diverged\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\nLOCAL_SHA=%s\nREMOTE_SHA=%s\n' \
|
|
157
|
+
"${SOURCE_REPO_ROOT}" "${REMOTE_NAME}" "${local_branch_sha}" "${remote_sha}"
|
|
158
|
+
exit 0
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
git -C "${SOURCE_REPO_ROOT}" update-ref "refs/heads/${DEFAULT_BRANCH}" "${remote_sha}" "${local_branch_sha}"
|
|
162
|
+
write_state "updated" "${REMOTE_NAME}" "${remote_sha}" "${remote_sha}" "fast-forward-local-ref"
|
|
163
|
+
printf 'SOURCE_REPO_SYNC_STATUS=updated\nSOURCE_REPO_ROOT=%s\nREMOTE_NAME=%s\nSOURCE_REPO_SYNC_SHA=%s\n' "${SOURCE_REPO_ROOT}" "${REMOTE_NAME}" "${remote_sha}"
|
|
@@ -117,13 +117,6 @@ if [[ "$status" == "UNKNOWN" && -f "$output_file" ]]; then
|
|
|
117
117
|
fi
|
|
118
118
|
fi
|
|
119
119
|
|
|
120
|
-
if [[ "$status" == "UNKNOWN" && -z "$failure_reason" ]]; then
|
|
121
|
-
failure_reason="$(failure_reason_from_output || true)"
|
|
122
|
-
if [[ -n "$failure_reason" ]]; then
|
|
123
|
-
status="FAILED"
|
|
124
|
-
fi
|
|
125
|
-
fi
|
|
126
|
-
|
|
127
120
|
if [[ "$status" == "UNKNOWN" && -n "$runner_state" ]]; then
|
|
128
121
|
case "$runner_state" in
|
|
129
122
|
running|waiting-auth-refresh|switching-account)
|
|
@@ -133,6 +126,7 @@ if [[ "$status" == "UNKNOWN" && -n "$runner_state" ]]; then
|
|
|
133
126
|
# Check BEFORE stale result.env to avoid false SUCCEEDED when a prior
|
|
134
127
|
# cycle's result.env happens to exist.
|
|
135
128
|
status="FAILED"
|
|
129
|
+
failure_reason="$(failure_reason_from_output || true)"
|
|
136
130
|
if [[ -z "$failure_reason" ]]; then
|
|
137
131
|
failure_reason="runner-aborted-before-completion"
|
|
138
132
|
fi
|
|
@@ -146,10 +140,19 @@ fi
|
|
|
146
140
|
if [[ "$status" == "UNKNOWN" && -f "$result_file" ]]; then
|
|
147
141
|
# A worker that managed to persist result.env already completed its contract,
|
|
148
142
|
# even if the tmux session disappeared before the exit marker was flushed.
|
|
143
|
+
# Check BEFORE failure_reason_from_output so that a completed result.env
|
|
144
|
+
# is not overridden by transient failure text in the log.
|
|
149
145
|
status="SUCCEEDED"
|
|
150
146
|
result_only_completion="yes"
|
|
151
147
|
fi
|
|
152
148
|
|
|
149
|
+
if [[ "$status" == "UNKNOWN" && -z "$failure_reason" ]]; then
|
|
150
|
+
failure_reason="$(failure_reason_from_output || true)"
|
|
151
|
+
if [[ -n "$failure_reason" ]]; then
|
|
152
|
+
status="FAILED"
|
|
153
|
+
fi
|
|
154
|
+
fi
|
|
155
|
+
|
|
153
156
|
if [[ "$status" == "UNKNOWN" && -f "$output_file" ]]; then
|
|
154
157
|
if rg -qi "You've hit your usage limit|You have reached your Codex usage limits|visit https://chatgpt.com/codex/settings/usage|Upgrade to Pro|rate limit exceeded|quota exceeded|usage cap (reached|exceeded)|usage quota (reached|exceeded)" "$output_file"; then
|
|
155
158
|
status="FAILED"
|
|
@@ -37,11 +37,16 @@ if [[ -n "$SESSION" ]]; then
|
|
|
37
37
|
ARGS+=(--session "$SESSION")
|
|
38
38
|
fi
|
|
39
39
|
|
|
40
|
+
cleanup_exit=0
|
|
40
41
|
AGENT_PROJECT_WORKTREE_ROOT="$WORKTREE_ROOT" \
|
|
41
42
|
F_LOSNING_WORKTREE_ROOT="$WORKTREE_ROOT" \
|
|
42
|
-
bash "${FLOW_TOOLS_DIR}/agent-project-cleanup-session" "${ARGS[@]}" >/dev/null
|
|
43
|
+
bash "${FLOW_TOOLS_DIR}/agent-project-cleanup-session" "${ARGS[@]}" >/dev/null || cleanup_exit=$?
|
|
43
44
|
|
|
44
45
|
F_LOSNING_AGENT_REPO_ROOT="$AGENT_REPO_ROOT" \
|
|
45
46
|
F_LOSNING_RETAINED_REPO_ROOT="$RETAINED_REPO_ROOT" \
|
|
46
47
|
F_LOSNING_VSCODE_WORKSPACE_FILE="$VSCODE_WORKSPACE_FILE" \
|
|
47
48
|
"${FLOW_TOOLS_DIR}/sync-vscode-workspace.sh" >/dev/null 2>&1 || true
|
|
49
|
+
|
|
50
|
+
if [[ "$cleanup_exit" -ne 0 ]]; then
|
|
51
|
+
exit "$cleanup_exit"
|
|
52
|
+
fi
|