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
@@ -15,15 +15,40 @@ FLOW_TOOLS_DIR="${FLOW_SKILL_DIR}/tools/bin"
15
15
  DETACHED_LAUNCH_BIN="${FLOW_TOOLS_DIR}/agent-project-detached-launch"
16
16
  RESIDENT_ISSUE_LOOP_BIN="${FLOW_TOOLS_DIR}/start-resident-issue-loop.sh"
17
17
  REPO_SLUG="$(flow_resolve_repo_slug "${CONFIG_YAML}")"
18
+ STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
19
+ PENDING_LAUNCH_DIR="${ACP_PENDING_LAUNCH_DIR:-${F_LOSNING_PENDING_LAUNCH_DIR:-${STATE_ROOT}/pending-launches}}"
18
20
  AGENT_PR_PREFIXES_JSON="$(flow_managed_pr_prefixes_json "${CONFIG_YAML}")"
19
21
  AGENT_PR_ISSUE_CAPTURE_REGEX="$(flow_managed_issue_branch_regex "${CONFIG_YAML}")"
20
22
  AGENT_PR_HANDOFF_LABEL="${AGENT_PR_HANDOFF_LABEL:-agent-handoff}"
21
23
  AGENT_EXCLUSIVE_LABEL="${AGENT_EXCLUSIVE_LABEL:-agent-exclusive}"
22
24
  CODING_WORKER="${ACP_CODING_WORKER:-${F_LOSNING_CODING_WORKER:-codex}}"
25
+ HEARTBEAT_ISSUE_JSON_CACHE_DIR="${TMPDIR:-/tmp}/heartbeat-issue-json.$$"
26
+
27
+ heartbeat_issue_json_cached() {
28
+ local issue_id="${1:?issue id required}"
29
+ local cache_file=""
30
+ local issue_json=""
31
+
32
+ if [[ ! -d "${HEARTBEAT_ISSUE_JSON_CACHE_DIR}" ]]; then
33
+ mkdir -p "${HEARTBEAT_ISSUE_JSON_CACHE_DIR}"
34
+ fi
35
+
36
+ cache_file="${HEARTBEAT_ISSUE_JSON_CACHE_DIR}/${issue_id}.json"
37
+ if [[ -f "${cache_file}" ]]; then
38
+ cat "${cache_file}"
39
+ return 0
40
+ fi
41
+
42
+ issue_json="$(flow_github_issue_view_json "$REPO_SLUG" "$issue_id" 2>/dev/null || true)"
43
+ printf '%s' "${issue_json}" >"${cache_file}"
44
+ printf '%s\n' "${issue_json}"
45
+ }
23
46
 
