agent-control-plane 0.1.14 → 0.1.16

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.
@@ -1,9 +1,56 @@
1
1
  const refreshButton = document.querySelector("#refresh-button");
2
+ const themeToggleButton = document.querySelector("#theme-toggle");
2
3
  const generatedAtNode = document.querySelector("#generated-at");
3
4
  const overviewNode = document.querySelector("#overview");
4
5
  const profilesNode = document.querySelector("#profiles");
5
6
  const seenAlertIds = new Set();
6
7
  let notificationPermissionRequested = false;
8
+ const THEME_STORAGE_KEY = "acp-dashboard-theme";
9
+
10
+ function systemPrefersDark() {
11
+ return typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches;
12
+ }
13
+
14
+ function currentThemePreference() {
15
+ try {
16
+ const stored = window.localStorage.getItem(THEME_STORAGE_KEY);
17
+ if (stored === "light" || stored === "dark") return stored;
18
+ } catch (_error) {
19
+ // Ignore storage access issues and fall back to system preference.
20
+ }
21
+ return systemPrefersDark() ? "dark" : "light";
22
+ }
23
+
24
+ function updateThemeToggleLabel(theme) {
25
+ if (!themeToggleButton) return;
26
+ const nextTheme = theme === "dark" ? "light" : "dark";
27
+ const label = nextTheme === "dark" ? "Dark mode" : "Light mode";
28
+ themeToggleButton.textContent = label;
29
+ themeToggleButton.setAttribute("aria-label", `Switch to ${label.toLowerCase()}`);
30
+ }
31
+
32
+ function applyTheme(theme) {
33
+ document.documentElement.dataset.theme = theme;
34
+ updateThemeToggleLabel(theme);
35
+ }
36
+
37
+ function persistTheme(theme) {
38
+ try {
39
+ window.localStorage.setItem(THEME_STORAGE_KEY, theme);
40
+ } catch (_error) {
41
+ // Ignore storage access issues.
42
+ }
43
+ }
44
+
45
+ function initializeTheme() {
46
+ applyTheme(currentThemePreference());
47
+ if (!themeToggleButton) return;
48
+ themeToggleButton.addEventListener("click", () => {
49
+ const nextTheme = document.documentElement.dataset.theme === "dark" ? "light" : "dark";
50
+ applyTheme(nextTheme);
51
+ persistTheme(nextTheme);
52
+ });
53
+ }
7
54
 
