agent-control-plane 0.7.1 → 0.9.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 +312 -7
- package/hooks/pr-reconcile-hooks.sh +12 -1
- package/package.json +11 -8
- package/tools/bin/adapter-interface.sh +58 -0
- package/tools/bin/claude-adapter.sh +14 -4
- package/tools/bin/codex-adapter.sh +1 -1
- package/tools/bin/flow-forge-lib.sh +4 -2
- package/tools/bin/flow-runtime-doctor.sh +68 -0
- package/tools/bin/heartbeat-safe-auto.sh +161 -0
- package/tools/bin/kilo-adapter.sh +1 -1
- package/tools/bin/ollama-adapter.sh +3 -18
- package/tools/bin/openclaw-adapter.sh +1 -1
- package/tools/bin/opencode-adapter.sh +1 -1
- package/tools/bin/pi-adapter.sh +1 -1
- package/tools/bin/render-flow-config.sh +98 -0
- package/tools/bin/sync-shared-agent-home.sh +23 -0
- package/tools/dashboard/app-v2.js +1120 -0
- package/tools/dashboard/app.js +129 -38
- package/tools/dashboard/index-inline.html +1533 -0
- package/tools/dashboard/index-v2.html +45 -0
- package/tools/dashboard/server.py +64 -15
- package/tools/dashboard/styles.css +595 -521
- package/tools/bin/profile-activate.sh +0 -109
- package/tools/bin/profile-adopt.sh +0 -225
- package/tools/bin/profile-smoke.sh +0 -461
- package/tools/bin/test-smoke.sh +0 -119
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="color-scheme" content="light dark" />
|
|
7
|
+
<title>ACP Worker Dashboard</title>
|
|
8
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<main class="page">
|
|
12
|
+
<header class="hero">
|
|
13
|
+
<div>
|
|
14
|
+
<p class="eyebrow">Agent Control Plane</p>
|
|
15
|
+
<h1>Worker Dashboard</h1>
|
|
16
|
+
<p class="subtitle">
|
|
17
|
+
Track active runs, resident controllers, queue pressure, and provider failover in one place.
|
|
18
|
+
</p>
|
|
19
|
+
<p class="subtitle">
|
|
20
|
+
Lifecycle shows whether a worker session finished cleanly. Result shows whether that cycle implemented work, reported findings, or stopped blocked.
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="hero-actions">
|
|
24
|
+
<div class="hero-controls">
|
|
25
|
+
<button id="theme-toggle" type="button" aria-label="Toggle dark mode">Dark mode</button>
|
|
26
|
+
<button id="refresh-button" type="button">Refresh now</button>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="meta">
|
|
29
|
+
<div>Auto refresh: <strong>5s</strong></div>
|
|
30
|
+
<div id="generated-at">Loading snapshot...</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</header>
|
|
34
|
+
|
|
35
|
+
<section id="overview" class="overview"></section>
|
|
36
|
+
<section id="profiles" class="profiles"></section>
|
|
37
|
+
</main>
|
|
38
|
+
|
|
39
|
+
<template id="empty-table-template">
|
|
40
|
+
<div class="empty-state">No data right now.</div>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<script src="./app.js" defer></script>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
|
@@ -27,11 +27,11 @@ async def broadcast_snapshot():
|
|
|
27
27
|
if not ws_clients:
|
|
28
28
|
return
|
|
29
29
|
payload = build_snapshot()
|
|
30
|
-
encoded = json.dumps(payload, indent=2)
|
|
30
|
+
encoded = json.dumps(payload, indent=2)
|
|
31
31
|
disconnected = set()
|
|
32
32
|
for ws in ws_clients:
|
|
33
33
|
try:
|
|
34
|
-
await ws.
|
|
34
|
+
await ws.send_str(encoded)
|
|
35
35
|
except Exception:
|
|
36
36
|
disconnected.add(ws)
|
|
37
37
|
for ws in disconnected:
|
|
@@ -40,13 +40,20 @@ async def broadcast_snapshot():
|
|
|
40
40
|
|
|
41
41
|
async def snapshot_handler(request: web.Request) -> web.Response:
|
|
42
42
|
"""HTTP endpoint: GET /api/snapshot.json"""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
try:
|
|
44
|
+
payload = build_snapshot()
|
|
45
|
+
encoded = json.dumps(payload, indent=2).encode("utf-8")
|
|
46
|
+
return web.Response(
|
|
47
|
+
body=encoded,
|
|
48
|
+
content_type="application/json",
|
|
49
|
+
headers={"Cache-Control": "no-store"},
|
|
50
|
+
)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
import traceback
|
|
53
|
+
error_detail = traceback.format_exc()
|
|
54
|
+
print(f"ERROR in snapshot_handler: {e}", flush=True)
|
|
55
|
+
print(error_detail, flush=True)
|
|
56
|
+
raise
|
|
50
57
|
|
|
51
58
|
|
|
52
59
|
async def doctor_handler(request: web.Request) -> web.Response:
|
|
@@ -80,7 +87,7 @@ async def doctor_handler(request: web.Request) -> web.Response:
|
|
|
80
87
|
payload = {"output": output.decode("utf-8", errors="replace")}
|
|
81
88
|
return web.Response(
|
|
82
89
|
body=json.dumps(payload),
|
|
83
|
-
content_type="application/json
|
|
90
|
+
content_type="application/json",
|
|
84
91
|
)
|
|
85
92
|
except asyncio.TimeoutError:
|
|
86
93
|
return web.Response(
|
|
@@ -92,10 +99,51 @@ async def doctor_handler(request: web.Request) -> web.Response:
|
|
|
92
99
|
payload = {"error": exc.returncode, "output": exc.output}
|
|
93
100
|
return web.Response(
|
|
94
101
|
body=json.dumps(payload),
|
|
95
|
-
content_type="application/json
|
|
102
|
+
content_type="application/json",
|
|
96
103
|
)
|
|
97
104
|
|
|
98
105
|
|
|
106
|
+
async def scheduler_status_handler(request: web.Request) -> web.Response:
|
|
107
|
+
"""HTTP endpoint: GET /api/scheduler-status"""
|
|
108
|
+
|
|
109
|
+
# Check if scheduler is running
|
|
110
|
+
state_dir = Path.home() / ".agent-runtime" / "control-plane" / "kick-scheduler"
|
|
111
|
+
pid_file = state_dir / "pid"
|
|
112
|
+
log_file = state_dir / "kick-scheduler.log"
|
|
113
|
+
|
|
114
|
+
is_running = False
|
|
115
|
+
pid = None
|
|
116
|
+
if pid_file.is_file():
|
|
117
|
+
pid = pid_file.read_text().strip()
|
|
118
|
+
if pid:
|
|
119
|
+
try:
|
|
120
|
+
os.kill(int(pid), 0) # Check if process exists
|
|
121
|
+
is_running = True
|
|
122
|
+
except (ProcessLookupError, PermissionError):
|
|
123
|
+
is_running = False
|
|
124
|
+
|
|
125
|
+
# Read last few lines of log
|
|
126
|
+
last_log_lines = []
|
|
127
|
+
if log_file.is_file():
|
|
128
|
+
try:
|
|
129
|
+
lines = log_file.read_text().strip().split("\n")
|
|
130
|
+
last_log_lines = lines[-5:] if len(lines) > 5 else lines
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
payload = {
|
|
135
|
+
"is_running": is_running,
|
|
136
|
+
"pid": pid if is_running else None,
|
|
137
|
+
"state_dir": str(state_dir),
|
|
138
|
+
"last_log_lines": last_log_lines,
|
|
139
|
+
"message": "Scheduler status from real state",
|
|
140
|
+
}
|
|
141
|
+
return web.Response(
|
|
142
|
+
body=json.dumps(payload, indent=2),
|
|
143
|
+
content_type="application/json",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
99
147
|
async def profile_export_handler(request: web.Request) -> web.Response:
|
|
100
148
|
"""HTTP endpoint: GET /api/profile/export?profile_id=xxx"""
|
|
101
149
|
profile_id = request.query.get("profile_id", "")
|
|
@@ -123,7 +171,7 @@ async def profile_export_handler(request: web.Request) -> web.Response:
|
|
|
123
171
|
payload = {"profile_id": profile_id, "config": config, "config_file": str(config_file)}
|
|
124
172
|
return web.Response(
|
|
125
173
|
body=json.dumps(payload),
|
|
126
|
-
content_type="application/json
|
|
174
|
+
content_type="application/json",
|
|
127
175
|
)
|
|
128
176
|
except Exception as exc:
|
|
129
177
|
return web.Response(
|
|
@@ -164,7 +212,7 @@ async def profile_import_handler(request: web.Request) -> web.Response:
|
|
|
164
212
|
payload = {"status": "ok", "profile_id": profile_id, "config_file": str(config_file)}
|
|
165
213
|
return web.Response(
|
|
166
214
|
body=json.dumps(payload),
|
|
167
|
-
content_type="application/json
|
|
215
|
+
content_type="application/json",
|
|
168
216
|
)
|
|
169
217
|
except Exception as exc:
|
|
170
218
|
return web.Response(
|
|
@@ -182,8 +230,8 @@ async def websocket_handler(request: web.Request) -> web.WebSocketResponse:
|
|
|
182
230
|
try:
|
|
183
231
|
# Send initial snapshot
|
|
184
232
|
payload = build_snapshot()
|
|
185
|
-
encoded = json.dumps(payload, indent=2)
|
|
186
|
-
await ws.
|
|
233
|
+
encoded = json.dumps(payload, indent=2)
|
|
234
|
+
await ws.send_str(encoded)
|
|
187
235
|
# Keep connection alive, listen for client messages (ping/pong)
|
|
188
236
|
async for msg in ws:
|
|
189
237
|
if msg.type == WSMsgType.TEXT:
|
|
@@ -204,6 +252,7 @@ def build_app() -> web.Application:
|
|
|
204
252
|
app.router.add_get("/api/snapshot.json", snapshot_handler)
|
|
205
253
|
app.router.add_get("/api/doctor", doctor_handler)
|
|
206
254
|
app.router.add_get("/api/profile/export", profile_export_handler)
|
|
255
|
+
app.router.add_get("/api/scheduler-status", scheduler_status_handler)
|
|
207
256
|
app.router.add_post("/api/profile/import", profile_import_handler)
|
|
208
257
|
app.router.add_get("/ws", websocket_handler)
|
|
209
258
|
# Static files (dashboard HTML/CSS/JS)
|