24
47
  heartbeat_open_agent_pr_issue_ids() {
25
- flow_github_pr_list_json "$REPO_SLUG" open 100 \
26
- | jq --argjson agentPrPrefixes "${AGENT_PR_PREFIXES_JSON}" --arg handoffLabel "${AGENT_PR_HANDOFF_LABEL}" --arg branchIssueRegex "${AGENT_PR_ISSUE_CAPTURE_REGEX}" '
48
+ local pr_issue_ids_json=""
49
+ pr_issue_ids_json="$(
50
+ flow_github_pr_list_json "$REPO_SLUG" open 100 \
51
+ | jq --argjson agentPrPrefixes "${AGENT_PR_PREFIXES_JSON}" --arg handoffLabel "${AGENT_PR_HANDOFF_LABEL}" --arg branchIssueRegex "${AGENT_PR_ISSUE_CAPTURE_REGEX}" '
27
52
  map(
28
53
  . as $pr
29
54
  | select(
@@ -48,6 +73,13 @@ heartbeat_open_agent_pr_issue_ids() {
48
73
  )
49
74
  | unique
50
75
  '
76
+ )"
77
+
78
+ if [[ -z "${pr_issue_ids_json:-}" ]]; then
79
+ printf '[]\n'
80
+ else
81
+ printf '%s\n' "${pr_issue_ids_json}"
82
+ fi
51
83
  }
52
84
 
53
85
  heartbeat_list_ready_issue_ids() {
@@ -124,7 +156,7 @@ heartbeat_issue_blocked_recovery_reason() {
124
156
  return 0
125
157
  fi
126
158
 
127
- issue_json="$(flow_github_issue_view_json "$REPO_SLUG" "$issue_id" 2>/dev/null || true)"
159
+ issue_json="$(heartbeat_issue_json_cached "$issue_id")"
128
160
  if [[ -z "${issue_json:-}" ]]; then
129
161
  return 0
130
162
  fi
@@ -163,6 +195,8 @@ if (explicitFailureReason) {
163
195
  reason = 'scope-guard-blocked';
164
196
  } else if (/verification guard/i.test(body)) {
165
197
  reason = 'verification-guard-blocked';
198
+ } else if (/localization guard/i.test(body) || /^# Blocker: Localization requirements were not satisfied$/im.test(body)) {
199
+ reason = 'localization-guard-blocked';
166
200
  } else if (/missing referenced OpenSpec paths/i.test(body)) {
167
201
  reason = 'missing-openspec-paths';
168
202
  } else if (/superseded by focused follow-up issues/i.test(body)) {
@@ -248,7 +282,7 @@ heartbeat_issue_is_heavy() {
248
282
  heartbeat_issue_is_recurring() {
249
283
  local issue_id="${1:?issue id required}"
250
284
  local issue_json
251
- issue_json="$(flow_github_issue_view_json "$REPO_SLUG" "$issue_id" 2>/dev/null || true)"
285
+ issue_json="$(heartbeat_issue_json_cached "$issue_id")"
252
286
  if [[ -n "$issue_json" ]] && jq -e 'any(.labels[]?; .name == "agent-keep-open")' >/dev/null <<<"$issue_json"; then
253
287
  printf 'yes\n'
254
288
  else
@@ -259,7 +293,7 @@ heartbeat_issue_is_recurring() {
259
293
  heartbeat_issue_schedule_interval_seconds() {
260
294
  local issue_id="${1:?issue id required}"
261
295
  local issue_json issue_body
262
- issue_json="$(flow_github_issue_view_json "$REPO_SLUG" "$issue_id" 2>/dev/null || true)"
296
+ issue_json="$(heartbeat_issue_json_cached "$issue_id")"
263
297
  if [[ -z "$issue_json" ]]; then
264
298
  issue_json='{}'
265
299
  fi
@@ -282,7 +316,7 @@ EOF
282
316
  heartbeat_issue_schedule_token() {
283
317
  local issue_id="${1:?issue id required}"
284
318
  local issue_json issue_body
285
- issue_json="$(flow_github_issue_view_json "$REPO_SLUG" "$issue_id" 2>/dev/null || true)"
319
+ issue_json="$(heartbeat_issue_json_cached "$issue_id")"
286
320
  if [[ -z "$issue_json" ]]; then
287
321
  issue_json='{}'
288
322
  fi
@@ -321,7 +355,7 @@ heartbeat_issue_is_scheduled() {
321
355
  heartbeat_issue_is_exclusive() {
322
356
  local issue_id="${1:?issue id required}"
323
357
  local issue_json
324
- issue_json="$(flow_github_issue_view_json "$REPO_SLUG" "$issue_id" 2>/dev/null || true)"
358
+ issue_json="$(heartbeat_issue_json_cached "$issue_id")"
325
359
  if [[ -n "$issue_json" ]] && jq -e --arg exclusiveLabel "${AGENT_EXCLUSIVE_LABEL}" 'any(.labels[]?; .name == $exclusiveLabel)' >/dev/null <<<"$issue_json"; then
326
360
  printf 'yes\n'
327
361
  else
@@ -379,7 +413,7 @@ heartbeat_sync_issue_labels() {
379
413
  local -a add_args=()
380
414
  local -a update_args=()
381
415
 
382
- issue_json="$(flow_github_issue_view_json "$REPO_SLUG" "$issue_id" 2>/dev/null || true)"
416
+ issue_json="$(heartbeat_issue_json_cached "$issue_id")"
383
417
  if [[ -z "$issue_json" ]]; then
384
418
  return 0
385
419
  fi
@@ -501,6 +535,52 @@ heartbeat_issue_resident_worker_key() {
501
535
  flow_resident_issue_lane_key "${CODING_WORKER}" "safe" "${lane_kind}" "${lane_value}"
502
536
  }
503
537
 
538
+ heartbeat_pending_issue_launch_pid() {
539
+ local issue_id="${1:?issue id required}"
540
+ local pending_file pid=""
541
+
542
+ pending_file="${PENDING_LAUNCH_DIR}/issue-${issue_id}.pid"
543
+ [[ -f "${pending_file}" ]] || return 1
544
+
545
+ pid="$(tr -d '[:space:]' <"${pending_file}" 2>/dev/null || true)"
546
+ [[ "${pid}" =~ ^[0-9]+$ ]] || return 1
547
+ kill -0 "${pid}" 2>/dev/null || return 1
548
+
549
+ printf '%s\n' "${pid}"
550
+ }
551
+
552
+ heartbeat_pending_resident_lane_launch_issue_id() {
553
+ local issue_id="${1:?issue id required}"
554
+ local worker_key=""
555
+ local pending_file=""
556
+ local candidate_issue_id=""
557
+ local candidate_worker_key=""
558
+
559
+ worker_key="$(heartbeat_issue_resident_worker_key "${issue_id}")"
560
+ [[ -n "${worker_key}" ]] || return 1
561
+ [[ -d "${PENDING_LAUNCH_DIR}" ]] || return 1
562
+
563
+ for pending_file in "${PENDING_LAUNCH_DIR}"/issue-*.pid; do
564
+ [[ -f "${pending_file}" ]] || continue
565
+ candidate_issue_id="${pending_file##*/issue-}"
566
+ candidate_issue_id="${candidate_issue_id%.pid}"
567
+ [[ -n "${candidate_issue_id}" ]] || continue
568
+ if ! heartbeat_pending_issue_launch_pid "${candidate_issue_id}" >/dev/null 2>&1; then
569
+ rm -f "${pending_file}" 2>/dev/null || true
570
+ continue
571
+ fi
572
+ if [[ "$(heartbeat_issue_uses_resident_loop "${candidate_issue_id}")" != "yes" ]]; then
573
+ continue
574
+ fi
575
+ candidate_worker_key="$(heartbeat_issue_resident_worker_key "${candidate_issue_id}")"
576
+ [[ -n "${candidate_worker_key}" && "${candidate_worker_key}" == "${worker_key}" ]] || continue
577
+ printf '%s\n' "${candidate_issue_id}"
578
+ return 0
579
+ done
580
+
581
+ return 1
582
+ }
583
+
504
584
  heartbeat_live_issue_controller_for_lane() {
505
585
  local issue_id="${1:?issue id required}"
506
586
  local worker_key=""
@@ -548,11 +628,20 @@ heartbeat_enqueue_issue_for_resident_controller() {
548
628
 
549
629
  heartbeat_start_issue_worker() {
550
630
  local issue_id="${1:?issue id required}"
631
+ local pending_lane_issue_id=""
551
632
  if [[ "$(heartbeat_issue_uses_resident_loop "${issue_id}")" == "yes" ]]; then
552
633
  if heartbeat_enqueue_issue_for_live_resident_lane "${issue_id}"; then
553
634
  printf 'LAUNCH_MODE=resident-lease\n'
554
635
  return 0
555
636
  fi
637
+ pending_lane_issue_id="$(heartbeat_pending_resident_lane_launch_issue_id "${issue_id}" || true)"
638
+ if [[ -n "${pending_lane_issue_id}" ]]; then
639
+ if [[ "${pending_lane_issue_id}" != "${issue_id}" ]]; then
640
+ flow_resident_issue_enqueue "${CONFIG_YAML}" "${issue_id}" "heartbeat-pending-lane" >/dev/null
641
+ fi
642
+ printf 'LAUNCH_MODE=resident-pending-lane\n'
643
+ return 0
644
+ fi
556
645
  if heartbeat_enqueue_issue_for_resident_controller "${issue_id}"; then
557
646
  printf 'LAUNCH_MODE=resident-lease\n'
558
647
  return 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-control-plane",
3
- "version": "0.1.9",
3
+ "version": "0.1.12",
4
4
  "description": "Help a repo keep GitHub-driven coding agents running reliably without constant human babysitting",
5
5
  "homepage": "https://github.com/ducminhnguyen0319/agent-control-plane",
6
6
  "bugs": {
@@ -21,11 +21,17 @@
21
21
  "README.md",
22
22
  "SKILL.md",
23
23
  "assets/workflow-catalog.json",
24
- "bin",
24
+ "bin/agent-control-plane",
25
+ "bin/issue-resource-class.sh",
26
+ "bin/label-follow-up-issues.sh",
27
+ "bin/pr-risk.sh",
28
+ "bin/sync-pr-labels.sh",
25
29
  "hooks",
26
30
  "npm/bin",
27
31
  "references",
28
32
  "tools/bin",
33
+ "!tools/bin/audit-*.sh",
34
+ "!tools/bin/check-skill-contracts.sh",
29
35
  "tools/dashboard/app.js",
30
36
  "tools/dashboard/dashboard_snapshot.py",
31
37
  "tools/dashboard/index.html",
@@ -76,6 +76,7 @@ tools/bin/profile-smoke.sh
76
76
  tools/bin/test-smoke.sh
77
77
  tools/bin/profile-adopt.sh --profile-id <id>
78
78
  tools/bin/project-runtimectl.sh status --profile-id <id>
79
+ tools/bin/project-runtimectl.sh sync --profile-id <id>
79
80
  tools/bin/project-runtimectl.sh stop --profile-id <id>
80
81
  tools/bin/project-runtimectl.sh start --profile-id <id>
81
82
  tools/bin/project-runtimectl.sh restart --profile-id <id>
@@ -15,6 +15,7 @@ shared_agent_home="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
15
15
  repo_root="${AGENT_PROJECT_REPO_ROOT:-}"
16
16
  runs_root="${AGENT_PROJECT_RUNS_ROOT:-}"
17
17
  history_root="${AGENT_PROJECT_HISTORY_ROOT:-}"
18
+ state_root="${AGENT_PROJECT_STATE_ROOT:-${F_LOSNING_STATE_ROOT:-}}"
18
19
  session=""
19
20
  worktree_path=""
20
21
  mode="generic"
@@ -30,6 +31,7 @@ while [[ $# -gt 0 ]]; do
30
31
  --repo-root) repo_root="${2:-}"; shift 2 ;;
31
32
  --runs-root) runs_root="${2:-}"; shift 2 ;;
32
33
  --history-root) history_root="${2:-}"; shift 2 ;;
34
+ --state-root) state_root="${2:-}"; shift 2 ;;
33
35
  --session) session="${2:-}"; shift 2 ;;
34
36
  --worktree) worktree_path="${2:-}"; shift 2 ;;
35
37
  --mode) mode="${2:-}"; shift 2 ;;
@@ -141,6 +143,131 @@ path_is_within_root() {
141
143
  [[ "$target_canonical" == "$root_canonical" || "$target_canonical" == "$root_canonical"/* ]]
142
144
  }
143
145
 
146
+ derive_state_root() {
147
+ if [[ -n "${state_root}" && -d "${state_root}" ]]; then
148
+ printf '%s\n' "${state_root}"
149
+ return 0
150
+ fi
151
+
152
+ if [[ -n "${runs_root}" && -d "${runs_root}" ]]; then
153
+ local candidate_root=""
154
+ candidate_root="$(cd "${runs_root}/.." 2>/dev/null && pwd -P || true)"
155
+ if [[ -n "${candidate_root}" && -d "${candidate_root}/state" ]]; then
156
+ printf '%s/state\n' "${candidate_root}"
157
+ return 0
158
+ fi
159
+ fi
160
+
161
+ return 1
162
+ }
163
+
164
+ resident_worktree_protected() {
165
+ local candidate_path="${1:-}"
166
+ local resolved_candidate=""
167
+ local resolved_state_root=""
168
+ local metadata_file=""
169
+ local resident_worktree=""
170
+ local resident_realpath=""
171
+ local resolved_resident=""
172
+
173
+ [[ -n "${candidate_path}" && -d "${candidate_path}" ]] || return 1
174
+ resolved_candidate="$(canonicalize_existing_dir "${candidate_path}" || true)"
175
+ [[ -n "${resolved_candidate}" ]] || return 1
176
+
177
+ resolved_state_root="$(derive_state_root || true)"
178
+ [[ -n "${resolved_state_root}" && -d "${resolved_state_root}/resident-workers/issues" ]] || return 1
179
+
180
+ for metadata_file in "${resolved_state_root}"/resident-workers/issues/*/metadata.env; do
181
+ [[ -f "${metadata_file}" ]] || continue
182
+ resident_worktree=""
183
+ resident_realpath=""
184
+ set +u
185
+ set -a
186
+ # shellcheck source=/dev/null
187
+ source "${metadata_file}"
188
+ set +a
189
+ set -u
190
+ resident_worktree="${WORKTREE_REALPATH:-${WORKTREE:-}}"
191
+ [[ -n "${resident_worktree}" && -d "${resident_worktree}" ]] || continue
192
+ resolved_resident="$(canonicalize_existing_dir "${resident_worktree}" || true)"
193
+ [[ -n "${resolved_resident}" ]] || continue
194
+ if [[ "${resolved_candidate}" == "${resolved_resident}" ]]; then
195
+ return 0
196
+ fi
197
+ done
198
+
199
+ return 1
200
+ }
201
+
202
+ active_resident_run_worktree_protected() {
203
+ local candidate_path="${1:-}"
204
+ local resolved_candidate=""
205
+ local run_meta=""
206
+ local run_dir=""
207
+ local session_name=""
208
+ local resident_enabled=""
209
+ local resident_worktree=""
210
+ local runner_state_file=""
211
+ local runner_state=""
212
+ local active_session="false"
213
+ local resolved_resident=""
214
+
215
+ [[ -n "${candidate_path}" && -d "${candidate_path}" ]] || return 1
216
+ [[ -n "${runs_root}" && -d "${runs_root}" ]] || return 1
217
+
218
+ resolved_candidate="$(canonicalize_existing_dir "${candidate_path}" || true)"
219
+ [[ -n "${resolved_candidate}" ]] || return 1
220
+
221
+ for run_meta in "${runs_root}"/*/run.env; do
222
+ [[ -f "${run_meta}" ]] || continue
223
+
224
+ session_name=""
225
+ resident_enabled=""
226
+ resident_worktree=""
227
+ runner_state=""
228
+ active_session="false"
229
+
230
+ set +u
231
+ set -a
232
+ # shellcheck source=/dev/null
233
+ source "${run_meta}"
234
+ set +a
235
+ set -u
236
+
237
+ resident_enabled="${RESIDENT_WORKER_ENABLED:-no}"
238
+ [[ "${resident_enabled}" == "yes" ]] || continue
239
+
240
+ resident_worktree="${WORKTREE_REALPATH:-${WORKTREE:-}}"
241
+ [[ -n "${resident_worktree}" && -d "${resident_worktree}" ]] || continue
242
+ resolved_resident="$(canonicalize_existing_dir "${resident_worktree}" || true)"
243
+ [[ -n "${resolved_resident}" ]] || continue
244
+ [[ "${resolved_candidate}" == "${resolved_resident}" ]] || continue
245
+
246
+ run_dir="$(dirname "${run_meta}")"
247
+ runner_state_file="${run_dir}/runner.env"
248
+ if [[ -f "${runner_state_file}" ]]; then
249
+ set +u
250
+ set -a
251
+ # shellcheck source=/dev/null
252
+ source "${runner_state_file}"
253
+ set +a
254
+ set -u
255
+ runner_state="${RUNNER_STATE:-}"
256
+ fi
257
+
258
+ session_name="${SESSION:-}"
259
+ if [[ -n "${session_name}" ]] && tmux has-session -t "${session_name}" 2>/dev/null; then
260
+ active_session="true"
261
+ fi
262
+
263
+ if [[ "${runner_state}" == "running" || "${active_session}" == "true" ]]; then
264
+ return 0
265
+ fi
266
+ done
267
+
268
+ return 1
269
+ }
270
+
144
271
  cleanup_with_branch_tool() {
145
272
  local include_path="${1:-yes}"
146
273
  local -a cleanup_args
@@ -185,6 +312,9 @@ cleanup_orphan_worktree_dir() {
185
312
 
186
313
  if [[ "$active_tmux_session" == "true" ]]; then
187
314
  cleanup_mode="deferred-active-session"
315
+ elif [[ "$skip_worktree_cleanup" != "true" && -n "${worktree_path}" ]] \
316
+ && { resident_worktree_protected "${worktree_path}" || active_resident_run_worktree_protected "${worktree_path}"; }; then
317
+ cleanup_mode="protected-resident-worktree"
188
318
  elif [[ "$skip_worktree_cleanup" != "true" && -n "$branch_name" ]]; then
189
319
  if cleanup_output="$(cleanup_with_branch_tool yes 2>&1)"; then
190
320
  cleanup_mode="branch"
@@ -207,6 +337,9 @@ elif [[ "$skip_worktree_cleanup" != "true" && -n "$worktree_path" ]] && git -C "
207
337
  git -C "$repo_root" worktree remove "$worktree_path" --force || true
208
338
  git -C "$repo_root" worktree prune
209
339
  cleanup_mode="worktree"
340
+ elif [[ "$skip_worktree_cleanup" != "true" ]] && cleanup_orphan_worktree_dir; then
341
+ orphan_fallback_used="true"
342
+ cleanup_mode="orphan-worktree"
210
343
  elif [[ "$skip_worktree_cleanup" == "true" ]]; then
211
344
  cleanup_mode="archived-only"
212
345
  fi