8
55
  function relativeTime(input) {
9
56
  if (!input) return "n/a";
@@ -314,6 +361,16 @@ function renderProfile(profile) {
314
361
  "No pending leased issues.",
315
362
  );
316
363
 
364
+ const claimsTable = renderTable(
365
+ [
366
+ { label: "Issue", key: "issue_id" },
367
+ { label: "Session", render: (row) => row.session ? `<div class="mono">${row.session}</div>` : "n/a" },
368
+ { label: "Updated", render: (row) => row.updated_at ? `${relativeTime(row.updated_at)}<div class="muted">${row.updated_at}</div>` : "n/a" },
369
+ ],
370
+ profile.issue_queue.claims || [],
371
+ "No claimed issues.",
372
+ );
373
+
317
374
  const codexRotationPanel =
318
375
  profile.coding_worker === "codex"
319
376
  ? `
@@ -384,6 +441,10 @@ function renderProfile(profile) {
384
441
  <h3>Pending Issue Queue</h3>
385
442
  ${queueTable}
386
443
  </section>
444
+ <section class="panel half">
445
+ <h3>Claimed Issues</h3>
446
+ ${claimsTable}
447
+ </section>
387
448
  </section>
388
449
  </article>
389
450
  `;
@@ -442,6 +503,7 @@ refreshButton.addEventListener("click", () => {
442
503
  void loadSnapshot();
443
504
  });
444
505
 
506
+ initializeTheme();
445
507
  void loadSnapshot();
446
508
  window.setInterval(() => {
447
509
  void loadSnapshot();
@@ -200,6 +200,11 @@ GITHUB_RATE_LIMIT_PATTERNS = [
200
200
  ),
201
201
  ]
202
202
 
203
+ WORKER_PREFLIGHT_NETWORK_BLOCKED_PATTERN = re.compile(
204
+ r"Blocked on external network access.*?What I ran:\s*-\s*`(?P<command>[^`]+)`.*?Exact failure:\s*`(?P<failure>[^`]+)`",
205
+ re.IGNORECASE | re.DOTALL,
206
+ )
207
+
203
208
 
204
209
  def summarize_whitespace(text: str) -> str:
205
210
  return re.sub(r"\s+", " ", text).strip()
@@ -242,6 +247,52 @@ def extract_github_rate_limit_alert(run_dir: Path, run: dict[str, Any]) -> dict[
242
247
  return None
243
248
 
244
249
 
250
+ def extract_worker_preflight_network_blocked_alert(run_dir: Path, run: dict[str, Any]) -> dict[str, Any] | None:
251
+ candidate_files = [
252
+ run_dir / "issue-comment.md",
253
+ run_dir / "pr-comment.md",
254
+ ]
255
+ for path in candidate_files:
256
+ text = read_tail_text(path)
257
+ if not text:
258
+ continue
259
+ match = WORKER_PREFLIGHT_NETWORK_BLOCKED_PATTERN.search(text)
260
+ if not match:
261
+ continue
262
+ command = summarize_whitespace(match.group("command"))
263
+ failure = summarize_whitespace(match.group("failure"))
264
+ message = f"Worker preflight `{command or 'unknown command'}` failed before implementation."
265
+ if failure:
266
+ message = f"{message} {failure}"
267
+ message = f"{message} Verify from the host if the same command succeeds; worker and host environment can diverge."
268
+ return {
269
+ "id": f"worker-preflight-network-blocked:{run.get('session', '')}:{command}:{failure}",
270
+ "kind": "worker-preflight-network-blocked",
271
+ "severity": "warn",
272
+ "title": "Worker preflight blocked by network",
273
+ "message": message,
274
+ "session": run.get("session", ""),
275
+ "task_kind": run.get("task_kind", ""),
276
+ "task_id": run.get("task_id", ""),
277
+ "reset_at": "",
278
+ "updated_at": run.get("updated_at", "") or file_mtime_iso(path),
279
+ "source_file": str(path),
280
+ }
281
+ return None
282
+
283
+
284
+ def extract_run_alerts(run_dir: Path, run: dict[str, Any]) -> list[dict[str, Any]]:
285
+ alerts: list[dict[str, Any]] = []
286
+ for extractor in (
287
+ extract_github_rate_limit_alert,
288
+ extract_worker_preflight_network_blocked_alert,
289
+ ):
290
+ alert = extractor(run_dir, run)
291
+ if alert:
292
+ alerts.append(alert)
293
+ return alerts
294
+
295
+
245
296
  def collect_runs(runs_root: Path) -> list[dict[str, Any]]:
246
297
  if not runs_root.is_dir():
247
298
  return []
@@ -296,8 +347,7 @@ def collect_runs(runs_root: Path) -> list[dict[str, Any]]:
296
347
  "provider_pool_name": run_env.get("ACTIVE_PROVIDER_POOL_NAME", ""),
297
348
  "run_dir": str(run_dir),
298
349
  }
299
- alert = extract_github_rate_limit_alert(run_dir, item)
300
- item["alerts"] = [alert] if alert else []
350
+ item["alerts"] = extract_run_alerts(run_dir, item)
301
351
  runs.append(item)
302
352
  return runs
303
353
 
@@ -351,8 +401,7 @@ def collect_recent_history(history_root: Path, limit: int = 8) -> list[dict[str,
351
401
  "run_dir": str(run_dir),
352
402
  "archived": True,
353
403
  }
354
- alert = extract_github_rate_limit_alert(run_dir, item)
355
- item["alerts"] = [alert] if alert else []
404
+ item["alerts"] = extract_run_alerts(run_dir, item)
356
405
  items.append(item)
357
406
  seen_sessions.add(session)
358
407
  if len(items) >= limit:
@@ -3,6 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="color-scheme" content="light dark" />
6
7
  <title>ACP Worker Dashboard</title>
7
8
  <link rel="stylesheet" href="./styles.css" />
8
9
  </head>
@@ -20,7 +21,10 @@
20
21
  </p>
21
22
  </div>
22
23
  <div class="hero-actions">
23
- <button id="refresh-button" type="button">Refresh now</button>
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>
24
28
  <div class="meta">
25
29
  <div>Auto refresh: <strong>5s</strong></div>
26
30
  <div id="generated-at">Loading snapshot...</div>
@@ -12,6 +12,56 @@
12
12
  --danger: #b42318;
13
13
  --danger-soft: #fdd8d2;
14
14
  --shadow: 0 18px 50px rgba(25, 33, 38, 0.08);
15
+ --button-bg: var(--ink);
16
+ --button-ink: #ffffff;
17
+ --button-hover: #0f1720;
18
+ --hero-bg: rgba(255, 253, 247, 0.92);
19
+ --profile-bg: rgba(255, 253, 247, 0.94);
20
+ --body-gradient-top: rgba(15, 118, 110, 0.08);
21
+ --body-gradient-bottom: #faf7ef;
22
+ --theme-toggle-bg: var(--panel);
23
+ --theme-toggle-ink: var(--ink);
24
+ --theme-toggle-line: var(--line);
25
+ --theme-toggle-hover: var(--panel-strong);
26
+ --reported-soft: #dbeafe;
27
+ --reported-ink: #1d4ed8;
28
+ --implemented-soft: #dcfce7;
29
+ --implemented-ink: #166534;
30
+ --blocked-soft: #fef3c7;
31
+ --blocked-ink: #92400e;
32
+ }
33
+
34
+ :root[data-theme="dark"] {
35
+ --bg: #0d1418;
36
+ --panel: #142026;
37
+ --panel-strong: #1a2a31;
38
+ --ink: #ebf1f3;
39
+ --muted: #9ab0bb;
40
+ --line: #2a3c44;
41
+ --accent: #5ad4c7;
42
+ --accent-soft: #183d3a;
43
+ --warn: #f4c35f;
44
+ --warn-soft: #4d3a12;
45
+ --danger: #ff8a80;
46
+ --danger-soft: #4a2220;
47
+ --shadow: 0 24px 60px rgba(0, 0, 0, 0.32);
48
+ --button-bg: #ebf1f3;
49
+ --button-ink: #0d1418;
50
+ --button-hover: #d7e2e6;
51
+ --hero-bg: rgba(20, 32, 38, 0.92);
52
+ --profile-bg: rgba(20, 32, 38, 0.94);
53
+ --body-gradient-top: rgba(90, 212, 199, 0.12);
54
+ --body-gradient-bottom: #10181d;
55
+ --theme-toggle-bg: #1d2d35;
56
+ --theme-toggle-ink: #ebf1f3;
57
+ --theme-toggle-line: #35505b;
58
+ --theme-toggle-hover: #23363f;
59
+ --reported-soft: #1b3552;
60
+ --reported-ink: #9fc8ff;
61
+ --implemented-soft: #173a2b;
62
+ --implemented-ink: #85ddb1;
63
+ --blocked-soft: #4a3711;
64
+ --blocked-ink: #f4c35f;
15
65
  }
16
66
 
17
67
  * {
@@ -22,8 +72,8 @@ body {
22
72
  margin: 0;
23
73
  font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif;
24
74
  background:
25
- radial-gradient(circle at top right, rgba(15, 118, 110, 0.08), transparent 34%),
26
- linear-gradient(180deg, #faf7ef 0%, var(--bg) 100%);
75
+ radial-gradient(circle at top right, var(--body-gradient-top), transparent 34%),
76
+ linear-gradient(180deg, var(--body-gradient-bottom) 0%, var(--bg) 100%);
27
77
  color: var(--ink);
28
78
  }
29
79
 
@@ -41,7 +91,7 @@ body {
41
91
  padding: 24px;
42
92
  border: 1px solid var(--line);
43
93
  border-radius: 28px;
44
- background: rgba(255, 253, 247, 0.92);
94
+ background: var(--hero-bg);
45
95
  box-shadow: var(--shadow);
46
96
  }
47
97
 
@@ -75,19 +125,42 @@ body {
75
125
  align-items: flex-end;
76
126
  }
77
127
 
128
+ .hero-controls {
129
+ display: flex;
130
+ gap: 10px;
131
+ align-items: center;
132
+ flex-wrap: wrap;
133
+ justify-content: flex-end;
134
+ }
135
+
78
136
  button {
79
137
  appearance: none;
80
138
  border: 0;
81
139
  border-radius: 999px;
82
140
  padding: 12px 18px;
83
141
  font: inherit;
84
- background: var(--ink);
85
- color: white;
142
+ background: var(--button-bg);
143
+ color: var(--button-ink);
86
144
  cursor: pointer;
145
+ transition: background 140ms ease, color 140ms ease, border-color 140ms ease, transform 140ms ease;
87
146
  }
88
147
 
89
148
  button:hover {
90
- background: #0f1720;
149
+ background: var(--button-hover);
150
+ }
151
+
152
+ button:active {
153
+ transform: translateY(1px);
154
+ }
155
+
156
+ #theme-toggle {
157
+ background: var(--theme-toggle-bg);
158
+ color: var(--theme-toggle-ink);
159
+ border: 1px solid var(--theme-toggle-line);
160
+ }
161
+
162
+ #theme-toggle:hover {
163
+ background: var(--theme-toggle-hover);
91
164
  }
92
165
 
93
166
  .meta {
@@ -135,7 +208,7 @@ button:hover {
135
208
  padding: 22px;
136
209
  border-radius: 28px;
137
210
  border: 1px solid var(--line);
138
- background: rgba(255, 253, 247, 0.94);
211
+ background: var(--profile-bg);
139
212
  box-shadow: var(--shadow);
140
213
  }
141
214
 
@@ -183,12 +256,12 @@ button:hover {
183
256
 
184
257
  .badge.good {
185
258
  background: var(--accent-soft);
186
- color: #0d5a54;
259
+ color: var(--accent);
187
260
  }
188
261
 
189
262
  .badge.warn {
190
263
  background: var(--warn-soft);
191
- color: #855500;
264
+ color: var(--warn);
192
265
  }
193
266
 
194
267
  .badge.danger {
@@ -243,8 +316,8 @@ button:hover {
243
316
  }
244
317
 
245
318
  .alert-card.warn {
246
- border-color: #e7c76d;
247
- background: #fff6dd;
319
+ border-color: color-mix(in srgb, var(--warn) 45%, var(--line));
320
+ background: color-mix(in srgb, var(--warn-soft) 82%, var(--panel) 18%);
248
321
  }
249
322
 
250
323
  .alert-card h4 {
@@ -323,7 +396,7 @@ th {
323
396
  .status-pill.launching,
324
397
  .status-pill.reconciling {
325
398
  background: var(--accent-soft);
326
- color: #0d5a54;
399
+ color: var(--accent);
327
400
  }
328
401
 
329
402
  .status-pill.waiting-provider,
@@ -332,7 +405,7 @@ th {
332
405
  .status-pill.idle,
333
406
  .status-pill.sleeping {
334
407
  background: var(--warn-soft);
335
- color: #855500;
408
+ color: var(--warn);
336
409
  }
337
410
 
338
411
  .status-pill.FAILED,
@@ -343,19 +416,19 @@ th {
343
416
  }
344
417
 
345
418
  .status-pill.implemented {
346
- background: #dcfce7;
347
- color: #166534;
419
+ background: var(--implemented-soft);
420
+ color: var(--implemented-ink);
348
421
  }
349
422
 
350
423
  .status-pill.reported,
351
424
  .status-pill.completed {
352
- background: #dbeafe;
353
- color: #1d4ed8;
425
+ background: var(--reported-soft);
426
+ color: var(--reported-ink);
354
427
  }
355
428
 
356
429
  .status-pill.blocked {
357
- background: #fef3c7;
358
- color: #92400e;
430
+ background: var(--blocked-soft);
431
+ color: var(--blocked-ink);
359
432
  }
360
433
 
361
434
  .status-pill.failed,
@@ -366,7 +439,7 @@ th {
366
439
 
367
440
  .status-pill.running {
368
441
  background: var(--accent-soft);
369
- color: #0d5a54;
442
+ color: var(--accent);
370
443
  }
371
444
 
372
445
  .empty-state {
@@ -387,6 +460,10 @@ th {
387
460
  text-align: left;
388
461
  }
389
462
 
463
+ .hero-controls {
464
+ justify-content: flex-start;
465
+ }
466
+
390
467
  .panel.half,
391
468
  .panel.third {
392
469
  grid-column: span 12;
@@ -3,11 +3,11 @@ You are the PR repair worker for `{REPO_SLUG}`.
3
3
  Before making any change:
4
4
 
5
5
  1. Read `{REPO_ROOT}/AGENTS.md`.
6
- 2. Read `{REPO_ROOT}/openspec/AGENT_RULES.md`.
7
- 3. Read `{REPO_ROOT}/openspec/AGENTS.md`.
8
- 4. Read `{REPO_ROOT}/openspec/project.md`.
9
- 5. Read `{REPO_ROOT}/openspec/CONVENTIONS.md`.
10
- 6. Read `{REPO_ROOT}/docs/TESTING_AND_SEED_POLICY.md`.
6
+ 2. If present, read `{REPO_ROOT}/openspec/AGENT_RULES.md`.
7
+ 3. If present, read `{REPO_ROOT}/openspec/AGENTS.md`.
8
+ 4. If present, read `{REPO_ROOT}/openspec/project.md`.
9
+ 5. If present, read `{REPO_ROOT}/openspec/CONVENTIONS.md`.
10
+ 6. If present, read `{REPO_ROOT}/docs/TESTING_AND_SEED_POLICY.md`.
11
11
  7. Stay on this PR branch worktree. Do not push or mutate GitHub from inside the worker.
12
12
 
13
13
  PR metadata:
@@ -58,7 +58,7 @@ PR body:
58
58
  Required flow:
59
59
 
60
60
  1. Inspect the current diff and the failing/pending CI signals first:
61
- - `openspec list`
61
+ - `openspec list` if the repo uses OpenSpec
62
62
  - `git diff --stat origin/main...HEAD`
63
63
  - `git status --short`
64
64
  - if `Merge state` is not `CLEAN` or `Mergeable` is `FALSE`, treat branch drift/conflicts as the concrete blocker first
@@ -3,11 +3,11 @@ You are the PR merge-repair worker for `{REPO_SLUG}`.
3
3
  Before making any change:
4
4
 
5
5
  1. Read `{REPO_ROOT}/AGENTS.md`.
6
- 2. Read `{REPO_ROOT}/openspec/AGENT_RULES.md`.
7
- 3. Read `{REPO_ROOT}/openspec/AGENTS.md`.
8
- 4. Read `{REPO_ROOT}/openspec/project.md`.
9
- 5. Read `{REPO_ROOT}/openspec/CONVENTIONS.md`.
10
- 6. Read `{REPO_ROOT}/docs/TESTING_AND_SEED_POLICY.md`.
6
+ 2. If present, read `{REPO_ROOT}/openspec/AGENT_RULES.md`.
7
+ 3. If present, read `{REPO_ROOT}/openspec/AGENTS.md`.
8
+ 4. If present, read `{REPO_ROOT}/openspec/project.md`.
9
+ 5. If present, read `{REPO_ROOT}/openspec/CONVENTIONS.md`.
10
+ 6. If present, read `{REPO_ROOT}/docs/TESTING_AND_SEED_POLICY.md`.
11
11
  7. Stay on this PR branch worktree. Do not push or mutate GitHub from inside the worker.
12
12
 
13
13
  PR metadata:
@@ -53,7 +53,7 @@ Required flow:
53
53
  - do not run `git fetch`, `git pull`, `git merge`, `git rebase`, `git commit`, `git push`, or any command that writes Git metadata
54
54
  - do not abort or restart the prepared merge state
55
55
  3. Inspect only the concrete branch-repair state you were given:
56
- - `openspec list`
56
+ - `openspec list` if the repo uses OpenSpec
57
57
  - `git status --short`
58
58
  - `git diff --check`
59
59
  - `git diff --name-only --diff-filter=U`