agent-control-plane 0.4.9 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +72 -9
  2. package/npm/bin/agent-control-plane.js +1 -1
  3. package/package.json +39 -33
  4. package/tools/bin/debug-session.sh +106 -0
  5. package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
  6. package/tools/bin/flow-runtime-doctor.sh +5 -1
  7. package/tools/bin/install-project-systemd.sh +255 -0
  8. package/tools/bin/project-runtimectl.sh +45 -0
  9. package/tools/bin/project-systemd-bootstrap.sh +74 -0
  10. package/tools/bin/uninstall-project-systemd.sh +87 -0
  11. package/tools/dashboard/app.js +198 -5
  12. package/tools/dashboard/issue_queue_state.py +101 -0
  13. package/tools/dashboard/server.py +123 -1
  14. package/tools/dashboard/styles.css +526 -455
  15. package/tools/bin/agent-cleanup-worktree +0 -247
  16. package/tools/bin/agent-github-update-labels +0 -105
  17. package/tools/bin/agent-init-worktree +0 -216
  18. package/tools/bin/agent-project-archive-run +0 -52
  19. package/tools/bin/agent-project-capture-worker +0 -46
  20. package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
  21. package/tools/bin/agent-project-catch-up-merged-prs +0 -195
  22. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
  23. package/tools/bin/agent-project-cleanup-session +0 -513
  24. package/tools/bin/agent-project-detached-launch +0 -127
  25. package/tools/bin/agent-project-heartbeat-loop +0 -1029
  26. package/tools/bin/agent-project-open-issue-worktree +0 -89
  27. package/tools/bin/agent-project-open-pr-worktree +0 -80
  28. package/tools/bin/agent-project-publish-issue-pr +0 -468
  29. package/tools/bin/agent-project-reconcile-issue-session +0 -1409
  30. package/tools/bin/agent-project-reconcile-pr-session +0 -1288
  31. package/tools/bin/agent-project-retry-state +0 -158
  32. package/tools/bin/agent-project-run-claude-session +0 -805
  33. package/tools/bin/agent-project-run-codex-resilient +0 -963
  34. package/tools/bin/agent-project-run-codex-session +0 -435
  35. package/tools/bin/agent-project-run-kilo-session +0 -369
  36. package/tools/bin/agent-project-run-ollama-session +0 -658
  37. package/tools/bin/agent-project-run-openclaw-session +0 -1309
  38. package/tools/bin/agent-project-run-opencode-session +0 -377
  39. package/tools/bin/agent-project-run-pi-session +0 -479
  40. package/tools/bin/agent-project-sync-anchor-repo +0 -139
  41. package/tools/bin/agent-project-sync-source-repo-main +0 -163
  42. package/tools/bin/agent-project-worker-status +0 -188
  43. package/tools/bin/branch-verification-guard.sh +0 -364
  44. package/tools/bin/capture-worker.sh +0 -18
  45. package/tools/bin/cleanup-worktree.sh +0 -52
  46. package/tools/bin/codex-quota +0 -31
  47. package/tools/bin/create-follow-up-issue.sh +0 -114
  48. package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
  49. package/tools/bin/issue-publish-localization-guard.sh +0 -142
  50. package/tools/bin/issue-publish-scope-guard.sh +0 -242
  51. package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
  52. package/tools/bin/issue-resource-class.sh +0 -12
  53. package/tools/bin/kick-scheduler.sh +0 -75
  54. package/tools/bin/label-follow-up-issues.sh +0 -14
  55. package/tools/bin/new-pr-worktree.sh +0 -50
  56. package/tools/bin/new-worktree.sh +0 -49
  57. package/tools/bin/pr-risk.sh +0 -12
  58. package/tools/bin/prepare-worktree.sh +0 -142
  59. package/tools/bin/provider-cooldown-state.sh +0 -204
  60. package/tools/bin/publish-issue-worker.sh +0 -31
  61. package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
  62. package/tools/bin/reconcile-issue-worker.sh +0 -34
  63. package/tools/bin/reconcile-pr-worker.sh +0 -34
  64. package/tools/bin/record-verification.sh +0 -71
  65. package/tools/bin/render-flow-config.sh +0 -98
  66. package/tools/bin/resident-issue-controller-lib.sh +0 -448
  67. package/tools/bin/retry-state.sh +0 -31
  68. package/tools/bin/reuse-issue-worktree.sh +0 -121
  69. package/tools/bin/run-codex-bypass.sh +0 -3
  70. package/tools/bin/run-codex-safe.sh +0 -3
  71. package/tools/bin/run-codex-task.sh +0 -280
  72. package/tools/bin/serve-dashboard.sh +0 -5
  73. package/tools/bin/start-issue-worker.sh +0 -943
  74. package/tools/bin/start-pr-fix-worker.sh +0 -528
  75. package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
  76. package/tools/bin/start-pr-review-worker.sh +0 -261
  77. package/tools/bin/start-resident-issue-loop.sh +0 -499
  78. package/tools/bin/update-github-labels.sh +0 -14
  79. package/tools/bin/worker-status.sh +0 -19
  80. package/tools/bin/workflow-catalog.sh +0 -77
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ Usage:
7
+ install-project-systemd.sh --profile-id <id> [options]
8
+
9
+ Install a per-user systemd service so one ACP project runtime starts
10
+ automatically after login/restart on Linux.
11
+
12
+ Options:
13
+ --profile-id <id> Installed profile id to manage
14
+ --unit-name <name> Override systemd unit name (default: agent-project-<id>.service)
15
+ --delay-seconds <n> Initial supervisor delay before first bootstrap (default: 0)
16
+ --interval-seconds <n> Supervisor interval between bootstrap passes (default: 15)
17
+ --help Show this help
18
+ EOF
19
+ }
20
+
21
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22
+ # shellcheck source=/dev/null
23
+ source "${script_dir}/flow-config-lib.sh"
24
+
25
+ append_path_dir() {
26
+ local value_name="${1:?value name required}"
27
+ local candidate="${2:-}"
28
+ local current=""
29
+
30
+ [[ -n "${candidate}" && -d "${candidate}" ]] || return 0
31
+ current="${!value_name:-}"
32
+ case ":${current}:" in
33
+ *":${candidate}:"*) return 0 ;;
34
+ esac
35
+ if [[ -n "${current}" ]]; then
36
+ printf -v "${value_name}" '%s:%s' "${current}" "${candidate}"
37
+ else
38
+ printf -v "${value_name}" '%s' "${candidate}"
39
+ fi
40
+ }
41
+
42
+ resolved_tool_dir() {
43
+ local tool_name="${1:-}"
44
+ local tool_path=""
45
+
46
+ [[ -n "${tool_name}" ]] || return 1
47
+ tool_path="$(command -v "${tool_name}" 2>/dev/null || true)"
48
+ [[ -n "${tool_path}" ]] || return 1
49
+ dirname "${tool_path}"
50
+ }
51
+
52
+ build_systemd_base_path() {
53
+ local path_value="${ACP_PROJECT_RUNTIME_PATH:-/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin}"
54
+ local tool_name=""
55
+ local tool_dir=""
56
+
57
+ for tool_name in node gh git python3 openclaw codex claude ollama pi crush kilo; do
58
+ tool_dir="$(resolved_tool_dir "${tool_name}" || true)"
59
+ append_path_dir path_value "${tool_dir}"
60
+ done
61
+
62
+ printf '%s\n' "${path_value}"
63
+ }
64
+
65
+ profile_id_override=""
66
+ unit_name_override=""
67
+ delay_seconds="0"
68
+ interval_seconds="15"
69
+ profile_registry_root_override="${ACP_PROJECT_RUNTIME_PROFILE_REGISTRY_ROOT:-${ACP_PROFILE_REGISTRY_ROOT:-}}"
70
+
71
+ while [[ $# -gt 0 ]]; do
72
+ case "$1" in
73
+ --profile-id) profile_id_override="${2:-}"; shift 2 ;;
74
+ --unit-name) unit_name_override="${2:-}"; shift 2 ;;
75
+ --delay-seconds) delay_seconds="${2:-}"; shift 2 ;;
76
+ --interval-seconds) interval_seconds="${2:-}"; shift 2 ;;
77
+ --help|-h) usage; exit 0 ;;
78
+ *) echo "Unknown argument: $1" >&2; usage >&2; exit 64 ;;
79
+ esac
80
+ done
81
+
82
+ if [[ -z "${profile_id_override}" ]]; then
83
+ usage >&2
84
+ exit 64
85
+ fi
86
+
87
+ case "${delay_seconds}" in
88
+ ''|*[!0-9]*) echo "--delay-seconds must be numeric" >&2; exit 64 ;;
89
+ esac
90
+
91
+ case "${interval_seconds}" in
92
+ ''|*[!0-9]*) echo "--interval-seconds must be numeric" >&2; exit 64 ;;
93
+ esac
94
+
95
+ export ACP_PROJECT_ID="${profile_id_override}"
96
+ export AGENT_PROJECT_ID="${profile_id_override}"
97
+
98
+ if [[ -n "${profile_registry_root_override}" ]]; then
99
+ export ACP_PROFILE_REGISTRY_ROOT="${profile_registry_root_override}"
100
+ fi
101
+
102
+ flow_skill_dir="$(resolve_flow_skill_dir "${BASH_SOURCE[0]}")"
103
+ if ! flow_require_explicit_profile_selection "${flow_skill_dir}" "install-project-systemd.sh"; then
104
+ exit 64
105
+ fi
106
+
107
+ config_yaml="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
108
+ if [[ ! -f "${config_yaml}" ]]; then
109
+ printf 'profile not installed: %s\n' "${profile_id_override}" >&2
110
+ exit 66
111
+ fi
112
+
113
+ profile_id="$(flow_resolve_adapter_id "${config_yaml}")"
114
+ profile_slug="$(printf '%s' "${profile_id}" | tr -c 'A-Za-z0-9._-' '-')"
115
+ home_dir="${ACP_PROJECT_RUNTIME_HOME_DIR:-${HOME:-}}"
116
+ source_home="${ACP_PROJECT_RUNTIME_SOURCE_HOME:-}"
117
+
118
+ if [[ -z "${source_home}" ]]; then
119
+ if flow_is_skill_root "${flow_skill_dir}"; then
120
+ source_home="${flow_skill_dir}"
121
+ else
122
+ source_home="$(cd "${flow_skill_dir}/../../.." && pwd)"
123
+ fi
124
+ fi
125
+
126
+ runtime_home="${ACP_PROJECT_RUNTIME_RUNTIME_HOME:-${home_dir}/.agent-runtime/runtime-home}"
127
+ workspace_dir="${ACP_PROJECT_RUNTIME_WORKSPACE_DIR:-${home_dir}/.agent-runtime/control-plane/workspace}"
128
+ profile_registry_root="${ACP_PROJECT_RUNTIME_PROFILE_REGISTRY_ROOT:-${ACP_PROFILE_REGISTRY_ROOT:-${home_dir}/.agent-runtime/control-plane/profiles}}"
129
+ systemd_dir="${ACP_PROJECT_RUNTIME_SYSTEMD_DIR:-${home_dir}/.config/systemd/user}"
130
+ log_dir="${ACP_PROJECT_RUNTIME_LOG_DIR:-${home_dir}/.agent-runtime/logs}"
131
+ unit_name="${unit_name_override:-${ACP_PROJECT_RUNTIME_SYSTEMD_UNIT:-agent-project-${profile_slug}.service}}"
132
+ base_path="$(build_systemd_base_path)"
133
+ coding_worker_override="${ACP_PROJECT_RUNTIME_CODING_WORKER:-${ACP_CODING_WORKER:-}}"
134
+ sync_script="${ACP_PROJECT_RUNTIME_SYNC_SCRIPT:-${flow_skill_dir}/tools/bin/sync-shared-agent-home.sh}"
135
+ runtime_skill_dir="${runtime_home}/skills/openclaw/agent-control-plane"
136
+ bootstrap_script="${ACP_PROJECT_RUNTIME_BOOTSTRAP_SCRIPT:-}"
137
+
138
+ if [[ -z "${bootstrap_script}" ]]; then
139
+ if [[ -x "${runtime_skill_dir}/tools/bin/project-systemd-bootstrap.sh" ]]; then
140
+ bootstrap_script="${runtime_skill_dir}/tools/bin/project-systemd-bootstrap.sh"
141
+ else
142
+ bootstrap_script="${flow_skill_dir}/tools/bin/project-systemd-bootstrap.sh"
143
+ fi
144
+ fi
145
+
146
+ supervisor_script="${ACP_PROJECT_RUNTIME_SUPERVISOR_SCRIPT:-}"
147
+ if [[ -z "${supervisor_script}" ]]; then
148
+ if [[ -x "${runtime_skill_dir}/tools/bin/project-runtime-supervisor.sh" ]]; then
149
+ supervisor_script="${runtime_skill_dir}/tools/bin/project-runtime-supervisor.sh"
150
+ else
151
+ supervisor_script="${flow_skill_dir}/tools/bin/project-runtime-supervisor.sh"
152
+ fi
153
+ fi
154
+
155
+ state_root="$(flow_resolve_state_root "${config_yaml}")"
156
+ supervisor_pid_file="${state_root}/runtime-supervisor.pid"
157
+ env_file="${ACP_PROJECT_RUNTIME_ENV_FILE:-${profile_registry_root}/${profile_id}/runtime.env}"
158
+ wrapper_path="${workspace_dir}/bin/agent-project-${profile_slug}-systemd.sh"
159
+ unit_file="${systemd_dir}/${unit_name}"
160
+ stdout_log="${log_dir}/agent-project-${profile_slug}.stdout.log"
161
+ stderr_log="${log_dir}/agent-project-${profile_slug}.stderr.log"
162
+
163
+ if [[ -z "${home_dir}" ]]; then
164
+ echo "install-project-systemd requires HOME or ACP_PROJECT_RUNTIME_HOME_DIR" >&2
165
+ exit 64
166
+ fi
167
+
168
+ # Check if systemd is available
169
+ if ! command -v systemctl &>/dev/null; then
170
+ echo "systemctl not found. Is systemd installed?" >&2
171
+ exit 1
172
+ fi
173
+
174
+ mkdir -p "${workspace_dir}/bin" "${systemd_dir}" "${log_dir}" "$(dirname "${supervisor_pid_file}")"
175
+
176
+ # Create wrapper script
177
+ cat >"${wrapper_path}" <<EOF
178
+ #!/usr/bin/env bash
179
+ set -euo pipefail
180
+ export ACP_PROJECT_RUNTIME_HOME_DIR='${home_dir}'
181
+ export ACP_PROJECT_RUNTIME_SOURCE_HOME='${source_home}'
182
+ export ACP_PROJECT_RUNTIME_RUNTIME_HOME='${runtime_home}'
183
+ export ACP_PROJECT_RUNTIME_PROFILE_REGISTRY_ROOT='${profile_registry_root}'
184
+ export ACP_PROJECT_RUNTIME_PROFILE_ID='${profile_id}'
185
+ export ACP_PROJECT_RUNTIME_ENV_FILE='${env_file}'
186
+ export ACP_PROJECT_ID='${profile_id}'
187
+ export AGENT_PROJECT_ID='${profile_id}'
188
+ export ACP_PROJECT_RUNTIME_PATH='${base_path}'
189
+ export ACP_PROJECT_RUNTIME_SYNC_SCRIPT='${sync_script}'
190
+ export ACP_PROFILE_REGISTRY_ROOT='${profile_registry_root}'
191
+ EOF
192
+
193
+ if [[ -n "${coding_worker_override}" ]]; then
194
+ cat >>"${wrapper_path}" <<EOF
195
+ export ACP_CODING_WORKER='${coding_worker_override}'
196
+ EOF
197
+ fi
198
+
199
+ cat >>"${wrapper_path}" <<EOF
200
+ exec bash '${supervisor_script}' --bootstrap-script '${bootstrap_script}' --pid-file '${supervisor_pid_file}' --delay-seconds '${delay_seconds}' --interval-seconds '${interval_seconds}'
201
+ EOF
202
+ chmod +x "${wrapper_path}"
203
+
204
+ # Create systemd unit file
205
+ cat >"${unit_file}" <<EOF
206
+ [Unit]
207
+ Description=Agent Control Plane - Project ${profile_id}
208
+ After=default.target
209
+ Wants=default.target
210
+
211
+ [Service]
212
+ Type=simple
213
+ ExecStart=${wrapper_path}
214
+ WorkingDirectory=${workspace_dir}
215
+ StandardOutput=append:${stdout_log}
216
+ StandardError=append:${stderr_log}
217
+ Restart=always
218
+ RestartSec=10
219
+ Environment=HOME=${home_dir}
220
+ Environment=PATH=${base_path}
221
+ Environment=ACP_PROFILE_REGISTRY_ROOT=${profile_registry_root}
222
+
223
+ [Install]
224
+ WantedBy=default.target
225
+ EOF
226
+
227
+ # Enable and start the service
228
+ if [[ "${ACP_PROJECT_RUNTIME_SKIP_SYSTEMCTL:-0}" == "1" ]]; then
229
+ printf 'SYSTEMD_INSTALL_STATUS=skipped-systemctl\n'
230
+ printf 'PROFILE_ID=%s\n' "${profile_id}"
231
+ printf 'UNIT_NAME=%s\n' "${unit_name}"
232
+ printf 'UNIT_FILE=%s\n' "${unit_file}"
233
+ printf 'WRAPPER=%s\n' "${wrapper_path}"
234
+ exit 0
235
+ fi
236
+
237
+ # Enable user service (create symlink)
238
+ systemctl --user enable "${unit_name}" 2>&1 || true
239
+
240
+ # Start the service
241
+ systemctl --user restart "${unit_name}" 2>&1 || true
242
+
243
+ # Wait briefly for service to start
244
+ for _ in $(seq 1 10); do
245
+ if systemctl --user is-active --quiet "${unit_name}" 2>/dev/null; then
246
+ break
247
+ fi
248
+ sleep 1
249
+ done
250
+
251
+ printf 'SYSTEMD_INSTALL_STATUS=ok\n'
252
+ printf 'PROFILE_ID=%s\n' "${profile_id}"
253
+ printf 'UNIT_NAME=%s\n' "${unit_name}"
254
+ printf 'UNIT_FILE=%s\n' "${unit_file}"
255
+ printf 'WRAPPER=%s\n' "${wrapper_path}"
@@ -107,6 +107,9 @@ LAUNCHCTL_BIN="${ACP_PROJECT_RUNTIME_LAUNCHCTL_BIN:-$(command -v launchctl || tr
107
107
  LAUNCH_AGENTS_DIR="${ACP_PROJECT_RUNTIME_LAUNCH_AGENTS_DIR:-${HOME}/Library/LaunchAgents}"
