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
package/bin/pr-risk.sh CHANGED
@@ -6,14 +6,15 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
6
  source "${SCRIPT_DIR}/../tools/bin/flow-config-lib.sh"
7
7
 
8
8
  PR_NUMBER="${1:?usage: pr-risk.sh PR_NUMBER}"
9
+ [[ "${PR_NUMBER}" =~ ^[1-9][0-9]*$ ]] || { printf 'pr-risk: PR_NUMBER must be a positive integer, got: %s\n' "${PR_NUMBER}" >&2; exit 1; }
9
10
  CONFIG_YAML="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
10
11
  MANAGED_PR_PREFIXES_JSON="$(flow_managed_pr_prefixes_json "${CONFIG_YAML}")"
11
12
  MANAGED_PR_ISSUE_CAPTURE_REGEX="$(flow_managed_issue_branch_regex "${CONFIG_YAML}")"
12
13
  REPO_SLUG="$(flow_resolve_repo_slug "${CONFIG_YAML}")"
13
14
  AGENT_ROOT="$(flow_resolve_agent_root "${CONFIG_YAML}")"
14
15
  STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
15
- ALLOW_INFRA_CI_BYPASS="${ACP_ALLOW_INFRA_CI_BYPASS:-${F_LOSNING_ALLOW_INFRA_CI_BYPASS:-1}}"
16
- LOCAL_FIRST_PR_POLICY="${ACP_LOCAL_FIRST_PR_POLICY:-${F_LOSNING_LOCAL_FIRST_PR_POLICY:-1}}"
16
+ ALLOW_INFRA_CI_BYPASS="${ACP_ALLOW_INFRA_CI_BYPASS:-1}"
17
+ LOCAL_FIRST_PR_POLICY="${ACP_LOCAL_FIRST_PR_POLICY:-1}"
17
18
  PR_LANE_OVERRIDE_FILE="${STATE_ROOT}/pr-lane-overrides/${PR_NUMBER}.env"
18
19
  PR_LANE_OVERRIDE=""
19
20
 
@@ -37,7 +38,8 @@ gh_api_json_matching_or_fallback() {
37
38
  printf '%s\n' "${fallback}"
38
39
  }
39
40
 
40
- PR_JSON="$(gh pr view "$PR_NUMBER" -R "$REPO_SLUG" --json number,title,url,body,isDraft,headRefName,headRefOid,baseRefName,labels,files,mergeStateStatus,reviewDecision,reviewRequests,statusCheckRollup,comments)"
41
+ PR_JSON="$(gh pr view "$PR_NUMBER" -R "$REPO_SLUG" --json number,title,url,body,isDraft,headRefName,headRefOid,baseRefName,labels,files,mergeStateStatus,reviewDecision,reviewRequests,statusCheckRollup,comments 2>/dev/null)" \
42
+ || { printf 'pr-risk: gh pr view failed for PR %s (repo: %s)\n' "$PR_NUMBER" "$REPO_SLUG" >&2; exit 1; }
41
43
  PR_HEAD_SHA="$(jq -r '.headRefOid // ""' <<<"$PR_JSON")"
42
44
  PR_HEAD_COMMITTED_AT=""
43
45
  if [[ -n "${PR_HEAD_SHA}" ]]; then
@@ -51,9 +53,24 @@ fi
51
53
 
52
54
  PR_JSON="$PR_JSON" PR_HEAD_SHA="$PR_HEAD_SHA" PR_HEAD_COMMITTED_AT="$PR_HEAD_COMMITTED_AT" REVIEW_COMMENTS_JSON="$REVIEW_COMMENTS_JSON" CHECK_RUNS_JSON="$CHECK_RUNS_JSON" PR_LANE_OVERRIDE="${PR_LANE_OVERRIDE:-}" MANAGED_PR_PREFIXES_JSON="$MANAGED_PR_PREFIXES_JSON" MANAGED_PR_ISSUE_CAPTURE_REGEX="$MANAGED_PR_ISSUE_CAPTURE_REGEX" ALLOW_INFRA_CI_BYPASS="$ALLOW_INFRA_CI_BYPASS" LOCAL_FIRST_PR_POLICY="$LOCAL_FIRST_PR_POLICY" node <<'EOF'
53
55
  const { execFileSync } = require('node:child_process');
