agent-control-plane 0.3.0 → 0.6.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 +141 -28
- package/assets/workflow-catalog.json +1 -1
- package/bin/pr-risk.sh +22 -7
- package/bin/sync-pr-labels.sh +1 -1
- package/hooks/heartbeat-hooks.sh +125 -12
- package/hooks/issue-reconcile-hooks.sh +1 -1
- package/hooks/pr-reconcile-hooks.sh +1 -1
- package/npm/bin/agent-control-plane.js +257 -59
- package/package.json +39 -32
- package/tools/bin/debug-session.sh +106 -0
- package/tools/bin/flow-config-lib.sh +1203 -60
- package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
- package/tools/bin/flow-runtime-doctor.sh +5 -1
- package/tools/bin/flow-shell-lib.sh +32 -0
- package/tools/bin/github-core-rate-limit-state.sh +77 -0
- package/tools/bin/github-write-outbox.sh +470 -0
- package/tools/bin/heartbeat-loop-scheduling-lib.sh +7 -7
- package/tools/bin/heartbeat-safe-auto.sh +42 -0
- package/tools/bin/install-project-launchd.sh +17 -2
- package/tools/bin/install-project-systemd.sh +255 -0
- package/tools/bin/project-init.sh +21 -1
- package/tools/bin/project-launchd-bootstrap.sh +5 -1
- package/tools/bin/project-runtimectl.sh +91 -2
- package/tools/bin/project-systemd-bootstrap.sh +74 -0
- package/tools/bin/scaffold-profile.sh +61 -3
- package/tools/bin/uninstall-project-systemd.sh +87 -0
- package/tools/dashboard/app.js +228 -6
- package/tools/dashboard/dashboard_snapshot.py +55 -0
- package/tools/dashboard/issue_queue_state.py +101 -0
- package/tools/dashboard/server.py +123 -1
- package/tools/dashboard/styles.css +526 -455
- package/tools/templates/pr-fix-template.md +3 -1
- package/tools/templates/pr-merge-repair-template.md +2 -1
- package/references/architecture.md +0 -217
- package/references/commands.md +0 -128
- package/references/control-plane-map.md +0 -124
- package/references/docs-map.md +0 -73
- package/references/release-checklist.md +0 -65
- package/references/repo-map.md +0 -36
- package/tools/bin/agent-cleanup-worktree +0 -247
- package/tools/bin/agent-github-update-labels +0 -71
- package/tools/bin/agent-init-worktree +0 -216
- package/tools/bin/agent-project-archive-run +0 -52
- package/tools/bin/agent-project-capture-worker +0 -46
- package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
- package/tools/bin/agent-project-catch-up-merged-prs +0 -194
- package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
- package/tools/bin/agent-project-cleanup-session +0 -513
- package/tools/bin/agent-project-detached-launch +0 -127
- package/tools/bin/agent-project-heartbeat-loop +0 -1029
- package/tools/bin/agent-project-open-issue-worktree +0 -89
- package/tools/bin/agent-project-open-pr-worktree +0 -80
- package/tools/bin/agent-project-publish-issue-pr +0 -465
- package/tools/bin/agent-project-reconcile-issue-session +0 -1398
- package/tools/bin/agent-project-reconcile-pr-session +0 -1230
- package/tools/bin/agent-project-retry-state +0 -147
- package/tools/bin/agent-project-run-claude-session +0 -805
- package/tools/bin/agent-project-run-codex-resilient +0 -955
- package/tools/bin/agent-project-run-codex-session +0 -435
- package/tools/bin/agent-project-run-kilo-session +0 -369
- package/tools/bin/agent-project-run-ollama-session +0 -658
- package/tools/bin/agent-project-run-openclaw-session +0 -1309
- package/tools/bin/agent-project-run-opencode-session +0 -377
- package/tools/bin/agent-project-run-pi-session +0 -479
- package/tools/bin/agent-project-sync-anchor-repo +0 -139
- package/tools/bin/agent-project-worker-status +0 -188
- package/tools/bin/branch-verification-guard.sh +0 -364
- package/tools/bin/capture-worker.sh +0 -18
- package/tools/bin/cleanup-worktree.sh +0 -52
- package/tools/bin/codex-quota +0 -31
- package/tools/bin/create-follow-up-issue.sh +0 -114
- package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
- package/tools/bin/issue-publish-localization-guard.sh +0 -142
- package/tools/bin/issue-publish-scope-guard.sh +0 -242
- package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
- package/tools/bin/issue-resource-class.sh +0 -12
- package/tools/bin/kick-scheduler.sh +0 -75
- package/tools/bin/label-follow-up-issues.sh +0 -14
- package/tools/bin/new-pr-worktree.sh +0 -50
- package/tools/bin/new-worktree.sh +0 -49
- package/tools/bin/pr-risk.sh +0 -12
- package/tools/bin/prepare-worktree.sh +0 -142
- package/tools/bin/provider-cooldown-state.sh +0 -204
- package/tools/bin/publish-issue-worker.sh +0 -31
- package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
- package/tools/bin/reconcile-issue-worker.sh +0 -34
- package/tools/bin/reconcile-pr-worker.sh +0 -34
- package/tools/bin/record-verification.sh +0 -71
- package/tools/bin/render-flow-config.sh +0 -98
- package/tools/bin/resident-issue-controller-lib.sh +0 -448
- package/tools/bin/resident-issue-queue-status.py +0 -35
- package/tools/bin/retry-state.sh +0 -31
- package/tools/bin/reuse-issue-worktree.sh +0 -121
- package/tools/bin/run-codex-bypass.sh +0 -3
- package/tools/bin/run-codex-safe.sh +0 -3
- package/tools/bin/run-codex-task.sh +0 -280
- package/tools/bin/serve-dashboard.sh +0 -5
- package/tools/bin/split-retained-slice.sh +0 -124
- package/tools/bin/start-issue-worker.sh +0 -943
- package/tools/bin/start-pr-fix-worker.sh +0 -491
- package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
- package/tools/bin/start-pr-review-worker.sh +0 -261
- package/tools/bin/start-resident-issue-loop.sh +0 -499
- package/tools/bin/update-github-labels.sh +0 -14
- package/tools/bin/worker-status.sh +0 -19
- package/tools/bin/workflow-catalog.sh +0 -77
|
@@ -14,7 +14,12 @@ Create a new installed project profile, profile templates, and profile notes.
|
|
|
14
14
|
|
|
15
15
|
Options:
|
|
16
16
|
--profile-id <id> Profile id, e.g. billing-api
|
|
17
|
-
--repo-slug <owner/repo>
|
|
17
|
+
--repo-slug <owner/repo> Forge repo slug
|
|
18
|
+
--forge-provider <github|gitea> Forge provider (default: github)
|
|
19
|
+
--gitea-base-url <url> Base URL for a local/self-hosted Gitea instance
|
|
20
|
+
--gitea-token <token> Gitea API token written to profile runtime.env
|
|
21
|
+
--gitea-username <user> Gitea username written to profile runtime.env
|
|
22
|
+
--gitea-password <pass> Gitea password written to profile runtime.env
|
|
18
23
|
--profile-home <path> Profile registry root (default: ~/.agent-runtime/control-plane/profiles)
|
|
19
24
|
--repo-root <path> Canonical repo root
|
|
20
25
|
--agent-repo-root <path> Agent-owned anchor repo root (defaults to repo root)
|
|
@@ -40,6 +45,11 @@ EOF
|
|
|
40
45
|
|
|
41
46
|
profile_id=""
|
|
42
47
|
repo_slug=""
|
|
48
|
+
forge_provider="github"
|
|
49
|
+
gitea_base_url=""
|
|
50
|
+
gitea_token=""
|
|
51
|
+
gitea_username=""
|
|
52
|
+
gitea_password=""
|
|
43
53
|
profile_home=""
|
|
44
54
|
repo_root=""
|
|
45
55
|
agent_repo_root=""
|
|
@@ -63,6 +73,11 @@ while [[ $# -gt 0 ]]; do
|
|
|
63
73
|
case "$1" in
|
|
64
74
|
--profile-id) profile_id="${2:-}"; shift 2 ;;
|
|
65
75
|
--repo-slug) repo_slug="${2:-}"; shift 2 ;;
|
|
76
|
+
--forge-provider) forge_provider="${2:-}"; shift 2 ;;
|
|
77
|
+
--gitea-base-url) gitea_base_url="${2:-}"; shift 2 ;;
|
|
78
|
+
--gitea-token) gitea_token="${2:-}"; shift 2 ;;
|
|
79
|
+
--gitea-username) gitea_username="${2:-}"; shift 2 ;;
|
|
80
|
+
--gitea-password) gitea_password="${2:-}"; shift 2 ;;
|
|
66
81
|
--profile-home) profile_home="${2:-}"; shift 2 ;;
|
|
67
82
|
--repo-root) repo_root="${2:-}"; shift 2 ;;
|
|
68
83
|
--agent-repo-root) agent_repo_root="${2:-}"; shift 2 ;;
|
|
@@ -104,6 +119,14 @@ case "$coding_worker" in
|
|
|
104
119
|
;;
|
|
105
120
|
esac
|
|
106
121
|
|
|
122
|
+
case "$forge_provider" in
|
|
123
|
+
github|gitea) ;;
|
|
124
|
+
*)
|
|
125
|
+
echo "--forge-provider must be github or gitea" >&2
|
|
126
|
+
exit 1
|
|
127
|
+
;;
|
|
128
|
+
esac
|
|
129
|
+
|
|
107
130
|
case "$claude_effort" in
|
|
108
131
|
low|medium|high|max) ;;
|
|
109
132
|
*)
|
|
@@ -133,6 +156,7 @@ profile_home="${profile_home:-$(resolve_flow_profile_registry_root)}"
|
|
|
133
156
|
profiles_dir="${profile_home}"
|
|
134
157
|
profile_dir="${profiles_dir}/${profile_id}"
|
|
135
158
|
profile_yaml="${profile_dir}/control-plane.yaml"
|
|
159
|
+
profile_runtime_env="${profile_dir}/runtime.env"
|
|
136
160
|
profile_templates_dir="${profile_dir}/templates"
|
|
137
161
|
profile_readme="${profile_dir}/README.md"
|
|
138
162
|
|
|
@@ -228,9 +252,9 @@ session_naming:
|
|
|
228
252
|
pr_worktree_branch_prefix: "${pr_worktree_branch_prefix}"
|
|
229
253
|
managed_pr_branch_globs: "${managed_pr_branch_globs}"
|
|
230
254
|
queue:
|
|
231
|
-
source: "
|
|
255
|
+
source: "${forge_provider}"
|
|
232
256
|
issue_labels:
|
|
233
|
-
ready: "
|
|
257
|
+
ready: ""
|
|
234
258
|
running: "agent-running"
|
|
235
259
|
blocked: "agent-blocked"
|
|
236
260
|
heavy: "agent-e2e-heavy"
|
|
@@ -366,7 +390,38 @@ policies:
|
|
|
366
390
|
EOF
|
|
367
391
|
}
|
|
368
392
|
|
|
393
|
+
write_profile_runtime_env() {
|
|
394
|
+
local target_file="${1:?target file required}"
|
|
395
|
+
|
|
396
|
+
: >"$target_file"
|
|
397
|
+
{
|
|
398
|
+
printf 'ACP_FORGE_PROVIDER=%s\n' "${forge_provider}"
|
|
399
|
+
printf 'F_LOSNING_FORGE_PROVIDER=%s\n' "${forge_provider}"
|
|
400
|
+
if [[ "${forge_provider}" == "gitea" ]]; then
|
|
401
|
+
if [[ -n "${gitea_base_url}" ]]; then
|
|
402
|
+
printf 'ACP_GITEA_BASE_URL=%s\n' "${gitea_base_url}"
|
|
403
|
+
printf 'GITEA_BASE_URL=%s\n' "${gitea_base_url}"
|
|
404
|
+
fi
|
|
405
|
+
if [[ -n "${gitea_token}" ]]; then
|
|
406
|
+
printf 'ACP_GITEA_TOKEN=%s\n' "${gitea_token}"
|
|
407
|
+
printf 'GITEA_TOKEN=%s\n' "${gitea_token}"
|
|
408
|
+
fi
|
|
409
|
+
if [[ -n "${gitea_username}" ]]; then
|
|
410
|
+
printf 'ACP_GITEA_USERNAME=%s\n' "${gitea_username}"
|
|
411
|
+
printf 'GITEA_USERNAME=%s\n' "${gitea_username}"
|
|
412
|
+
fi
|
|
413
|
+
if [[ -n "${gitea_password}" ]]; then
|
|
414
|
+
printf 'ACP_GITEA_PASSWORD=%s\n' "${gitea_password}"
|
|
415
|
+
printf 'GITEA_PASSWORD=%s\n' "${gitea_password}"
|
|
416
|
+
fi
|
|
417
|
+
printf 'ACP_SOURCE_SYNC_REMOTE=gitea\n'
|
|
418
|
+
printf 'F_LOSNING_SOURCE_SYNC_REMOTE=gitea\n'
|
|
419
|
+
fi
|
|
420
|
+
} >"$target_file"
|
|
421
|
+
}
|
|
422
|
+
|
|
369
423
|
write_profile_yaml "$profile_yaml"
|
|
424
|
+
write_profile_runtime_env "$profile_runtime_env"
|
|
370
425
|
write_profile_readme "$profile_readme"
|
|
371
426
|
|
|
372
427
|
if compgen -G "${flow_skill_dir}/tools/templates/*.md" >/dev/null; then
|
|
@@ -375,15 +430,18 @@ fi
|
|
|
375
430
|
|
|
376
431
|
profile_home_real="$(mkdir -p "$profile_home" && cd "$profile_home" && pwd -P)"
|
|
377
432
|
profile_yaml_real="$(cd "$(dirname "$profile_yaml")" && pwd -P)/$(basename "$profile_yaml")"
|
|
433
|
+
profile_runtime_env_real="$(cd "$(dirname "$profile_runtime_env")" && pwd -P)/$(basename "$profile_runtime_env")"
|
|
378
434
|
profile_templates_dir_real="$(cd "$profile_templates_dir" && pwd -P)"
|
|
379
435
|
profile_readme_real="$(cd "$(dirname "$profile_readme")" && pwd -P)/$(basename "$profile_readme")"
|
|
380
436
|
|
|
381
437
|
printf 'PROFILE_ID=%s\n' "$profile_id"
|
|
382
438
|
printf 'PROFILE_HOME=%s\n' "$profile_home_real"
|
|
383
439
|
printf 'PROFILE_YAML=%s\n' "$profile_yaml_real"
|
|
440
|
+
printf 'PROFILE_RUNTIME_ENV=%s\n' "$profile_runtime_env_real"
|
|
384
441
|
printf 'PROFILE_TEMPLATE_DIR=%s\n' "$profile_templates_dir_real"
|
|
385
442
|
printf 'PROFILE_README=%s\n' "$profile_readme_real"
|
|
386
443
|
printf 'REPO_SLUG=%s\n' "$repo_slug"
|
|
444
|
+
printf 'FORGE_PROVIDER=%s\n' "$forge_provider"
|
|
387
445
|
printf 'CODING_WORKER=%s\n' "$coding_worker"
|
|
388
446
|
printf 'NEXT_STEP=ACP_PROJECT_ID=%s bash %s/tools/bin/render-flow-config.sh\n' "$profile_id" "$flow_skill_dir"
|
|
389
447
|
printf 'NEXT_STEP=bash %s/tools/bin/sync-shared-agent-home.sh\n' "$flow_skill_dir"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
usage() {
|
|
5
|
+
cat <<'EOF'
|
|
6
|
+
Usage:
|
|
7
|
+
uninstall-project-systemd.sh --profile-id <id> [options]
|
|
8
|
+
|
|
9
|
+
Remove a previously installed systemd user service for an ACP project.
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
--profile-id <id> Installed profile id to manage
|
|
13
|
+
--unit-name <name> Override systemd unit name (default: agent-project-<id>.service)
|
|
14
|
+
--help Show this help
|
|
15
|
+
EOF
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
# shellcheck source=/dev/null
|
|
20
|
+
source "${script_dir}/flow-config-lib.sh"
|
|
21
|
+
|
|
22
|
+
profile_id_override=""
|
|
23
|
+
unit_name_override=""
|
|
24
|
+
profile_registry_root_override="${ACP_PROJECT_RUNTIME_PROFILE_REGISTRY_ROOT:-${ACP_PROFILE_REGISTRY_ROOT:-}}"
|
|
25
|
+
|
|
26
|
+
while [[ $# -gt 0 ]]; do
|
|
27
|
+
case "$1" in
|
|
28
|
+
--profile-id) profile_id_override="${2:-}"; shift 2 ;;
|
|
29
|
+
--unit-name) unit_name_override="${2:-}"; shift 2 ;;
|
|
30
|
+
--help|-h) usage; exit 0 ;;
|
|
31
|
+
*) echo "Unknown argument: $1" >&2; usage >&2; exit 64 ;;
|
|
32
|
+
esac
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
if [[ -z "${profile_id_override}" ]]; then
|
|
36
|
+
usage >&2
|
|
37
|
+
exit 64
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
export ACP_PROJECT_ID="${profile_id_override}"
|
|
41
|
+
export AGENT_PROJECT_ID="${profile_id_override}"
|
|
42
|
+
|
|
43
|
+
if [[ -n "${profile_registry_root_override}" ]]; then
|
|
44
|
+
export ACP_PROFILE_REGISTRY_ROOT="${profile_registry_root_override}"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
flow_skill_dir="$(resolve_flow_skill_dir "${BASH_SOURCE[0]}")"
|
|
48
|
+
if ! flow_require_explicit_profile_selection "${flow_skill_dir}" "uninstall-project-systemd.sh"; then
|
|
49
|
+
exit 64
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
config_yaml="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
|
|
53
|
+
if [[ ! -f "${config_yaml}" ]]; then
|
|
54
|
+
printf 'profile not installed: %s\n' "${profile_id_override}" >&2
|
|
55
|
+
exit 66
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
profile_id="$(flow_resolve_adapter_id "${config_yaml}")"
|
|
59
|
+
profile_slug="$(printf '%s' "${profile_id}" | tr -c 'A-Za-z0-9._-' '-')"
|
|
60
|
+
home_dir="${ACP_PROJECT_RUNTIME_HOME_DIR:-${HOME:-}}"
|
|
61
|
+
systemd_dir="${ACP_PROJECT_RUNTIME_SYSTEMD_DIR:-${home_dir}/.config/systemd/user}"
|
|
62
|
+
unit_name="${unit_name_override:-${ACP_PROJECT_RUNTIME_SYSTEMD_UNIT:-agent-project-${profile_slug}.service}}"
|
|
63
|
+
unit_file="${systemd_dir}/${unit_name}"
|
|
64
|
+
workspace_dir="${ACP_PROJECT_RUNTIME_WORKSPACE_DIR:-${home_dir}/.agent-runtime/control-plane/workspace}"
|
|
65
|
+
wrapper_path="${workspace_dir}/bin/agent-project-${profile_slug}-systemd.sh"
|
|
66
|
+
|
|
67
|
+
# Check if systemd is available
|
|
68
|
+
if ! command -v systemctl &>/dev/null; then
|
|
69
|
+
echo "systemctl not found. Is systemd installed?" >&2
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Stop and disable the service
|
|
74
|
+
"${SYSTEMCTL_BIN}" --user stop "${unit_name}" 2>&1 || true
|
|
75
|
+
"${SYSTEMCTL_BIN}" --user disable "${unit_name}" 2>&1 || true
|
|
76
|
+
|
|
77
|
+
# Remove unit file
|
|
78
|
+
rm -f "${unit_file}"
|
|
79
|
+
|
|
80
|
+
# Remove wrapper script
|
|
81
|
+
rm -f "${wrapper_path}"
|
|
82
|
+
|
|
83
|
+
printf 'SYSTEMD_UNINSTALL_STATUS=ok\n'
|
|
84
|
+
printf 'PROFILE_ID=%s\n' "${profile_id}"
|
|
85
|
+
printf 'UNIT_NAME=%s\n' "${unit_name}"
|
|
86
|
+
printf 'UNIT_FILE=%s\n' "${unit_file}"
|
|
87
|
+
printf 'WRAPPER=%s\n' "${wrapper_path}"
|
package/tools/dashboard/app.js
CHANGED
|
@@ -72,6 +72,35 @@ function relativeTime(input) {
|
|
|
72
72
|
return seconds >= 0 ? `${absolute}s ago` : `in ${absolute}s`;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
function formatDuration(seconds) {
|
|
76
|
+
if (!seconds && seconds !== 0) return "n/a";
|
|
77
|
+
const absSeconds = Math.abs(seconds);
|
|
78
|
+
const parts = [];
|
|
79
|
+
const units = [
|
|
80
|
+
[86400, "d"],
|
|
81
|
+
[3600, "h"],
|
|
82
|
+
[60, "m"],
|
|
83
|
+
[1, "s"],
|
|
84
|
+
];
|
|
85
|
+
for (const [unitSeconds, label] of units) {
|
|
86
|
+
if (absSeconds >= unitSeconds) {
|
|
87
|
+
const amount = Math.floor(absSeconds / unitSeconds);
|
|
88
|
+
parts.push(`${amount}${label}`);
|
|
89
|
+
seconds -= amount * unitSeconds;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return parts.slice(0, 2).join(" ") || "0s";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function timeRemaining(isoString) {
|
|
96
|
+
if (!isoString) return "n/a";
|
|
97
|
+
const next = new Date(isoString);
|
|
98
|
+
if (Number.isNaN(next.getTime())) return isoString;
|
|
99
|
+
const diffSeconds = Math.round((next.getTime() - Date.now()) / 1000);
|
|
100
|
+
if (diffSeconds <= 0) return "ready now";
|
|
101
|
+
return formatDuration(diffSeconds);
|
|
102
|
+
}
|
|
103
|
+
|
|
75
104
|
function statusClass(status) {
|
|
76
105
|
if (!status) return "";
|
|
77
106
|
return status.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
@@ -111,9 +140,10 @@ function renderOverview(snapshot) {
|
|
|
111
140
|
acc.cooldowns += profile.counts.provider_cooldowns;
|
|
112
141
|
acc.queue += profile.counts.queued_issues;
|
|
113
142
|
acc.alerts += profile.counts.alerts || 0;
|
|
143
|
+
acc.pendingGithubWrites += profile.counts.pending_github_writes || 0;
|
|
114
144
|
return acc;
|
|
115
145
|
},
|
|
116
|
-
{ activeRuns: 0, runningRuns: 0, implementedRuns: 0, reportedRuns: 0, blockedRuns: 0, controllers: 0, cooldowns: 0, queue: 0, alerts: 0 },
|
|
146
|
+
{ activeRuns: 0, runningRuns: 0, implementedRuns: 0, reportedRuns: 0, blockedRuns: 0, controllers: 0, cooldowns: 0, queue: 0, alerts: 0, pendingGithubWrites: 0 },
|
|
117
147
|
);
|
|
118
148
|
|
|
119
149
|
overviewNode.innerHTML = [
|
|
@@ -125,8 +155,11 @@ function renderOverview(snapshot) {
|
|
|
125
155
|
["Blocked", totals.blockedRuns],
|
|
126
156
|
["Live Controllers", totals.controllers],
|
|
127
157
|
["Provider Cooldowns", totals.cooldowns],
|
|
158
|
+
["Pending GitHub Writes", totals.pendingGithubWrites],
|
|
128
159
|
["Alerts", totals.alerts],
|
|
129
160
|
["Queued Issues", totals.queue],
|
|
161
|
+
["Retries", totals.retries || 0],
|
|
162
|
+
["Blockers", totals.blockers || 0],
|
|
130
163
|
]
|
|
131
164
|
.map(
|
|
132
165
|
([label, value]) => `
|
|
@@ -237,6 +270,8 @@ function renderProfile(profile) {
|
|
|
237
270
|
["Live controllers", profile.counts.live_resident_controllers],
|
|
238
271
|
["Stale controllers", profile.counts.stale_resident_controllers],
|
|
239
272
|
["Provider cooldowns", profile.counts.provider_cooldowns],
|
|
273
|
+
["Pending GitHub writes", profile.counts.pending_github_writes || 0],
|
|
274
|
+
["Failed GitHub writes", profile.counts.failed_github_writes || 0],
|
|
240
275
|
["Alerts", profile.counts.alerts || 0],
|
|
241
276
|
["Issue retries", profile.counts.active_retries],
|
|
242
277
|
["Queued issues", profile.counts.queued_issues],
|
|
@@ -252,6 +287,23 @@ function renderProfile(profile) {
|
|
|
252
287
|
)
|
|
253
288
|
.join("");
|
|
254
289
|
|
|
290
|
+
const runsFilterState = window._acpRunsFilter || { search: "", status: "all" };
|
|
291
|
+
window._acpRunsFilter = runsFilterState;
|
|
292
|
+
|
|
293
|
+
const filteredRuns = profile.runs.filter((row) => {
|
|
294
|
+
if (runsFilterState.status !== "all" && row.status !== runsFilterState.status) return false;
|
|
295
|
+
if (runsFilterState.search) {
|
|
296
|
+
const q = runsFilterState.search.toLowerCase();
|
|
297
|
+
return (
|
|
298
|
+
(row.session || "").toLowerCase().includes(q) ||
|
|
299
|
+
(row.coding_worker || "").toLowerCase().includes(q) ||
|
|
300
|
+
(row.task_kind || "").toLowerCase().includes(q) ||
|
|
301
|
+
(row.task_id || "").toLowerCase().includes(q)
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return true;
|
|
305
|
+
});
|
|
306
|
+
|
|
255
307
|
const runsTable = renderTable(
|
|
256
308
|
[
|
|
257
309
|
{ label: "Session", render: (row) => `<div class="mono">${row.session}</div>` },
|
|
@@ -262,10 +314,26 @@ function renderProfile(profile) {
|
|
|
262
314
|
{ label: "Result", render: renderResult },
|
|
263
315
|
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${row.updated_at}</div>` : "n/a" },
|
|
264
316
|
],
|
|
265
|
-
|
|
317
|
+
filteredRuns,
|
|
266
318
|
"No active run directories for this profile.",
|
|
267
319
|
);
|
|
268
320
|
|
|
321
|
+
const historyFilterState = window._acpHistoryFilter || { search: "", result: "all" };
|
|
322
|
+
window._acpHistoryFilter = historyFilterState;
|
|
323
|
+
|
|
324
|
+
const filteredHistory = (profile.recent_history || []).filter((row) => {
|
|
325
|
+
if (historyFilterState.result !== "all" && row.result_kind !== historyFilterState.result) return false;
|
|
326
|
+
if (historyFilterState.search) {
|
|
327
|
+
const q = historyFilterState.search.toLowerCase();
|
|
328
|
+
return (
|
|
329
|
+
(row.session || "").toLowerCase().includes(q) ||
|
|
330
|
+
(row.coding_worker || "").toLowerCase().includes(q) ||
|
|
331
|
+
(row.task_kind || "").toLowerCase().includes(q)
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return true;
|
|
335
|
+
});
|
|
336
|
+
|
|
269
337
|
const recentHistoryTable = renderTable(
|
|
270
338
|
[
|
|
271
339
|
{ label: "Session", render: (row) => `<div class="mono">${row.session}</div>` },
|
|
@@ -275,7 +343,7 @@ function renderProfile(profile) {
|
|
|
275
343
|
{ label: "Result", render: renderResult },
|
|
276
344
|
{ label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${row.updated_at}</div>` : "n/a" },
|
|
277
345
|
],
|
|
278
|
-
|
|
346
|
+
filteredHistory,
|
|
279
347
|
"No recently archived runs.",
|
|
280
348
|
);
|
|
281
349
|
|
|
@@ -301,6 +369,7 @@ function renderProfile(profile) {
|
|
|
301
369
|
{ label: "Reason", render: (row) => row.last_reason || "n/a" },
|
|
302
370
|
{ label: "Attempts", key: "attempts" },
|
|
303
371
|
{ label: "Next attempt", render: (row) => row.next_attempt_at ? `${relativeTime(row.next_attempt_at)}<div class="muted">${row.next_attempt_at}</div>` : "n/a" },
|
|
372
|
+
{ label: "Time Remaining", render: (row) => row.next_attempt_at ? timeRemaining(row.next_attempt_at) : "n/a" },
|
|
304
373
|
],
|
|
305
374
|
profile.issue_retries || [],
|
|
306
375
|
"No issue retries recorded.",
|
|
@@ -313,6 +382,7 @@ function renderProfile(profile) {
|
|
|
313
382
|
{ label: "Reason", render: (row) => row.last_reason || "n/a" },
|
|
314
383
|
{ label: "Attempts", key: "attempts" },
|
|
315
384
|
{ label: "Next attempt", render: (row) => row.next_attempt_at ? `${relativeTime(row.next_attempt_at)}<div class="muted">${row.next_attempt_at}</div>` : "n/a" },
|
|
385
|
+
{ label: "Time Remaining", render: (row) => row.next_attempt_at ? timeRemaining(row.next_attempt_at) : "n/a" },
|
|
316
386
|
],
|
|
317
387
|
profile.pr_retries || [],
|
|
318
388
|
"No PR retries recorded.",
|
|
@@ -340,6 +410,7 @@ function renderProfile(profile) {
|
|
|
340
410
|
{ label: "Reason", render: (row) => row.last_reason || "n/a" },
|
|
341
411
|
{ label: "Attempts", key: "attempts" },
|
|
342
412
|
{ label: "Next attempt", render: (row) => row.next_attempt_at ? `${relativeTime(row.next_attempt_at)}<div class="muted">${row.next_attempt_at}</div>` : "n/a" },
|
|
413
|
+
{ label: "Time Remaining", render: (row) => row.next_attempt_at ? timeRemaining(row.next_attempt_at) : "n/a" },
|
|
343
414
|
],
|
|
344
415
|
profile.provider_cooldowns,
|
|
345
416
|
"No provider cooldowns recorded.",
|
|
@@ -350,6 +421,7 @@ function renderProfile(profile) {
|
|
|
350
421
|
{ label: "Issue", key: "issue_id" },
|
|
351
422
|
{ label: "Interval", render: (row) => `${row.interval_seconds}s` },
|
|
352
423
|
{ label: "Next due", render: (row) => row.next_due_at ? `${relativeTime(row.next_due_at)}<div class="muted">${row.next_due_at}</div>` : "n/a" },
|
|
424
|
+
{ label: "Time Remaining", render: (row) => row.next_due_at ? timeRemaining(row.next_due_at) : "n/a" },
|
|
353
425
|
{ label: "Last started", render: (row) => row.last_started_at ? `${relativeTime(row.last_started_at)}<div class="muted">${row.last_started_at}</div>` : "n/a" },
|
|
354
426
|
],
|
|
355
427
|
profile.scheduled_issues,
|
|
@@ -378,6 +450,26 @@ function renderProfile(profile) {
|
|
|
378
450
|
"No claimed issues.",
|
|
379
451
|
);
|
|
380
452
|
|
|
453
|
+
const githubOutbox = profile.github_outbox || { counts: {}, pending: [] };
|
|
454
|
+
const githubOutboxTable = renderTable(
|
|
455
|
+
[
|
|
456
|
+
{ label: "Type", render: (row) => row.type || "n/a" },
|
|
457
|
+
{ label: "Target", render: (row) => `${row.kind || row.type || "write"} #${row.number || "?"}` },
|
|
458
|
+
{
|
|
459
|
+
label: "Payload",
|
|
460
|
+
render: (row) => {
|
|
461
|
+
if (row.type === "labels") {
|
|
462
|
+
return `+${row.add_count || 0} / -${row.remove_count || 0}`;
|
|
463
|
+
}
|
|
464
|
+
return row.body_preview || "n/a";
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
{ label: "Created", render: (row) => row.created_at ? `${relativeTime(row.created_at)}<div class="muted">${row.created_at}</div>` : "n/a" },
|
|
468
|
+
],
|
|
469
|
+
githubOutbox.pending || [],
|
|
470
|
+
"No pending GitHub write intents.",
|
|
471
|
+
);
|
|
472
|
+
|
|
381
473
|
const codexRotationPanel =
|
|
382
474
|
profile.coding_worker === "codex"
|
|
383
475
|
? `
|
|
@@ -389,6 +481,28 @@ function renderProfile(profile) {
|
|
|
389
481
|
`
|
|
390
482
|
: "";
|
|
391
483
|
|
|
484
|
+
const runsFilterBar = `
|
|
485
|
+
<div class="filter-bar">
|
|
486
|
+
<input type="text" class="filter-search" placeholder="Search runs..." value="${runsFilterState.search}"
|
|
487
|
+
oninput="window._acpRunsFilter.search=this.value; rerenderAll();" />
|
|
488
|
+
<button class="filter-btn ${runsFilterState.status === 'all' ? 'active' : ''}" onclick="window._acpRunsFilter.status='all'; rerenderAll();">All</button>
|
|
489
|
+
<button class="filter-btn ${runsFilterState.status === 'RUNNING' ? 'active' : ''}" onclick="window._acpRunsFilter.status='RUNNING'; rerenderAll();">Running</button>
|
|
490
|
+
<button class="filter-btn ${runsFilterState.status === 'SUCCEEDED' ? 'active' : ''}" onclick="window._acpRunsFilter.status='SUCCEEDED'; rerenderAll();">Completed</button>
|
|
491
|
+
<button class="filter-btn ${runsFilterState.status === 'FAILED' ? 'active' : ''}" onclick="window._acpRunsFilter.status='FAILED'; rerenderAll();">Failed</button>
|
|
492
|
+
</div>
|
|
493
|
+
`;
|
|
494
|
+
|
|
495
|
+
const historyFilterBar = `
|
|
496
|
+
<div class="filter-bar">
|
|
497
|
+
<input type="text" class="filter-search" placeholder="Search history..." value="${historyFilterState.search}"
|
|
498
|
+
oninput="window._acpHistoryFilter.search=this.value; rerenderAll();" />
|
|
499
|
+
<button class="filter-btn ${historyFilterState.result === 'all' ? 'active' : ''}" onclick="window._acpHistoryFilter.result='all'; rerenderAll();">All</button>
|
|
500
|
+
<button class="filter-btn ${historyFilterState.result === 'implemented' ? 'active' : ''}" onclick="window._acpHistoryFilter.result='implemented'; rerenderAll();">Implemented</button>
|
|
501
|
+
<button class="filter-btn ${historyFilterState.result === 'reported' ? 'active' : ''}" onclick="window._acpHistoryFilter.result='reported'; rerenderAll();">Reported</button>
|
|
502
|
+
<button class="filter-btn ${historyFilterState.result === 'blocked' ? 'active' : ''}" onclick="window._acpHistoryFilter.result='blocked'; rerenderAll();">Blocked</button>
|
|
503
|
+
</div>
|
|
504
|
+
`;
|
|
505
|
+
|
|
392
506
|
return `
|
|
393
507
|
<article class="profile">
|
|
394
508
|
<header class="profile-header">
|
|
@@ -411,11 +525,13 @@ function renderProfile(profile) {
|
|
|
411
525
|
<section class="panel">
|
|
412
526
|
<h3>Active Runs</h3>
|
|
413
527
|
<p class="panel-subtitle">Lifecycle shows technical session completion. Result shows what the run achieved: implemented, reported, or blocked.</p>
|
|
528
|
+
${runsFilterBar}
|
|
414
529
|
${runsTable}
|
|
415
530
|
</section>
|
|
416
531
|
<section class="panel">
|
|
417
532
|
<h3>Recent Completed Runs</h3>
|
|
418
533
|
<p class="panel-subtitle">Recently archived runs so they do not disappear from the dashboard immediately after completion.</p>
|
|
534
|
+
${historyFilterBar}
|
|
419
535
|
${recentHistoryTable}
|
|
420
536
|
</section>
|
|
421
537
|
<section class="panel">
|
|
@@ -436,6 +552,18 @@ function renderProfile(profile) {
|
|
|
436
552
|
<h3>Resident Worker Metadata</h3>
|
|
437
553
|
${workerTable}
|
|
438
554
|
</section>
|
|
555
|
+
<section class="panel">
|
|
556
|
+
<h3>Troubleshooting</h3>
|
|
557
|
+
<p class="panel-subtitle">Run diagnostics or debugging tools against this live profile.</p>
|
|
558
|
+
<div class="action-bar">
|
|
559
|
+
<button class="action-btn" onclick="runDoctor('${profile.id}')">Run Doctor</button>
|
|
560
|
+
<button class="action-btn" onclick="exportProfile('${profile.id}')">Export Profile</button>
|
|
561
|
+
<button class="action-btn" onclick="document.getElementById('import-file-${profile.id}').click()">Import Profile</button>
|
|
562
|
+
<input type="file" id="import-file-${profile.id}" style="display:none" accept=".json" onchange="importProfile('${profile.id}', this)">
|
|
563
|
+
<span id="doctor-status-${profile.id}"></span>
|
|
564
|
+
</div>
|
|
565
|
+
<pre id="doctor-output-${profile.id}" class="doctor-output" style="display:none;"></pre>
|
|
566
|
+
</section>
|
|
439
567
|
<section class="panel half">
|
|
440
568
|
<h3>Provider Cooldowns</h3>
|
|
441
569
|
${cooldownTable}
|
|
@@ -452,6 +580,11 @@ function renderProfile(profile) {
|
|
|
452
580
|
<h3>Claimed Issues</h3>
|
|
453
581
|
${claimsTable}
|
|
454
582
|
</section>
|
|
583
|
+
<section class="panel">
|
|
584
|
+
<h3>GitHub Outbox</h3>
|
|
585
|
+
<p class="panel-subtitle">Local write intents queued while ACP defers or retries GitHub sync. Pending ${githubOutbox.counts?.pending || 0}, sent ${githubOutbox.counts?.sent || 0}, failed ${githubOutbox.counts?.failed || 0}.</p>
|
|
586
|
+
${githubOutboxTable}
|
|
587
|
+
</section>
|
|
455
588
|
</section>
|
|
456
589
|
</article>
|
|
457
590
|
`;
|
|
@@ -494,9 +627,8 @@ async function loadSnapshot() {
|
|
|
494
627
|
throw new Error(`Snapshot request failed with ${response.status}`);
|
|
495
628
|
}
|
|
496
629
|
const snapshot = await response.json();
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
profilesNode.innerHTML = snapshot.profiles.map(renderProfile).join("");
|
|
630
|
+
window._acpSnapshot = snapshot;
|
|
631
|
+
renderFromSnapshot(snapshot);
|
|
500
632
|
await maybeNotifyAlerts(snapshot);
|
|
501
633
|
} catch (error) {
|
|
502
634
|
generatedAtNode.textContent = `Snapshot load failed: ${error.message}`;
|
|
@@ -506,6 +638,96 @@ async function loadSnapshot() {
|
|
|
506
638
|
}
|
|
507
639
|
}
|
|
508
640
|
|
|
641
|
+
function renderFromSnapshot(snapshot) {
|
|
642
|
+
generatedAtNode.textContent = `Snapshot: ${snapshot.generated_at}`;
|
|
643
|
+
renderOverview(snapshot);
|
|
644
|
+
profilesNode.innerHTML = snapshot.profiles.map(renderProfile).join("");
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function rerenderAll() {
|
|
648
|
+
const snapshot = window._acpSnapshot;
|
|
649
|
+
if (!snapshot) return;
|
|
650
|
+
renderFromSnapshot(snapshot);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function runDoctor(profileId) {
|
|
654
|
+
const statusEl = document.getElementById(`doctor-status-${profileId}`);
|
|
655
|
+
const outputEl = document.getElementById(`doctor-output-${profileId}`);
|
|
656
|
+
if (statusEl) statusEl.textContent = "Running...";
|
|
657
|
+
if (outputEl) {
|
|
658
|
+
outputEl.style.display = "none";
|
|
659
|
+
outputEl.textContent = "";
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
const response = await fetch(`/api/doctor?profile_id=${encodeURIComponent(profileId)}`, { cache: "no-store" });
|
|
663
|
+
const data = await response.json();
|
|
664
|
+
if (statusEl) statusEl.textContent = response.ok ? "Done" : `Error: ${data.error || response.status}`;
|
|
665
|
+
if (outputEl) {
|
|
666
|
+
outputEl.style.display = "block";
|
|
667
|
+
outputEl.textContent = data.output || data.error || "No output";
|
|
668
|
+
}
|
|
669
|
+
} catch (error) {
|
|
670
|
+
if (statusEl) statusEl.textContent = `Error: ${error.message}`;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async function exportProfile(profileId) {
|
|
675
|
+
try {
|
|
676
|
+
const response = await fetch(`/api/profile/export?profile_id=${encodeURIComponent(profileId)}`, { cache: "no-store" });
|
|
677
|
+
if (!response.ok) {
|
|
678
|
+
const data = await response.json();
|
|
679
|
+
alert(`Export failed: ${data.error || response.status}`);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const data = await response.json();
|
|
683
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
|
684
|
+
const url = URL.createObjectURL(blob);
|
|
685
|
+
const a = document.createElement("a");
|
|
686
|
+
a.href = url;
|
|
687
|
+
a.download = `acp-profile-${profileId}.json`;
|
|
688
|
+
document.body.appendChild(a);
|
|
689
|
+
a.click();
|
|
690
|
+
document.body.removeChild(a);
|
|
691
|
+
URL.revokeObjectURL(url);
|
|
692
|
+
} catch (error) {
|
|
693
|
+
alert(`Export failed: ${error.message}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async function importProfile(profileId, inputEl) {
|
|
698
|
+
const file = inputEl.files[0];
|
|
699
|
+
if (!file) return;
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
const text = await file.text();
|
|
703
|
+
const data = JSON.parse(text);
|
|
704
|
+
|
|
705
|
+
if (!data.profile_id || !data.config) {
|
|
706
|
+
alert("Invalid profile file: missing profile_id or config");
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const response = await fetch("/api/profile/import", {
|
|
711
|
+
method: "POST",
|
|
712
|
+
headers: { "Content-Type": "application/json" },
|
|
713
|
+
body: JSON.stringify(data),
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
const result = await response.json();
|
|
717
|
+
if (response.ok) {
|
|
718
|
+
alert(`Profile ${profileId} imported successfully!`);
|
|
719
|
+
// Refresh the page to show imported profile
|
|
720
|
+
setTimeout(() => window.location.reload(), 1000);
|
|
721
|
+
} else {
|
|
722
|
+
alert(`Import failed: ${result.error || response.status}`);
|
|
723
|
+
}
|
|
724
|
+
} catch (error) {
|
|
725
|
+
alert(`Import failed: ${error.message}`);
|
|
726
|
+
} finally {
|
|
727
|
+
inputEl.value = "";
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
509
731
|
refreshButton.addEventListener("click", () => {
|
|
510
732
|
void loadSnapshot();
|
|
511
733
|
});
|
|
@@ -697,6 +697,57 @@ def collect_pr_retries(state_root: Path) -> list[dict[str, Any]]:
|
|
|
697
697
|
return items
|
|
698
698
|
|
|
699
699
|
|
|
700
|
+
def collect_github_outbox(state_root: Path) -> dict[str, Any]:
|
|
701
|
+
outbox_root = state_root / "github-outbox"
|
|
702
|
+
pending_root = outbox_root / "pending"
|
|
703
|
+
sent_root = outbox_root / "sent"
|
|
704
|
+
failed_root = outbox_root / "failed"
|
|
705
|
+
|
|
706
|
+
def list_items(root: Path, limit: int | None = None) -> list[dict[str, Any]]:
|
|
707
|
+
if not root.is_dir():
|
|
708
|
+
return []
|
|
709
|
+
|
|
710
|
+
items: list[dict[str, Any]] = []
|
|
711
|
+
for path in sorted(root.glob("*.json"), key=lambda item: item.stat().st_mtime, reverse=True):
|
|
712
|
+
payload = read_json_file(path)
|
|
713
|
+
items.append(
|
|
714
|
+
{
|
|
715
|
+
"type": str(payload.get("type", "")),
|
|
716
|
+
"repo_slug": str(payload.get("repo_slug", "")),
|
|
717
|
+
"number": str(payload.get("number", "")),
|
|
718
|
+
"kind": str(payload.get("kind", "")),
|
|
719
|
+
"created_at": str(payload.get("created_at", "")),
|
|
720
|
+
"updated_at": file_mtime_iso(path),
|
|
721
|
+
"file": str(path),
|
|
722
|
+
"add_count": len(payload.get("add", []) or []),
|
|
723
|
+
"remove_count": len(payload.get("remove", []) or []),
|
|
724
|
+
"body_preview": summarize_whitespace(str(payload.get("body", "")))[:120],
|
|
725
|
+
}
|
|
726
|
+
)
|
|
727
|
+
if limit is not None and len(items) >= limit:
|
|
728
|
+
break
|
|
729
|
+
return items
|
|
730
|
+
|
|
731
|
+
all_pending_items = list_items(pending_root)
|
|
732
|
+
pending_items = all_pending_items[:20]
|
|
733
|
+
sent_items = list_items(sent_root, limit=5)
|
|
734
|
+
failed_items = list_items(failed_root, limit=5)
|
|
735
|
+
|
|
736
|
+
return {
|
|
737
|
+
"pending": pending_items,
|
|
738
|
+
"sent_recent": sent_items,
|
|
739
|
+
"failed_recent": failed_items,
|
|
740
|
+
"counts": {
|
|
741
|
+
"pending": len(all_pending_items),
|
|
742
|
+
"sent": len(list(sent_root.glob("*.json"))) if sent_root.is_dir() else 0,
|
|
743
|
+
"failed": len(list(failed_root.glob("*.json"))) if failed_root.is_dir() else 0,
|
|
744
|
+
"pending_comments": sum(1 for item in all_pending_items if item["type"] == "comment"),
|
|
745
|
+
"pending_approvals": sum(1 for item in all_pending_items if item["type"] == "approval"),
|
|
746
|
+
"pending_label_updates": sum(1 for item in all_pending_items if item["type"] == "labels"),
|
|
747
|
+
},
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
|
|
700
751
|
def resolve_history_root(render_env: dict[str, str], yaml_env: dict[str, str], runs_root: Path) -> Path:
|
|
701
752
|
configured = (
|
|
702
753
|
render_env.get("EFFECTIVE_HISTORY_ROOT", "").strip()
|
|
@@ -726,6 +777,7 @@ def build_profile_snapshot(profile_id: str, registry_root: Path) -> dict[str, An
|
|
|
726
777
|
retries = collect_issue_retries(state_root)
|
|
727
778
|
pr_retries = collect_pr_retries(state_root)
|
|
728
779
|
queue = collect_issue_queue(state_root)
|
|
780
|
+
github_outbox = collect_github_outbox(state_root)
|
|
729
781
|
alerts = [alert for run in (runs + recent_history) for alert in run.get("alerts", [])]
|
|
730
782
|
codex_rotation = collect_codex_rotation(render_env)
|
|
731
783
|
|
|
@@ -773,6 +825,8 @@ def build_profile_snapshot(profile_id: str, registry_root: Path) -> dict[str, An
|
|
|
773
825
|
"active_retries": sum(1 for item in retries if not item.get("ready", True)),
|
|
774
826
|
"scheduled_issues": len(scheduled),
|
|
775
827
|
"alerts": len(alerts),
|
|
828
|
+
"pending_github_writes": github_outbox["counts"]["pending"],
|
|
829
|
+
"failed_github_writes": github_outbox["counts"]["failed"],
|
|
776
830
|
},
|
|
777
831
|
"runs": runs,
|
|
778
832
|
"recent_history": recent_history,
|
|
@@ -784,6 +838,7 @@ def build_profile_snapshot(profile_id: str, registry_root: Path) -> dict[str, An
|
|
|
784
838
|
"issue_retries": retries,
|
|
785
839
|
"pr_retries": pr_retries,
|
|
786
840
|
"issue_queue": queue,
|
|
841
|
+
"github_outbox": github_outbox,
|
|
787
842
|
}
|
|
788
843
|
|
|
789
844
|
|