agent-control-plane 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +141 -28
  2. package/assets/workflow-catalog.json +1 -1
  3. package/bin/pr-risk.sh +22 -7
  4. package/bin/sync-pr-labels.sh +1 -1
  5. package/hooks/heartbeat-hooks.sh +125 -12
  6. package/hooks/issue-reconcile-hooks.sh +1 -1
  7. package/hooks/pr-reconcile-hooks.sh +1 -1
  8. package/npm/bin/agent-control-plane.js +257 -59
  9. package/package.json +39 -32
  10. package/tools/bin/debug-session.sh +106 -0
  11. package/tools/bin/flow-config-lib.sh +1203 -60
  12. package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
  13. package/tools/bin/flow-runtime-doctor.sh +5 -1
  14. package/tools/bin/flow-shell-lib.sh +32 -0
  15. package/tools/bin/github-core-rate-limit-state.sh +77 -0
  16. package/tools/bin/github-write-outbox.sh +470 -0
  17. package/tools/bin/heartbeat-loop-scheduling-lib.sh +7 -7
  18. package/tools/bin/heartbeat-safe-auto.sh +42 -0
  19. package/tools/bin/install-project-launchd.sh +17 -2
  20. package/tools/bin/install-project-systemd.sh +255 -0
  21. package/tools/bin/project-init.sh +21 -1
  22. package/tools/bin/project-launchd-bootstrap.sh +5 -1
  23. package/tools/bin/project-runtimectl.sh +91 -2
  24. package/tools/bin/project-systemd-bootstrap.sh +74 -0
  25. package/tools/bin/scaffold-profile.sh +61 -3
  26. package/tools/bin/uninstall-project-systemd.sh +87 -0
  27. package/tools/dashboard/app.js +228 -6
  28. package/tools/dashboard/dashboard_snapshot.py +55 -0
  29. package/tools/dashboard/issue_queue_state.py +101 -0
  30. package/tools/dashboard/server.py +123 -1
  31. package/tools/dashboard/styles.css +526 -455
  32. package/tools/templates/pr-fix-template.md +3 -1
  33. package/tools/templates/pr-merge-repair-template.md +2 -1
  34. package/references/architecture.md +0 -217
  35. package/references/commands.md +0 -128
  36. package/references/control-plane-map.md +0 -124
  37. package/references/docs-map.md +0 -73
  38. package/references/release-checklist.md +0 -65
  39. package/references/repo-map.md +0 -36
  40. package/tools/bin/agent-cleanup-worktree +0 -247
  41. package/tools/bin/agent-github-update-labels +0 -71
  42. package/tools/bin/agent-init-worktree +0 -216
  43. package/tools/bin/agent-project-archive-run +0 -52
  44. package/tools/bin/agent-project-capture-worker +0 -46
  45. package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
  46. package/tools/bin/agent-project-catch-up-merged-prs +0 -194
  47. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
  48. package/tools/bin/agent-project-cleanup-session +0 -513
  49. package/tools/bin/agent-project-detached-launch +0 -127
  50. package/tools/bin/agent-project-heartbeat-loop +0 -1029
  51. package/tools/bin/agent-project-open-issue-worktree +0 -89
  52. package/tools/bin/agent-project-open-pr-worktree +0 -80
  53. package/tools/bin/agent-project-publish-issue-pr +0 -465
  54. package/tools/bin/agent-project-reconcile-issue-session +0 -1398
  55. package/tools/bin/agent-project-reconcile-pr-session +0 -1230
  56. package/tools/bin/agent-project-retry-state +0 -147
  57. package/tools/bin/agent-project-run-claude-session +0 -805
  58. package/tools/bin/agent-project-run-codex-resilient +0 -955
  59. package/tools/bin/agent-project-run-codex-session +0 -435
  60. package/tools/bin/agent-project-run-kilo-session +0 -369
  61. package/tools/bin/agent-project-run-ollama-session +0 -658
  62. package/tools/bin/agent-project-run-openclaw-session +0 -1309
  63. package/tools/bin/agent-project-run-opencode-session +0 -377
  64. package/tools/bin/agent-project-run-pi-session +0 -479
  65. package/tools/bin/agent-project-sync-anchor-repo +0 -139
  66. package/tools/bin/agent-project-worker-status +0 -188
  67. package/tools/bin/branch-verification-guard.sh +0 -364
  68. package/tools/bin/capture-worker.sh +0 -18
  69. package/tools/bin/cleanup-worktree.sh +0 -52
  70. package/tools/bin/codex-quota +0 -31
  71. package/tools/bin/create-follow-up-issue.sh +0 -114
  72. package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
  73. package/tools/bin/issue-publish-localization-guard.sh +0 -142
  74. package/tools/bin/issue-publish-scope-guard.sh +0 -242
  75. package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
  76. package/tools/bin/issue-resource-class.sh +0 -12
  77. package/tools/bin/kick-scheduler.sh +0 -75
  78. package/tools/bin/label-follow-up-issues.sh +0 -14
  79. package/tools/bin/new-pr-worktree.sh +0 -50
  80. package/tools/bin/new-worktree.sh +0 -49
  81. package/tools/bin/pr-risk.sh +0 -12
  82. package/tools/bin/prepare-worktree.sh +0 -142
  83. package/tools/bin/provider-cooldown-state.sh +0 -204
  84. package/tools/bin/publish-issue-worker.sh +0 -31
  85. package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
  86. package/tools/bin/reconcile-issue-worker.sh +0 -34
  87. package/tools/bin/reconcile-pr-worker.sh +0 -34
  88. package/tools/bin/record-verification.sh +0 -71
  89. package/tools/bin/render-flow-config.sh +0 -98
  90. package/tools/bin/resident-issue-controller-lib.sh +0 -448
  91. package/tools/bin/resident-issue-queue-status.py +0 -35
  92. package/tools/bin/retry-state.sh +0 -31
  93. package/tools/bin/reuse-issue-worktree.sh +0 -121
  94. package/tools/bin/run-codex-bypass.sh +0 -3
  95. package/tools/bin/run-codex-safe.sh +0 -3
  96. package/tools/bin/run-codex-task.sh +0 -280
  97. package/tools/bin/serve-dashboard.sh +0 -5
  98. package/tools/bin/split-retained-slice.sh +0 -124
  99. package/tools/bin/start-issue-worker.sh +0 -943
  100. package/tools/bin/start-pr-fix-worker.sh +0 -491
  101. package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
  102. package/tools/bin/start-pr-review-worker.sh +0 -261
  103. package/tools/bin/start-resident-issue-loop.sh +0 -499
  104. package/tools/bin/update-github-labels.sh +0 -14
  105. package/tools/bin/worker-status.sh +0 -19
  106. package/tools/bin/workflow-catalog.sh +0 -77
