agent-control-plane 0.1.14 → 0.2.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 (53) hide show
  1. package/README.md +323 -349
  2. package/bin/pr-risk.sh +28 -6
  3. package/hooks/heartbeat-hooks.sh +62 -22
  4. package/npm/bin/agent-control-plane.js +434 -12
  5. package/package.json +1 -1
  6. package/references/architecture.md +8 -0
  7. package/references/control-plane-map.md +6 -2
  8. package/references/release-checklist.md +0 -2
  9. package/tools/bin/agent-github-update-labels +6 -1
  10. package/tools/bin/agent-project-catch-up-issue-pr-links +118 -0
  11. package/tools/bin/agent-project-catch-up-merged-prs +77 -21
  12. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +123 -0
  13. package/tools/bin/agent-project-cleanup-session +84 -0
  14. package/tools/bin/agent-project-heartbeat-loop +10 -3
  15. package/tools/bin/agent-project-reconcile-issue-session +45 -12
  16. package/tools/bin/agent-project-reconcile-pr-session +25 -0
  17. package/tools/bin/agent-project-run-claude-session +2 -2
  18. package/tools/bin/agent-project-run-codex-resilient +57 -2
  19. package/tools/bin/agent-project-run-kilo-session +346 -14
  20. package/tools/bin/agent-project-run-ollama-session +658 -0
  21. package/tools/bin/agent-project-run-openclaw-session +73 -25
  22. package/tools/bin/agent-project-run-opencode-session +354 -14
  23. package/tools/bin/agent-project-run-pi-session +479 -0
  24. package/tools/bin/agent-project-worker-status +38 -1
  25. package/tools/bin/flow-config-lib.sh +123 -3
  26. package/tools/bin/flow-resident-worker-lib.sh +1 -1
  27. package/tools/bin/flow-shell-lib.sh +7 -2
  28. package/tools/bin/heartbeat-recovery-preflight.sh +1 -0
  29. package/tools/bin/heartbeat-safe-auto.sh +105 -17
  30. package/tools/bin/install-project-launchd.sh +19 -2
  31. package/tools/bin/prepare-worktree.sh +4 -4
  32. package/tools/bin/profile-activate.sh +2 -2
  33. package/tools/bin/profile-adopt.sh +2 -2
  34. package/tools/bin/project-init.sh +1 -1
  35. package/tools/bin/project-runtimectl.sh +90 -7
  36. package/tools/bin/provider-cooldown-state.sh +14 -14
  37. package/tools/bin/render-flow-config.sh +30 -33
  38. package/tools/bin/run-codex-task.sh +53 -4
  39. package/tools/bin/scaffold-profile.sh +18 -3
  40. package/tools/bin/start-issue-worker.sh +4 -1
  41. package/tools/bin/start-pr-fix-worker.sh +33 -0
  42. package/tools/bin/start-pr-review-worker.sh +34 -0
  43. package/tools/bin/start-resident-issue-loop.sh +5 -4
  44. package/tools/bin/sync-agent-repo.sh +2 -2
  45. package/tools/bin/sync-dependency-baseline.sh +3 -3
  46. package/tools/bin/sync-shared-agent-home.sh +4 -1
  47. package/tools/dashboard/app.js +62 -0
  48. package/tools/dashboard/dashboard_snapshot.py +53 -4
  49. package/tools/dashboard/index.html +5 -1
  50. package/tools/dashboard/styles.css +97 -20
  51. package/tools/templates/pr-fix-template.md +4 -8
  52. package/tools/templates/pr-merge-repair-template.md +4 -8
  53. package/tools/templates/pr-review-template.md +2 -1
@@ -1699,6 +1699,48 @@ flow_provider_pool_openclaw_timeout_seconds() {
1699
1699
  flow_provider_pool_value "${config_file}" "${pool_name}" "openclaw.timeout_seconds"
1700
1700
  }
1701
1701
 
1702
+ flow_provider_pool_ollama_model() {
1703
+ local config_file="${1:?config file required}"
1704
+ local pool_name="${2:?pool name required}"
1705
+
1706
+ flow_provider_pool_value "${config_file}" "${pool_name}" "ollama.model"
1707
+ }
1708
+
1709
+ flow_provider_pool_ollama_base_url() {
1710
+ local config_file="${1:?config file required}"
1711
+ local pool_name="${2:?pool name required}"
1712
+
1713
+ flow_provider_pool_value "${config_file}" "${pool_name}" "ollama.base_url"
1714
+ }
1715
+
1716
+ flow_provider_pool_ollama_timeout_seconds() {
1717
+ local config_file="${1:?config file required}"
1718
+ local pool_name="${2:?pool name required}"
1719
+
1720
+ flow_provider_pool_value "${config_file}" "${pool_name}" "ollama.timeout_seconds"
1721
+ }
1722
+
1723
+ flow_provider_pool_pi_model() {
1724
+ local config_file="${1:?config file required}"
1725
+ local pool_name="${2:?pool name required}"
1726
+
1727
+ flow_provider_pool_value "${config_file}" "${pool_name}" "pi.model"
1728
+ }
1729
+
1730
+ flow_provider_pool_pi_thinking() {
1731
+ local config_file="${1:?config file required}"
1732
+ local pool_name="${2:?pool name required}"
1733
+
1734
+ flow_provider_pool_value "${config_file}" "${pool_name}" "pi.thinking"
1735
+ }
1736
+
1737
+ flow_provider_pool_pi_timeout_seconds() {
1738
+ local config_file="${1:?config file required}"
1739
+ local pool_name="${2:?pool name required}"
1740
+
1741
+ flow_provider_pool_value "${config_file}" "${pool_name}" "pi.timeout_seconds"
1742
+ }
1743
+
1702
1744
  flow_sanitize_provider_key() {
1703
1745
  local raw_key="${1:?raw key required}"
1704
1746
 
@@ -1723,6 +1765,12 @@ flow_provider_pool_model_identity() {
1723
1765
  openclaw)
1724
1766
  flow_provider_pool_openclaw_model "${config_file}" "${pool_name}"
1725
1767
  ;;
1768
+ ollama)
1769
+ flow_provider_pool_ollama_model "${config_file}" "${pool_name}"
1770
+ ;;
1771
+ pi)
1772
+ flow_provider_pool_pi_model "${config_file}" "${pool_name}"
1773
+ ;;
1726
1774
  *)
