diff --git a/apps/server/src/gits/Layers/AutomodeProposalSweep.ts b/apps/server/src/gits/Layers/AutomodeProposalSweep.ts
index 1a3ff3c69b3..9a72c587c9b 100644
--- a/apps/server/src/gits/Layers/AutomodeProposalSweep.ts
+++ b/apps/server/src/gits/Layers/AutomodeProposalSweep.ts
@@ -212,11 +212,13 @@ export const AutomodeProposalSweepLive = Layer.effect(
yield* Ref.set(stateRef, next);
// Sequential: hermes invocations share codex capacity — never parallelise.
const newGoals: AutomodeGoal[] = [];
+ let attemptedRepos = 0;
for (const repo of policy.proposalRepos) {
if (!repoAllowed(policy, repo)) {
yield* Effect.logWarning("gits.sweep.repo-not-allowed", { repo });
continue;
}
+ attemptedRepos += 1;
const goal = yield* sweepRepo(repo);
if (goal === null) continue;
newGoals.push(goal);
@@ -235,6 +237,21 @@ export const AutomodeProposalSweepLive = Layer.effect(
);
}
}
+ if (attemptedRepos > 0 && newGoals.length === 0) {
+ // A sweep night is consumed up front, so a silent zero-goal run means the owner
+ // waits a whole day without knowing anything went wrong. Always say so.
+ // ponytail: generic message; per-repo reasons live in the gits.sweep.* logs.
+ yield* notifier
+ .notify({
+ subject: "GITS nightly sweep",
+ text: `Sweep ran for ${attemptedRepos} repo(s) but drafted no goals. Check the cockpit logs (gits.sweep.*) for reasons.`,
+ })
+ .pipe(
+ Effect.catchCause((cause) =>
+ Effect.logWarning("gits.sweep.telegram-failed", { cause: Cause.pretty(cause) }),
+ ),
+ );
+ }
if (newGoals.length > 0) {
const goalLines = newGoals.map(
(goal) => `${goal.title} — ${path.basename(goal.repo)} [${goal.id}]`,
diff --git a/apps/web/index.html b/apps/web/index.html
index 3671338a578..bc67a98f082 100644
--- a/apps/web/index.html
+++ b/apps/web/index.html
@@ -92,15 +92,30 @@
color: #171717;
user-select: none;
}
+
+ html.dark .gits-boot {
+ background: #161616;
+ }
+
+ html.dark .gits-boot-title {
+ color: #f5f5f5;
+ }
GITS
-

+
GITS
+
diff --git a/apps/web/public/gits-logo-anim-dark.gif b/apps/web/public/gits-logo-anim-dark.gif
new file mode 100644
index 00000000000..8b4c8218d98
Binary files /dev/null and b/apps/web/public/gits-logo-anim-dark.gif differ
diff --git a/apps/web/public/gits-logo-hold-dark.png b/apps/web/public/gits-logo-hold-dark.png
new file mode 100644
index 00000000000..809d2fbb8ee
Binary files /dev/null and b/apps/web/public/gits-logo-hold-dark.png differ
diff --git a/apps/web/public/gits-logo-hold.png b/apps/web/public/gits-logo-hold.png
new file mode 100644
index 00000000000..9d505945a9c
Binary files /dev/null and b/apps/web/public/gits-logo-hold.png differ
diff --git a/apps/web/src/components/SplashScreen.tsx b/apps/web/src/components/SplashScreen.tsx
index 25efae0be43..b99872cb4dd 100644
--- a/apps/web/src/components/SplashScreen.tsx
+++ b/apps/web/src/components/SplashScreen.tsx
@@ -1,11 +1,10 @@
import { useEffect, useState } from "react";
-// One full cycle of /gits-logo-anim.gif. Elapsed time counts from the static
+// One full cycle of /gits-logo-anim(-dark).gif. Elapsed time counts from the static
// index.html boot splash (window.__gitsBootStart) so the animation plays once
-// total across both layers, then holds the static logo instead of looping.
+// total across both layers, then holds the last frame instead of looping.
// ponytail: constant must track the gif asset; re-measure if the gif changes.
const LOGO_CYCLE_MS = 4000;
-const HOLD_LOGO_SRC = "/apple-touch-icon.png";
function elapsedSinceBoot(): number {
const start = (window as Window & { __gitsBootStart?: number }).__gitsBootStart;
@@ -13,6 +12,8 @@ function elapsedSinceBoot(): number {
}
export function SplashScreen() {
+ // Same signal the index.html boot script uses; read once — the splash is transient.
+ const isDark = document.documentElement.classList.contains("dark");
const [hold, setHold] = useState(() => elapsedSinceBoot() >= LOGO_CYCLE_MS);
useEffect(() => {
if (hold) return;
@@ -20,21 +21,25 @@ export function SplashScreen() {
return () => clearTimeout(timer);
}, [hold]);
return (
-