agent-control-plane 0.2.0 → 0.3.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 (33) hide show
  1. package/npm/bin/agent-control-plane.js +39 -2
  2. package/package.json +6 -3
  3. package/tools/bin/agent-project-catch-up-merged-prs +1 -0
  4. package/tools/bin/agent-project-cleanup-session +49 -5
  5. package/tools/bin/agent-project-heartbeat-loop +119 -1471
  6. package/tools/bin/agent-project-reconcile-issue-session +66 -105
  7. package/tools/bin/agent-project-reconcile-pr-session +76 -111
  8. package/tools/bin/agent-project-run-claude-session +10 -0
  9. package/tools/bin/agent-project-run-codex-resilient +86 -9
  10. package/tools/bin/agent-project-run-codex-session +16 -5
  11. package/tools/bin/agent-project-run-kilo-session +10 -0
  12. package/tools/bin/agent-project-run-openclaw-session +10 -0
  13. package/tools/bin/agent-project-run-opencode-session +10 -0
  14. package/tools/bin/agent-project-worker-status +10 -7
  15. package/tools/bin/cleanup-worktree.sh +6 -1
  16. package/tools/bin/flow-config-lib.sh +80 -0
  17. package/tools/bin/flow-resident-worker-lib.sh +119 -1
  18. package/tools/bin/flow-shell-lib.sh +24 -0
  19. package/tools/bin/heartbeat-loop-cache-lib.sh +164 -0
  20. package/tools/bin/heartbeat-loop-counting-lib.sh +306 -0
  21. package/tools/bin/heartbeat-loop-pr-strategy-lib.sh +199 -0
  22. package/tools/bin/heartbeat-loop-scheduling-lib.sh +506 -0
  23. package/tools/bin/heartbeat-loop-worker-lib.sh +319 -0
  24. package/tools/bin/heartbeat-recovery-preflight.sh +12 -1
  25. package/tools/bin/heartbeat-safe-auto.sh +14 -3
  26. package/tools/bin/project-launchd-bootstrap.sh +11 -8
  27. package/tools/bin/reconcile-bootstrap-lib.sh +113 -0
  28. package/tools/bin/resident-issue-controller-lib.sh +448 -0
  29. package/tools/bin/resident-issue-queue-status.py +35 -0
  30. package/tools/bin/start-resident-issue-loop.sh +26 -437
  31. package/tools/dashboard/app.js +7 -0
  32. package/tools/dashboard/dashboard_snapshot.py +13 -29
  33. package/SKILL.md +0 -149
@@ -2,82 +2,33 @@
2
2
  set -euo pipefail
3
3
 
