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.
- package/README.md +323 -349
- package/bin/pr-risk.sh +28 -6
- package/hooks/heartbeat-hooks.sh +62 -22
- package/npm/bin/agent-control-plane.js +434 -12
- package/package.json +1 -1
- package/references/architecture.md +8 -0
- package/references/control-plane-map.md +6 -2
- package/references/release-checklist.md +0 -2
- package/tools/bin/agent-github-update-labels +6 -1
- package/tools/bin/agent-project-catch-up-issue-pr-links +118 -0
- package/tools/bin/agent-project-catch-up-merged-prs +77 -21
- package/tools/bin/agent-project-catch-up-scheduled-issue-retries +123 -0
- package/tools/bin/agent-project-cleanup-session +84 -0
- package/tools/bin/agent-project-heartbeat-loop +10 -3
- package/tools/bin/agent-project-reconcile-issue-session +45 -12
- package/tools/bin/agent-project-reconcile-pr-session +25 -0
- package/tools/bin/agent-project-run-claude-session +2 -2
- package/tools/bin/agent-project-run-codex-resilient +57 -2
- package/tools/bin/agent-project-run-kilo-session +346 -14
- package/tools/bin/agent-project-run-ollama-session +658 -0
- package/tools/bin/agent-project-run-openclaw-session +73 -25
- package/tools/bin/agent-project-run-opencode-session +354 -14
- package/tools/bin/agent-project-run-pi-session +479 -0
- package/tools/bin/agent-project-worker-status +38 -1
- package/tools/bin/flow-config-lib.sh +123 -3
- package/tools/bin/flow-resident-worker-lib.sh +1 -1
- package/tools/bin/flow-shell-lib.sh +7 -2
- package/tools/bin/heartbeat-recovery-preflight.sh +1 -0
- package/tools/bin/heartbeat-safe-auto.sh +105 -17
- package/tools/bin/install-project-launchd.sh +19 -2
- package/tools/bin/prepare-worktree.sh +4 -4
- package/tools/bin/profile-activate.sh +2 -2
- package/tools/bin/profile-adopt.sh +2 -2
- package/tools/bin/project-init.sh +1 -1
- package/tools/bin/project-runtimectl.sh +90 -7
- package/tools/bin/provider-cooldown-state.sh +14 -14
- package/tools/bin/render-flow-config.sh +30 -33
- package/tools/bin/run-codex-task.sh +53 -4
- package/tools/bin/scaffold-profile.sh +18 -3
- package/tools/bin/start-issue-worker.sh +4 -1
- package/tools/bin/start-pr-fix-worker.sh +33 -0
- package/tools/bin/start-pr-review-worker.sh +34 -0
- package/tools/bin/start-resident-issue-loop.sh +5 -4
- package/tools/bin/sync-agent-repo.sh +2 -2
- package/tools/bin/sync-dependency-baseline.sh +3 -3
- package/tools/bin/sync-shared-agent-home.sh +4 -1
- package/tools/dashboard/app.js +62 -0
- package/tools/dashboard/dashboard_snapshot.py +53 -4
- package/tools/dashboard/index.html +5 -1
- package/tools/dashboard/styles.css +97 -20
- package/tools/templates/pr-fix-template.md +4 -8
- package/tools/templates/pr-merge-repair-template.md +4 -8
- 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
|
|
16
|
-
LOCAL_FIRST_PR_POLICY="${ACP_LOCAL_FIRST_PR_POLICY
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
package/hooks/heartbeat-hooks.sh
CHANGED
|
@@ -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:-${
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|