@@ -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}"
@@ -34,7 +34,12 @@ runtime copy.
34
34
 
35
35
  Common options:
36
36
  --profile-id <id> Profile id, e.g. billing-api
37
- --repo-slug <owner/repo> GitHub repo slug
37
+ --repo-slug <owner/repo> Forge repo slug
38
+ --forge-provider <github|gitea> Forge provider for this profile
39
+ --gitea-base-url <url> Base URL for a local/self-hosted Gitea instance
40
+ --gitea-token <token> Gitea API token written to profile runtime.env
41
+ --gitea-username <user> Gitea username written to profile runtime.env
42
+ --gitea-password <pass> Gitea password written to profile runtime.env
38
43
  --profile-home <path> Installed profile registry root
39
44
  --repo-root <path> Canonical repo root
40
45
  --agent-repo-root <path> Agent-owned anchor repo root
@@ -68,6 +73,11 @@ EOF
68
73
 
69
74
  profile_id=""
70
75
  repo_slug=""
76
+ forge_provider=""
77
+ gitea_base_url=""
78
+ gitea_token=""
79
+ gitea_username=""
80
+ gitea_password=""
71
81
  profile_home=""
72
82
  repo_root=""
73
83
  agent_repo_root=""
@@ -98,6 +108,11 @@ while [[ $# -gt 0 ]]; do
98
108
  case "$1" in
99
109
  --profile-id) profile_id="${2:-}"; shift 2 ;;
100
110
  --repo-slug) repo_slug="${2:-}"; shift 2 ;;
111
+ --forge-provider) forge_provider="${2:-}"; shift 2 ;;
112
+ --gitea-base-url) gitea_base_url="${2:-}"; shift 2 ;;
113
+ --gitea-token) gitea_token="${2:-}"; shift 2 ;;
114
+ --gitea-username) gitea_username="${2:-}"; shift 2 ;;
115
+ --gitea-password) gitea_password="${2:-}"; shift 2 ;;
101
116
  --profile-home) profile_home="${2:-}"; shift 2 ;;