4
4
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
-
6
- bootstrap_flow_shell_lib() {
7
- local candidate=""
8
- local skill_name=""
9
-
10
- for candidate in \
11
- "${SCRIPT_DIR}/flow-shell-lib.sh" \
12
- "${AGENT_CONTROL_PLANE_ROOT:-}/tools/bin/flow-shell-lib.sh" \
13
- "${ACP_ROOT:-}/tools/bin/flow-shell-lib.sh" \
14
- "${F_LOSNING_FLOW_ROOT:-}/tools/bin/flow-shell-lib.sh" \
15
- "${AGENT_FLOW_SKILL_ROOT:-}/tools/bin/flow-shell-lib.sh" \
16
- "${SHARED_AGENT_HOME:-}/tools/bin/flow-shell-lib.sh" \
17
- "$(pwd)/tools/bin/flow-shell-lib.sh"; do
18
- if [[ -n "${candidate}" && -f "${candidate}" ]]; then
19
- printf '%s\n' "${candidate}"
20
- return 0
21
- fi
22
- done
23
-
24
- if [[ -n "${SHARED_AGENT_HOME:-}" ]]; then
25
- for skill_name in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
26
- [[ -n "${skill_name}" ]] || continue
27
- candidate="${SHARED_AGENT_HOME}/skills/openclaw/${skill_name}/tools/bin/flow-shell-lib.sh"
28
- if [[ -f "${candidate}" ]]; then
29
- printf '%s\n' "${candidate}"
30
- return 0
31
- fi
32
- done
5
+ RECONCILE_BOOTSTRAP_LIB=""
6
+ for _rbl_candidate in \
7
+ "${SCRIPT_DIR}/reconcile-bootstrap-lib.sh" \
8
+ "${AGENT_CONTROL_PLANE_ROOT:-}/tools/bin/reconcile-bootstrap-lib.sh" \
9
+ "${ACP_ROOT:-}/tools/bin/reconcile-bootstrap-lib.sh" \
10
+ "${SHARED_AGENT_HOME:-}/tools/bin/reconcile-bootstrap-lib.sh"; do
11
+ if [[ -n "${_rbl_candidate}" && -f "${_rbl_candidate}" ]]; then
12
+ RECONCILE_BOOTSTRAP_LIB="${_rbl_candidate}"
13
+ break
33
14
  fi
34
-
35
- echo "unable to locate flow-shell-lib.sh for reconcile bootstrap" >&2
36
- return 1
37
- }
38
-
39
- FLOW_SHELL_LIB_PATH="$(bootstrap_flow_shell_lib)"
40
- BOOTSTRAP_TOOLS_DIR="$(cd "$(dirname "${FLOW_SHELL_LIB_PATH}")" && pwd)"
41
- # shellcheck source=/dev/null
42
- source "${FLOW_SHELL_LIB_PATH}"
43
-
44
- resolve_reconcile_tools_dir() {
45
- local candidate_root=""
46
- local skill_name=""
47
-
48
- for candidate_root in \
49
- "${AGENT_CONTROL_PLANE_ROOT:-}" \
50
- "${ACP_ROOT:-}" \
51
- "${F_LOSNING_FLOW_ROOT:-}" \
52
- "${AGENT_FLOW_SKILL_ROOT:-}"; do
53
- if [[ -n "${candidate_root}" && -d "${candidate_root}/tools/bin" ]]; then
54
- printf '%s/tools/bin\n' "${candidate_root}"
55
- return 0
15
+ done
16
+ if [[ -n "${SHARED_AGENT_HOME:-}" && -z "${RECONCILE_BOOTSTRAP_LIB}" ]]; then
17
+ for _rbl_skill in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
18
+ [[ -n "${_rbl_skill}" ]] || continue
19
+ _rbl_candidate="${SHARED_AGENT_HOME}/skills/openclaw/${_rbl_skill}/tools/bin/reconcile-bootstrap-lib.sh"
20
+ if [[ -f "${_rbl_candidate}" ]]; then
21
+ RECONCILE_BOOTSTRAP_LIB="${_rbl_candidate}"
22
+ break
56
23
  fi
57
24
  done
58
-
59
- if [[ -n "${SHARED_AGENT_HOME:-}" ]]; then
60
- if [[ -d "${SHARED_AGENT_HOME}/tools/bin" ]]; then
61
- printf '%s/tools/bin\n' "${SHARED_AGENT_HOME}"
62
- return 0
63
- fi
64
- for skill_name in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
65
- [[ -n "${skill_name}" ]] || continue
66
- candidate_root="${SHARED_AGENT_HOME}/skills/openclaw/${skill_name}"
67
- if [[ -d "${candidate_root}/tools/bin" ]]; then
68
- printf '%s/tools/bin\n' "${candidate_root}"
69
- return 0
70
- fi
71
- done
72
- fi
73
-
74
- if [[ -d "${SCRIPT_DIR}" ]]; then
75
- printf '%s\n' "${SCRIPT_DIR}"
76
- return 0
77
- fi
78
-
79
- printf '%s\n' "${BOOTSTRAP_TOOLS_DIR}"
80
- }
25
+ fi
26
+ if [[ -z "${RECONCILE_BOOTSTRAP_LIB}" ]]; then
27
+ echo "unable to locate reconcile-bootstrap-lib.sh" >&2
28
+ exit 1
29
+ fi
30
+ # shellcheck source=/dev/null
31
+ source "${RECONCILE_BOOTSTRAP_LIB}"
81
32
 
82
33
  usage() {
83
34
  cat <<'EOF'
@@ -89,30 +40,8 @@ allowing project adapters to inject policy hooks.
89
40
  EOF
90
41
  }
91
42
 
