From 62ce1570cdaa85dbaa8b13a33243e56be5220f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Tue, 14 Jul 2026 00:07:30 -0400 Subject: [PATCH] Founder-CEO direct conversation is a structural invariant Every org now keeps one direct chat between the primary employee and the active c_suite agent: created at onboarding bootstrap (hard failure if missing), repaired for existing orgs at startup. The existing wakeDirectCounterparts path already wakes the CEO on every founder message, so this space is the pinned "talk to your AI CEO" entry the Messages page will surface next. --- src/main/index.ts | 18 ++++ src/main/services/ceo-direct-space.ts | 29 ++++++ src/main/services/onboarding-service.ts | 6 ++ tests/unit/ceo-direct-space.test.ts | 119 ++++++++++++++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 src/main/services/ceo-direct-space.ts create mode 100644 tests/unit/ceo-direct-space.test.ts diff --git a/src/main/index.ts b/src/main/index.ts index ece4cd9b..ba3e603f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -6,6 +6,7 @@ import { DatabaseConnection } from "./database/connection"; import { createMainWindow } from "./app/create-window"; import { registerIpcHandlers } from "./app/ipc-handlers"; import { builtinSkillsSourceOverride, importStagedSkills, stageBuiltinSkillsLibrary } from "./services/builtin-skills"; +import { ensureCeoDirectSpace } from "./services/ceo-direct-space"; import { SkillService } from "./services/skill-service"; import { startProviderBackgroundWorker } from "./app/provider-background-worker"; import { recoverInterruptedWork } from "./app/startup-recovery"; @@ -45,6 +46,7 @@ if (!app.requestSingleInstanceLock()) { builtinSkillsLibrary, }); syncAllOrganizationSkills(connection, join(profileDir, "artifacts"), builtinSkillsSource, builtinSkillsLibrary); + ensureAllCeoConversations(connection); const stopProviderBackgroundWorker = startProviderBackgroundWorker({ db: connection.db, mainWindow: win, @@ -109,3 +111,19 @@ function syncAllOrganizationSkills(connection: DatabaseConnection, artifactRoot: console.error(`[skills] built-in skill sync failed: ${error instanceof Error ? error.message : String(error)}`); } } + +/** + * Repairs the founder⇄CEO conversation invariant for every organization at + * startup — orgs created before the pinned CEO chat existed get theirs here. + * A broken org is loud in the log but must not stop the app. + */ +function ensureAllCeoConversations(connection: DatabaseConnection): void { + const orgs = connection.db.prepare("select id from organizations").all() as Array<{ id: string }>; + for (const org of orgs) { + try { + connection.transaction(() => ensureCeoDirectSpace(connection.db, org.id)); + } catch (error) { + console.error(`[chat] ensuring the CEO conversation for org ${org.id} failed: ${error instanceof Error ? error.message : String(error)}`); + } + } +} diff --git a/src/main/services/ceo-direct-space.ts b/src/main/services/ceo-direct-space.ts new file mode 100644 index 00000000..46136730 --- /dev/null +++ b/src/main/services/ceo-direct-space.ts @@ -0,0 +1,29 @@ +import type { DatabaseSync } from "node:sqlite"; +import { ChatService } from "./chat-service"; + +/** + * The founder's direct line to the CEO: every organization keeps one direct + * conversation between its primary employee (the human operator) and its CEO + * (the c_suite agent — the same predicate the grant system enforces). The + * Messages page pins this conversation at the top, so it exists as a + * structural invariant instead of appearing lazily on first use. + * + * Returns the space id, or null while the org has no employee or no active + * CEO yet (mid-bootstrap) — the next ensure pass picks it up. + */ +export function ensureCeoDirectSpace(db: DatabaseSync, orgId: string): string | null { + const employee = db.prepare("select id from employees where org_id = ? order by created_at asc limit 1") + .get(orgId) as { id: string } | undefined; + const ceo = db.prepare(` + select id from agents + where org_id = ? and role_level = 'c_suite' and lifecycle = 'active' + order by created_at asc + limit 1 + `).get(orgId) as { id: string } | undefined; + if (!employee || !ceo) return null; + return new ChatService(db).ensureDirectSpace( + orgId, + { type: "employee", id: employee.id }, + { type: "agent", id: ceo.id }, + ); +} diff --git a/src/main/services/onboarding-service.ts b/src/main/services/onboarding-service.ts index 19151153..d8d11236 100644 --- a/src/main/services/onboarding-service.ts +++ b/src/main/services/onboarding-service.ts @@ -8,6 +8,7 @@ import { PROVIDER_AUTH_TYPE, RISK_LEVEL, TASK_STATUS } from "../../shared/domain import { PROVIDER_CATALOG } from "../../shared/provider-catalog"; import { createId } from "../../shared/ids"; import { AuditService } from "./audit-service"; +import { ensureCeoDirectSpace } from "./ceo-direct-space"; import { goalKickoffDescription, goalKickoffTitle } from "./goal-kickoff"; import { OrganizationService } from "./organization-service"; import { ChatService } from "./chat-service"; @@ -70,6 +71,11 @@ export class OnboardingService { const goalId = this.createGoal(orgId, ceoAgentId, input, now); const projectId = this.createProject(orgId, goalId, ceoAgentId, input, now); this.createProjectChatSpace(orgId, projectName(input)); + // The founder⇄CEO conversation is a bootstrap deliverable, not an option: + // the Messages page pins it, so its absence here is a hard failure. + if (!ensureCeoDirectSpace(this.deps.db, orgId)) { + throw new Error("The founder⇄CEO conversation was not created during bootstrap."); + } if (workspaceId) this.bindWorkspaceToProject(workspaceId, projectId, now); const taskId = this.organization.createTask({ orgId, diff --git a/tests/unit/ceo-direct-space.test.ts b/tests/unit/ceo-direct-space.test.ts new file mode 100644 index 00000000..bd545d21 --- /dev/null +++ b/tests/unit/ceo-direct-space.test.ts @@ -0,0 +1,119 @@ +import { randomUUID } from "node:crypto"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, test } from "vitest"; +import { DatabaseConnection } from "../../src/main/database/connection"; +import { OrganizationService } from "../../src/main/services/organization-service"; +import { ChatService } from "../../src/main/services/chat-service"; +import { ensureCeoDirectSpace } from "../../src/main/services/ceo-direct-space"; + +interface Harness { + connection: DatabaseConnection; + orgId: string; + employeeId: string; + providerId: string; +} + +function harness(options: { enableProvider?: boolean } = {}): Harness { + const connection = new DatabaseConnection(join(tmpdir(), `ac-ceo-dm-${randomUUID()}.db`)); + const organization = new OrganizationService(connection.db); + let orgId = ""; + let providerId = ""; + connection.transaction(() => { + orgId = organization.createOrganization({ organizationName: "OrbitOps", employeeName: "Yang", mission: "m", goal: "g" }); + providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(orgId) as { id: string }).id; + if (options.enableProvider) { + connection.db.prepare("update providers set enabled = 1 where id = ? and org_id = ?").run(providerId, orgId); + } + }); + const employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id; + return { connection, orgId, employeeId, providerId }; +} + +function createCeo(h: Harness, name = "CEO"): string { + const organization = new OrganizationService(h.connection.db); + let id = ""; + h.connection.transaction(() => { + id = organization.createAgent({ + orgId: h.orgId, name, role: "CEO", department: "Executive", + modelProviderId: h.providerId, modelId: "gpt-5.4", budgetCents: 25000, + managerAgentId: null, roleLevel: "c_suite", + }); + }); + return id; +} + +function spaceMembers(h: Harness, spaceId: string): Array<{ principal_type: string; principal_id: string }> { + return h.connection.db.prepare("select principal_type, principal_id from chat_memberships where space_id = ? order by principal_type") + .all(spaceId) as Array<{ principal_type: string; principal_id: string }>; +} + +describe("ensureCeoDirectSpace", () => { + test("creates the founder⇄CEO direct conversation with exactly those two members", () => { + const h = harness(); + const ceoId = createCeo(h); + + const spaceId = ensureCeoDirectSpace(h.connection.db, h.orgId); + expect(spaceId).toBeTruthy(); + + const space = h.connection.db.prepare("select space_type, visibility from chat_spaces where id = ? and org_id = ?") + .get(spaceId, h.orgId) as { space_type: string; visibility: string }; + expect(space.space_type).toBe("direct"); + expect(space.visibility).toBe("restricted"); + + const members = spaceMembers(h, spaceId as string); + expect(members).toEqual([ + { principal_type: "agent", principal_id: ceoId }, + { principal_type: "employee", principal_id: h.employeeId }, + ]); + }); + + test("is idempotent: repeated ensures return the same conversation", () => { + const h = harness(); + createCeo(h); + + const first = ensureCeoDirectSpace(h.connection.db, h.orgId); + const second = ensureCeoDirectSpace(h.connection.db, h.orgId); + expect(second).toBe(first); + + const directSpaces = h.connection.db.prepare("select id from chat_spaces where org_id = ? and space_type = 'direct'") + .all(h.orgId) as Array<{ id: string }>; + expect(directSpaces).toHaveLength(1); + }); + + test("returns null and creates nothing while the org has no CEO", () => { + const h = harness(); + expect(ensureCeoDirectSpace(h.connection.db, h.orgId)).toBeNull(); + const directSpaces = h.connection.db.prepare("select id from chat_spaces where org_id = ? and space_type = 'direct'") + .all(h.orgId) as Array<{ id: string }>; + expect(directSpaces).toHaveLength(0); + }); + + test("ignores a retired CEO", () => { + const h = harness(); + const ceoId = createCeo(h); + h.connection.db.prepare("update agents set lifecycle = 'retired' where id = ?").run(ceoId); + expect(ensureCeoDirectSpace(h.connection.db, h.orgId)).toBeNull(); + }); + + test("the founder's message in the ensured conversation wakes the CEO without an @mention", () => { + const h = harness({ enableProvider: true }); + const ceoId = createCeo(h); + const spaceId = ensureCeoDirectSpace(h.connection.db, h.orgId) as string; + + new ChatService(h.connection.db).sendMessage({ + orgId: h.orgId, + spaceId, + threadId: null, + senderType: "employee", + senderId: h.employeeId, + bodyMd: "How is the launch tracking against the goal?", + attachmentFileIds: [], + }); + + const runs = h.connection.db.prepare( + "select count(*) as count from task_runs where org_id = ? and agent_id = ? and trigger = 'chat_mention'", + ).get(h.orgId, ceoId) as { count: number }; + expect(runs.count).toBe(1); + }); +});