feat(08-02): add ensureGeneralistAgents startup migration for existing workspaces

- Import agentService and agents table into server/src/index.ts
- ensureGeneralistAgents() queries all companies, skips any that already have
  a general-role agent (idempotent), creates Generalist via agentService.create()
- metadata includes pendingSkillGroups: [Creative] and backfilled: true flag
- Called with fire-and-forget void pattern after ensureLocalTrustedBoardPrincipal
- Existing workspaces get Generalist on next server upgrade without user action
This commit is contained in:
Mikkel Georgsen 2026-04-01 00:19:07 +02:00 committed by Nexus Dev
parent 53dfd52b72
commit e80a419aff

View file

@ -19,6 +19,7 @@ import {
formatDatabaseBackupResult,
runDatabaseBackup,
authUsers,
agents,
companies,
companyMemberships,
instanceUserRoles,
@ -28,7 +29,7 @@ import { createApp } from "./app.js";
import { loadConfig } from "./config.js";
import { logger } from "./middleware/logger.js";
import { setupLiveEventsWebSocketServer } from "./realtime/live-events-ws.js";
import { heartbeatService, reconcilePersistedRuntimeServicesOnStartup, routineService } from "./services/index.js";
import { agentService, heartbeatService, reconcilePersistedRuntimeServicesOnStartup, routineService } from "./services/index.js";
import { createStorageServiceFromConfig } from "./storage/index.js";
import { printStartupBanner } from "./startup-banner.js";
import { getBoardClaimWarningUrl, initializeBoardClaimChallenge } from "./board-claim.js";
@ -243,6 +244,32 @@ export async function startServer(): Promise<StartedServer> {
}
}
// [nexus] Backfill Generalist agent for existing workspaces that pre-date Phase 8
async function ensureGeneralistAgents(db: any): Promise<{ backfilled: number }> {
const companyRows = await db.select({ id: companies.id }).from(companies);
let backfilled = 0;
for (const company of companyRows) {
const existing = await db
.select({ id: agents.id })
.from(agents)
.where(and(eq(agents.companyId, company.id), eq(agents.role, "general")))
.then((rows: Array<{ id: string }>) => rows[0] ?? null);
if (existing) continue;
const agentSvc = agentService(db);
await agentSvc.create(company.id, {
name: "Generalist",
role: "general",
adapterType: "claude_local",
adapterConfig: {},
runtimeConfig: {},
metadata: { pendingSkillGroups: ["Creative"], backfilled: true },
});
logger.info({ companyId: company.id }, "backfilled Generalist agent for existing workspace");
backfilled++;
}
return { backfilled };
}
let db;
let embeddedPostgres: EmbeddedPostgresInstance | null = null;
let embeddedPostgresStartedByThisProcess = false;
@ -459,6 +486,18 @@ export async function startServer(): Promise<StartedServer> {
if (config.deploymentMode === "local_trusted") {
await ensureLocalTrustedBoardPrincipal(db as any);
}
// [nexus] Backfill Generalist agents for any workspace that pre-dates Phase 8
void ensureGeneralistAgents(db as any)
.then((result) => {
if (result.backfilled > 0) {
logger.info({ backfilled: result.backfilled }, "backfilled Generalist agents for existing workspaces");
}
})
.catch((err) => {
logger.error({ err }, "failed to backfill Generalist agents");
});
if (config.deploymentMode === "authenticated") {
const {
createBetterAuthHandler,