92
- shared_tools_dir="$(resolve_reconcile_tools_dir)"
93
- resolve_reconcile_helper_path() {
94
- local helper_name="${1:?helper name required}"
95
- local candidate=""
96
-
97
- for candidate in \
98
- "${SCRIPT_DIR}/${helper_name}" \
99
- "${BOOTSTRAP_TOOLS_DIR}/${helper_name}" \
100
- "${shared_tools_dir}/${helper_name}"; do
101
- if [[ -n "${candidate}" && -f "${candidate}" ]]; then
102
- printf '%s\n' "${candidate}"
103
- return 0
104
- fi
105
- done
106
-
107
- echo "unable to locate ${helper_name} for reconcile bootstrap" >&2
108
- return 1
109
- }
110
-
111
- FLOW_CONFIG_LIB_PATH="$(resolve_reconcile_helper_path "flow-config-lib.sh")"
112
43
  FLOW_RESIDENT_WORKER_LIB_PATH="$(resolve_reconcile_helper_path "flow-resident-worker-lib.sh")"
113
44
  # shellcheck source=/dev/null
114
- source "${FLOW_CONFIG_LIB_PATH}"
115
- # shellcheck source=/dev/null
116
45
  source "${FLOW_RESIDENT_WORKER_LIB_PATH}"
117
46
  session=""
118
47
  repo_slug=""
@@ -1053,15 +982,6 @@ extract_recovery_worktree_from_publish_output() {
1053
982
  awk -F= '/^RECOVERY_WORKTREE=/{print $2}' <<<"$publish_out" | tail -n 1
1054
983
  }
1055
984
 