1727
1775
  printf '\n'
1728
1776
  ;;
@@ -1756,6 +1804,12 @@ flow_provider_pool_state_get() {
1756
1804
  local openclaw_model=""
1757
1805
  local openclaw_thinking=""
1758
1806
  local openclaw_timeout_seconds=""
1807
+ local ollama_model=""
1808
+ local ollama_base_url=""
1809
+ local ollama_timeout_seconds=""
1810
+ local pi_model=""
1811
+ local pi_thinking=""
1812
+ local pi_timeout_seconds=""
1759
1813
 
1760
1814
  backend="$(flow_provider_pool_backend "${config_file}" "${pool_name}")"
1761
1815
  safe_profile="$(flow_provider_pool_safe_profile "${config_file}" "${pool_name}")"
@@ -1769,6 +1823,12 @@ flow_provider_pool_state_get() {
1769
1823
  openclaw_model="$(flow_provider_pool_openclaw_model "${config_file}" "${pool_name}")"
1770
1824
  openclaw_thinking="$(flow_provider_pool_openclaw_thinking "${config_file}" "${pool_name}")"
1771
1825
  openclaw_timeout_seconds="$(flow_provider_pool_openclaw_timeout_seconds "${config_file}" "${pool_name}")"
1826
+ ollama_model="$(flow_provider_pool_ollama_model "${config_file}" "${pool_name}")"
1827
+ ollama_base_url="$(flow_provider_pool_ollama_base_url "${config_file}" "${pool_name}")"
1828
+ ollama_timeout_seconds="$(flow_provider_pool_ollama_timeout_seconds "${config_file}" "${pool_name}")"
1829
+ pi_model="$(flow_provider_pool_pi_model "${config_file}" "${pool_name}")"
1830
+ pi_thinking="$(flow_provider_pool_pi_thinking "${config_file}" "${pool_name}")"
1831
+ pi_timeout_seconds="$(flow_provider_pool_pi_timeout_seconds "${config_file}" "${pool_name}")"
1772
1832
  model="$(flow_provider_pool_model_identity "${config_file}" "${pool_name}")"
1773
1833
 
1774
1834
  case "${backend}" in
@@ -1781,6 +1841,12 @@ flow_provider_pool_state_get() {
1781
1841
  openclaw)
1782
1842
  [[ -n "${openclaw_model}" && -n "${openclaw_thinking}" && -n "${openclaw_timeout_seconds}" ]] || valid="no"
1783
1843
  ;;
1844
+ ollama)
1845
+ [[ -n "${ollama_model}" ]] || valid="no"
1846
+ ;;
1847
+ pi)
1848
+ [[ -n "${pi_model}" ]] || valid="no"
1849
+ ;;
1784
1850
  *)
1785
1851
  valid="no"
1786
1852
  ;;
@@ -1833,6 +1899,12 @@ flow_provider_pool_state_get() {
1833
1899
  printf 'OPENCLAW_MODEL=%s\n' "${openclaw_model}"
1834
1900
  printf 'OPENCLAW_THINKING=%s\n' "${openclaw_thinking}"
1835
1901
  printf 'OPENCLAW_TIMEOUT_SECONDS=%s\n' "${openclaw_timeout_seconds}"
1902
+ printf 'OLLAMA_MODEL=%s\n' "${ollama_model}"
1903
+ printf 'OLLAMA_BASE_URL=%s\n' "${ollama_base_url}"
1904
+ printf 'OLLAMA_TIMEOUT_SECONDS=%s\n' "${ollama_timeout_seconds}"
1905
+ printf 'PI_MODEL=%s\n' "${pi_model}"
1906
+ printf 'PI_THINKING=%s\n' "${pi_thinking}"
1907
+ printf 'PI_TIMEOUT_SECONDS=%s\n' "${pi_timeout_seconds}"
1836
1908
  }
1837
1909
 