54
- const data = JSON.parse(process.env.PR_JSON);
55
- const reviewComments = JSON.parse(process.env.REVIEW_COMMENTS_JSON || '[]');
56
- const checkRunsPayload = JSON.parse(process.env.CHECK_RUNS_JSON || '{"check_runs":[]}');
56
+ let data;
57
+ try {
58
+ data = JSON.parse(process.env.PR_JSON);
59
+ } catch (err) {
60
+ process.stderr.write(`pr-risk: failed to parse PR_JSON: ${err.message}\n`);
61
+ process.stdout.write(JSON.stringify({ agentLane: 'ignore', error: `parse-error: ${err.message}` }));
62
+ process.exit(0);
63
+ }
64
+ let reviewComments, checkRunsPayload;
65
+ try {
66
+ reviewComments = JSON.parse(process.env.REVIEW_COMMENTS_JSON || '[]');
67
+ checkRunsPayload = JSON.parse(process.env.CHECK_RUNS_JSON || '{"check_runs":[]}');
68
+ } catch (err) {
69
+ process.stderr.write(`pr-risk: failed to parse auxiliary JSON env: ${err.message}\n`);
70
+ reviewComments = [];
71
+ checkRunsPayload = { check_runs: [] };
72
+ }
73
+ try {
57
74
  const checkRuns = checkRunsPayload.check_runs || [];
58
75
  const files = (data.files || []).map((file) => file.path);
59
76
  const labelNames = (data.labels || []).map((label) => label.name);
@@ -573,4 +590,9 @@ const result = {
573
590
  };
574
591
 
575
592
  process.stdout.write(JSON.stringify(result));
593
+ } catch (err) {
594
+ process.stderr.write(`pr-risk: unexpected error: ${err.message}\n${err.stack || ''}\n`);
595
+ process.stdout.write(JSON.stringify({ agentLane: 'ignore', error: `runtime-error: ${err.message}` }));
596
+ process.exit(1);
597
+ }
576
598
  EOF
@@ -18,14 +18,51 @@ REPO_SLUG="$(flow_resolve_repo_slug "${CONFIG_YAML}")"
18
18
  AGENT_REPO_ROOT="$(flow_resolve_agent_repo_root "${CONFIG_YAML}")"
19
19
  DEFAULT_BRANCH="$(flow_resolve_default_branch "${CONFIG_YAML}")"
20
20
  STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
21
- PENDING_LAUNCH_DIR="${ACP_PENDING_LAUNCH_DIR:-${F_LOSNING_PENDING_LAUNCH_DIR:-${STATE_ROOT}/pending-launches}}"
21
+ PENDING_LAUNCH_DIR="${ACP_PENDING_LAUNCH_DIR:-${STATE_ROOT}/pending-launches}"
22
22
  AGENT_PR_PREFIXES_JSON="$(flow_managed_pr_prefixes_json "${CONFIG_YAML}")"
23
23
  AGENT_PR_ISSUE_CAPTURE_REGEX="$(flow_managed_issue_branch_regex "${CONFIG_YAML}")"
24
24
  AGENT_PR_HANDOFF_LABEL="${AGENT_PR_HANDOFF_LABEL:-agent-handoff}"
25
25
  AGENT_EXCLUSIVE_LABEL="${AGENT_EXCLUSIVE_LABEL:-agent-exclusive}"
26
- CODING_WORKER="${ACP_CODING_WORKER:-${F_LOSNING_CODING_WORKER:-codex}}"
26
+ CODING_WORKER="${ACP_CODING_WORKER:-codex}"
27
27
  HEARTBEAT_ISSUE_JSON_CACHE_DIR="${TMPDIR:-/tmp}/heartbeat-issue-json.$$"
28
28
 
