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.
Files changed (37) hide show
  1. package/hooks/heartbeat-hooks.sh +97 -8
  2. package/package.json +8 -2
  3. package/references/commands.md +1 -0
  4. package/tools/bin/agent-project-cleanup-session +133 -0
  5. package/tools/bin/agent-project-publish-issue-pr +178 -62
  6. package/tools/bin/agent-project-reconcile-issue-session +171 -3
  7. package/tools/bin/agent-project-run-codex-resilient +121 -16
  8. package/tools/bin/agent-project-run-codex-session +60 -10
  9. package/tools/bin/agent-project-run-openclaw-session +82 -8
  10. package/tools/bin/cleanup-worktree.sh +4 -1
  11. package/tools/bin/dashboard-launchd-bootstrap.sh +16 -4
  12. package/tools/bin/ensure-runtime-sync.sh +182 -0
  13. package/tools/bin/flow-config-lib.sh +76 -30
  14. package/tools/bin/flow-resident-worker-lib.sh +28 -2
  15. package/tools/bin/flow-shell-lib.sh +15 -1
  16. package/tools/bin/heartbeat-safe-auto.sh +32 -0
  17. package/tools/bin/issue-publish-localization-guard.sh +142 -0
  18. package/tools/bin/project-launchd-bootstrap.sh +17 -4
  19. package/tools/bin/project-runtime-supervisor.sh +7 -1
  20. package/tools/bin/project-runtimectl.sh +78 -15
  21. package/tools/bin/reuse-issue-worktree.sh +46 -0
  22. package/tools/bin/start-issue-worker.sh +76 -6
  23. package/tools/bin/start-resident-issue-loop.sh +1 -0
  24. package/tools/bin/sync-shared-agent-home.sh +26 -0
  25. package/tools/bin/test-smoke.sh +6 -1
  26. package/tools/dashboard/app.js +71 -1
  27. package/tools/dashboard/dashboard_snapshot.py +74 -0
  28. package/tools/dashboard/styles.css +43 -0
  29. package/tools/templates/issue-prompt-template.md +18 -66
  30. package/tools/templates/legacy/issue-prompt-template-pre-slim.md +109 -0
  31. package/bin/audit-issue-routing.sh +0 -74
  32. package/tools/bin/audit-agent-worktrees.sh +0 -310
  33. package/tools/bin/audit-issue-routing.sh +0 -11
  34. package/tools/bin/audit-retained-layout.sh +0 -58
  35. package/tools/bin/audit-retained-overlap.sh +0 -135
  36. package/tools/bin/audit-retained-worktrees.sh +0 -228
  37. 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:-$(cd "${FLOW_SKILL_DIR}/../../.." && pwd)}"
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 script: ${SYNC_SCRIPT}" >&2
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 [[ "${ALWAYS_SYNC}" == "1" || ! -x "${RUNTIME_HEARTBEAT_SCRIPT}" ]]; then
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}" >/dev/null 2>&1 || true
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 "${controller_pid}" || -n "${active_session}" || -n "${supervisor}" ]]
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
- nohup env ACP_PROJECT_ID="${PROFILE_ID}" AGENT_PROJECT_ID="${PROFILE_ID}" \
541
- bash "${SUPERVISOR_SCRIPT}" \
542
- --bootstrap-script "${BOOTSTRAP_SCRIPT}" \
543
- --pid-file "${SUPERVISOR_PID_FILE}" \
544
- --delay-seconds "${delay_seconds}" \
545
- --interval-seconds "${ACP_PROJECT_RUNTIME_SUPERVISOR_INTERVAL_SECONDS:-15}" \
546
- </dev/null >/dev/null 2>&1 &
547
- fallback_pid="$!"
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" && -n "${previous_issue_id}" && "${previous_issue_id}" != "${current_issue_id}" ]]; then
645
+ if [[ "${CODING_WORKER}" == "openclaw" ]]; then
583
646
  # Keep the resident lane's warm workspace/agent files, but rotate the
584
- # OpenClaw conversation thread when switching issues to reduce context drift.
585
- RESIDENT_OPENCLAW_SESSION_ID="$(flow_resident_issue_openclaw_session_id "${CONFIG_YAML}" "${current_issue_id}")"
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}"
@@ -58,7 +58,12 @@ run_step() {
58
58
  return "${status}"
59
59
  }
60
60
 
61
- run_step "check-skill-contracts" bash "${check_contracts_script}"
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