agent-control-plane 0.2.0 → 0.4.9

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 (59) hide show
  1. package/README.md +69 -19
  2. package/assets/workflow-catalog.json +1 -1
  3. package/bin/pr-risk.sh +22 -7
  4. package/bin/sync-pr-labels.sh +1 -1
  5. package/hooks/heartbeat-hooks.sh +125 -12
  6. package/hooks/issue-reconcile-hooks.sh +1 -1
  7. package/hooks/pr-reconcile-hooks.sh +1 -1
  8. package/npm/bin/agent-control-plane.js +296 -61
  9. package/package.json +11 -7
  10. package/tools/bin/agent-github-update-labels +36 -2
  11. package/tools/bin/agent-project-catch-up-merged-prs +4 -2
  12. package/tools/bin/agent-project-cleanup-session +49 -5
  13. package/tools/bin/agent-project-heartbeat-loop +119 -1471
  14. package/tools/bin/agent-project-publish-issue-pr +6 -3
  15. package/tools/bin/agent-project-reconcile-issue-session +78 -106
  16. package/tools/bin/agent-project-reconcile-pr-session +166 -143
  17. package/tools/bin/agent-project-retry-state +18 -7
  18. package/tools/bin/agent-project-run-claude-session +10 -0
  19. package/tools/bin/agent-project-run-codex-resilient +99 -14
  20. package/tools/bin/agent-project-run-codex-session +16 -5
  21. package/tools/bin/agent-project-run-kilo-session +10 -0
  22. package/tools/bin/agent-project-run-openclaw-session +10 -0
  23. package/tools/bin/agent-project-run-opencode-session +10 -0
  24. package/tools/bin/agent-project-sync-source-repo-main +163 -0
  25. package/tools/bin/agent-project-worker-status +10 -7
  26. package/tools/bin/cleanup-worktree.sh +6 -1
  27. package/tools/bin/flow-config-lib.sh +1257 -34
  28. package/tools/bin/flow-resident-worker-lib.sh +119 -1
  29. package/tools/bin/flow-shell-lib.sh +56 -0
  30. package/tools/bin/github-core-rate-limit-state.sh +77 -0
  31. package/tools/bin/github-write-outbox.sh +470 -0
  32. package/tools/bin/heartbeat-loop-cache-lib.sh +164 -0
  33. package/tools/bin/heartbeat-loop-counting-lib.sh +306 -0
  34. package/tools/bin/heartbeat-loop-pr-strategy-lib.sh +199 -0
  35. package/tools/bin/heartbeat-loop-scheduling-lib.sh +506 -0
  36. package/tools/bin/heartbeat-loop-worker-lib.sh +319 -0
  37. package/tools/bin/heartbeat-recovery-preflight.sh +12 -1
  38. package/tools/bin/heartbeat-safe-auto.sh +56 -3
  39. package/tools/bin/install-project-launchd.sh +17 -2
  40. package/tools/bin/project-init.sh +21 -1
  41. package/tools/bin/project-launchd-bootstrap.sh +16 -9
  42. package/tools/bin/project-runtimectl.sh +46 -2
  43. package/tools/bin/reconcile-bootstrap-lib.sh +113 -0
  44. package/tools/bin/resident-issue-controller-lib.sh +448 -0
  45. package/tools/bin/scaffold-profile.sh +61 -3
  46. package/tools/bin/start-pr-fix-worker.sh +47 -10
  47. package/tools/bin/start-resident-issue-loop.sh +28 -439
  48. package/tools/dashboard/app.js +37 -1
  49. package/tools/dashboard/dashboard_snapshot.py +65 -26
  50. package/tools/templates/pr-fix-template.md +3 -1
  51. package/tools/templates/pr-merge-repair-template.md +2 -1
  52. package/SKILL.md +0 -149
  53. package/references/architecture.md +0 -217
  54. package/references/commands.md +0 -128
  55. package/references/control-plane-map.md +0 -124
  56. package/references/docs-map.md +0 -73
  57. package/references/release-checklist.md +0 -65
  58. package/references/repo-map.md +0 -36
  59. package/tools/bin/split-retained-slice.sh +0 -124
@@ -285,6 +285,22 @@ flow_resident_issue_queue_file() {
285
285
  printf '%s/issue-%s.env\n' "$(flow_resident_issue_queue_pending_dir "${config_file}")" "${issue_id}"
286
286
  }