1056
- require_transition() {
1057
- local step="${1:?step required}"
1058
- shift
1059
- if ! "$@"; then
1060
- echo "reconcile transition failed: ${step}" >&2
1061
- exit 1
1062
- fi
1063
- }
1064
-
1065
985
  mark_reconciled() {
1066
986
  local reconciled_at tmp_file
1067
987
  if [[ -d "$run_dir" ]]; then
@@ -1141,6 +1061,40 @@ update_resident_issue_metadata() {
1141
1061
  "LAST_WORKTREE_REUSED=${last_worktree_reused:-no}"
1142
1062
  }
1143
1063
 
1064
+ cleanup_output_value() {
1065
+ local cleanup_output="${1:-}"
1066
+ local key="${2:?key required}"
1067
+ awk -F= -v target_key="${key}" '$1 == target_key { print substr($0, index($0, "=") + 1); exit }' <<<"${cleanup_output}"
1068
+ }
1069
+
1070
+ warn_cleanup_issue_session() {
1071
+ local cleanup_output="${1:-}"
1072
+ local cleanup_exit="${2:-0}"
1073
+ local cleanup_status=""
1074
+ local cleanup_mode=""
1075
+ local cleanup_error=""
1076
+
1077
+ cleanup_status="$(cleanup_output_value "${cleanup_output}" "CLEANUP_STATUS")"
1078
+ if [[ -z "${cleanup_status}" ]]; then
1079
+ cleanup_status="${cleanup_exit}"
1080
+ fi
1081
+ [[ "${cleanup_status}" != "0" ]] || return 0
1082
+
1083
+ cleanup_mode="$(cleanup_output_value "${cleanup_output}" "CLEANUP_MODE")"
1084
+ cleanup_error="$(cleanup_output_value "${cleanup_output}" "CLEANUP_ERROR")"
1085
+ printf '[%s] issue cleanup warning session=%s status=%s mode=%s\n' \
1086
+ "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
1087
+ "${session}" \
1088
+ "${cleanup_status}" \
1089
+ "${cleanup_mode:-unknown}" >&2
1090
+ if [[ -n "${cleanup_error}" ]]; then
1091
+ printf '[%s] issue cleanup detail session=%s %s\n' \
1092
+ "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
1093
+ "${session}" \
1094
+ "${cleanup_error}" >&2
1095
+ fi
1096
+ }
1097
+
1144
1098
  cleanup_issue_session() {
1145
1099
  local -a cleanup_args=(
1146
1100
  --repo-root "$repo_root"
@@ -1156,7 +1110,14 @@ cleanup_issue_session() {
1156
1110
  cleanup_args+=(--skip-worktree-cleanup)
1157
1111
  fi
1158
1112
 
1159
- "${shared_tools_dir}/agent-project-cleanup-session" "${cleanup_args[@]}" >/dev/null
1113
+ local cleanup_output=""
1114
+ local cleanup_exit="0"
1115
+ if cleanup_output="$("${shared_tools_dir}/agent-project-cleanup-session" "${cleanup_args[@]}" 2>&1)"; then
1116
+ cleanup_exit="0"
1117
+ else
1118
+ cleanup_exit="$?"
1119
+ fi
1120
+ warn_cleanup_issue_session "${cleanup_output}" "${cleanup_exit}"
1160
1121
  }
1161
1122
 
1162
1123
  notify_issue_reconciled() {
@@ -2,82 +2,33 @@
2
2
  set -euo pipefail
3
3
 
4
4
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
-
6
- bootstrap_flow_shell_lib() {
7
- local candidate=""
8
- local skill_name=""
9
-
10
- for candidate in \
11
- "${SCRIPT_DIR}/flow-shell-lib.sh" \
12
- "${AGENT_CONTROL_PLANE_ROOT:-}/tools/bin/flow-shell-lib.sh" \
13
- "${ACP_ROOT:-}/tools/bin/flow-shell-lib.sh" \
14
- "${F_LOSNING_FLOW_ROOT:-}/tools/bin/flow-shell-lib.sh" \
15
- "${AGENT_FLOW_SKILL_ROOT:-}/tools/bin/flow-shell-lib.sh" \
16
- "${SHARED_AGENT_HOME:-}/tools/bin/flow-shell-lib.sh" \
17
- "$(pwd)/tools/bin/flow-shell-lib.sh"; do
18
- if [[ -n "${candidate}" && -f "${candidate}" ]]; then
19
- printf '%s\n' "${candidate}"
20
- return 0
21
- fi
22
- done
23
-
24
- if [[ -n "${SHARED_AGENT_HOME:-}" ]]; then
25
- for skill_name in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
26
- [[ -n "${skill_name}" ]] || continue
27
- candidate="${SHARED_AGENT_HOME}/skills/openclaw/${skill_name}/tools/bin/flow-shell-lib.sh"
28
- if [[ -f "${candidate}" ]]; then
29
- printf '%s\n' "${candidate}"
30
- return 0
31
- fi
32
- done
5
+ RECONCILE_BOOTSTRAP_LIB=""
6
+ for _rbl_candidate in \
7
+ "${SCRIPT_DIR}/reconcile-bootstrap-lib.sh" \
8
+ "${AGENT_CONTROL_PLANE_ROOT:-}/tools/bin/reconcile-bootstrap-lib.sh" \
9
+ "${ACP_ROOT:-}/tools/bin/reconcile-bootstrap-lib.sh" \
10
+ "${SHARED_AGENT_HOME:-}/tools/bin/reconcile-bootstrap-lib.sh"; do
11
+ if [[ -n "${_rbl_candidate}" && -f "${_rbl_candidate}" ]]; then
12
+ RECONCILE_BOOTSTRAP_LIB="${_rbl_candidate}"
13
+ break
33
14
  fi
34
-
35
- echo "unable to locate flow-shell-lib.sh for reconcile bootstrap" >&2
36
- return 1
37
- }
38
-
39
- FLOW_SHELL_LIB_PATH="$(bootstrap_flow_shell_lib)"
40
- BOOTSTRAP_TOOLS_DIR="$(cd "$(dirname "${FLOW_SHELL_LIB_PATH}")" && pwd)"
41
- # shellcheck source=/dev/null
42
- source "${FLOW_SHELL_LIB_PATH}"
43
-
44
- resolve_reconcile_tools_dir() {
45
- local candidate_root=""
46
- local skill_name=""
47
-
48
- for candidate_root in \
49
- "${AGENT_CONTROL_PLANE_ROOT:-}" \
50
- "${ACP_ROOT:-}" \
51
- "${F_LOSNING_FLOW_ROOT:-}" \
52
- "${AGENT_FLOW_SKILL_ROOT:-}"; do
53
- if [[ -n "${candidate_root}" && -d "${candidate_root}/tools/bin" ]]; then
54
- printf '%s/tools/bin\n' "${candidate_root}"
55
- return 0
15
+ done
16
+ if [[ -n "${SHARED_AGENT_HOME:-}" && -z "${RECONCILE_BOOTSTRAP_LIB}" ]]; then
17
+ for _rbl_skill in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
18
+ [[ -n "${_rbl_skill}" ]] || continue
19
+ _rbl_candidate="${SHARED_AGENT_HOME}/skills/openclaw/${_rbl_skill}/tools/bin/reconcile-bootstrap-lib.sh"
20
+ if [[ -f "${_rbl_candidate}" ]]; then
21
+ RECONCILE_BOOTSTRAP_LIB="${_rbl_candidate}"
22
+ break
56
23
  fi
57
24
  done
58
-
59
- if [[ -n "${SHARED_AGENT_HOME:-}" ]]; then
60
- if [[ -d "${SHARED_AGENT_HOME}/tools/bin" ]]; then
61
- printf '%s/tools/bin\n' "${SHARED_AGENT_HOME}"
62
- return 0
63
- fi
64
- for skill_name in "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}" "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"; do
65
- [[ -n "${skill_name}" ]] || continue
66
- candidate_root="${SHARED_AGENT_HOME}/skills/openclaw/${skill_name}"
67
- if [[ -d "${candidate_root}/tools/bin" ]]; then
68
- printf '%s/tools/bin\n' "${candidate_root}"
69
- return 0
70
- fi
71
- done
72
- fi
73
-
74
- if [[ -d "${SCRIPT_DIR}" ]]; then
75
- printf '%s\n' "${SCRIPT_DIR}"
76
- return 0
77
- fi
78
-
79
- printf '%s\n' "${BOOTSTRAP_TOOLS_DIR}"
80
- }
25
+ fi
26
+ if [[ -z "${RECONCILE_BOOTSTRAP_LIB}" ]]; then
27
+ echo "unable to locate reconcile-bootstrap-lib.sh" >&2
28
+ exit 1
29
+ fi
30
+ # shellcheck source=/dev/null
31
+ source "${RECONCILE_BOOTSTRAP_LIB}"
81
32
 
82
33
  usage() {
83
34
  cat <<'EOF'
@@ -89,28 +40,6 @@ allowing project adapters to inject policy hooks.
89
40
  EOF
90
41
  }
91
42
 
92
- shared_tools_dir="$(resolve_reconcile_tools_dir)"
93
- resolve_reconcile_helper_path() {
94
- local helper_name="${1:?helper name required}"
95
- local candidate=""
96
-
97
- for candidate in \
98
- "${SCRIPT_DIR}/${helper_name}" \
99
- "${BOOTSTRAP_TOOLS_DIR}/${helper_name}" \
100
- "${shared_tools_dir}/${helper_name}"; do
101
- if [[ -n "${candidate}" && -f "${candidate}" ]]; then
102
- printf '%s\n' "${candidate}"
103
- return 0
104
- fi
105
- done
106
-
107
- echo "unable to locate ${helper_name} for reconcile bootstrap" >&2
108
- return 1
109
- }
110
-
111
- FLOW_CONFIG_LIB_PATH="$(resolve_reconcile_helper_path "flow-config-lib.sh")"
112
- # shellcheck source=/dev/null
113
- source "${FLOW_CONFIG_LIB_PATH}"
114
43
  verification_guard_script="${shared_tools_dir}/branch-verification-guard.sh"
115
44
  session=""
116
45
  repo_slug=""
@@ -257,6 +186,40 @@ if [[ "$status" == "RUNNING" && "$pr_state" != "MERGED" && "$pr_state" != "CLOSE
257
186
  exit 0
258
187
  fi
259
188
 
189
+ cleanup_output_value() {
190
+ local cleanup_output="${1:-}"
191
+ local key="${2:?key required}"
192
+ awk -F= -v target_key="${key}" '$1 == target_key { print substr($0, index($0, "=") + 1); exit }' <<<"${cleanup_output}"
193
+ }
194
+
195
+ warn_cleanup_pr_session() {
196
+ local cleanup_output="${1:-}"
197
+ local cleanup_exit="${2:-0}"
198
+ local cleanup_status=""
199
+ local cleanup_mode=""
200
+ local cleanup_error=""
201
+
202
+ cleanup_status="$(cleanup_output_value "${cleanup_output}" "CLEANUP_STATUS")"
203
+ if [[ -z "${cleanup_status}" ]]; then
204
+ cleanup_status="${cleanup_exit}"
205
+ fi
206
+ [[ "${cleanup_status}" != "0" ]] || return 0
207
+
208
+ cleanup_mode="$(cleanup_output_value "${cleanup_output}" "CLEANUP_MODE")"
209
+ cleanup_error="$(cleanup_output_value "${cleanup_output}" "CLEANUP_ERROR")"
210
+ printf '[%s] pr cleanup warning session=%s status=%s mode=%s\n' \
211
+ "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
212
+ "${session}" \
213
+ "${cleanup_status}" \
214
+ "${cleanup_mode:-unknown}" >&2
215
+ if [[ -n "${cleanup_error}" ]]; then
216
+ printf '[%s] pr cleanup detail session=%s %s\n' \
217
+ "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
218
+ "${session}" \
219
+ "${cleanup_error}" >&2
220
+ fi
221
+ }
222
+
260
223
  review_pass_action_from_result_action() {
261
224
  case "${1:-}" in
262
225
  host-advance-double-check-2)
@@ -684,15 +647,6 @@ attempt_blocked_pr_host_verification_recovery() {
684
647
  return 0
685
648
  }
686
649
 
687
- require_transition() {
688
- local step="${1:?step required}"
689
- shift
690
- if ! "$@"; then
691
- echo "reconcile transition failed: ${step}" >&2
692
- exit 1
693
- fi
694
- }
695
-
696
650
  close_linked_issue_if_open() {
697
651
  local issue_id="${1:-}"
698
652
  [[ -n "$issue_id" ]] || return 0
@@ -957,13 +911,24 @@ approve_and_merge() {
957
911
  }
958
912
 
959
913
  cleanup_pr_session() {
960
- "${shared_tools_dir}/agent-project-cleanup-session" \
961
- --repo-root "$repo_root" \
962
- --runs-root "$runs_root" \
963
- --history-root "$history_root" \
964
- --session "$session" \
965
- --worktree "$pr_worktree" \
966
- --mode pr >/dev/null || true
914
+ local cleanup_output=""
915
+ local cleanup_exit="0"
916
+
917
+ if cleanup_output="$(
918
+ "${shared_tools_dir}/agent-project-cleanup-session" \
919
+ --repo-root "$repo_root" \
920
+ --runs-root "$runs_root" \
921
+ --history-root "$history_root" \
922
+ --session "$session" \
923
+ --worktree "$pr_worktree" \
924
+ --mode pr 2>&1
925
+ )"; then
926
+ cleanup_exit="0"
927
+ else
928
+ cleanup_exit="$?"
929
+ fi
930
+
931
+ warn_cleanup_pr_session "${cleanup_output}" "${cleanup_exit}"
967
932
  }
968
933
 
969
934
  notify_pr_reconciled() {
@@ -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"
@@ -177,7 +200,7 @@ run_with_timeout() {
177
200
  local timeout_seconds="${1:?timeout seconds required}"
178
201
  shift
179
202
 
180
- /opt/homebrew/bin/python3 - "$timeout_seconds" "$@" <<'PY'
203
+ "$python_bin" - "$timeout_seconds" "$@" <<'PY'
181
204
  import os
182
205
  import signal
183
206
  import subprocess
@@ -220,6 +243,60 @@ sys.exit(proc.returncode)
220
243
  PY
221
244
  }
222
245
 
246
+ stat_file_size() {
247
+ local path="${1:?path required}"
248
+ local value=""
249
+
250
+ value="$(stat -f %z "$path" 2>/dev/null || true)"
251
+ if [[ "$value" =~ ^[0-9]+$ ]]; then
252
+ printf '%s\n' "$value"
253
+ return 0
254
+ fi
255
+
256
+ value="$(stat -c %s "$path" 2>/dev/null || true)"
257
+ if [[ "$value" =~ ^[0-9]+$ ]]; then
258
+ printf '%s\n' "$value"
259
+ return 0
260
+ fi
261
+
262
+ "$python_bin" - "$path" <<'PY'
263
+ import os
264
+ import sys
265
+
266
+ try:
267
+ print(os.path.getsize(sys.argv[1]))
268
+ except OSError:
269
+ print("0")
270
+ PY
271
+ }
272
+
273
+ stat_file_mtime() {
274
+ local path="${1:?path required}"
275
+ local value=""
276
+
277
+ value="$(stat -f %m "$path" 2>/dev/null || true)"
278
+ if [[ "$value" =~ ^[0-9]+$ ]]; then
279
+ printf '%s\n' "$value"
280
+ return 0
281
+ fi
282
+
283
+ value="$(stat -c %Y "$path" 2>/dev/null || true)"
284
+ if [[ "$value" =~ ^[0-9]+$ ]]; then
285
+ printf '%s\n' "$value"
286
+ return 0
287
+ fi
288
+
289
+ "$python_bin" - "$path" <<'PY'
290
+ import os
291
+ import sys
292
+
293
+ try:
294
+ print(int(os.path.getmtime(sys.argv[1])))
295
+ except OSError:
296
+ print("0")
297
+ PY
298
+ }
299
+
223
300
  auth_fingerprint() {
224
301
  if [[ ! -f "$auth_file" ]]; then
225
302
  printf 'missing\n'
@@ -227,8 +304,8 @@ auth_fingerprint() {
227
304
  fi
228
305
 
229
306
  local mtime size sha
230
- mtime="$(stat -f %m "$auth_file" 2>/dev/null || printf '0')"
231
- size="$(stat -f %z "$auth_file" 2>/dev/null || printf '0')"
307
+ mtime="$(stat_file_mtime "$auth_file" 2>/dev/null || printf '0')"
308
+ size="$(stat_file_size "$auth_file" 2>/dev/null || printf '0')"
232
309
  sha="$(shasum -a 256 "$auth_file" | awk '{print $1}')"
233
310
  printf '%s:%s:%s\n' "$mtime" "$size" "$sha"
234
311
  }
@@ -256,8 +333,8 @@ quota_switch_signature() {
256
333
  fi
257
334
 
258
335
  local mtime size sha
259
- mtime="$(stat -f %m "$quota_switch_state_file" 2>/dev/null || printf '0')"
260
- size="$(stat -f %z "$quota_switch_state_file" 2>/dev/null || printf '0')"
336
+ mtime="$(stat_file_mtime "$quota_switch_state_file" 2>/dev/null || printf '0')"
337
+ size="$(stat_file_size "$quota_switch_state_file" 2>/dev/null || printf '0')"
261
338
  sha="$(shasum -a 256 "$quota_switch_state_file" | awk '{print $1}')"
262
339
  printf '%s:%s:%s\n' "$mtime" "$size" "$sha"
263
340
  }
@@ -390,7 +467,7 @@ run_quota_autoswitch() {
390
467
  new_output_since() {
391
468
  local start_size="${1:?start size required}"
392
469
  local file_size
393
- file_size="$(stat -f %z "$output_file" 2>/dev/null || printf '0')"
470
+ file_size="$(stat_file_size "$output_file" 2>/dev/null || printf '0')"
394
471
  if (( file_size <= start_size )); then
395
472
  return 0
396
473
  fi
@@ -456,7 +533,7 @@ stream_codex_exec() {
456
533
  local progress_file=""
457
534
  local line=""
458
535
 
459
- last_attempt_start_size="$(stat -f %z "$output_file" 2>/dev/null || printf '0')"
536
+ last_attempt_start_size="$(stat_file_size "$output_file" 2>/dev/null || printf '0')"
460
537
  last_attempt_started_epoch="$(date +%s)"
461
538
  progress_file="${host_run_dir}/.codex-progress.$$"
462
539
  rm -f "$progress_file"
@@ -514,7 +591,7 @@ stream_codex_exec() {
514
591
  break
515
592
  fi
516
593
  else
517
- last_progress_epoch="$(stat -f %m "$progress_file" 2>/dev/null || printf '0')"
594
+ last_progress_epoch="$(stat_file_mtime "$progress_file" 2>/dev/null || printf '0')"
518
595
  if [[ -n "$last_progress_epoch" && "$last_progress_epoch" != "0" ]]; then
519
596
  idle_for=$((now - last_progress_epoch))
520
597
  if (( idle_for >= codex_stall_seconds )); then
@@ -556,7 +633,7 @@ stream_codex_exec() {
556
633
  }
557
634
 
558
635
  extract_thread_id() {
559
- /opt/homebrew/bin/python3 -c '
636
+ "$python_bin" -c '
560
637
  import json
561
638
  import sys
562
639
 
@@ -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
- candidate="$(command -v codex)"
59
+ command -v codex
60
+ return 0
61
+ fi
62
+
63
+ for candidate in "${fallback_paths[@]}"; do
54
64
  if [[ -x "$candidate" ]]; then
55
- best_bin="$candidate"
65
+ printf '%s\n' "$candidate"
66
+ return 0
56
67
  fi
57
- fi
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 '/(Users|Volumes|tmp)/[^[:space:])"]+' ${output_q} 2>/dev/null || true)
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"