agent-control-plane 0.1.9 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/heartbeat-hooks.sh +97 -8
- package/package.json +8 -2
- package/references/commands.md +1 -0
- package/tools/bin/agent-project-cleanup-session +133 -0
- package/tools/bin/agent-project-publish-issue-pr +178 -62
- package/tools/bin/agent-project-reconcile-issue-session +171 -3
- package/tools/bin/agent-project-run-codex-resilient +121 -16
- package/tools/bin/agent-project-run-codex-session +60 -10
- package/tools/bin/agent-project-run-openclaw-session +82 -8
- package/tools/bin/cleanup-worktree.sh +4 -1
- package/tools/bin/dashboard-launchd-bootstrap.sh +16 -4
- package/tools/bin/ensure-runtime-sync.sh +182 -0
- package/tools/bin/flow-config-lib.sh +76 -30
- package/tools/bin/flow-resident-worker-lib.sh +28 -2
- package/tools/bin/flow-shell-lib.sh +15 -1
- package/tools/bin/heartbeat-safe-auto.sh +32 -0
- package/tools/bin/issue-publish-localization-guard.sh +142 -0
- package/tools/bin/project-launchd-bootstrap.sh +17 -4
- package/tools/bin/project-runtime-supervisor.sh +7 -1
- package/tools/bin/project-runtimectl.sh +78 -15
- package/tools/bin/reuse-issue-worktree.sh +46 -0
- package/tools/bin/start-issue-worker.sh +76 -6
- package/tools/bin/start-resident-issue-loop.sh +1 -0
- package/tools/bin/sync-shared-agent-home.sh +26 -0
- package/tools/bin/test-smoke.sh +6 -1
- package/tools/dashboard/app.js +71 -1
- package/tools/dashboard/dashboard_snapshot.py +74 -0
- package/tools/dashboard/styles.css +43 -0
- package/tools/templates/issue-prompt-template.md +18 -66
- package/tools/templates/legacy/issue-prompt-template-pre-slim.md +109 -0
- package/bin/audit-issue-routing.sh +0 -74
- package/tools/bin/audit-agent-worktrees.sh +0 -310
- package/tools/bin/audit-issue-routing.sh +0 -11
- package/tools/bin/audit-retained-layout.sh +0 -58
- package/tools/bin/audit-retained-overlap.sh +0 -135
- package/tools/bin/audit-retained-worktrees.sh +0 -228
- package/tools/bin/check-skill-contracts.sh +0 -324
|
@@ -4,13 +4,14 @@ set -euo pipefail
|
|
|
4
4
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
5
|
FLOW_SKILL_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
|
6
6
|
HOME_DIR="${ACP_PROJECT_RUNTIME_HOME_DIR:-${HOME:-}}"
|
|
7
|
-
SOURCE_HOME="${ACP_PROJECT_RUNTIME_SOURCE_HOME
|
|
7
|
+
SOURCE_HOME="${ACP_PROJECT_RUNTIME_SOURCE_HOME:-}"
|
|
8
8
|
RUNTIME_HOME="${ACP_PROJECT_RUNTIME_RUNTIME_HOME:-${HOME_DIR}/.agent-runtime/runtime-home}"
|
|
9
9
|
PROFILE_REGISTRY_ROOT="${ACP_PROJECT_RUNTIME_PROFILE_REGISTRY_ROOT:-${ACP_PROFILE_REGISTRY_ROOT:-${HOME_DIR}/.agent-runtime/control-plane/profiles}}"
|
|
10
10
|
PROFILE_ID="${ACP_PROJECT_RUNTIME_PROFILE_ID:-${ACP_PROJECT_ID:-${AGENT_PROJECT_ID:-}}}"
|
|
11
11
|
ENV_FILE="${ACP_PROJECT_RUNTIME_ENV_FILE:-${PROFILE_REGISTRY_ROOT}/${PROFILE_ID}/runtime.env}"
|
|
12
12
|
BASE_PATH="${ACP_PROJECT_RUNTIME_PATH:-/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin}"
|
|
13
13
|
SYNC_SCRIPT="${ACP_PROJECT_RUNTIME_SYNC_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/sync-shared-agent-home.sh}"
|
|
14
|
+
ENSURE_SYNC_SCRIPT="${ACP_PROJECT_RUNTIME_ENSURE_SYNC_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/ensure-runtime-sync.sh}"
|
|
14
15
|
RUNTIME_HEARTBEAT_SCRIPT="${ACP_PROJECT_RUNTIME_HEARTBEAT_SCRIPT:-${RUNTIME_HOME}/skills/openclaw/agent-control-plane/tools/bin/heartbeat-safe-auto.sh}"
|
|
15
16
|
ALWAYS_SYNC="${ACP_PROJECT_RUNTIME_ALWAYS_SYNC:-0}"
|
|
16
17
|
|
|
@@ -37,12 +38,24 @@ if [[ -f "${ENV_FILE}" ]]; then
|
|
|
37
38
|
set +a
|
|
38
39
|
fi
|
|
39
40
|
|
|
40
|
-
if [[ ! -x "${SYNC_SCRIPT}" ]]; then
|
|
41
|
-
echo "project launchd bootstrap missing sync
|
|
41
|
+
if [[ ! -x "${ENSURE_SYNC_SCRIPT}" && ! -x "${SYNC_SCRIPT}" ]]; then
|
|
42
|
+
echo "project launchd bootstrap missing sync helper: ${ENSURE_SYNC_SCRIPT}" >&2
|
|
42
43
|
exit 65
|
|
43
44
|
fi
|
|
44
45
|
|
|
45
|
-
if [[
|
|
46
|
+
if [[ -x "${ENSURE_SYNC_SCRIPT}" ]]; then
|
|
47
|
+
ensure_args=(--runtime-home "${RUNTIME_HOME}" --quiet)
|
|
48
|
+
if [[ -n "${SOURCE_HOME}" ]]; then
|
|
49
|
+
ensure_args=(--source-home "${SOURCE_HOME}" "${ensure_args[@]}")
|
|
50
|
+
fi
|
|
51
|
+
if [[ "${ALWAYS_SYNC}" == "1" ]]; then
|
|
52
|
+
ensure_args=(--force "${ensure_args[@]}")
|
|
53
|
+
fi
|
|
54
|
+
bash "${ENSURE_SYNC_SCRIPT}" "${ensure_args[@]}"
|
|
55
|
+
elif [[ "${ALWAYS_SYNC}" == "1" || ! -x "${RUNTIME_HEARTBEAT_SCRIPT}" ]]; then
|
|
56
|
+
if [[ -z "${SOURCE_HOME}" ]]; then
|
|
57
|
+
SOURCE_HOME="${FLOW_SKILL_DIR}"
|
|
58
|
+
fi
|
|
46
59
|
bash "${SYNC_SCRIPT}" "${SOURCE_HOME}" "${RUNTIME_HOME}" >/dev/null
|
|
47
60
|
fi
|
|
48
61
|
|
|
@@ -47,10 +47,16 @@ trap '' HUP
|
|
|
47
47
|
|
|
48
48
|
first_pass="1"
|
|
49
49
|
while true; do
|
|
50
|
+
printf '[%s] supervisor bootstrap start pid=%s script=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$$" "${bootstrap_script}" >&2
|
|
50
51
|
if [[ "${first_pass}" == "1" && "${delay_seconds}" != "0" ]]; then
|
|
51
52
|
sleep "${delay_seconds}"
|
|
52
53
|
fi
|
|
53
54
|
first_pass="0"
|
|
54
|
-
"${bootstrap_script}"
|
|
55
|
+
if "${bootstrap_script}"; then
|
|
56
|
+
printf '[%s] supervisor bootstrap end status=0 pid=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$$" >&2
|
|
57
|
+
else
|
|
58
|
+
bootstrap_status=$?
|
|
59
|
+
printf '[%s] supervisor bootstrap end status=%s pid=%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "${bootstrap_status}" "$$" >&2
|
|
60
|
+
fi
|
|
55
61
|
sleep "${interval_seconds}"
|
|
56
62
|
done
|
|
@@ -8,7 +8,7 @@ source "${SCRIPT_DIR}/flow-config-lib.sh"
|
|
|
8
8
|
usage() {
|
|
9
9
|
cat <<'EOF'
|
|
10
10
|
Usage:
|
|
11
|
-
project-runtimectl.sh <status|start|stop|restart> --profile-id <id> [options]
|
|
11
|
+
project-runtimectl.sh <status|start|stop|restart|sync> --profile-id <id> [options]
|
|
12
12
|
|
|
13
13
|
Manage runtime processes for one installed profile.
|
|
14
14
|
|
|
@@ -16,6 +16,7 @@ Options:
|
|
|
16
16
|
--profile-id <id> Profile id to manage
|
|
17
17
|
--delay-seconds <n> Delay for start via kick-scheduler (default: 0)
|
|
18
18
|
--wait-seconds <n> Wait for stop to settle before SIGKILL (default: 10)
|
|
19
|
+
--force Force a runtime sync refresh when using `sync`
|
|
19
20
|
--help Show this help
|
|
20
21
|
EOF
|
|
21
22
|
}
|
|
@@ -30,19 +31,21 @@ shift || true
|
|
|
30
31
|
profile_id_override=""
|
|
31
32
|
delay_seconds="0"
|
|
32
33
|
wait_seconds="10"
|
|
34
|
+
force_sync="0"
|
|
33
35
|
|
|
34
36
|
while [[ $# -gt 0 ]]; do
|
|
35
37
|
case "$1" in
|
|
36
38
|
--profile-id) profile_id_override="${2:-}"; shift 2 ;;
|
|
37
39
|
--delay-seconds) delay_seconds="${2:-}"; shift 2 ;;
|
|
38
40
|
--wait-seconds) wait_seconds="${2:-}"; shift 2 ;;
|
|
41
|
+
--force) force_sync="1"; shift ;;
|
|
39
42
|
--help|-h) usage; exit 0 ;;
|
|
40
43
|
*) echo "Unknown argument: $1" >&2; usage >&2; exit 64 ;;
|
|
41
44
|
esac
|
|
42
45
|
done
|
|
43
46
|
|
|
44
47
|
case "${subcommand}" in
|
|
45
|
-
status|start|stop|restart) ;;
|
|
48
|
+
status|start|stop|restart|sync) ;;
|
|
46
49
|
*)
|
|
47
50
|
echo "Unknown subcommand: ${subcommand}" >&2
|
|
48
51
|
usage >&2
|
|
@@ -92,16 +95,21 @@ REPO_SLUG="$(flow_resolve_repo_slug "${CONFIG_YAML}")"
|
|
|
92
95
|
RUNS_ROOT="$(flow_resolve_runs_root "${CONFIG_YAML}")"
|
|
93
96
|
STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
|
|
94
97
|
SUPERVISOR_PID_FILE="${STATE_ROOT}/runtime-supervisor.pid"
|
|
98
|
+
SUPERVISOR_LOG_FILE="${STATE_ROOT}/runtime-supervisor.log"
|
|
95
99
|
PROFILE_ID_SLUG="$(printf '%s' "${PROFILE_ID}" | tr -c 'A-Za-z0-9._-' '-')"
|
|
96
100
|
BOOTSTRAP_SCRIPT="${ACP_PROJECT_RUNTIME_BOOTSTRAP_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/project-launchd-bootstrap.sh}"
|
|
97
101
|
KICK_SCRIPT="${ACP_PROJECT_RUNTIME_KICK_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/kick-scheduler.sh}"
|
|
98
102
|
SUPERVISOR_SCRIPT="${ACP_PROJECT_RUNTIME_SUPERVISOR_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/project-runtime-supervisor.sh}"
|
|
103
|
+
ENSURE_SYNC_SCRIPT="${ACP_PROJECT_RUNTIME_ENSURE_SYNC_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/ensure-runtime-sync.sh}"
|
|
99
104
|
UPDATE_LABELS_SCRIPT="${ACP_PROJECT_RUNTIME_UPDATE_LABELS_SCRIPT:-${FLOW_SKILL_DIR}/tools/bin/agent-github-update-labels}"
|
|
100
105
|
TMUX_BIN="${ACP_PROJECT_RUNTIME_TMUX_BIN:-$(command -v tmux || true)}"
|
|
101
106
|
LAUNCHCTL_BIN="${ACP_PROJECT_RUNTIME_LAUNCHCTL_BIN:-$(command -v launchctl || true)}"
|
|
102
107
|
LAUNCH_AGENTS_DIR="${ACP_PROJECT_RUNTIME_LAUNCH_AGENTS_DIR:-${HOME}/Library/LaunchAgents}"
|
|
103
108
|
LAUNCHD_LABEL="${ACP_PROJECT_RUNTIME_LAUNCHD_LABEL:-ai.agent.project.${PROFILE_ID_SLUG}}"
|
|
104
109
|
LAUNCHD_PLIST="${ACP_PROJECT_RUNTIME_LAUNCHD_PLIST:-${LAUNCH_AGENTS_DIR}/${LAUNCHD_LABEL}.plist}"
|
|
110
|
+
SOURCE_HOME="${ACP_PROJECT_RUNTIME_SOURCE_HOME:-}"
|
|
111
|
+
RUNTIME_HOME="${ACP_PROJECT_RUNTIME_RUNTIME_HOME:-$(resolve_runtime_home)}"
|
|
112
|
+
SYNC_STAMP_FILE="${RUNTIME_HOME}/.agent-control-plane-runtime-sync.env"
|
|
105
113
|
|
|
106
114
|
case "${delay_seconds}" in
|
|
107
115
|
''|*[!0-9]*) echo "--delay-seconds must be numeric" >&2; exit 64 ;;
|
|
@@ -160,17 +168,13 @@ join_by_comma() {
|
|
|
160
168
|
runtime_started() {
|
|
161
169
|
local heartbeat=""
|
|
162
170
|
local shared_loop=""
|
|
163
|
-
local controller_pid=""
|
|
164
|
-
local active_session=""
|
|
165
171
|
local supervisor=""
|
|
166
172
|
|
|
167
173
|
heartbeat="$(heartbeat_pid)"
|
|
168
174
|
shared_loop="$(shared_loop_pid)"
|
|
169
|
-
controller_pid="$(collect_controller_pids | head -n 1 || true)"
|
|
170
|
-
active_session="$(collect_active_tmux_sessions | head -n 1 || true)"
|
|
171
175
|
supervisor="$(supervisor_pid)"
|
|
172
176
|
|
|
173
|
-
[[ -n "${heartbeat}" || -n "${shared_loop}" || -n "${
|
|
177
|
+
[[ -n "${heartbeat}" || -n "${shared_loop}" || -n "${supervisor}" ]]
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
wait_for_runtime_start() {
|
|
@@ -186,6 +190,13 @@ wait_for_runtime_start() {
|
|
|
186
190
|
return 1
|
|
187
191
|
}
|
|
188
192
|
|
|
193
|
+
sync_stamp_value() {
|
|
194
|
+
local key="${1:?key required}"
|
|
195
|
+
[[ -f "${SYNC_STAMP_FILE}" ]] || return 1
|
|
196
|
+
awk -F= -v target="${key}" '$1 == target {print $2; exit}' "${SYNC_STAMP_FILE}" 2>/dev/null \
|
|
197
|
+
| sed -e "s/^'//" -e "s/'$//"
|
|
198
|
+
}
|
|
199
|
+
|
|
189
200
|
tmux_session_exists() {
|
|
190
201
|
local session="${1:-}"
|
|
191
202
|
[[ -n "${TMUX_BIN}" && -n "${session}" ]] || return 1
|
|
@@ -331,6 +342,9 @@ print_status() {
|
|
|
331
342
|
local active_session_count="0"
|
|
332
343
|
local pending_count="0"
|
|
333
344
|
local launchd_state=""
|
|
345
|
+
local runtime_sync_status=""
|
|
346
|
+
local runtime_sync_updated_at=""
|
|
347
|
+
local runtime_sync_fingerprint=""
|
|
334
348
|
|
|
335
349
|
heartbeat="$(heartbeat_pid)"
|
|
336
350
|
shared_loop="$(shared_loop_pid)"
|
|
@@ -352,6 +366,9 @@ print_status() {
|
|
|
352
366
|
fi
|
|
353
367
|
|
|
354
368
|
launchd_state="$(launchd_service_state)"
|
|
369
|
+
runtime_sync_status="$(sync_stamp_value "SYNC_STATUS" || true)"
|
|
370
|
+
runtime_sync_updated_at="$(sync_stamp_value "UPDATED_AT" || true)"
|
|
371
|
+
runtime_sync_fingerprint="$(sync_stamp_value "SOURCE_FINGERPRINT" || true)"
|
|
355
372
|
|
|
356
373
|
printf 'PROFILE_ID=%s\n' "${PROFILE_ID}"
|
|
357
374
|
printf 'CONFIG_YAML=%s\n' "${CONFIG_YAML}"
|
|
@@ -372,6 +389,10 @@ print_status() {
|
|
|
372
389
|
printf 'CONTROLLER_PIDS=%s\n' "$(printf '%s\n' "${controller_pids}" | join_by_comma)"
|
|
373
390
|
printf 'ACTIVE_TMUX_SESSIONS=%s\n' "$(printf '%s\n' "${active_sessions}" | join_by_comma)"
|
|
374
391
|
printf 'PENDING_LAUNCH_PIDS=%s\n' "$(printf '%s\n' "${pending_pids}" | join_by_comma)"
|
|
392
|
+
printf 'SYNC_STAMP_FILE=%s\n' "${SYNC_STAMP_FILE}"
|
|
393
|
+
printf 'RUNTIME_SYNC_STATUS=%s\n' "${runtime_sync_status}"
|
|
394
|
+
printf 'RUNTIME_SYNC_UPDATED_AT=%s\n' "${runtime_sync_updated_at}"
|
|
395
|
+
printf 'RUNTIME_SYNC_FINGERPRINT=%s\n' "${runtime_sync_fingerprint}"
|
|
375
396
|
}
|
|
376
397
|
|
|
377
398
|
terminate_pid_list() {
|
|
@@ -499,6 +520,7 @@ stop_runtime() {
|
|
|
499
520
|
start_runtime() {
|
|
500
521
|
local kick_output=""
|
|
501
522
|
local fallback_pid=""
|
|
523
|
+
local fallback_log_file="${SUPERVISOR_LOG_FILE}"
|
|
502
524
|
local start_timeout="${ACP_PROJECT_RUNTIME_START_WAIT_SECONDS:-${wait_seconds}}"
|
|
503
525
|
local runtime_started_after_kick="0"
|
|
504
526
|
local supervisor_spawned="0"
|
|
@@ -537,14 +559,26 @@ start_runtime() {
|
|
|
537
559
|
|
|
538
560
|
if [[ "${runtime_started_after_kick}" != "1" && -z "$(supervisor_pid)" ]]; then
|
|
539
561
|
mkdir -p "${STATE_ROOT}"
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
562
|
+
: >"${fallback_log_file}"
|
|
563
|
+
if command -v setsid >/dev/null 2>&1; then
|
|
564
|
+
setsid env ACP_PROJECT_ID="${PROFILE_ID}" AGENT_PROJECT_ID="${PROFILE_ID}" \
|
|
565
|
+
bash "${SUPERVISOR_SCRIPT}" \
|
|
566
|
+
--bootstrap-script "${BOOTSTRAP_SCRIPT}" \
|
|
567
|
+
--pid-file "${SUPERVISOR_PID_FILE}" \
|
|
568
|
+
--delay-seconds "${delay_seconds}" \
|
|
569
|
+
--interval-seconds "${ACP_PROJECT_RUNTIME_SUPERVISOR_INTERVAL_SECONDS:-15}" \
|
|
570
|
+
</dev/null >>"${fallback_log_file}" 2>&1 &
|
|
571
|
+
fallback_pid="$!"
|
|
572
|
+
else
|
|
573
|
+
nohup env ACP_PROJECT_ID="${PROFILE_ID}" AGENT_PROJECT_ID="${PROFILE_ID}" \
|
|
574
|
+
bash "${SUPERVISOR_SCRIPT}" \
|
|
575
|
+
--bootstrap-script "${BOOTSTRAP_SCRIPT}" \
|
|
576
|
+
--pid-file "${SUPERVISOR_PID_FILE}" \
|
|
577
|
+
--delay-seconds "${delay_seconds}" \
|
|
578
|
+
--interval-seconds "${ACP_PROJECT_RUNTIME_SUPERVISOR_INTERVAL_SECONDS:-15}" \
|
|
579
|
+
</dev/null >>"${fallback_log_file}" 2>&1 &
|
|
580
|
+
fallback_pid="$!"
|
|
581
|
+
fi
|
|
548
582
|
supervisor_spawned="1"
|
|
549
583
|
wait_for_runtime_start "${start_timeout}" || true
|
|
550
584
|
fi
|
|
@@ -563,9 +597,35 @@ start_runtime() {
|
|
|
563
597
|
printf '%s\n' "${kick_output}"
|
|
564
598
|
if [[ -n "${fallback_pid}" ]]; then
|
|
565
599
|
printf 'FALLBACK_SUPERVISOR_PID=%s\n' "${fallback_pid}"
|
|
600
|
+
printf 'FALLBACK_SUPERVISOR_LOG=%s\n' "${fallback_log_file}"
|
|
566
601
|
fi
|
|
567
602
|
}
|
|
568
603
|
|
|
604
|
+
sync_runtime() {
|
|
605
|
+
local ensure_output=""
|
|
606
|
+
local ensure_args=()
|
|
607
|
+
|
|
608
|
+
if [[ ! -x "${ENSURE_SYNC_SCRIPT}" ]]; then
|
|
609
|
+
echo "missing runtime sync helper: ${ENSURE_SYNC_SCRIPT}" >&2
|
|
610
|
+
exit 65
|
|
611
|
+
fi
|
|
612
|
+
|
|
613
|
+
ensure_args=(--runtime-home "${RUNTIME_HOME}")
|
|
614
|
+
if [[ -n "${SOURCE_HOME}" ]]; then
|
|
615
|
+
ensure_args=(--source-home "${SOURCE_HOME}" "${ensure_args[@]}")
|
|
616
|
+
fi
|
|
617
|
+
if [[ "${force_sync}" == "1" ]]; then
|
|
618
|
+
ensure_args=(--force "${ensure_args[@]}")
|
|
619
|
+
ensure_output="$(bash "${ENSURE_SYNC_SCRIPT}" "${ensure_args[@]}")"
|
|
620
|
+
else
|
|
621
|
+
ensure_output="$(bash "${ENSURE_SYNC_SCRIPT}" "${ensure_args[@]}")"
|
|
622
|
+
fi
|
|
623
|
+
|
|
624
|
+
printf 'ACTION=sync\n'
|
|
625
|
+
printf 'PROFILE_ID=%s\n' "${PROFILE_ID}"
|
|
626
|
+
printf '%s\n' "${ensure_output}"
|
|
627
|
+
}
|
|
628
|
+
|
|
569
629
|
case "${subcommand}" in
|
|
570
630
|
status)
|
|
571
631
|
print_status
|
|
@@ -583,4 +643,7 @@ case "${subcommand}" in
|
|
|
583
643
|
start_runtime
|
|
584
644
|
print_status
|
|
585
645
|
;;
|
|
646
|
+
sync)
|
|
647
|
+
sync_runtime
|
|
648
|
+
;;
|
|
586
649
|
esac
|
|
@@ -31,6 +31,7 @@ fi
|
|
|
31
31
|
|
|
32
32
|
CONFIG_YAML="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
|
|
33
33
|
AGENT_REPO_ROOT="$(flow_resolve_agent_repo_root "${CONFIG_YAML}")"
|
|
34
|
+
WORKTREE_ROOT="$(flow_resolve_worktree_root "${CONFIG_YAML}")"
|
|
34
35
|
ISSUE_BRANCH_PREFIX="$(flow_resolve_issue_branch_prefix "${CONFIG_YAML}")"
|
|
35
36
|
DEFAULT_BRANCH="$(flow_resolve_default_branch "${CONFIG_YAML}")"
|
|
36
37
|
BASE_REF="origin/${DEFAULT_BRANCH}"
|
|
@@ -46,6 +47,9 @@ fi
|
|
|
46
47
|
stamp="$(date +%Y%m%d-%H%M%S)"
|
|
47
48
|
branch_name="${ISSUE_BRANCH_PREFIX}-${ISSUE_ID}-${safe_slug}-${stamp}"
|
|
48
49
|
previous_branch="$(git -C "${WORKTREE}" branch --show-current 2>/dev/null || true)"
|
|
50
|
+
resolved_worktree=""
|
|
51
|
+
actual_branch=""
|
|
52
|
+
rotated_worktree=""
|
|
49
53
|
|
|
50
54
|
if ! git -C "${WORKTREE}" rev-parse --git-dir >/dev/null 2>&1; then
|
|
51
55
|
echo "invalid managed worktree: ${WORKTREE}" >&2
|
|
@@ -69,6 +73,48 @@ fi
|
|
|
69
73
|
|
|
70
74
|
"${PREPARE_SCRIPT}" "${WORKTREE}" >/dev/null
|
|
71
75
|
|
|
76
|
+
if ! git -C "${WORKTREE}" rev-parse --git-dir >/dev/null 2>&1; then
|
|
77
|
+
echo "invalid managed worktree after reuse: ${WORKTREE}" >&2
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
resolved_worktree="$(cd "${WORKTREE}" 2>/dev/null && pwd -P || true)"
|
|
82
|
+
if [[ -z "${resolved_worktree}" || ! -d "${resolved_worktree}" ]]; then
|
|
83
|
+
echo "reused worktree path is unavailable: ${WORKTREE}" >&2
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
WORKTREE="${resolved_worktree}"
|
|
87
|
+
|
|
88
|
+
if [[ -n "${WORKTREE_ROOT}" ]]; then
|
|
89
|
+
mkdir -p "${WORKTREE_ROOT}"
|
|
90
|
+
rotated_worktree="${WORKTREE_ROOT}/issue-${ISSUE_ID}-${stamp}"
|
|
91
|
+
if [[ "${resolved_worktree}" != "${rotated_worktree}" ]]; then
|
|
92
|
+
if [[ -e "${rotated_worktree}" ]]; then
|
|
93
|
+
echo "rotated worktree path already exists: ${rotated_worktree}" >&2
|
|
94
|
+
exit 1
|
|
95
|
+
fi
|
|
96
|
+
git -C "${AGENT_REPO_ROOT}" worktree move "${resolved_worktree}" "${rotated_worktree}" >/dev/null
|
|
97
|
+
WORKTREE="${rotated_worktree}"
|
|
98
|
+
resolved_worktree="$(cd "${WORKTREE}" 2>/dev/null && pwd -P || true)"
|
|
99
|
+
if [[ -z "${resolved_worktree}" || ! -d "${resolved_worktree}" ]]; then
|
|
100
|
+
echo "rotated worktree path is unavailable: ${WORKTREE}" >&2
|
|
101
|
+
exit 1
|
|
102
|
+
fi
|
|
103
|
+
WORKTREE="${resolved_worktree}"
|
|
104
|
+
fi
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
if ! git -C "${AGENT_REPO_ROOT}" worktree list --porcelain | grep -Fqx "worktree ${resolved_worktree}"; then
|
|
108
|
+
echo "reused worktree is no longer registered: ${resolved_worktree}" >&2
|
|
109
|
+
exit 1
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
actual_branch="$(git -C "${WORKTREE}" branch --show-current 2>/dev/null || true)"
|
|
113
|
+
if [[ -z "${actual_branch}" || "${actual_branch}" != "${branch_name}" ]]; then
|
|
114
|
+
echo "reused worktree branch mismatch: expected ${branch_name} got ${actual_branch:-<none>}" >&2
|
|
115
|
+
exit 1
|
|
116
|
+
fi
|
|
117
|
+
|
|
72
118
|
printf 'WORKTREE=%s\n' "${WORKTREE}"
|
|
73
119
|
printf 'BRANCH=%s\n' "${branch_name}"
|
|
74
120
|
printf 'BASE_REF=%s\n' "${BASE_REF}"
|
|
@@ -153,6 +153,54 @@ reap_stale_run_dir() {
|
|
|
153
153
|
mv "$RUN_DIR" "${HISTORY_ROOT}/${SESSION}-stale-$(date +%Y%m%d-%H%M%S)"
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
find_archived_issue_session_dir() {
|
|
157
|
+
local root="${1:-}"
|
|
158
|
+
local target_session="${2:-}"
|
|
159
|
+
[[ -n "$root" && -d "$root" && -n "$target_session" ]] || return 1
|
|
160
|
+
|
|
161
|
+
find "$root" -mindepth 1 -maxdepth 1 -type d -name "${target_session}-*" ! -name "${target_session}-stale-*" 2>/dev/null \
|
|
162
|
+
| sort -r \
|
|
163
|
+
| head -n 1
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
issue_retry_state_value() {
|
|
167
|
+
local key="${1:?retry-state key required}"
|
|
168
|
+
awk -F= -v target_key="$key" '$1 == target_key { print substr($0, index($0, "=") + 1); exit }' <<<"${ISSUE_RETRY_STATE:-}"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
issue_host_publish_replay_dir() {
|
|
172
|
+
local last_reason=""
|
|
173
|
+
local archived_dir=""
|
|
174
|
+
local runner_state=""
|
|
175
|
+
local result_outcome=""
|
|
176
|
+
local result_action=""
|
|
177
|
+
|
|
178
|
+
last_reason="$(issue_retry_state_value LAST_REASON)"
|
|
179
|
+
case "${last_reason}" in
|
|
180
|
+
host-publish-failed|issue-worker-blocked) ;;
|
|
181
|
+
*) return 1 ;;
|
|
182
|
+
esac
|
|
183
|
+
|
|
184
|
+
archived_dir="$(find_archived_issue_session_dir "$HISTORY_ROOT" "$SESSION" || true)"
|
|
185
|
+
[[ -n "${archived_dir}" && -f "${archived_dir}/run.env" && -f "${archived_dir}/runner.env" && -f "${archived_dir}/result.env" ]] || return 1
|
|
186
|
+
|
|
187
|
+
runner_state="$(awk -F= '/^RUNNER_STATE=/{print $2; exit}' "${archived_dir}/runner.env")"
|
|
188
|
+
result_outcome="$(awk -F= '/^OUTCOME=/{print $2; exit}' "${archived_dir}/result.env")"
|
|
189
|
+
result_action="$(awk -F= '/^ACTION=/{print $2; exit}' "${archived_dir}/result.env")"
|
|
190
|
+
|
|
191
|
+
[[ "${runner_state}" == "succeeded" ]] || return 1
|
|
192
|
+
[[ "${result_outcome}" == "implemented" ]] || return 1
|
|
193
|
+
[[ "${result_action}" == "host-publish-issue-pr" ]] || return 1
|
|
194
|
+
|
|
195
|
+
printf '%s\n' "${archived_dir}"
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
replay_issue_host_publish_retry() {
|
|
199
|
+
local archived_dir="${1:?archived dir required}"
|
|
200
|
+
printf 'ISSUE_HOST_PUBLISH_REPLAY=session=%s archived_run_dir=%s\n' "${SESSION}" "${archived_dir}" >&2
|
|
201
|
+
bash "${WORKSPACE_DIR}/bin/reconcile-issue-worker.sh" "${SESSION}"
|
|
202
|
+
}
|
|
203
|
+
|
|
156
204
|
if tmux has-session -t "$SESSION" 2>/dev/null; then
|
|
157
205
|
echo "worker session already exists: $SESSION" >&2
|
|
158
206
|
exit 1
|
|
@@ -180,6 +228,9 @@ EOF
|
|
|
180
228
|
ISSUE_REQUIRES_LOCAL_WORKSPACE_INSTALL="$(
|
|
181
229
|
ISSUE_BODY="$ISSUE_BODY" bash "$LOCAL_INSTALL_POLICY_BIN"
|
|
182
230
|
)"
|
|
231
|
+
ISSUE_RETRY_STATE="$(
|
|
232
|
+
bash "${WORKSPACE_DIR}/bin/retry-state.sh" issue "$ISSUE_ID" get 2>/dev/null || true
|
|
233
|
+
)"
|
|
183
234
|
if [[ "${ISSUE_SCHEDULE_INTERVAL_SECONDS}" =~ ^[1-9][0-9]*$ ]]; then
|
|
184
235
|
TEMPLATE_FILE="${SCHEDULED_TEMPLATE_FILE}"
|
|
185
236
|
fi
|
|
@@ -227,6 +278,16 @@ if [[ -d "$RUN_DIR" ]]; then
|
|
|
227
278
|
reap_stale_run_dir
|
|
228
279
|
fi
|
|
229
280
|
|
|
281
|
+
ISSUE_HOST_PUBLISH_REPLAY_DIR="$(issue_host_publish_replay_dir || true)"
|
|
282
|
+
if [[ -n "${ISSUE_HOST_PUBLISH_REPLAY_DIR}" ]]; then
|
|
283
|
+
if ! replay_issue_host_publish_retry "${ISSUE_HOST_PUBLISH_REPLAY_DIR}"; then
|
|
284
|
+
echo "host publish replay failed for session ${SESSION}" >&2
|
|
285
|
+
exit 1
|
|
286
|
+
fi
|
|
287
|
+
launch_success="yes"
|
|
288
|
+
exit 0
|
|
289
|
+
fi
|
|
290
|
+
|
|
230
291
|
block_if_recurring_checklist_complete
|
|
231
292
|
|
|
232
293
|
mkdir -p "$RUN_DIR"
|
|
@@ -348,9 +409,6 @@ if (completedPrs.length > 0) {
|
|
|
348
409
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
349
410
|
EOF
|
|
350
411
|
ISSUE_RECURRING_CONTEXT="$(cat "$ISSUE_RECURRING_CONTEXT_FILE")"
|
|
351
|
-
ISSUE_RETRY_STATE="$(
|
|
352
|
-
bash "${WORKSPACE_DIR}/bin/retry-state.sh" issue "$ISSUE_ID" get 2>/dev/null || true
|
|
353
|
-
)"
|
|
354
412
|
ISSUE_BLOCKER_CONTEXT="$(
|
|
355
413
|
ISSUE_JSON="$ISSUE_JSON" ISSUE_RETRY_STATE="$ISSUE_RETRY_STATE" node <<'EOF'
|
|
356
414
|
const issue = JSON.parse(process.env.ISSUE_JSON || '{}');
|
|
@@ -401,6 +459,9 @@ const inferCommentReason = (bodyText) => {
|
|
|
401
459
|
if (/^# Blocker: Verification requirements were not satisfied$/im.test(body)) {
|
|
402
460
|
return 'verification-guard-blocked';
|
|
403
461
|
}
|
|
462
|
+
if (/^# Blocker: Localization requirements were not satisfied$/im.test(body)) {
|
|
463
|
+
return 'localization-guard-blocked';
|
|
464
|
+
}
|
|
404
465
|
if (/^# Blocker: (All checklist items already completed|Worker produced no publishable delta)$/im.test(body)) {
|
|
405
466
|
return 'no-publishable-commits';
|
|
406
467
|
}
|
|
@@ -468,6 +529,8 @@ if (effectiveLastReason === 'scope-guard-blocked') {
|
|
|
468
529
|
}
|
|
469
530
|
} else if (effectiveLastReason === 'verification-guard-blocked') {
|
|
470
531
|
lines.push('- Add the missing verification or shrink the touched surface before attempting another publish cycle.');
|
|
532
|
+
} else if (effectiveLastReason === 'localization-guard-blocked') {
|
|
533
|
+
lines.push('- Finish moving the remaining user-facing literals behind translation keys before attempting another publish cycle.');
|
|
471
534
|
}
|
|
472
535
|
|
|
473
536
|
lines.push('', clippedBody);
|
|
@@ -579,10 +642,11 @@ open_or_reuse_issue_worktree() {
|
|
|
579
642
|
RESIDENT_OPENCLAW_CONFIG_PATH="${current_resident_openclaw_config_path}"
|
|
580
643
|
RESIDENT_TASK_COUNT="$(( ${TASK_COUNT:-0} + 1 ))"
|
|
581
644
|
RESIDENT_WORKTREE_REUSED="yes"
|
|
582
|
-
if [[ "${CODING_WORKER}" == "openclaw"
|
|
645
|
+
if [[ "${CODING_WORKER}" == "openclaw" ]]; then
|
|
583
646
|
# Keep the resident lane's warm workspace/agent files, but rotate the
|
|
584
|
-
# OpenClaw conversation thread
|
|
585
|
-
|
|
647
|
+
# OpenClaw conversation thread every cycle so a new task does not inherit
|
|
648
|
+
# stale conversational context from the previous one.
|
|
649
|
+
RESIDENT_OPENCLAW_SESSION_ID="$(flow_resident_issue_openclaw_session_id "${CONFIG_YAML}" "${current_issue_id}" "${RESIDENT_TASK_COUNT}")"
|
|
586
650
|
fi
|
|
587
651
|
if reuse_output="$("${WORKSPACE_DIR}/bin/reuse-issue-worktree.sh" "${WORKTREE}" "${ISSUE_ID}" "${ISSUE_SLUG}" 2>&1)"; then
|
|
588
652
|
WORKTREE_OUT="${reuse_output}"
|
|
@@ -590,6 +654,9 @@ open_or_reuse_issue_worktree() {
|
|
|
590
654
|
printf 'RESIDENT_REUSE_FALLBACK=issue-%s reason=%s\n' "${ISSUE_ID}" "$(printf '%s' "${reuse_output}" | tr '\n' ' ' | sed 's/ */ /g')" >&2
|
|
591
655
|
RESIDENT_TASK_COUNT="1"
|
|
592
656
|
RESIDENT_WORKTREE_REUSED="no"
|
|
657
|
+
if [[ "${CODING_WORKER}" == "openclaw" ]]; then
|
|
658
|
+
RESIDENT_OPENCLAW_SESSION_ID="$(flow_resident_issue_openclaw_session_id "${CONFIG_YAML}" "${current_issue_id}" "${RESIDENT_TASK_COUNT}")"
|
|
659
|
+
fi
|
|
593
660
|
if [[ "$ISSUE_REQUIRES_LOCAL_WORKSPACE_INSTALL" == "yes" ]]; then
|
|
594
661
|
WORKTREE_OUT="$(ACP_WORKTREE_LOCAL_INSTALL=true F_LOSNING_WORKTREE_LOCAL_INSTALL=true "${WORKSPACE_DIR}/bin/new-worktree.sh" "$ISSUE_ID" "$ISSUE_SLUG")"
|
|
595
662
|
else
|
|
@@ -599,6 +666,9 @@ open_or_reuse_issue_worktree() {
|
|
|
599
666
|
else
|
|
600
667
|
RESIDENT_TASK_COUNT="1"
|
|
601
668
|
RESIDENT_WORKTREE_REUSED="no"
|
|
669
|
+
if [[ "${CODING_WORKER}" == "openclaw" ]]; then
|
|
670
|
+
RESIDENT_OPENCLAW_SESSION_ID="$(flow_resident_issue_openclaw_session_id "${CONFIG_YAML}" "${current_issue_id}" "${RESIDENT_TASK_COUNT}")"
|
|
671
|
+
fi
|
|
602
672
|
if [[ "$ISSUE_REQUIRES_LOCAL_WORKSPACE_INSTALL" == "yes" ]]; then
|
|
603
673
|
WORKTREE_OUT="$(ACP_WORKTREE_LOCAL_INSTALL=true F_LOSNING_WORKTREE_LOCAL_INSTALL=true "${WORKSPACE_DIR}/bin/new-worktree.sh" "$ISSUE_ID" "$ISSUE_SLUG")"
|
|
604
674
|
else
|
|
@@ -785,6 +785,7 @@ while true; do
|
|
|
785
785
|
controller_refresh_execution_context
|
|
786
786
|
controller_refresh_issue_lane_context "${is_scheduled}" "${schedule_interval_seconds}"
|
|
787
787
|
controller_track_provider_selection "provider-selection"
|
|
788
|
+
controller_write_state "starting" ""
|
|
788
789
|
|
|
789
790
|
if controller_yield_to_live_lane_peer; then
|
|
790
791
|
break
|
|
@@ -69,6 +69,31 @@ sync_skill_copies() {
|
|
|
69
69
|
fi
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
refresh_legacy_profile_templates() {
|
|
73
|
+
local profiles_root=""
|
|
74
|
+
local current_issue_template=""
|
|
75
|
+
local legacy_issue_template=""
|
|
76
|
+
local profile_dir=""
|
|
77
|
+
local profile_issue_template=""
|
|
78
|
+
|
|
79
|
+
profiles_root="$(resolve_flow_profile_registry_root)"
|
|
80
|
+
current_issue_template="${FLOW_SKILL_SOURCE}/tools/templates/issue-prompt-template.md"
|
|
81
|
+
legacy_issue_template="${FLOW_SKILL_SOURCE}/tools/templates/legacy/issue-prompt-template-pre-slim.md"
|
|
82
|
+
|
|
83
|
+
[[ -d "${profiles_root}" ]] || return 0
|
|
84
|
+
[[ -f "${current_issue_template}" ]] || return 0
|
|
85
|
+
[[ -f "${legacy_issue_template}" ]] || return 0
|
|
86
|
+
|
|
87
|
+
while IFS= read -r profile_dir; do
|
|
88
|
+
[[ -n "${profile_dir}" ]] || continue
|
|
89
|
+
profile_issue_template="${profile_dir}/templates/issue-prompt-template.md"
|
|
90
|
+
[[ -f "${profile_issue_template}" ]] || continue
|
|
91
|
+
if cmp -s "${profile_issue_template}" "${legacy_issue_template}"; then
|
|
92
|
+
cp "${current_issue_template}" "${profile_issue_template}"
|
|
93
|
+
fi
|
|
94
|
+
done < <(find "${profiles_root}" -mindepth 2 -maxdepth 2 -type f -name 'control-plane.yaml' -exec dirname {} \; 2>/dev/null | sort)
|
|
95
|
+
}
|
|
96
|
+
|
|
72
97
|
remove_repo_local_profile_dirs() {
|
|
73
98
|
local candidate=""
|
|
74
99
|
|
|
@@ -210,5 +235,6 @@ fi
|
|
|
210
235
|
sync_skill_copies
|
|
211
236
|
remove_repo_local_profile_dirs
|
|
212
237
|
normalize_script_permissions
|
|
238
|
+
refresh_legacy_profile_templates
|
|
213
239
|
|
|
214
240
|
printf 'SHARED_AGENT_HOME=%s\n' "${TARGET_HOME}"
|
package/tools/bin/test-smoke.sh
CHANGED
|
@@ -58,7 +58,12 @@ run_step() {
|
|
|
58
58
|
return "${status}"
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
if [[ -f "${check_contracts_script}" ]]; then
|
|
62
|
+
run_step "check-skill-contracts" bash "${check_contracts_script}"
|
|
63
|
+
else
|
|
64
|
+
printf 'SMOKE_STEP=%s\n' "check-skill-contracts"
|
|
65
|
+
printf 'SMOKE_STEP_STATUS=%s\n' "skipped"
|
|
66
|
+
fi
|
|
62
67
|
|
|
63
68
|
run_profile_smoke_fixture() (
|
|
64
69
|
set -euo pipefail
|