1838
1910
  flow_selected_provider_pool_env() {
@@ -2039,11 +2111,18 @@ flow_export_execution_env() {
2039
2111
  local openclaw_model=""
2040
2112
  local openclaw_thinking=""
2041
2113
  local openclaw_timeout=""
2114
+ local openclaw_stall=""
2115
+ local ollama_model=""
2116
+ local ollama_base_url=""
2117
+ local ollama_timeout=""
2118
+ local pi_model=""
2119
+ local pi_thinking=""
2120
+ local pi_timeout=""
2042
2121
 
2043
2122
  repo_id="$(flow_resolve_repo_id "${config_file}")"
2044
2123
  provider_quota_cooldowns="$(flow_resolve_provider_quota_cooldowns "${config_file}")"
2045
2124
  provider_pool_order="$(flow_resolve_provider_pool_order "${config_file}")"
2046
- explicit_coding_worker="${ACP_CODING_WORKER:-${F_LOSNING_CODING_WORKER:-}}"
2125
+ explicit_coding_worker="${ACP_CODING_WORKER:-}"
2047
2126
  if [[ -z "${explicit_coding_worker}" && -n "${provider_pool_order}" ]]; then
2048
2127
  provider_pool_selection="$(flow_selected_provider_pool_env "${config_file}" || true)"
2049
2128
  fi
@@ -2071,11 +2150,18 @@ flow_export_execution_env() {
2071
2150
  openclaw_model="$(flow_kv_get "${provider_pool_selection}" "OPENCLAW_MODEL")"
2072
2151
  openclaw_thinking="$(flow_kv_get "${provider_pool_selection}" "OPENCLAW_THINKING")"
2073
2152
  openclaw_timeout="$(flow_kv_get "${provider_pool_selection}" "OPENCLAW_TIMEOUT_SECONDS")"
2153
+ openclaw_stall="$(flow_kv_get "${provider_pool_selection}" "OPENCLAW_STALL_SECONDS")"
2154
+ ollama_model="$(flow_kv_get "${provider_pool_selection}" "OLLAMA_MODEL")"
2155
+ ollama_base_url="$(flow_kv_get "${provider_pool_selection}" "OLLAMA_BASE_URL")"
2156
+ ollama_timeout="$(flow_kv_get "${provider_pool_selection}" "OLLAMA_TIMEOUT_SECONDS")"
2157
+ pi_model="$(flow_kv_get "${provider_pool_selection}" "PI_MODEL")"
2158
+ pi_thinking="$(flow_kv_get "${provider_pool_selection}" "PI_THINKING")"
2159
+ pi_timeout="$(flow_kv_get "${provider_pool_selection}" "PI_TIMEOUT_SECONDS")"
2074
2160
  else
2075
2161
  if [[ -n "${explicit_coding_worker}" ]]; then
2076
2162
  active_provider_selection_reason="env-override"
2077
2163
  fi
2078
- coding_worker="$(flow_env_or_config "${config_file}" "ACP_CODING_WORKER F_LOSNING_CODING_WORKER" "execution.coding_worker" "")"
2164
+ coding_worker="$(flow_env_or_config "${config_file}" "ACP_CODING_WORKER" "execution.coding_worker" "")"
2079
2165
  safe_profile="$(flow_env_or_config "${config_file}" "ACP_CODEX_PROFILE_SAFE F_LOSNING_CODEX_PROFILE_SAFE" "execution.safe_profile" "")"
2080
2166
  bypass_profile="$(flow_env_or_config "${config_file}" "ACP_CODEX_PROFILE_BYPASS F_LOSNING_CODEX_PROFILE_BYPASS" "execution.bypass_profile" "")"
2081
2167
  claude_model="$(flow_env_or_config "${config_file}" "ACP_CLAUDE_MODEL F_LOSNING_CLAUDE_MODEL" "execution.claude.model" "")"
@@ -2087,10 +2173,16 @@ flow_export_execution_env() {
2087
2173
  openclaw_model="$(flow_env_or_config "${config_file}" "ACP_OPENCLAW_MODEL F_LOSNING_OPENCLAW_MODEL" "execution.openclaw.model" "")"
2088
2174
  openclaw_thinking="$(flow_env_or_config "${config_file}" "ACP_OPENCLAW_THINKING F_LOSNING_OPENCLAW_THINKING" "execution.openclaw.thinking" "")"
2089
2175
  openclaw_timeout="$(flow_env_or_config "${config_file}" "ACP_OPENCLAW_TIMEOUT_SECONDS F_LOSNING_OPENCLAW_TIMEOUT_SECONDS" "execution.openclaw.timeout_seconds" "")"
2176
+ openclaw_stall="$(flow_env_or_config "${config_file}" "ACP_OPENCLAW_STALL_SECONDS F_LOSNING_OPENCLAW_STALL_SECONDS" "execution.openclaw.stall_seconds" "")"
2177
+ ollama_model="$(flow_env_or_config "${config_file}" "ACP_OLLAMA_MODEL F_LOSNING_OLLAMA_MODEL" "execution.ollama.model" "")"
2178
+ ollama_base_url="$(flow_env_or_config "${config_file}" "ACP_OLLAMA_BASE_URL F_LOSNING_OLLAMA_BASE_URL" "execution.ollama.base_url" "")"
2179
+ ollama_timeout="$(flow_env_or_config "${config_file}" "ACP_OLLAMA_TIMEOUT_SECONDS F_LOSNING_OLLAMA_TIMEOUT_SECONDS" "execution.ollama.timeout_seconds" "")"
2180
+ pi_model="$(flow_env_or_config "${config_file}" "ACP_PI_MODEL F_LOSNING_PI_MODEL" "execution.pi.model" "")"
2181
+ pi_thinking="$(flow_env_or_config "${config_file}" "ACP_PI_THINKING F_LOSNING_PI_THINKING" "execution.pi.thinking" "")"
2182
+ pi_timeout="$(flow_env_or_config "${config_file}" "ACP_PI_TIMEOUT_SECONDS F_LOSNING_PI_TIMEOUT_SECONDS" "execution.pi.timeout_seconds" "")"
2090
2183
  fi
2091
2184
 
2092
2185
  if [[ -n "${coding_worker}" ]]; then
2093
- export F_LOSNING_CODING_WORKER="${coding_worker}"
2094
2186
  export ACP_CODING_WORKER="${coding_worker}"
2095
2187
  fi
2096
2188
  if [[ -n "${repo_id}" ]]; then
@@ -2167,6 +2259,34 @@ flow_export_execution_env() {
2167
2259
  export F_LOSNING_OPENCLAW_TIMEOUT_SECONDS="${openclaw_timeout}"
2168
2260
  export ACP_OPENCLAW_TIMEOUT_SECONDS="${openclaw_timeout}"
2169
2261
  fi
2262
+ if [[ -n "${openclaw_stall}" ]]; then
2263
+ export F_LOSNING_OPENCLAW_STALL_SECONDS="${openclaw_stall}"
2264
+ export ACP_OPENCLAW_STALL_SECONDS="${openclaw_stall}"
2265
+ fi
2266
+ if [[ -n "${ollama_model}" ]]; then
2267
+ export F_LOSNING_OLLAMA_MODEL="${ollama_model}"
2268
+ export ACP_OLLAMA_MODEL="${ollama_model}"
2269
+ fi
2270
+ if [[ -n "${ollama_base_url}" ]]; then
2271
+ export F_LOSNING_OLLAMA_BASE_URL="${ollama_base_url}"
2272
+ export ACP_OLLAMA_BASE_URL="${ollama_base_url}"
2273
+ fi
2274
+ if [[ -n "${ollama_timeout}" ]]; then
2275
+ export F_LOSNING_OLLAMA_TIMEOUT_SECONDS="${ollama_timeout}"
2276
+ export ACP_OLLAMA_TIMEOUT_SECONDS="${ollama_timeout}"
2277
+ fi
2278
+ if [[ -n "${pi_model}" ]]; then
2279
+ export F_LOSNING_PI_MODEL="${pi_model}"
2280
+ export ACP_PI_MODEL="${pi_model}"
2281
+ fi
2282
+ if [[ -n "${pi_thinking}" ]]; then
2283
+ export F_LOSNING_PI_THINKING="${pi_thinking}"
2284
+ export ACP_PI_THINKING="${pi_thinking}"
2285
+ fi
2286
+ if [[ -n "${pi_timeout}" ]]; then
2287
+ export F_LOSNING_PI_TIMEOUT_SECONDS="${pi_timeout}"
2288
+ export ACP_PI_TIMEOUT_SECONDS="${pi_timeout}"
2289
+ fi
2170
2290
 
2171
2291
  flow_export_github_cli_auth_env "$(flow_resolve_repo_slug "${config_file}")"
2172
2292
  flow_export_project_env_aliases
@@ -118,7 +118,7 @@ flow_resident_issue_backend_supported() {
118
118
  local backend="${1:-}"
119
119
 
120
120
  case "${backend}" in
121
- codex|openclaw|claude)
121
+ codex|openclaw|claude|ollama|pi)
122
122
  return 0
123
123
  ;;
124
124
  *)
@@ -35,7 +35,6 @@ flow_export_compat_env_aliases() {
35
35
  flow_export_env_alias_if_unset F_LOSNING_MEMORY_DIR ACP_MEMORY_DIR
36
36
  flow_export_env_alias_if_unset F_LOSNING_RETAINED_REPO_ROOT ACP_RETAINED_REPO_ROOT
37
37
  flow_export_env_alias_if_unset F_LOSNING_VSCODE_WORKSPACE_FILE ACP_VSCODE_WORKSPACE_FILE
38
- flow_export_env_alias_if_unset F_LOSNING_CODING_WORKER ACP_CODING_WORKER
39
38
  flow_export_env_alias_if_unset F_LOSNING_CODEX_PROFILE_SAFE ACP_CODEX_PROFILE_SAFE
40
39
  flow_export_env_alias_if_unset F_LOSNING_CODEX_PROFILE_BYPASS ACP_CODEX_PROFILE_BYPASS
41
40
  flow_export_env_alias_if_unset F_LOSNING_CLAUDE_MODEL ACP_CLAUDE_MODEL
@@ -52,6 +51,7 @@ flow_export_compat_env_aliases() {
52
51
  flow_export_env_alias_if_unset F_LOSNING_OPENCLAW_MODEL ACP_OPENCLAW_MODEL
53
52
  flow_export_env_alias_if_unset F_LOSNING_OPENCLAW_THINKING ACP_OPENCLAW_THINKING
54
53
  flow_export_env_alias_if_unset F_LOSNING_OPENCLAW_TIMEOUT_SECONDS ACP_OPENCLAW_TIMEOUT_SECONDS
54
+ flow_export_env_alias_if_unset F_LOSNING_OPENCLAW_STALL_SECONDS ACP_OPENCLAW_STALL_SECONDS
55
55
  flow_export_env_alias_if_unset F_LOSNING_ALLOW_INFRA_CI_BYPASS ACP_ALLOW_INFRA_CI_BYPASS
56
56
  flow_export_env_alias_if_unset F_LOSNING_LOCAL_FIRST_PR_POLICY ACP_LOCAL_FIRST_PR_POLICY
57
57
  flow_export_env_alias_if_unset F_LOSNING_PR_RISK_CACHE_TTL_SECONDS ACP_PR_RISK_CACHE_TTL_SECONDS
@@ -83,7 +83,6 @@ flow_export_canonical_env_aliases() {
83
83
  flow_export_env_alias_if_unset ACP_MEMORY_DIR F_LOSNING_MEMORY_DIR
84
84
  flow_export_env_alias_if_unset ACP_RETAINED_REPO_ROOT F_LOSNING_RETAINED_REPO_ROOT
85
85
  flow_export_env_alias_if_unset ACP_VSCODE_WORKSPACE_FILE F_LOSNING_VSCODE_WORKSPACE_FILE
86
- flow_export_env_alias_if_unset ACP_CODING_WORKER F_LOSNING_CODING_WORKER
87
86
  flow_export_env_alias_if_unset ACP_CODEX_PROFILE_SAFE F_LOSNING_CODEX_PROFILE_SAFE
88
87
  flow_export_env_alias_if_unset ACP_CODEX_PROFILE_BYPASS F_LOSNING_CODEX_PROFILE_BYPASS
89
88
  flow_export_env_alias_if_unset ACP_CLAUDE_MODEL F_LOSNING_CLAUDE_MODEL
@@ -100,6 +99,7 @@ flow_export_canonical_env_aliases() {
100
99
  flow_export_env_alias_if_unset ACP_OPENCLAW_MODEL F_LOSNING_OPENCLAW_MODEL
101
100
  flow_export_env_alias_if_unset ACP_OPENCLAW_THINKING F_LOSNING_OPENCLAW_THINKING
102
101
  flow_export_env_alias_if_unset ACP_OPENCLAW_TIMEOUT_SECONDS F_LOSNING_OPENCLAW_TIMEOUT_SECONDS
102
+ flow_export_env_alias_if_unset ACP_OPENCLAW_STALL_SECONDS F_LOSNING_OPENCLAW_STALL_SECONDS
103
103
  flow_export_env_alias_if_unset ACP_ALLOW_INFRA_CI_BYPASS F_LOSNING_ALLOW_INFRA_CI_BYPASS
104
104
  flow_export_env_alias_if_unset ACP_LOCAL_FIRST_PR_POLICY F_LOSNING_LOCAL_FIRST_PR_POLICY
105
105
  flow_export_env_alias_if_unset ACP_PR_RISK_CACHE_TTL_SECONDS F_LOSNING_PR_RISK_CACHE_TTL_SECONDS
@@ -221,6 +221,11 @@ resolve_shared_agent_home() {
221
221
  flow_root="$(resolve_flow_skill_dir "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}")"
222
222
  fi
223
223
 
224
+ if flow_is_skill_root "${flow_root}"; then
225
+ flow_print_dir "${flow_root}"
226
+ return 0
227
+ fi
228
+
224
229
  flow_print_dir "${flow_root}/../../.."
225
230
  }
226
231
 
@@ -80,6 +80,7 @@ run_preflight_test "heartbeat-no-tmux-sessions" "${TEST_DIR}/test-heartbeat-safe
80
80
  run_preflight_test "heartbeat-static-capacity-without-quota-cache" "${TEST_DIR}/test-heartbeat-safe-auto-static-capacity-without-quota-cache.sh"
81
81
  run_preflight_test "heartbeat-openclaw-skips-codex-quota" "${TEST_DIR}/test-heartbeat-safe-auto-openclaw-skips-codex-quota.sh"
82
82
  run_preflight_test "heartbeat-empty-schedule-label-sync" "${TEST_DIR}/test-heartbeat-sync-issue-labels-empty-schedule.sh"
83
+ run_preflight_test "heartbeat-open-issue-terminal-sync" "${TEST_DIR}/test-heartbeat-sync-open-agent-issues-terminal-clears-running.sh"
83
84
  run_preflight_test "heartbeat-open-pr-terminal-sync" "${TEST_DIR}/test-heartbeat-sync-open-agent-prs-terminal-clears-running.sh"
84
85
  run_preflight_test "heartbeat-pr-launch-dedup" "${TEST_DIR}/test-heartbeat-loop-pr-launch-dedup.sh"
85
86
  run_preflight_test "heartbeat-auth-wait-capacity" "${TEST_DIR}/test-heartbeat-loop-auth-wait-does-not-consume-capacity.sh"
@@ -17,6 +17,8 @@ flow_export_execution_env "${CONFIG_YAML}"
17
17
  AGENT_ROOT="$(flow_resolve_agent_root "${CONFIG_YAML}")"
18
18
  RUNS_ROOT="$(flow_resolve_runs_root "${CONFIG_YAML}")"
19
19
  STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
20
+ HISTORY_ROOT="$(flow_resolve_history_root "${CONFIG_YAML}")"
21
+ WORKTREE_ROOT="$(flow_resolve_worktree_root "${CONFIG_YAML}")"
20
22
  MEMORY_DIR="${ACP_MEMORY_DIR:-${F_LOSNING_MEMORY_DIR:-${AGENT_CONTROL_PLANE_WORKSPACE:-$HOME/.agent-runtime/control-plane/workspace}/memory}}"
21
23
  REPO_SLUG="$(flow_resolve_repo_slug "${CONFIG_YAML}")"
22
24
  MAX_CONCURRENT_WORKERS="${ACP_MAX_CONCURRENT_WORKERS:-${F_LOSNING_MAX_CONCURRENT_WORKERS:-20}}"
@@ -29,7 +31,7 @@ MAX_CONCURRENT_BLOCKED_RECOVERY_ISSUE_WORKERS="${ACP_MAX_CONCURRENT_BLOCKED_RECO
29
31
  BLOCKED_RECOVERY_COOLDOWN_SECONDS="${ACP_BLOCKED_RECOVERY_COOLDOWN_SECONDS:-${F_LOSNING_BLOCKED_RECOVERY_COOLDOWN_SECONDS:-900}}"
30
32
  MAX_OPEN_AGENT_PRS_FOR_RECURRING="${ACP_MAX_OPEN_AGENT_PRS_FOR_RECURRING:-${F_LOSNING_MAX_OPEN_AGENT_PRS_FOR_RECURRING:-12}}"
31
33
  MAX_LAUNCHES_PER_HEARTBEAT="${ACP_MAX_LAUNCHES_PER_HEARTBEAT:-${F_LOSNING_MAX_LAUNCHES_PER_HEARTBEAT:-$MAX_CONCURRENT_WORKERS}}"
32
- CODING_WORKER="${ACP_CODING_WORKER:-${F_LOSNING_CODING_WORKER:-codex}}"
34
+ CODING_WORKER="${ACP_CODING_WORKER:-codex}"
33
35
  # The catchup and shared heartbeat passes can legitimately take a few minutes
34
36
  # once they reconcile stale sessions, sync labels, and launch multiple workers.
35
37
  CATCHUP_TIMEOUT_SECONDS="${ACP_CATCHUP_TIMEOUT_SECONDS:-${F_LOSNING_CATCHUP_TIMEOUT_SECONDS:-180}}"
@@ -67,9 +69,12 @@ AGENT_WORKTREE_AUDIT_TIMEOUT_SECONDS="${ACP_AGENT_WORKTREE_AUDIT_TIMEOUT_SECONDS
67
69
  LOCK_DIR="${STATE_ROOT}/heartbeat-loop.lock"
68
70
  PID_FILE="${LOCK_DIR}/pid"
69
71
  SHARED_LOOP_PID_FILE="${STATE_ROOT}/shared-heartbeat-loop.pid"
72
+ SHARED_LOOP_STATUS_FILE="${STATE_ROOT}/shared-heartbeat-loop.env"
70
73
  QUOTA_LOCK_DIR="${STATE_ROOT}/quota-preflight.lock"
71
74
  QUOTA_PID_FILE="${QUOTA_LOCK_DIR}/pid"
72
75
 
76
+ mkdir -p "${AGENT_ROOT}" "${RUNS_ROOT}" "${STATE_ROOT}" "${HISTORY_ROOT}" "${WORKTREE_ROOT}" "${MEMORY_DIR}"
77
+
73
78
  acquire_lock() {
74
79
  mkdir -p "${STATE_ROOT}"
75
80
 
@@ -124,6 +129,29 @@ release_quota_lock() {
124
129
  rm -rf "${QUOTA_LOCK_DIR}"
125
130
  }
126
131
 
132
+ write_shared_loop_status() {
133
+ local state="${1:-}"
134
+ local status="${2:-}"
135
+ local timestamp=""
136
+ local tmp_file=""
137
+
138
+ timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
139
+ mkdir -p "${STATE_ROOT}"
140
+ tmp_file="$(mktemp)"
141
+ if [[ -f "${SHARED_LOOP_STATUS_FILE}" ]]; then
142
+ grep -Ev '^(STATE|STATUS|STARTED_AT|UPDATED_AT)=' "${SHARED_LOOP_STATUS_FILE}" >"${tmp_file}" || true
143
+ fi
144
+ printf 'STATE=%s\n' "${state}" >>"${tmp_file}"
145
+ if [[ -n "${status}" ]]; then
146
+ printf 'STATUS=%s\n' "${status}" >>"${tmp_file}"
147
+ fi
148
+ if [[ "${state}" == "running" ]]; then
149
+ printf 'STARTED_AT=%s\n' "${timestamp}" >>"${tmp_file}"
150
+ fi
151
+ printf 'UPDATED_AT=%s\n' "${timestamp}" >>"${tmp_file}"
152
+ mv "${tmp_file}" "${SHARED_LOOP_STATUS_FILE}"
153
+ }
154
+
127
155
  run_with_timeout() {
128
156
  local timeout_seconds="${1:?timeout seconds required}"
129
157
  shift
@@ -530,6 +558,7 @@ fi
530
558
  derive_dynamic_limits
531
559
 
532
560
  printf '[%s] shared heartbeat loop start\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
561
+ write_shared_loop_status "running" ""
533
562
  if ACP_TIMEOUT_CHILD_PID_FILE="${SHARED_LOOP_PID_FILE}" \
534
563
  F_LOSNING_TIMEOUT_CHILD_PID_FILE="${SHARED_LOOP_PID_FILE}" \
535
564
  run_with_timeout "${HEARTBEAT_LOOP_TIMEOUT_SECONDS}" \
@@ -563,9 +592,11 @@ printf '[%s] shared heartbeat loop start\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
563
592
  --heavy-running-label "E2E_ISSUE" \
564
593
  --heavy-deferred-key "E2E_DEFERRED" \
565
594
  --heavy-deferred-message "E2E-heavy issues remain queued until the single e2e slot is free."; then
595
+ write_shared_loop_status "idle" "0"
566
596
  printf '[%s] shared heartbeat loop end status=0\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
567
597
  else
568
598
  loop_status=$?
599
+ write_shared_loop_status "idle" "${loop_status}"
569
600
  if [[ "${loop_status}" -eq 124 ]]; then
570
601
  printf 'HEARTBEAT_LOOP_TIMEOUT=yes\n'
571
602
  fi
@@ -573,21 +604,78 @@ else
573
604
  exit "${loop_status}"
574
605
  fi
575
606
 
576
- printf '[%s] merged-pr catchup start\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
577
- if run_with_timeout "${CATCHUP_TIMEOUT_SECONDS}" \
578
- env \
579
- ACP_RUNS_ROOT="$RUNS_ROOT" \
580
- F_LOSNING_RUNS_ROOT="$RUNS_ROOT" \
581
- bash "${FLOW_TOOLS_DIR}/agent-project-catch-up-merged-prs" \
582
- --repo-slug "$REPO_SLUG" \
583
- --state-root "$STATE_ROOT" \
584
- --hook-file "$HOOK_FILE" \
585
- --limit 100; then
586
- printf '[%s] merged-pr catchup end status=0\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
587
- else
588
- catchup_status=$?
589
- if [[ "${catchup_status}" -eq 124 ]]; then
590
- printf 'CATCHUP_TIMEOUT=yes\n'
607
+ # ── Throttled catch-up passes ──────────────────────────────────────────────────
608
+ # These scripts fetch merged/closed PRs and linked issues which change rarely.
609
+ # Run them at most once every CATCHUP_INTERVAL_SECONDS (default 300 = 5 min)
610
+ # to avoid burning API quota on every heartbeat cycle.
611
+ CATCHUP_INTERVAL_SECONDS="${ACP_CATCHUP_INTERVAL_SECONDS:-${F_LOSNING_CATCHUP_INTERVAL_SECONDS:-300}}"
612
+ CATCHUP_STAMP_FILE="${STATE_ROOT}/last-catchup-timestamp"
613
+ _catchup_now="$(date +%s)"
614
+ _catchup_last="0"
615
+ if [[ -f "${CATCHUP_STAMP_FILE}" ]]; then
616
+ _catchup_last="$(cat "${CATCHUP_STAMP_FILE}" 2>/dev/null || echo 0)"
617
+ fi
618
+ _catchup_age=$(( _catchup_now - _catchup_last ))
619
+
620
+ if [[ "${_catchup_age}" -ge "${CATCHUP_INTERVAL_SECONDS}" ]]; then
621
+ printf '[%s] merged-pr catchup start\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
622
+ if run_with_timeout "${CATCHUP_TIMEOUT_SECONDS}" \
623
+ env \
624
+ ACP_RUNS_ROOT="$RUNS_ROOT" \
625
+ F_LOSNING_RUNS_ROOT="$RUNS_ROOT" \
626
+ bash "${FLOW_TOOLS_DIR}/agent-project-catch-up-merged-prs" \
627
+ --repo-slug "$REPO_SLUG" \
628
+ --state-root "$STATE_ROOT" \
629
+ --hook-file "$HOOK_FILE" \
630
+ --limit 100; then
631
+ printf '[%s] merged-pr catchup end status=0\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
632
+ else
633
+ catchup_status=$?
634
+ if [[ "${catchup_status}" -eq 124 ]]; then
635
+ printf 'CATCHUP_TIMEOUT=yes\n'
636
+ fi
637
+ printf '[%s] merged-pr catchup end status=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "${catchup_status}"
638
+ fi
639
+
640
+ printf '[%s] linked-pr issue catchup start\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
641
+ if run_with_timeout "${CATCHUP_TIMEOUT_SECONDS}" \
642
+ env \
643
+ ACP_RUNS_ROOT="$RUNS_ROOT" \
644
+ F_LOSNING_RUNS_ROOT="$RUNS_ROOT" \
645
+ bash "${FLOW_TOOLS_DIR}/agent-project-catch-up-issue-pr-links" \
646
+ --repo-slug "$REPO_SLUG" \
647
+ --state-root "$STATE_ROOT" \
648
+ --hook-file "$HOOK_FILE" \
649
+ --limit 100; then
650
+ printf '[%s] linked-pr issue catchup end status=0\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
651
+ else
652
+ linked_issue_catchup_status=$?
653
+ if [[ "${linked_issue_catchup_status}" -eq 124 ]]; then
654
+ printf 'LINKED_ISSUE_CATCHUP_TIMEOUT=yes\n'
655
+ fi
656
+ printf '[%s] linked-pr issue catchup end status=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "${linked_issue_catchup_status}"
657
+ fi
658
+
659
+ printf '[%s] scheduled-issue retry catchup start\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
660
+ if run_with_timeout "${CATCHUP_TIMEOUT_SECONDS}" \
661
+ env \
662
+ ACP_RUNS_ROOT="$RUNS_ROOT" \
663
+ F_LOSNING_RUNS_ROOT="$RUNS_ROOT" \
664
+ bash "${FLOW_TOOLS_DIR}/agent-project-catch-up-scheduled-issue-retries" \
665
+ --repo-slug "$REPO_SLUG" \
666
+ --state-root "$STATE_ROOT" \
667
+ --hook-file "$HOOK_FILE" \
668
+ --limit 100; then
669
+ printf '[%s] scheduled-issue retry catchup end status=0\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
670
+ else
671
+ scheduled_issue_catchup_status=$?
672
+ if [[ "${scheduled_issue_catchup_status}" -eq 124 ]]; then
673
+ printf 'SCHEDULED_ISSUE_CATCHUP_TIMEOUT=yes\n'
674
+ fi
675
+ printf '[%s] scheduled-issue retry catchup end status=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "${scheduled_issue_catchup_status}"
591
676
  fi
592
- printf '[%s] merged-pr catchup end status=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "${catchup_status}"
677
+
678
+ printf '%s' "${_catchup_now}" >"${CATCHUP_STAMP_FILE}"
679
+ else
680
+ printf '[%s] catchup skipped (age=%ss, interval=%ss)\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "${_catchup_age}" "${CATCHUP_INTERVAL_SECONDS}"
593
681
  fi
@@ -54,7 +54,7 @@ build_launchd_base_path() {
54
54
  local tool_name=""
55
55
  local tool_dir=""
56
56
 
57
- for tool_name in node gh git python3 openclaw codex claude; do
57
+ for tool_name in node gh git python3 openclaw codex claude ollama pi crush kilo; do
58
58
  tool_dir="$(resolved_tool_dir "${tool_name}" || true)"
59
59
  append_path_dir path_value "${tool_dir}"
60
60
  done
@@ -112,7 +112,14 @@ fi
112
112
  PROFILE_ID="$(flow_resolve_adapter_id "${CONFIG_YAML}")"
113
113
  profile_slug="$(printf '%s' "${PROFILE_ID}" | tr -c 'A-Za-z0-9._-' '-')"
114
114
  HOME_DIR="${ACP_PROJECT_RUNTIME_HOME_DIR:-${HOME:-}}"
115
- SOURCE_HOME="${ACP_PROJECT_RUNTIME_SOURCE_HOME:-$(cd "${FLOW_SKILL_DIR}/../../.." && pwd)}"
115
+ SOURCE_HOME="${ACP_PROJECT_RUNTIME_SOURCE_HOME:-}"
116
+ if [[ -z "${SOURCE_HOME}" ]]; then
117
+ if flow_is_skill_root "${FLOW_SKILL_DIR}"; then
118
+ SOURCE_HOME="${FLOW_SKILL_DIR}"
119
+ else
120
+ SOURCE_HOME="$(cd "${FLOW_SKILL_DIR}/../../.." && pwd)"
121
+ fi
122
+ fi
116
123
  RUNTIME_HOME="${ACP_PROJECT_RUNTIME_RUNTIME_HOME:-${HOME_DIR}/.agent-runtime/runtime-home}"
117
124
  WORKSPACE_DIR="${ACP_PROJECT_RUNTIME_WORKSPACE_DIR:-${HOME_DIR}/.agent-runtime/control-plane/workspace}"
118
125
  PROFILE_REGISTRY_ROOT="${ACP_PROJECT_RUNTIME_PROFILE_REGISTRY_ROOT:-${ACP_PROFILE_REGISTRY_ROOT:-${HOME_DIR}/.agent-runtime/control-plane/profiles}}"
@@ -120,6 +127,7 @@ LAUNCH_AGENTS_DIR="${ACP_PROJECT_RUNTIME_LAUNCH_AGENTS_DIR:-${HOME_DIR}/Library/
120
127
  LOG_DIR="${ACP_PROJECT_RUNTIME_LOG_DIR:-${HOME_DIR}/.agent-runtime/logs}"
121
128
  LABEL="${label_override:-${ACP_PROJECT_RUNTIME_LAUNCHD_LABEL:-ai.agent.project.${profile_slug}}}"
122
129
  BASE_PATH="$(build_launchd_base_path)"
130
+ CODING_WORKER_OVERRIDE="${ACP_PROJECT_RUNTIME_CODING_WORKER:-${ACP_CODING_WORKER:-}}"
123
131
  SYNC_SCRIPT="${ACP_PROJECT_RUNTIME_SYNC_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/sync-shared-agent-home.sh}"
124
132
  BOOTSTRAP_SCRIPT="${ACP_PROJECT_RUNTIME_BOOTSTRAP_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/project-launchd-bootstrap.sh}"
125
133
  SUPERVISOR_SCRIPT="${ACP_PROJECT_RUNTIME_SUPERVISOR_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/project-runtime-supervisor.sh}"
@@ -152,6 +160,15 @@ export AGENT_PROJECT_ID='${PROFILE_ID}'
152
160
  export ACP_PROJECT_RUNTIME_PATH='${BASE_PATH}'
153
161
  export ACP_PROJECT_RUNTIME_SYNC_SCRIPT='${SYNC_SCRIPT}'
154
162
  export ACP_PROFILE_REGISTRY_ROOT='${PROFILE_REGISTRY_ROOT}'
163
+ EOF
164
+
165
+ if [[ -n "${CODING_WORKER_OVERRIDE}" ]]; then
166
+ cat >>"${WRAPPER_PATH}" <<EOF
167
+ export ACP_CODING_WORKER='${CODING_WORKER_OVERRIDE}'
168
+ EOF
169
+ fi
170
+
171
+ cat >>"${WRAPPER_PATH}" <<EOF
155
172
  exec bash '${SUPERVISOR_SCRIPT}' --bootstrap-script '${BOOTSTRAP_SCRIPT}' --pid-file '${SUPERVISOR_PID_FILE}' --delay-seconds '${delay_seconds}' --interval-seconds '${interval_seconds}'
156
173
  EOF
157
174
  chmod +x "${WRAPPER_PATH}"
@@ -9,10 +9,10 @@ CONFIG_YAML="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
9
9
  AGENT_REPO_ROOT="$(flow_resolve_agent_repo_root "${CONFIG_YAML}")"
10
10
  CANONICAL_REPO_ROOT="$(flow_resolve_repo_root "${CONFIG_YAML}")"
11
11
  DEFAULT_DEPENDENCY_SOURCE_ROOT="$CANONICAL_REPO_ROOT"
12
- DEPENDENCY_SOURCE_ROOT="${ACP_DEPENDENCY_SOURCE_ROOT:-${F_LOSNING_DEPENDENCY_SOURCE_ROOT:-$DEFAULT_DEPENDENCY_SOURCE_ROOT}}"
13
- SYNC_DEPENDENCY_BASELINE_SCRIPT="${ACP_SYNC_DEPENDENCY_BASELINE_SCRIPT:-${F_LOSNING_SYNC_DEPENDENCY_BASELINE_SCRIPT:-${SCRIPT_DIR}/sync-dependency-baseline.sh}}"
14
- PACKAGE_MANAGER_BIN="${ACP_PACKAGE_MANAGER_BIN:-${F_LOSNING_PACKAGE_MANAGER_BIN:-pnpm}}"
15
- LOCAL_WORKSPACE_INSTALL="${ACP_WORKTREE_LOCAL_INSTALL:-${F_LOSNING_WORKTREE_LOCAL_INSTALL:-false}}"
12
+ DEPENDENCY_SOURCE_ROOT="${ACP_DEPENDENCY_SOURCE_ROOT:-$DEFAULT_DEPENDENCY_SOURCE_ROOT}"
13
+ SYNC_DEPENDENCY_BASELINE_SCRIPT="${ACP_SYNC_DEPENDENCY_BASELINE_SCRIPT:-${SCRIPT_DIR}/sync-dependency-baseline.sh}"
14
+ PACKAGE_MANAGER_BIN="${ACP_PACKAGE_MANAGER_BIN:-pnpm}"
15
+ LOCAL_WORKSPACE_INSTALL="${ACP_WORKTREE_LOCAL_INSTALL:-false}"
16
16
  WORKTREE="${1:?usage: prepare-worktree.sh WORKTREE}"
17
17
 
18
18
  realpath_safe() {
@@ -56,8 +56,8 @@ AGENT_REPO_ROOT="$(flow_resolve_agent_repo_root "${CONFIG_YAML}")"
56
56
  WORKTREE_ROOT="$(flow_resolve_worktree_root "${CONFIG_YAML}")"
57
57
  RUNS_ROOT="$(flow_resolve_runs_root "${CONFIG_YAML}")"
58
58
  STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
59
- CODING_WORKER="${ACP_CODING_WORKER:-${F_LOSNING_CODING_WORKER:-codex}}"
60
- ACTIVE_PROVIDER_POOL_NAME="${ACP_ACTIVE_PROVIDER_POOL_NAME:-${F_LOSNING_ACTIVE_PROVIDER_POOL_NAME:-}}"
59
+ CODING_WORKER="${ACP_CODING_WORKER:-codex}"
60
+ ACTIVE_PROVIDER_POOL_NAME="${ACP_ACTIVE_PROVIDER_POOL_NAME:-}"
61
61
 
62
62
  if [[ "${exports_only}" == "1" ]]; then
63
63
  printf 'export ACP_PROJECT_ID=%q
@@ -69,8 +69,8 @@ HISTORY_ROOT="$(flow_resolve_history_root "${CONFIG_YAML}")"
69
69
  WORKTREE_ROOT="$(flow_resolve_worktree_root "${CONFIG_YAML}")"
70
70
  RETAINED_REPO_ROOT="$(flow_resolve_retained_repo_root "${CONFIG_YAML}")"
71
71
  VSCODE_WORKSPACE_FILE="$(flow_resolve_vscode_workspace_file "${CONFIG_YAML}")"
72
- REMOTE_NAME="${ACP_REMOTE_NAME:-${F_LOSNING_REMOTE_NAME:-origin}}"
73
- SOURCE_REPO_ROOT="${source_repo_root_override:-${ACP_SOURCE_REPO_ROOT:-${F_LOSNING_SOURCE_REPO_ROOT:-${RETAINED_REPO_ROOT}}}}"
72
+ REMOTE_NAME="${ACP_REMOTE_NAME:-origin}"
73
+ SOURCE_REPO_ROOT="${source_repo_root_override:-${ACP_SOURCE_REPO_ROOT:-${RETAINED_REPO_ROOT}}}"
74
74
  PROFILE_LINK="${AGENT_ROOT}/control-plane.yaml"
75
75
  WORKSPACE_LINK="${AGENT_ROOT}/workspace.code-workspace"
76
76
  INSTALLED_PROFILE_DIR="$(dirname "${CONFIG_YAML}")"
@@ -42,7 +42,7 @@ Common options:
42
42
  --worktree-root <path> Worktree parent root
43
43
  --retained-repo-root <path> Retained/manual checkout root
44
44
  --vscode-workspace-file <path> VS Code workspace file
45
- --coding-worker <codex|openclaw|claude>
45
+ --coding-worker <codex|openclaw|claude|ollama|pi|opencode|kilo>
46
46
  --claude-model <model>
47
47
  --claude-permission-mode <mode>
48
48
  --claude-effort <level>