108
108
  LAUNCHD_LABEL="${ACP_PROJECT_RUNTIME_LAUNCHD_LABEL:-ai.agent.project.${PROFILE_ID_SLUG}}"
109
109
  LAUNCHD_PLIST="${ACP_PROJECT_RUNTIME_LAUNCHD_PLIST:-${LAUNCH_AGENTS_DIR}/${LAUNCHD_LABEL}.plist}"
110
+ SYSTEMCTL_BIN="${ACP_PROJECT_RUNTIME_SYSTEMCTL_BIN:-$(command -v systemctl || true)}"
111
+ SYSTEMD_DIR="${ACP_PROJECT_RUNTIME_SYSTEMD_DIR:-${HOME}/.config/systemd/user}"
112
+ SYSTEMD_UNIT_NAME="${ACP_PROJECT_RUNTIME_SYSTEMD_UNIT:-agent-project-${PROFILE_ID_SLUG}.service}"
110
113
  SOURCE_HOME="${ACP_PROJECT_RUNTIME_SOURCE_HOME:-}"
111
114
  RUNTIME_HOME="${ACP_PROJECT_RUNTIME_RUNTIME_HOME:-$(resolve_runtime_home)}"
112
115
  SYNC_STAMP_FILE="${RUNTIME_HOME}/.agent-control-plane-runtime-sync.env"