102
117
  --repo-root) repo_root="${2:-}"; shift 2 ;;
103
118
  --agent-repo-root) agent_repo_root="${2:-}"; shift 2 ;;
@@ -144,6 +159,11 @@ SOURCE_HOME="${source_home:-${ACP_PROJECT_INIT_SOURCE_HOME:-$(cd "${FLOW_SKILL_D
144
159
  RUNTIME_HOME="${runtime_home:-${ACP_PROJECT_INIT_RUNTIME_HOME:-${HOME}/.agent-runtime/runtime-home}}"
145
160
 
146
161
  scaffold_cmd=(bash "${SCAFFOLD_SCRIPT}" --profile-id "${profile_id}" --repo-slug "${repo_slug}")
162
+ [[ -n "${forge_provider}" ]] && scaffold_cmd+=(--forge-provider "${forge_provider}")
163
+ [[ -n "${gitea_base_url}" ]] && scaffold_cmd+=(--gitea-base-url "${gitea_base_url}")
164
+ [[ -n "${gitea_token}" ]] && scaffold_cmd+=(--gitea-token "${gitea_token}")
165
+ [[ -n "${gitea_username}" ]] && scaffold_cmd+=(--gitea-username "${gitea_username}")
166
+ [[ -n "${gitea_password}" ]] && scaffold_cmd+=(--gitea-password "${gitea_password}")
147
167
  [[ -n "${profile_home}" ]] && scaffold_cmd+=(--profile-home "${profile_home}")
148
168
  [[ -n "${repo_root}" ]] && scaffold_cmd+=(--repo-root "${repo_root}")
149
169
  [[ -n "${agent_repo_root}" ]] && scaffold_cmd+=(--agent-repo-root "${agent_repo_root}")
@@ -54,7 +54,11 @@ if [[ -x "${ENSURE_SYNC_SCRIPT}" ]]; then
54
54
  if [[ "${ALWAYS_SYNC}" == "1" ]]; then
55
55
  ensure_args=(--force "${ensure_args[@]}")
56
56
  fi
57
- bash "${ENSURE_SYNC_SCRIPT}" "${ensure_args[@]}"
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
58
62
  elif [[ "${ALWAYS_SYNC}" == "1" || ! -x "${RUNTIME_HEARTBEAT_SCRIPT}" ]]; then
59
63
  if [[ -z "${SOURCE_HOME}" ]]; then
60
64
  SOURCE_HOME="${FLOW_SKILL_DIR}"
@@ -107,9 +107,13 @@ 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"
116
+ SOURCE_REPO_SYNC_STATE_FILE="${STATE_ROOT}/source-repo-main-sync.env"
113
117
 
114
118
  case "${delay_seconds}" in
115
119
  ''|*[!0-9]*) echo "--delay-seconds must be numeric" >&2; exit 64 ;;
@@ -199,6 +203,13 @@ sync_stamp_value() {
199
203
  | sed -e "s/^'//" -e "s/'$//"
200
204
  }
201
205
 
