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
@@ -32,6 +32,9 @@ rollback_labels_on_failure() {
32
32
  if [[ "${label_rollback_armed}" != "yes" || "${launch_success}" == "yes" ]]; then
33
33
  return 0
34
34
  fi
35
+ if [[ -d "${RUN_DIR}" && ! -f "${RUN_DIR}/run.env" && ! -f "${RUN_DIR}/runner.env" && ! -f "${RUN_DIR}/result.env" ]]; then
36
+ rm -rf "${RUN_DIR}" >/dev/null 2>&1 || true
37
+ fi
35
38
  if [[ -x "${UPDATE_LABELS_BIN}" ]]; then
36
39
  bash "${UPDATE_LABELS_BIN}" --repo-slug "${REPO_SLUG}" --number "${PR_NUMBER}" --remove agent-running >/dev/null 2>&1 || true
37
40
  fi
@@ -120,6 +123,35 @@ PR_FILES_TEXT="$(jq -r '.files[] | "- " + .' <<<"$RISK_JSON")"
120
123
  PR_REPO_ROOT="$(flow_resolve_repo_root "${CONFIG_YAML}")"
121
124
  PR_DEPENDENCY_SOURCE_ROOT="${ACP_DEPENDENCY_SOURCE_ROOT:-${F_LOSNING_DEPENDENCY_SOURCE_ROOT:-$PR_REPO_ROOT}}"
122
125
 
126
+ render_pr_context_reads_text() {
127
+ local repo_root="${1:?repo root required}"
128
+ local -a candidate_paths=(
129
+ "${repo_root}/AGENTS.md"
130
+ "${repo_root}/openspec/AGENT_RULES.md"
131
+ "${repo_root}/openspec/AGENTS.md"
132
+ "${repo_root}/openspec/project.md"
133
+ "${repo_root}/openspec/CONVENTIONS.md"
134
+ "${repo_root}/docs/TESTING_AND_SEED_POLICY.md"
135
+ )
136
+ local -a existing_paths=()
137
+ local candidate_path=""
138
+
139
+ for candidate_path in "${candidate_paths[@]}"; do
140
+ if [[ -f "${candidate_path}" ]]; then
141
+ existing_paths+=("${candidate_path}")
142
+ fi
143
+ done
144
+
145
+ if [[ "${#existing_paths[@]}" -eq 0 ]]; then
146
+ printf '%s\n' '- No repo-specific context files were found under the expected AGENTS/OpenSpec/testing-doc locations; rely on the current diff and nearby source.'
147
+ return 0
148
+ fi
149
+
150
+ printf '%s\n' "${existing_paths[@]}" | sed 's/^/- `/' | sed 's/$/`/'
151
+ }
152
+
153
+ PR_CONTEXT_READS_TEXT="$(render_pr_context_reads_text "${PR_REPO_ROOT}")"
154
+
123
155
  case "$PR_AGENT_LANE" in
124
156
  double-check-1)
125
157
  PR_REVIEW_STAGE_TEXT="Independent agent double-check 1 of 2. A clean pass should advance this PR to the second review pass, not merge it yet."
@@ -154,6 +186,7 @@ PR_CHECKS_BYPASSED="$PR_CHECKS_BYPASSED" \
154
186
  PR_MERGE_STATE_STATUS="$PR_MERGE_STATE_STATUS" \
155
187
  PR_CHECKS_TEXT="$PR_CHECKS_TEXT" \
156
188
  PR_FILES_TEXT="$PR_FILES_TEXT" \
189
+ PR_CONTEXT_READS_TEXT="$PR_CONTEXT_READS_TEXT" \
157
190
  PR_REPO_ROOT="$PR_REPO_ROOT" \
158
191
  PR_DEPENDENCY_SOURCE_ROOT="$PR_DEPENDENCY_SOURCE_ROOT" \
159
192
  REPO_SLUG="$REPO_SLUG" \
@@ -180,6 +213,7 @@ const replacements = {
180
213
  '{PR_MERGE_STATE_STATUS}': process.env.PR_MERGE_STATE_STATUS || '',
181
214
  '{PR_CHECKS_TEXT}': process.env.PR_CHECKS_TEXT || '',
182
215
  '{PR_FILES_TEXT}': process.env.PR_FILES_TEXT || '',
216
+ '{PR_CONTEXT_READS_TEXT}': process.env.PR_CONTEXT_READS_TEXT || '',
183
217
  '{REPO_ROOT}': process.env.PR_REPO_ROOT || '',
184
218
  '{DEPENDENCY_SOURCE_ROOT}': process.env.PR_DEPENDENCY_SOURCE_ROOT || '',
185
219
  };
@@ -29,7 +29,7 @@ PENDING_LAUNCH_DIR="${ACP_PENDING_LAUNCH_DIR:-${F_LOSNING_PENDING_LAUNCH_DIR:-${
29
29
  SCHEDULED_STATE_DIR="${STATE_ROOT}/scheduled-issues"
30
30
  CONTROLLER_FILE="$(flow_resident_issue_controller_file "${CONFIG_YAML}" "${ISSUE_ID}")"