287
287
 
288
+ flow_resident_issue_claim_file() {
289
+ local config_file="${1:-}"
290
+ local issue_id="${2:?issue id required}"
291
+ local claimer_key="${3:?claimer key required}"
292
+
293
+ if [[ -z "${config_file}" ]]; then
294
+ config_file="$(resolve_flow_config_yaml "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}")"
295
+ fi
296
+
297
+ printf '%s/issue-%s.%s.%s.env\n' \
298
+ "$(flow_resident_issue_queue_claims_dir "${config_file}")" \
299
+ "${issue_id}" \
300
+ "${claimer_key}" \
301
+ "$$"
302
+ }
303
+
288
304
  flow_resident_issue_controller_file() {
289
305
  local config_file="${1:-}"
290
306
  local issue_id="${2:?issue id required}"
@@ -342,8 +358,11 @@ flow_resident_issue_enqueue() {
342
358
 
343
359
  tmp_file="${queue_file}.tmp.$$"
344
360
  flow_resident_write_metadata "${tmp_file}" \
361
+ "STATE_FORMAT_VERSION=1" \
362
+ "STATE_KIND=pending" \
345
363
  "ISSUE_ID=${issue_id}" \
346
364
  "QUEUED_BY=${queued_by}" \
365
+ "UPDATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
347
366
  "QUEUED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
348
367
  mv "${tmp_file}" "${queue_file}"
349
368
 
@@ -361,6 +380,9 @@ flow_resident_issue_claim_next() {
361
380
  local issue_id=""
362
381
  local claim_file=""
363
382
  local claim_key=""
383
+ local queued_by=""
384
+ local queued_at=""
385
+ local claimed_at=""
364
386
 
365
387
  if [[ -z "${config_file}" ]]; then
366
388
  config_file="$(resolve_flow_config_yaml "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}")"
@@ -378,8 +400,22 @@ flow_resident_issue_claim_next() {
378
400
  [[ -n "${issue_id}" ]] || continue
379
401
  [[ "${issue_id}" != "${skip_issue_id}" ]] || continue
380
402
 
381
- claim_file="${claims_dir}/issue-${issue_id}.${claim_key}.$$"
403
+ queued_by="$(flow_resident_metadata_value "${queue_file}" "QUEUED_BY" || true)"
404
+ queued_at="$(flow_resident_metadata_value "${queue_file}" "QUEUED_AT" || true)"
405
+ claimed_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
406
+ claim_file="$(flow_resident_issue_claim_file "${config_file}" "${issue_id}" "${claim_key}")"
382
407
  if mv "${queue_file}" "${claim_file}" 2>/dev/null; then
408
+ flow_resident_write_metadata "${claim_file}" \
409
+ "STATE_FORMAT_VERSION=1" \
410
+ "STATE_KIND=claim" \
411
+ "ISSUE_ID=${issue_id}" \
412
+ "QUEUED_BY=${queued_by}" \
413
+ "QUEUED_AT=${queued_at}" \
414
+ "SESSION=${claimer_key}" \
415
+ "CLAIMED_BY=${claim_key}" \
416
+ "CLAIMED_AT=${claimed_at}" \
417
+ "UPDATED_AT=${claimed_at}" \
418
+ "CLAIM_FILE=${claim_file}"
383
419
  printf 'ISSUE_ID=%s\n' "${issue_id}"
384
420
  printf 'CLAIM_FILE=%s\n' "${claim_file}"
385
421
  return 0
@@ -441,6 +477,87 @@ flow_resident_issue_controller_reap_file() {
441
477
  return 0
442
478
  }
443
479
 
480
+ flow_resident_issue_reap_stale_claims() {
481
+ local config_file="${1:-}"
482
+ local claims_dir=""
483
+ local claim_file=""
484
+ local claim_token=""
485
+ local claim_pid=""
486
+ local issue_id=""
487
+ local queued_by=""
488
+ local queued_at=""
489
+ local claimed_at=""
490
+ local existing_pending_file=""
491
+ local other_claim=""
492
+ local other_token=""
493
+ local other_pid=""
494
+
495
+ if [[ -z "${config_file}" ]]; then
496
+ config_file="$(resolve_flow_config_yaml "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}")"
497
+ fi
498
+
499
+ claims_dir="$(flow_resident_issue_queue_claims_dir "${config_file}")"
500
+ mkdir -p "${claims_dir}"
501
+
502
+ for claim_file in "${claims_dir}"/issue-*.env; do
503
+ [[ -f "${claim_file}" ]] || continue
504
+
505
+ claim_token="${claim_file##*/}"
506
+ claim_token="${claim_token%.env}"
507
+ claim_pid="${claim_token##*.}"
508
+ [[ "${claim_pid}" =~ ^[0-9]+$ ]] || continue
509
+
510
+ if flow_resident_controller_pid_live "${claim_pid}" "start-resident-issue-loop.sh"; then
511
+ continue
512
+ fi
513
+
514
+ issue_id="$(flow_resident_metadata_value "${claim_file}" "ISSUE_ID" || true)"
515
+ [[ -n "${issue_id}" ]] || issue_id="${claim_token#issue-}"
516
+ issue_id="${issue_id%%.*}"
517
+ [[ -n "${issue_id}" ]] || continue
518
+
519
+ # If another live claim exists for the same issue, do not re-queue this one.
520
+ for other_claim in "${claims_dir}/issue-${issue_id}."*; do
521
+ [[ -f "${other_claim}" ]] || continue
522
+ other_token="${other_claim##*/}"
523
+ other_token="${other_token%.env}"
524
+ other_pid="${other_token##*.}"
525
+ [[ "${other_pid}" =~ ^[0-9]+$ ]] || continue
526
+ if [[ "${other_pid}" == "${claim_pid}" ]]; then
527
+ continue
528
+ fi
529
+ if flow_resident_controller_pid_live "${other_pid}" "start-resident-issue-loop.sh"; then
530
+ issue_id=""
531
+ break
532
+ fi
533
+ done
534
+ [[ -n "${issue_id}" ]] || continue
535
+
536
+ existing_pending_file="$(flow_resident_issue_queue_file "${config_file}" "${issue_id}")"
537
+ if [[ -f "${existing_pending_file}" ]]; then
538
+ rm -f "${claim_file}"
539
+ continue
540
+ fi
541
+
542
+ queued_by="$(flow_resident_metadata_value "${claim_file}" "QUEUED_BY" || true)"
543
+ queued_at="$(flow_resident_metadata_value "${claim_file}" "QUEUED_AT" || true)"
544
+ claimed_at="$(flow_resident_metadata_value "${claim_file}" "CLAIMED_AT" || true)"
545
+
546
+ [[ -n "${queued_by}" ]] || queued_by="heartbeat"
547
+ [[ -n "${queued_at}" ]] || queued_at="${claimed_at:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")}"
548
+
549
+ flow_resident_write_metadata "${existing_pending_file}" \
550
+ "STATE_FORMAT_VERSION=1" \
551
+ "STATE_KIND=pending" \
552
+ "ISSUE_ID=${issue_id}" \
553
+ "QUEUED_BY=${queued_by}" \
554
+ "QUEUED_AT=${queued_at}" \
555
+ "UPDATED_AT=${claimed_at:-${queued_at}}"
556
+ rm -f "${claim_file}"
557
+
558
+ done
559
+ }
560
+
444
561
  flow_resident_issue_reap_stale_state() {
445
562
  local config_file="${1:-}"
446
563
  local resident_root=""
@@ -458,6 +575,7 @@ flow_resident_issue_reap_stale_state() {
458
575
  reaped=$((reaped + 1))
459
576
  fi
460
577
  done
578
+ flow_resident_issue_reap_stale_claims "${config_file}" || true
461
579
 
462
580
  printf '%s\n' "${reaped}"
463
581
  }
@@ -5,6 +5,62 @@ flow_canonical_skill_name() {
5
5
  printf '%s\n' "${AGENT_CONTROL_PLANE_SKILL_NAME:-agent-control-plane}"
6
6
  }
7
7
 
8
+ flow_resolve_python_bin() {
9
+ if [[ -n "${PYTHON_BIN:-}" && -x "${PYTHON_BIN:-}" ]]; then
10
+ printf '%s\n' "${PYTHON_BIN}"
11
+ return 0
12
+ fi
13
+
14
+ if command -v python3 >/dev/null 2>&1; then
15
+ command -v python3
16
+ return 0
17
+ fi
18
+
19
+ if [[ -x /opt/homebrew/bin/python3 ]]; then
20
+ printf '%s\n' "/opt/homebrew/bin/python3"
21
+ return 0
22
+ fi
23
+
24
+ if command -v python >/dev/null 2>&1; then
25
+ command -v python
26
+ return 0
27
+ fi
28
+
29
+ return 1
30
+ }
31
+
32
+ flow_format_epoch_utc() {
33
+ local epoch="${1:-}"
34
+ local python_bin=""
35
+
36
+ if ! [[ "${epoch}" =~ ^[0-9]+$ ]] || [[ "${epoch}" == "0" ]]; then
37
+ return 1
38
+ fi
39
+
40
+ if date -u -r "${epoch}" +"%Y-%m-%dT%H:%M:%SZ" >/dev/null 2>&1; then
41
+ date -u -r "${epoch}" +"%Y-%m-%dT%H:%M:%SZ"
42
+ return 0
43
+ fi
44
+
45
+ if date -u -d "@${epoch}" +"%Y-%m-%dT%H:%M:%SZ" >/dev/null 2>&1; then
46
+ date -u -d "@${epoch}" +"%Y-%m-%dT%H:%M:%SZ"
47
+ return 0
48
+ fi
49
+
50
+ python_bin="$(flow_resolve_python_bin 2>/dev/null || true)"
51
+ if [[ -n "${python_bin}" ]]; then
52
+ "${python_bin}" - "${epoch}" <<'PY'
53
+ import datetime
54
+ import sys
55
+
56
+ print(datetime.datetime.fromtimestamp(int(sys.argv[1]), datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"))
57
+ PY
58
+ return 0
59
+ fi
60
+
61
+ return 1
62
+ }
63
+
8
64
  flow_compat_skill_alias() {
9
65
  printf '%s\n' "${AGENT_CONTROL_PLANE_COMPAT_ALIAS:-}"
10
66
  }
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ # shellcheck source=/dev/null
6
+ source "${SCRIPT_DIR}/flow-config-lib.sh"
7
+
8
+ usage() {
9
+ cat <<'EOF'
10
+ Usage:
11
+ github-core-rate-limit-state.sh get
12
+ github-core-rate-limit-state.sh schedule [reason] [--next-at-epoch <unix-seconds>]
13
+ github-core-rate-limit-state.sh clear
14
+ EOF
15
+ }
16
+
17
+ action="${1:-}"
18
+ reason="${2:-github-api-rate-limit}"
19
+ next_at_epoch=""
20
+
21
+ if [[ $# -lt 1 ]]; then
22
+ usage >&2
23
+ exit 1
24
+ fi
25
+
26
+ shift || true
27
+ if [[ $# -gt 0 ]]; then
28
+ reason="${1:-github-api-rate-limit}"
29
+ shift || true
30
+ fi
31
+
32
+ while [[ $# -gt 0 ]]; do
33
+ case "$1" in
34
+ --next-at-epoch)
35
+ next_at_epoch="${2:-}"
36
+ shift 2
37
+ ;;
38
+ --help|-h)
39
+ usage
40
+ exit 0
41
+ ;;
42
+ *)
43
+ echo "unknown argument: $1" >&2
44
+ usage >&2
45
+ exit 1
46
+ ;;
47
+ esac
48
+ done
49
+
50
+ case "${action}" in
51
+ get|schedule|clear) ;;
52
+ *)
53
+ usage >&2
54
+ exit 1
55
+ ;;
56
+ esac
57
+
58
+ CONFIG_YAML="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
59
+ STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
60
+ COOLDOWNS="$(flow_resolve_retry_cooldowns "${CONFIG_YAML}")"
61
+
62
+ exec_args=(
63
+ --state-root "${STATE_ROOT}"
64
+ --kind github
65
+ --item-id core-api
66
+ --action "${action}"
67
+ --reason "${reason}"
68
+ --cooldowns "${COOLDOWNS}"
69
+ )
70
+
71
+ if [[ "${action}" == "schedule" && "${next_at_epoch}" =~ ^[0-9]+$ ]]; then
72
+ exec_args+=(--next-at-epoch "${next_at_epoch}")
73
+ fi
74
+
75
+ ACP_STATE_ROOT="${STATE_ROOT}" \
76
+ ACP_RETRY_COOLDOWNS="${COOLDOWNS}" \
77
+ exec bash "${SCRIPT_DIR}/agent-project-retry-state" "${exec_args[@]}"