@@ -385,6 +388,24 @@ launchd_service_state() {
385
388
  fi
386
389
  }
387
390
 
391
+ systemd_service_enabled_for_profile() {
392
+ [[ -n "${SYSTEMCTL_BIN}" && -x "${SYSTEMCTL_BIN}" ]] || return 1
393
+ [[ -f "${SYSTEMD_DIR}/${SYSTEMD_UNIT_NAME}" ]] || return 1
394
+ return 0
395
+ }
396
+
397
+ systemd_service_state() {
398
+ if ! systemd_service_enabled_for_profile; then
399
+ printf 'n/a\n'
400
+ return 0
401
+ fi
402
+ if "${SYSTEMCTL_BIN}" --user is-active --quiet "${SYSTEMD_UNIT_NAME}" 2>/dev/null; then
403
+ printf 'running\n'
404
+ else
405
+ printf 'stopped\n'
406
+ fi
407
+ }
408
+
388
409
  print_status() {
389
410
  local heartbeat=""
390
411
  local shared_loop=""
@@ -439,6 +460,7 @@ print_status() {
439
460
  fi
440
461
 
441
462
  launchd_state="$(launchd_service_state)"
463
+ systemd_state="$(systemd_service_state)"
442
464
  runtime_sync_status="$(sync_stamp_value "SYNC_STATUS" || true)"
443
465
  runtime_sync_updated_at="$(sync_stamp_value "UPDATED_AT" || true)"
444
466
  runtime_sync_fingerprint="$(sync_stamp_value "SOURCE_FINGERPRINT" || true)"
@@ -471,6 +493,7 @@ print_status() {
471
493
  printf 'STATE_ROOT=%s\n' "${STATE_ROOT}"
472
494
  printf 'RUNTIME_STATUS=%s\n' "${runtime_status}"
473
495
  printf 'LAUNCHD_STATE=%s\n' "${launchd_state}"
496
+ printf 'SYSTEMD_STATE=%s\n' "${systemd_state}"
474
497
  printf 'LAUNCHD_LABEL=%s\n' "${LAUNCHD_LABEL}"
475
498
  printf 'LAUNCHD_PLIST=%s\n' "${LAUNCHD_PLIST}"
476
499
  printf 'HEARTBEAT_PID=%s\n' "${heartbeat}"
@@ -587,6 +610,7 @@ stop_runtime() {
587
610
  local session=""
588
611
  local pid=""
589
612
  local launchd_stopped="no"
613
+ local systemd_stopped="no"
590
614
 
591
615
  while IFS= read -r session; do
592
616
  [[ -n "${session}" ]] || continue
@@ -619,6 +643,11 @@ stop_runtime() {
619
643
  launchd_stopped="yes"
620
644
  fi
621
645
 
646
+ if systemd_service_enabled_for_profile; then
647
+ "${SYSTEMCTL_BIN}" --user stop "${SYSTEMD_UNIT_NAME}" >/dev/null 2>&1 || true
648
+ systemd_stopped="yes"
649
+ fi
650
+
622
651
  if [[ -n "${TMUX_BIN}" ]]; then
623
652
  for session in "${tmux_sessions[@]+"${tmux_sessions[@]}"}"; do
624
653
  "${TMUX_BIN}" kill-session -t "${session}" >/dev/null 2>&1 || true
@@ -636,6 +665,7 @@ stop_runtime() {
636
665
  printf 'ACTION=stop\n'
637
666
  printf 'PROFILE_ID=%s\n' "${PROFILE_ID}"
638
667
  printf 'LAUNCHD_STOPPED=%s\n' "${launchd_stopped}"
668
+ printf 'SYSTEMD_STOPPED=%s\n' "${systemd_stopped}"
639
669
  printf 'STOPPED_PID_COUNT=%s\n' "$(printf '%s\n' "${pid_targets[@]+"${pid_targets[@]}"}" | awk 'NF {c+=1} END {print c+0}')"
640
670
  printf 'STOPPED_TMUX_SESSION_COUNT=%s\n' "$(printf '%s\n' "${tmux_sessions[@]+"${tmux_sessions[@]}"}" | awk 'NF {c+=1} END {print c+0}')"
641
671
  printf 'STOPPED_STALE_TMUX_SESSION_COUNT=%s\n' "$(printf '%s\n' "${stale_tmux_sessions[@]+"${stale_tmux_sessions[@]}"}" | awk 'NF {c+=1} END {print c+0}')"
@@ -679,6 +709,21 @@ start_runtime() {
679
709
  return 0
680
710
  fi
681
711
 
712
+ if systemd_service_enabled_for_profile; then
713
+ "${SYSTEMCTL_BIN}" --user restart "${SYSTEMD_UNIT_NAME}" >/dev/null 2>&1 || true
714
+ for _ in 1 2 3 4 5 6 7 8 9 10; do
715
+ if "${SYSTEMCTL_BIN}" --user is-active --quiet "${SYSTEMD_UNIT_NAME}" 2>/dev/null; then
716
+ break
717
+ fi
718
+ sleep 1
719
+ done
720
+ printf 'ACTION=start\n'
721
+ printf 'PROFILE_ID=%s\n' "${PROFILE_ID}"
722
+ printf 'START_MODE=systemd\n'
723
+ printf 'SYSTEMD_UNIT=%s\n' "${SYSTEMD_UNIT_NAME}"
724
+ return 0
725
+ fi
726
+
682
727
  kick_output="$(ACP_PROJECT_ID="${PROFILE_ID}" AGENT_PROJECT_ID="${PROFILE_ID}" bash "${KICK_SCRIPT}" "${delay_seconds}")"
683
728
  if wait_for_runtime_start "${start_timeout}"; then
684
729
  runtime_started_after_kick="1"
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ flow_skill_dir="$(cd "${script_dir}/../.." && pwd)"
6
+ home_dir="${ACP_PROJECT_RUNTIME_HOME_DIR:-${HOME:-}}"
7
+ profile_registry_root="${ACP_PROJECT_RUNTIME_PROFILE_REGISTRY_ROOT:-${ACP_PROFILE_REGISTRY_ROOT:-${home_dir}/.agent-runtime/control-plane/profiles}}"
8
+ profile_id="${ACP_PROJECT_RUNTIME_PROFILE_ID:-${ACP_PROJECT_ID:-${AGENT_PROJECT_ID:-}}}"
9
+ env_file="${ACP_PROJECT_RUNTIME_ENV_FILE:-${profile_registry_root}/${profile_id}/runtime.env}"
10
+
11
+ if [[ -z "${home_dir}" ]]; then
12
+ echo "project systemd bootstrap requires HOME or ACP_PROJECT_RUNTIME_HOME_DIR" >&2
13
+ exit 64
14
+ fi
15
+
16
+ if [[ -z "${profile_id}" ]]; then
17
+ echo "project systemd bootstrap requires ACP_PROJECT_RUNTIME_PROFILE_ID or ACP_PROJECT_ID" >&2
18
+ exit 64
19
+ fi
20
+
21
+ export HOME="${home_dir}"
22
+ export ACP_PROFILE_REGISTRY_ROOT="${profile_registry_root}"
23
+ export ACP_PROJECT_ID="${profile_id}"
24
+ export AGENT_PROJECT_ID="${profile_id}"
25
+
26
+ if [[ -f "${env_file}" ]]; then
27
+ set -a
28
+ # shellcheck source=/dev/null
29
+ source "${env_file}"
30
+ set +a
31
+ fi
32
+
33
+ # Resolve launch paths after runtime.env overrides are loaded so systemd can
34
+ # pin the project runtime to a source checkout or alternate runtime home.
35
+ source_home="${ACP_PROJECT_RUNTIME_SOURCE_HOME:-}"
36
+ runtime_home="${ACP_PROJECT_RUNTIME_RUNTIME_HOME:-${home_dir}/.agent-runtime/runtime-home}"
37
+ base_path="${ACP_PROJECT_RUNTIME_PATH:-/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin}"
38
+ sync_script="${ACP_PROJECT_RUNTIME_SYNC_SCRIPT:-${flow_skill_dir}/tools/bin/sync-shared-agent-home.sh}"
39
+ ensure_sync_script="${ACP_PROJECT_RUNTIME_ENSURE_SYNC_SCRIPT:-${flow_skill_dir}/tools/bin/ensure-runtime-sync.sh}"
40
+ runtime_heartbeat_script="${ACP_PROJECT_RUNTIME_HEARTBEAT_SCRIPT:-${runtime_home}/skills/openclaw/agent-control-plane/tools/bin/heartbeat-safe-auto.sh}"
41
+ always_sync="${ACP_PROJECT_RUNTIME_ALWAYS_SYNC:-0}"
42
+ export PATH="${base_path}"
43
+
44
+ if [[ ! -x "${ensure_sync_script}" && ! -x "${sync_script}" ]]; then
45
+ echo "project systemd bootstrap missing sync helper: ${ensure_sync_script}" >&2
46
+ exit 65
47
+ fi
48
+
49
+ if [[ -x "${ensure_sync_script}" ]]; then
50
+ ensure_args=(--runtime-home "${runtime_home}" --quiet)
51
+ if [[ -n "${source_home}" ]]; then
52
+ ensure_args=(--source-home "${source_home}" "${ensure_args[@]}")
53
+ fi
54
+ if [[ "${always_sync}" == "1" ]]; then
55
+ ensure_args=(--force "${ensure_args[@]}")
56
+ fi
57
+ if [[ "${flow_skill_dir}" == "${runtime_home}"/* ]]; then
58
+ printf 'RUNTIME_SYNC_SKIPPED=active-runtime-home\n'
59
+ else
60
+ bash "${ensure_sync_script}" "${ensure_args[@]}"
61
+ fi
62
+ elif [[ "${always_sync}" == "1" || ! -x "${runtime_heartbeat_script}" ]]; then
63
+ if [[ -z "${source_home}" ]]; then
64
+ source_home="${flow_skill_dir}"
65
+ fi
66
+ bash "${sync_script}" "${source_home}" "${runtime_home}" >/dev/null
67
+ fi
68
+
69
+ if [[ ! -x "${runtime_heartbeat_script}" ]]; then
70
+ echo "project systemd bootstrap missing runtime heartbeat: ${runtime_heartbeat_script}" >&2
71
+ exit 66
72
+ fi
73
+
74
+ exec bash "${runtime_heartbeat_script}"
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ Usage:
7
+ uninstall-project-systemd.sh --profile-id <id> [options]
8
+
9
+ Remove a previously installed systemd user service for an ACP project.
10
+
11
+ Options:
12
+ --profile-id <id> Installed profile id to manage
13
+ --unit-name <name> Override systemd unit name (default: agent-project-<id>.service)
14
+ --help Show this help
15
+ EOF
16
+ }
17
+
18
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ # shellcheck source=/dev/null
20
+ source "${script_dir}/flow-config-lib.sh"
21
+
22
+ profile_id_override=""
23
+ unit_name_override=""
24
+ profile_registry_root_override="${ACP_PROJECT_RUNTIME_PROFILE_REGISTRY_ROOT:-${ACP_PROFILE_REGISTRY_ROOT:-}}"
25
+
26
+ while [[ $# -gt 0 ]]; do
27
+ case "$1" in
28
+ --profile-id) profile_id_override="${2:-}"; shift 2 ;;
29
+ --unit-name) unit_name_override="${2:-}"; shift 2 ;;
30
+ --help|-h) usage; exit 0 ;;
31
+ *) echo "Unknown argument: $1" >&2; usage >&2; exit 64 ;;
32
+ esac
33
+ done
34
+
35
+ if [[ -z "${profile_id_override}" ]]; then
36
+ usage >&2
37
+ exit 64
38
+ fi
39
+
40
+ export ACP_PROJECT_ID="${profile_id_override}"
41
+ export AGENT_PROJECT_ID="${profile_id_override}"
42
+
43
+ if [[ -n "${profile_registry_root_override}" ]]; then
44
+ export ACP_PROFILE_REGISTRY_ROOT="${profile_registry_root_override}"
45
+ fi
46
+
47
+ flow_skill_dir="$(resolve_flow_skill_dir "${BASH_SOURCE[0]}")"
48
+ if ! flow_require_explicit_profile_selection "${flow_skill_dir}" "uninstall-project-systemd.sh"; then
49
+ exit 64
50
+ fi
51
+
52
+ config_yaml="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
53
+ if [[ ! -f "${config_yaml}" ]]; then
54
+ printf 'profile not installed: %s\n' "${profile_id_override}" >&2
55
+ exit 66
56
+ fi
57
+
58
+ profile_id="$(flow_resolve_adapter_id "${config_yaml}")"
59
+ profile_slug="$(printf '%s' "${profile_id}" | tr -c 'A-Za-z0-9._-' '-')"
60
+ home_dir="${ACP_PROJECT_RUNTIME_HOME_DIR:-${HOME:-}}"
61
+ systemd_dir="${ACP_PROJECT_RUNTIME_SYSTEMD_DIR:-${home_dir}/.config/systemd/user}"
62
+ unit_name="${unit_name_override:-${ACP_PROJECT_RUNTIME_SYSTEMD_UNIT:-agent-project-${profile_slug}.service}}"
63
+ unit_file="${systemd_dir}/${unit_name}"
64
+ workspace_dir="${ACP_PROJECT_RUNTIME_WORKSPACE_DIR:-${home_dir}/.agent-runtime/control-plane/workspace}"
65
+ wrapper_path="${workspace_dir}/bin/agent-project-${profile_slug}-systemd.sh"
66
+
67
+ # Check if systemd is available
68
+ if ! command -v systemctl &>/dev/null; then
69
+ echo "systemctl not found. Is systemd installed?" >&2
70
+ exit 1
71
+ fi
72
+
73
+ # Stop and disable the service
74
+ "${SYSTEMCTL_BIN}" --user stop "${unit_name}" 2>&1 || true
75
+ "${SYSTEMCTL_BIN}" --user disable "${unit_name}" 2>&1 || true
76
+
77
+ # Remove unit file
78
+ rm -f "${unit_file}"
79
+
80
+ # Remove wrapper script
81
+ rm -f "${wrapper_path}"
82
+
83
+ printf 'SYSTEMD_UNINSTALL_STATUS=ok\n'
84
+ printf 'PROFILE_ID=%s\n' "${profile_id}"
85
+ printf 'UNIT_NAME=%s\n' "${unit_name}"
86
+ printf 'UNIT_FILE=%s\n' "${unit_file}"
87
+ printf 'WRAPPER=%s\n' "${wrapper_path}"