31
31
  RESIDENT_META_FILE="$(flow_resident_issue_meta_file "${CONFIG_YAML}" "${ISSUE_ID}")"
32
- CODING_WORKER="${ACP_CODING_WORKER:-${F_LOSNING_CODING_WORKER:-codex}}"
32
+ CODING_WORKER="${ACP_CODING_WORKER:-codex}"
33
33
  MAX_IMMEDIATE_CYCLES="$(flow_resident_issue_controller_max_immediate_cycles "${CONFIG_YAML}")"
34
34
  POLL_SECONDS="$(flow_resident_issue_controller_poll_seconds "${CONFIG_YAML}")"
35
35
  IDLE_TIMEOUT_SECONDS="$(flow_resident_issue_controller_idle_timeout_seconds "${CONFIG_YAML}")"
@@ -192,7 +192,7 @@ issue_id_is_scheduled() {
192
192
 
193
193
  controller_refresh_execution_context() {
194
194
  unset \
195
- ACP_CODING_WORKER F_LOSNING_CODING_WORKER \
195
+ ACP_CODING_WORKER \
196
196
  ACP_CODEX_PROFILE_SAFE F_LOSNING_CODEX_PROFILE_SAFE \
197
197
  ACP_CODEX_PROFILE_BYPASS F_LOSNING_CODEX_PROFILE_BYPASS \
198
198
  ACP_CLAUDE_MODEL F_LOSNING_CLAUDE_MODEL \
@@ -204,6 +204,7 @@ controller_refresh_execution_context() {
204
204
  ACP_OPENCLAW_MODEL F_LOSNING_OPENCLAW_MODEL \
205
205
  ACP_OPENCLAW_THINKING F_LOSNING_OPENCLAW_THINKING \
206
206
  ACP_OPENCLAW_TIMEOUT_SECONDS F_LOSNING_OPENCLAW_TIMEOUT_SECONDS \
207
+ ACP_OPENCLAW_STALL_SECONDS F_LOSNING_OPENCLAW_STALL_SECONDS \
207
208
  ACP_ACTIVE_PROVIDER_POOL_NAME F_LOSNING_ACTIVE_PROVIDER_POOL_NAME \
208
209
  ACP_ACTIVE_PROVIDER_BACKEND F_LOSNING_ACTIVE_PROVIDER_BACKEND \
209
210
  ACP_ACTIVE_PROVIDER_MODEL F_LOSNING_ACTIVE_PROVIDER_MODEL \
@@ -215,7 +216,7 @@ controller_refresh_execution_context() {
215
216
  ACP_PROVIDER_POOL_LAST_REASON F_LOSNING_PROVIDER_POOL_LAST_REASON
216
217
  flow_export_execution_env "${CONFIG_YAML}"
217
218
  flow_export_project_env_aliases
218
- CODING_WORKER="${ACP_CODING_WORKER:-${F_LOSNING_CODING_WORKER:-codex}}"
219
+ CODING_WORKER="${ACP_CODING_WORKER:-codex}"
219
220
  controller_capture_active_provider_context
220
221
  }
221
222
 
@@ -543,7 +544,7 @@ controller_provider_state() {
543
544
 
544
545
  provider_state="$(
545
546
  env \
546
- -u ACP_CODING_WORKER -u F_LOSNING_CODING_WORKER \
547
+ -u ACP_CODING_WORKER \
547
548
  -u ACP_CODEX_PROFILE_SAFE -u F_LOSNING_CODEX_PROFILE_SAFE \
548
549
  -u ACP_CODEX_PROFILE_BYPASS -u F_LOSNING_CODEX_PROFILE_BYPASS \
549
550
  -u ACP_CLAUDE_MODEL -u F_LOSNING_CLAUDE_MODEL \
@@ -6,11 +6,11 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
6
  source "${SCRIPT_DIR}/flow-config-lib.sh"
7
7
 
8
8
  CONFIG_YAML="$(resolve_flow_config_yaml "${BASH_SOURCE[0]}")"
9
- SOURCE_REPO_ROOT="${ACP_SOURCE_REPO_ROOT:-${F_LOSNING_SOURCE_REPO_ROOT:-$(flow_resolve_retained_repo_root "${CONFIG_YAML}")}}"
9
+ SOURCE_REPO_ROOT="${ACP_SOURCE_REPO_ROOT:-$(flow_resolve_retained_repo_root "${CONFIG_YAML}")}"
10
10
  CANONICAL_REPO_ROOT="$(flow_resolve_repo_root "${CONFIG_YAML}")"
11
11
  AGENT_REPO_ROOT="$(flow_resolve_agent_repo_root "${CONFIG_YAML}")"
12
12
  DEFAULT_BRANCH="$(flow_resolve_default_branch "${CONFIG_YAML}")"
13
- REMOTE_NAME="${ACP_REMOTE_NAME:-${F_LOSNING_REMOTE_NAME:-origin}}"
13
+ REMOTE_NAME="${ACP_REMOTE_NAME:-origin}"
14
14
  FLOW_SKILL_DIR="$(resolve_flow_skill_dir "${BASH_SOURCE[0]}")"
15
15
  FLOW_TOOLS_DIR="${FLOW_SKILL_DIR}/tools/bin"
16
16
 
@@ -13,9 +13,9 @@ STATE_ROOT="$(flow_resolve_state_root "${CONFIG_YAML}")"
13
13
  LOCK_DIR="${STATE_ROOT}/dependency-baseline.lock"
14
14
  PID_FILE="${LOCK_DIR}/pid"
15
15
  HASH_FILE="${STATE_ROOT}/dependency-baseline.sha256"
16
- PACKAGE_MANAGER_BIN="${ACP_PACKAGE_MANAGER_BIN:-${F_LOSNING_PACKAGE_MANAGER_BIN:-pnpm}}"
17
- WORKSPACE_BUILD_PACKAGES_RAW="${ACP_WORKSPACE_BUILD_PACKAGES:-${F_LOSNING_WORKSPACE_BUILD_PACKAGES:-}}"
18
- WORKSPACE_BUILD_ARTIFACTS_RAW="${ACP_WORKSPACE_BUILD_ARTIFACTS:-${F_LOSNING_WORKSPACE_BUILD_ARTIFACTS:-}}"
16
+ PACKAGE_MANAGER_BIN="${ACP_PACKAGE_MANAGER_BIN:-pnpm}"
17
+ WORKSPACE_BUILD_PACKAGES_RAW="${ACP_WORKSPACE_BUILD_PACKAGES:-}"
18
+ WORKSPACE_BUILD_ARTIFACTS_RAW="${ACP_WORKSPACE_BUILD_ARTIFACTS:-}"
19
19
  declare -a WORKSPACE_BUILD_PACKAGES=()
20
20
  declare -a WORKSPACE_BUILD_ARTIFACTS=()
21
21
 
@@ -207,7 +207,10 @@ sync_tree_rsync() {
207
207
  local target_dir="${2:?target dir required}"
208
208
  [[ -d "${source_dir}" ]] || return 0
209
209
  mkdir -p "${target_dir}"
210
- rsync -a --delete --exclude='.git/' "${source_dir}/" "${target_dir}/"
210
+ if rsync -a --delete --exclude='.git/' "${source_dir}/" "${target_dir}/"; then
211
+ return 0
212
+ fi
213
+ sync_tree_copy_mode "${source_dir}" "${target_dir}"
211
214
  }
212
215
 
213
216
  reset_runtime_skill_targets() {
@@ -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;
@@ -2,13 +2,9 @@ You are the PR repair worker for `{REPO_SLUG}`.
2
2
 
3
3
  Before making any change:
4
4
 
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`.
11
- 7. Stay on this PR branch worktree. Do not push or mutate GitHub from inside the worker.
5
+ 1. Read the following repo context before changing code:
6
+ {PR_CONTEXT_READS_TEXT}
7
+ 2. Stay on this PR branch worktree. Do not push or mutate GitHub from inside the worker.
12
8
 
13
9
  PR metadata:
14
10
 
@@ -58,7 +54,7 @@ PR body:
58
54
  Required flow:
59
55
 
60
56
  1. Inspect the current diff and the failing/pending CI signals first:
61
- - `openspec list`
57
+ - `openspec list` if the repo uses OpenSpec
62
58
  - `git diff --stat origin/main...HEAD`
63
59
  - `git status --short`
64
60
  - if `Merge state` is not `CLEAN` or `Mergeable` is `FALSE`, treat branch drift/conflicts as the concrete blocker first
@@ -2,13 +2,9 @@ You are the PR merge-repair worker for `{REPO_SLUG}`.
2
2
 
3
3
  Before making any change:
4
4
 
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`.
11
- 7. Stay on this PR branch worktree. Do not push or mutate GitHub from inside the worker.
5
+ 1. Read the following repo context before changing code:
6
+ {PR_CONTEXT_READS_TEXT}
7
+ 2. Stay on this PR branch worktree. Do not push or mutate GitHub from inside the worker.
12
8
 
13
9
  PR metadata:
14
10
 
@@ -53,7 +49,7 @@ Required flow:
53
49
  - do not run `git fetch`, `git pull`, `git merge`, `git rebase`, `git commit`, `git push`, or any command that writes Git metadata
54
50
  - do not abort or restart the prepared merge state
55
51
  3. Inspect only the concrete branch-repair state you were given:
56
- - `openspec list`
52
+ - `openspec list` if the repo uses OpenSpec
57
53
  - `git status --short`
58
54
  - `git diff --check`
59
55
  - `git diff --name-only --diff-filter=U`
@@ -2,7 +2,8 @@ You are the PR review and final-merge worker for `{REPO_SLUG}`.
2
2
 
3
3
  Before making any decision:
4
4
 
5
- 1. Read `{REPO_ROOT}/AGENTS.md` and any repo-specific conventions or design docs relevant to the PR.
5
+ 1. Read the following repo context before deciding:
6
+ {PR_CONTEXT_READS_TEXT}
6
7
  2. Do not edit product code in this worktree. This is review and final-review only.
7
8
  3. Never run dependency bootstrap or workspace-mutating commands here.
8
9