206
+ source_repo_sync_value() {
207
+ local key="${1:?key required}"
208
+ [[ -f "${SOURCE_REPO_SYNC_STATE_FILE}" ]] || return 1
209
+ awk -F= -v target="${key}" '$1 == target {print $2; exit}' "${SOURCE_REPO_SYNC_STATE_FILE}" 2>/dev/null \
210
+ | sed -e "s/^'//" -e "s/'$//"
211
+ }
212
+
202
213
  shared_loop_status_value() {
203
214
  local key="${1:?key required}"
204
215
  local file="${STATE_ROOT}/shared-heartbeat-loop.env"
@@ -377,6 +388,24 @@ launchd_service_state() {
377
388
  fi
378
389
  }
379
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
+
380
409
  print_status() {
381
410
  local heartbeat=""
382
411
  local shared_loop=""
@@ -399,6 +428,15 @@ print_status() {
399
428
  local shared_loop_last_status=""
400
429
  local shared_loop_started_at=""
401
430
  local shared_loop_updated_at=""
431
+ local source_repo_sync_status=""
432
+ local source_repo_sync_updated_at=""
433
+ local source_repo_sync_root=""
434
+ local source_repo_sync_branch=""
435
+ local source_repo_sync_remote=""
436
+ local source_repo_sync_remote_sha=""
437
+ local source_repo_sync_local_sha=""
438
+ local source_repo_sync_detail=""
439
+ local source_repo_sync_aligned="unknown"
402
440
 
403
441
  heartbeat="$(heartbeat_pid)"
404
442
  shared_loop="$(shared_loop_pid)"
@@ -422,6 +460,7 @@ print_status() {
422
460
  fi
423
461
 
424
462
  launchd_state="$(launchd_service_state)"
463
+ systemd_state="$(systemd_service_state)"
425
464
  runtime_sync_status="$(sync_stamp_value "SYNC_STATUS" || true)"
426
465
  runtime_sync_updated_at="$(sync_stamp_value "UPDATED_AT" || true)"
427
466
  runtime_sync_fingerprint="$(sync_stamp_value "SOURCE_FINGERPRINT" || true)"
@@ -429,6 +468,23 @@ print_status() {
429
468
  shared_loop_last_status="$(shared_loop_status_value "STATUS" || true)"
430
469
  shared_loop_started_at="$(shared_loop_status_value "STARTED_AT" || true)"
431
470
  shared_loop_updated_at="$(shared_loop_status_value "UPDATED_AT" || true)"
471
+ source_repo_sync_status="$(source_repo_sync_value "STATUS" || true)"
472
+ source_repo_sync_updated_at="$(source_repo_sync_value "UPDATED_AT" || true)"
473
+ source_repo_sync_root="$(source_repo_sync_value "SOURCE_REPO_ROOT" || true)"
474
+ source_repo_sync_branch="$(source_repo_sync_value "DEFAULT_BRANCH" || true)"
475
+ source_repo_sync_remote="$(source_repo_sync_value "REMOTE_NAME" || true)"
476
+ source_repo_sync_remote_sha="$(source_repo_sync_value "REMOTE_SHA" || true)"
477
+ source_repo_sync_local_sha="$(source_repo_sync_value "LOCAL_SHA" || true)"
478
+ source_repo_sync_detail="$(source_repo_sync_value "DETAIL" || true)"
479
+ if [[ -n "${source_repo_sync_status}" ]]; then
480
+ if [[ -n "${source_repo_sync_remote_sha}" && -n "${source_repo_sync_local_sha}" && "${source_repo_sync_remote_sha}" == "${source_repo_sync_local_sha}" ]]; then
481
+ source_repo_sync_aligned="yes"
482
+ elif [[ "${source_repo_sync_status}" == "blocked" || "${source_repo_sync_status}" == "failed" ]]; then
483
+ source_repo_sync_aligned="no"
484
+ else
485
+ source_repo_sync_aligned="unknown"
486
+ fi
487
+ fi
432
488
 
433
489
  printf 'PROFILE_ID=%s\n' "${PROFILE_ID}"
434
490
  printf 'CONFIG_YAML=%s\n' "${CONFIG_YAML}"
@@ -437,6 +493,7 @@ print_status() {
437
493
  printf 'STATE_ROOT=%s\n' "${STATE_ROOT}"
438
494
  printf 'RUNTIME_STATUS=%s\n' "${runtime_status}"
439
495
  printf 'LAUNCHD_STATE=%s\n' "${launchd_state}"
496
+ printf 'SYSTEMD_STATE=%s\n' "${systemd_state}"
440
497
  printf 'LAUNCHD_LABEL=%s\n' "${LAUNCHD_LABEL}"
441
498
  printf 'LAUNCHD_PLIST=%s\n' "${LAUNCHD_PLIST}"
442
499
  printf 'HEARTBEAT_PID=%s\n' "${heartbeat}"
@@ -459,6 +516,16 @@ print_status() {
459
516
  printf 'RUNTIME_SYNC_STATUS=%s\n' "${runtime_sync_status}"
460
517
  printf 'RUNTIME_SYNC_UPDATED_AT=%s\n' "${runtime_sync_updated_at}"
461
518
  printf 'RUNTIME_SYNC_FINGERPRINT=%s\n' "${runtime_sync_fingerprint}"
519
+ printf 'SOURCE_REPO_SYNC_STATE_FILE=%s\n' "${SOURCE_REPO_SYNC_STATE_FILE}"
520
+ printf 'SOURCE_REPO_SYNC_STATUS=%s\n' "${source_repo_sync_status}"
521
+ printf 'SOURCE_REPO_SYNC_UPDATED_AT=%s\n' "${source_repo_sync_updated_at}"
522
+ printf 'SOURCE_REPO_SYNC_ROOT=%s\n' "${source_repo_sync_root}"
523
+ printf 'SOURCE_REPO_SYNC_BRANCH=%s\n' "${source_repo_sync_branch}"
524
+ printf 'SOURCE_REPO_SYNC_REMOTE=%s\n' "${source_repo_sync_remote}"
525
+ printf 'SOURCE_REPO_SYNC_REMOTE_SHA=%s\n' "${source_repo_sync_remote_sha}"
526
+ printf 'SOURCE_REPO_SYNC_LOCAL_SHA=%s\n' "${source_repo_sync_local_sha}"
527
+ printf 'SOURCE_REPO_SYNC_DETAIL=%s\n' "${source_repo_sync_detail}"
528
+ printf 'SOURCE_REPO_SYNC_ALIGNED=%s\n' "${source_repo_sync_aligned}"
462
529
  }
463
530
 
464
531
  terminate_pid_list() {
@@ -518,7 +585,7 @@ clear_running_labels_after_stop() {
518
585
  fi
519
586
 
520
587
  issue_json="$(flow_github_issue_list_json "${REPO_SLUG}" open 100 2>/dev/null || printf '[]\n')"
521
- if [[ "${issue_json}" == "[]" ]]; then
588
+ if [[ "${issue_json}" == "[]" ]] && ! flow_using_gitea; then
522
589
  issue_json="$(gh issue list -R "${REPO_SLUG}" --state open --limit 100 --json number,labels 2>/dev/null || printf '[]\n')"
523
590
  fi
524
591
  while IFS= read -r number; do
@@ -527,7 +594,7 @@ clear_running_labels_after_stop() {
527
594
  done < <(jq -r '.[] | select(any(.labels[]?; .name == "agent-running")) | .number' <<<"${issue_json}" 2>/dev/null || true)
528
595
 
529
596
  pr_json="$(flow_github_pr_list_json "${REPO_SLUG}" open 100 2>/dev/null || printf '[]\n')"
530
- if [[ "${pr_json}" == "[]" ]]; then
597
+ if [[ "${pr_json}" == "[]" ]] && ! flow_using_gitea; then
531
598
  pr_json="$(gh pr list -R "${REPO_SLUG}" --state open --limit 100 --json number,labels 2>/dev/null || printf '[]\n')"
532
599
  fi
533
600
  while IFS= read -r number; do
@@ -543,6 +610,7 @@ stop_runtime() {
543
610
  local session=""
544
611
  local pid=""
545
612
  local launchd_stopped="no"
613
+ local systemd_stopped="no"
546
614
 
547
615
  while IFS= read -r session; do
548
616
  [[ -n "${session}" ]] || continue
@@ -575,6 +643,11 @@ stop_runtime() {
575
643
  launchd_stopped="yes"
576
644
  fi
577
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
+
578
651
  if [[ -n "${TMUX_BIN}" ]]; then
579
652
  for session in "${tmux_sessions[@]+"${tmux_sessions[@]}"}"; do
580
653
  "${TMUX_BIN}" kill-session -t "${session}" >/dev/null 2>&1 || true
@@ -592,6 +665,7 @@ stop_runtime() {
592
665
  printf 'ACTION=stop\n'
593
666
  printf 'PROFILE_ID=%s\n' "${PROFILE_ID}"
594
667
  printf 'LAUNCHD_STOPPED=%s\n' "${launchd_stopped}"
668
+ printf 'SYSTEMD_STOPPED=%s\n' "${systemd_stopped}"
595
669
  printf 'STOPPED_PID_COUNT=%s\n' "$(printf '%s\n' "${pid_targets[@]+"${pid_targets[@]}"}" | awk 'NF {c+=1} END {print c+0}')"
596
670
  printf 'STOPPED_TMUX_SESSION_COUNT=%s\n' "$(printf '%s\n' "${tmux_sessions[@]+"${tmux_sessions[@]}"}" | awk 'NF {c+=1} END {print c+0}')"
597
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}')"
@@ -635,6 +709,21 @@ start_runtime() {
635
709
  return 0
636
710
  fi
637
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
+
638
727
  kick_output="$(ACP_PROJECT_ID="${PROFILE_ID}" AGENT_PROJECT_ID="${PROFILE_ID}" bash "${KICK_SCRIPT}" "${delay_seconds}")"
639
728
  if wait_for_runtime_start "${start_timeout}"; then
640
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}"