29
+ # ── Per-heartbeat snapshot cache ──────────────────────────────────────────────
30
+ # Fetch open issues and open PRs once per heartbeat cycle and reuse the
31
+ # snapshot for every list query. This eliminates 4x issue_list + 3x pr_list
32
+ # redundant GitHub API calls per cycle.
33
+ HEARTBEAT_SNAPSHOT_CACHE_DIR="${TMPDIR:-/tmp}/heartbeat-snapshot.$$"
34
+
35
+ # Snapshot functions are always called from subshells (command substitution or
36
+ # pipes), so in-memory variables would be lost immediately. We rely exclusively
37
+ # on the PID-scoped disk cache under HEARTBEAT_SNAPSHOT_CACHE_DIR.
38
+
39
+ heartbeat_cached_issue_list_json() {
40
+ mkdir -p "${HEARTBEAT_SNAPSHOT_CACHE_DIR}"
41
+ local cache_file="${HEARTBEAT_SNAPSHOT_CACHE_DIR}/issues.json"
42
+ if [[ ! -f "${cache_file}" ]]; then
43
+ local snapshot
44
+ snapshot="$(flow_github_issue_list_json "$REPO_SLUG" open 100 2>/dev/null || true)"
45
+ printf '%s' "${snapshot}" >"${cache_file}"
46
+ fi
47
+ cat "${cache_file}"
48
+ }
49
+
50
+ heartbeat_cached_pr_list_json() {
51
+ mkdir -p "${HEARTBEAT_SNAPSHOT_CACHE_DIR}"
52
+ local cache_file="${HEARTBEAT_SNAPSHOT_CACHE_DIR}/prs.json"
53
+ if [[ ! -f "${cache_file}" ]]; then
54
+ local snapshot
55
+ snapshot="$(flow_github_pr_list_json "$REPO_SLUG" open 100 2>/dev/null || true)"
56
+ printf '%s' "${snapshot}" >"${cache_file}"
57
+ fi
58
+ cat "${cache_file}"
59
+ }
60
+
61
+ heartbeat_invalidate_snapshot_cache() {
62
+ rm -rf "${HEARTBEAT_SNAPSHOT_CACHE_DIR}" 2>/dev/null || true
63
+ rm -rf "${HEARTBEAT_ISSUE_JSON_CACHE_DIR}" 2>/dev/null || true
64
+ }
65
+
29
66
  heartbeat_issue_retry_state_file() {
30
67
  local issue_id="${1:?issue id required}"
31
68
  printf '%s/retries/issues/%s.env\n' "${STATE_ROOT}" "${issue_id}"
@@ -92,10 +129,15 @@ heartbeat_issue_json_cached() {
92
129
  }
93
130
 
94
131
  heartbeat_open_agent_pr_issue_ids() {
132
+ mkdir -p "${HEARTBEAT_SNAPSHOT_CACHE_DIR}"
133
+ local cache_file="${HEARTBEAT_SNAPSHOT_CACHE_DIR}/pr_issue_ids.json"
134
+ if [[ -f "${cache_file}" ]]; then
135
+ cat "${cache_file}"
136
+ return 0
137
+ fi
95
138
  local pr_issue_ids_json=""
96
139
  pr_issue_ids_json="$(
97
- flow_github_pr_list_json "$REPO_SLUG" open 100 \
98
- 2>/dev/null \
140
+ heartbeat_cached_pr_list_json \
99
141
  | jq --argjson agentPrPrefixes "${AGENT_PR_PREFIXES_JSON}" --arg handoffLabel "${AGENT_PR_HANDOFF_LABEL}" --arg branchIssueRegex "${AGENT_PR_ISSUE_CAPTURE_REGEX}" '
100
142
  map(
101
143
  . as $pr
@@ -124,10 +166,10 @@ heartbeat_open_agent_pr_issue_ids() {
124
166
  )"
125
167
 
126
168
  if [[ -z "${pr_issue_ids_json:-}" ]]; then
127
- printf '[]\n'
128
- else
129
- printf '%s\n' "${pr_issue_ids_json}"
169
+ pr_issue_ids_json="[]"
130
170
  fi
171
+ printf '%s' "${pr_issue_ids_json}" >"${cache_file}"
172
+ printf '%s\n' "${pr_issue_ids_json}"
131
173
  }
132
174
 
133
175
  heartbeat_list_ready_issue_ids() {
@@ -136,8 +178,7 @@ heartbeat_list_ready_issue_ids() {
136
178
  open_agent_pr_issue_ids="$(heartbeat_open_agent_pr_issue_ids)"
137
179
 
138
180
  ready_issue_rows="$(
139
- flow_github_issue_list_json "$REPO_SLUG" open 100 \
140
- 2>/dev/null \
181
+ heartbeat_cached_issue_list_json \
141
182
  | jq -r --argjson openAgentPrIssueIds "${open_agent_pr_issue_ids}" '
142
183
  map(select(
143
184
  (any(.labels[]?; .name == "agent-running") | not)
@@ -171,8 +212,7 @@ heartbeat_list_blocked_recovery_issue_ids() {
171
212
  open_agent_pr_issue_ids="$(heartbeat_open_agent_pr_issue_ids)"
172
213
 
173
214
  blocked_issue_rows="$(
174
- flow_github_issue_list_json "$REPO_SLUG" open 100 \
175
- 2>/dev/null \
215
+ heartbeat_cached_issue_list_json \
176
216
  | jq -r --argjson openAgentPrIssueIds "${open_agent_pr_issue_ids}" '
177
217
  map(select(
178
218
  any(.labels[]?; .name == "agent-blocked")
@@ -270,8 +310,7 @@ heartbeat_list_exclusive_issue_ids() {
270
310
  local open_agent_pr_issue_ids
271
311
  open_agent_pr_issue_ids="$(heartbeat_open_agent_pr_issue_ids)"
272
312
 
273
- flow_github_issue_list_json "$REPO_SLUG" open 100 \
274
- 2>/dev/null \
313
+ heartbeat_cached_issue_list_json \
275
314
  | jq -r --arg exclusiveLabel "${AGENT_EXCLUSIVE_LABEL}" --argjson openAgentPrIssueIds "${open_agent_pr_issue_ids}" '
276
315
  map(select(
277
316
  any(.labels[]?; .name == $exclusiveLabel)
@@ -285,8 +324,7 @@ heartbeat_list_exclusive_issue_ids() {
285
324
  }
286
325
 
287
326
  heartbeat_list_running_issue_ids() {
288
- flow_github_issue_list_json "$REPO_SLUG" open 100 \
289
- 2>/dev/null \
327
+ heartbeat_cached_issue_list_json \
290
328
  | jq -r '
291
329
  map(select(any(.labels[]?; .name == "agent-running")))
292
330
  | sort_by(.createdAt, .number)
@@ -295,8 +333,7 @@ heartbeat_list_running_issue_ids() {
295
333
  }
296
334
 
297
335
  heartbeat_list_open_agent_pr_ids() {
298
- flow_github_pr_list_json "$REPO_SLUG" open 100 \
299
- 2>/dev/null \
336
+ heartbeat_cached_pr_list_json \
300
337
  | jq -r --argjson agentPrPrefixes "${AGENT_PR_PREFIXES_JSON}" --arg handoffLabel "${AGENT_PR_HANDOFF_LABEL}" '
301
338
  map(select(
302
339
  . as $pr
@@ -312,8 +349,7 @@ heartbeat_list_open_agent_pr_ids() {
312
349
  }
313
350
 
314
351
  heartbeat_list_exclusive_pr_ids() {
315
- flow_github_pr_list_json "$REPO_SLUG" open 100 \
316
- 2>/dev/null \
352
+ heartbeat_cached_pr_list_json \
317
353
  | jq -r --argjson agentPrPrefixes "${AGENT_PR_PREFIXES_JSON}" --arg handoffLabel "${AGENT_PR_HANDOFF_LABEL}" --arg exclusiveLabel "${AGENT_EXCLUSIVE_LABEL}" '
318
354
  map(select(
319
355
  . as $pr
@@ -444,16 +480,20 @@ heartbeat_pr_risk_json() {
444
480
  heartbeat_mark_issue_running() {
445
481
  local issue_id="${1:?issue id required}"
446
482
  local is_heavy="${2:-no}"
483
+ local cached_json
484
+ cached_json="$(heartbeat_issue_json_cached "$issue_id" 2>/dev/null || true)"
447
485
  if [[ "$is_heavy" == "yes" ]]; then
448
- bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-ready --remove agent-blocked --add agent-running --add agent-e2e-heavy >/dev/null
486
+ ACP_CACHED_ISSUE_JSON="${cached_json}" bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-ready --remove agent-blocked --add agent-running --add agent-e2e-heavy >/dev/null || true
449
487
  else
450
- bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-ready --remove agent-blocked --add agent-running >/dev/null
488
+ ACP_CACHED_ISSUE_JSON="${cached_json}" bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-ready --remove agent-blocked --add agent-running >/dev/null || true
451
489
  fi
452
490
  }
453
491
 
454
492
  heartbeat_issue_launch_failed() {
455
493
  local issue_id="${1:?issue id required}"
456
- bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-running >/dev/null || true
494
+ local cached_json
495
+ cached_json="$(heartbeat_issue_json_cached "$issue_id" 2>/dev/null || true)"
496
+ ACP_CACHED_ISSUE_JSON="${cached_json}" bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-running >/dev/null || true
457
497
  }
458
498
 
459
499
  heartbeat_ensure_issue_label_exists() {