Implement original onboarding bootstrap

This commit is contained in:
ZacharyZhang-NY committed 2026-05-21 08:43:50 -04:00
1 parent 49c1dd2888
commit 6992b3e70e
16 files changed
+873 -47

No files matched your search

@@ -0,0 +1,215 @@
import type { DatabaseSync } from "node:sqlite";
import type {
BootstrapOnboardingInput,
BootstrapOnboardingSummary,
SaveProviderCredentialInput,
} from "../../shared/commands";
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 { OrganizationService } from "./organization-service";
interface OnboardingServiceDeps {
db: DatabaseSync;
saveProviderCredential(input: SaveProviderCredentialInput): void;
}
export class OnboardingService {
private readonly audit: AuditService;
private readonly organization: OrganizationService;
constructor(private readonly deps: OnboardingServiceDeps) {
this.audit = new AuditService(deps.db);
this.organization = new OrganizationService(deps.db);
}
bootstrap(input: BootstrapOnboardingInput): BootstrapOnboardingSummary {
const provider = this.resolveProvider(input.providerKey);
const orgId = this.organization.createOrganization({
organizationName: input.companyName,
description: input.companyDescription,
mission: input.companyDescription,
goal: input.goalTitle,
employeeName: input.employeeName,
executionMode: input.executionMode,
autoApproveHires: input.autoApproveHires,
reviewDeliverables: input.reviewDeliverables,
});
const employeeId = this.requirePrimaryEmployee(orgId);
const providerId = this.requireProvider(orgId, provider.key);
this.deps.saveProviderCredential({
orgId,
providerId,
secret: input.providerSecret,
model: input.providerModel,
});
const now = new Date().toISOString();
const workspaceId = this.createWorkspace(orgId, input.workspacePath.trim(), now);
const ceoAgentId = this.organization.createAgent({
orgId,
name: "CEO",
role: "CEO",
title: "Chief Executive Officer",
department: "Executive",
modelProviderId: providerId,
modelId: input.providerModel.trim(),
budgetCents: 25000,
managerAgentId: null,
roleLevel: "c_suite",
modelStrategy: "explicit",
workspaceId,
heartbeatEnabled: true,
heartbeatIntervalSec: 120,
});
const goalId = this.createGoal(orgId, ceoAgentId, input, now);
const projectId = this.createProject(orgId, goalId, ceoAgentId, input, now);
if (workspaceId) this.bindWorkspaceToProject(workspaceId, projectId, now);
const taskId = this.createKickoffTask(orgId, goalId, projectId, workspaceId, ceoAgentId, input, now);
const runId = this.createTaskRun(orgId, taskId, ceoAgentId, providerId, now);
this.audit.record({
orgId,
entityType: "organization",
entityId: orgId,
actorType: "employee",
actorId: employeeId,
action: "bootstrapped",
payload: { providerKey: provider.key, goalId, projectId, taskId, runId },
});
return { orgId, employeeId, ceoAgentId, workspaceId, goalId, projectId, taskId, runId, providerId };
}
private resolveProvider(providerKey: string) {
const provider = PROVIDER_CATALOG.find((entry) => entry.key === providerKey);
if (!provider) throw new Error(`Provider ${providerKey} is unavailable.`);
if (provider.authType !== PROVIDER_AUTH_TYPE.API_KEY) {
throw new Error("Onboarding requires an API key provider.");
}
return provider;
}
private requirePrimaryEmployee(orgId: string): string {
const row = this.deps.db.prepare("select id from employees where org_id = ? order by created_at asc limit 1")
.get(orgId) as { id: string } | undefined;
if (!row) throw new Error("Primary employee was not created.");
return row.id;
}
private requireProvider(orgId: string, providerKey: string): string {
const row = this.deps.db.prepare("select id from providers where org_id = ? and provider_key = ?")
.get(orgId, providerKey) as { id: string } | undefined;
if (!row) throw new Error(`Provider ${providerKey} was not created.`);
return row.id;
}
private createWorkspace(orgId: string, workspacePath: string, now: string): string | null {
if (!workspacePath) return null;
const workspaceId = createId("wsp");
this.deps.db.prepare(`
insert into workspaces(
id, org_id, name, local_path, repo_url, repo_ref, project_id, is_primary, created_at, updated_at
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(workspaceId, orgId, "Default Workspace", workspacePath, "", "", null, 1, now, now);
return workspaceId;
}
private createGoal(orgId: string, ceoAgentId: string, input: BootstrapOnboardingInput, now: string): string {
const goalTitle = input.goalTitle.trim();
if (!goalTitle) throw new Error("Goal title is required.");
const goalId = createId("gol");
this.deps.db.prepare(`
insert into goals(id, org_id, title, description, status, owner_agent_id, created_at, updated_at)
values (?, ?, ?, ?, ?, ?, ?, ?)
`).run(goalId, orgId, goalTitle, input.goalDescription.trim(), "active", ceoAgentId, now, now);
return goalId;
}
private createProject(
orgId: string,
goalId: string,
ceoAgentId: string,
input: BootstrapOnboardingInput,
now: string,
): string {
const projectId = createId("prj");
this.deps.db.prepare(`
insert into projects(id, org_id, goal_id, name, description, lead_agent_id, status, created_at, updated_at)
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
projectId,
orgId,
goalId,
`Execution plan: ${input.goalTitle.trim()}`,
input.companyDescription.trim(),
ceoAgentId,
"in_progress",
now,
now,
);
return projectId;
}
private bindWorkspaceToProject(workspaceId: string, projectId: string, now: string): void {
this.deps.db.prepare("update workspaces set project_id = ?, updated_at = ? where id = ?")
.run(projectId, now, workspaceId);
}
private createKickoffTask(
orgId: string,
goalId: string,
projectId: string,
workspaceId: string | null,
ceoAgentId: string,
input: BootstrapOnboardingInput,
now: string,
): string {
const taskId = createId("tsk");
this.deps.db.prepare(`
insert into tasks(
id, org_id, title, description, status, assignee_agent_id, risk,
project_id, goal_id, workspace_id, priority, requires_filesystem, created_at, updated_at
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
taskId,
orgId,
`Operationalize goal: ${input.goalTitle.trim()}`,
this.createKickoffTaskDescription(input),
TASK_STATUS.BACKLOG,
ceoAgentId,
RISK_LEVEL.MEDIUM,
projectId,
goalId,
workspaceId,
"high",
workspaceId ? 1 : 0,
now,
now,
);
return taskId;
}
private createTaskRun(orgId: string, taskId: string, ceoAgentId: string, providerId: string, now: string): string {
const runId = createId("run");
this.deps.db.prepare(`
insert into task_runs(
id, org_id, task_id, agent_id, provider_id, trigger, state, created_at, started_at, completed_at, error_message
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(runId, orgId, taskId, ceoAgentId, providerId, "bootstrap", "queued", now, null, null, null);
return runId;
}
private createKickoffTaskDescription(input: BootstrapOnboardingInput): string {
return [
`Company: ${input.companyName.trim()}`,
`Goal: ${input.goalTitle.trim()}`,
input.goalDescription.trim(),
"Read the bound workspace, create the first operating plan, delegate work, and route decisions through approvals and messages.",
].filter((line) => line.length > 0).join("\n\n");
}
}
@@ -20,8 +20,22 @@ export class OrganizationService {
const employeeId = createId("emp");
const now = new Date().toISOString();
this.db.prepare("insert into organizations(id, name, mission, goal, created_at) values (?, ?, ?, ?, ?)")
.run(orgId, input.organizationName.trim(), input.mission.trim(), input.goal.trim(), now);
this.db.prepare(`
insert into organizations(
id, name, mission, goal, description, execution_mode, auto_approve_hires, review_deliverables, created_at
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
orgId,
input.organizationName.trim(),
input.mission.trim(),
input.goal.trim(),
input.description?.trim() ?? "",
input.executionMode ?? "ai_ceo",
input.autoApproveHires === false ? 0 : 1,
input.reviewDeliverables === true ? 1 : 0,
now,
);
this.db.prepare("insert into employees(id, org_id, display_name, role, created_at) values (?, ?, ?, ?, ?)")
.run(employeeId, orgId, input.employeeName.trim(), "Board Chair", now);
@@ -78,8 +92,9 @@ export class OrganizationService {
this.db.prepare(`
insert into agents(
id, org_id, name, role, department, model_provider_id, model_id,
budget_cents, manager_agent_id, lifecycle, created_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
budget_cents, manager_agent_id, title, role_level, model_strategy,
workspace_id, heartbeat_enabled, heartbeat_interval_sec, lifecycle, created_at
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id,
input.orgId,
@@ -90,6 +105,12 @@ export class OrganizationService {
input.modelId,
input.budgetCents,
input.managerAgentId,
input.title?.trim() || input.role.trim(),
input.roleLevel ?? "staff",
input.modelStrategy ?? "pooled",
input.workspaceId ?? null,
input.heartbeatEnabled === true ? 1 : 0,
input.heartbeatIntervalSec ?? 0,
"active",
now,
);
@@ -109,8 +130,11 @@ export class OrganizationService {
const id = createId("tsk");
const now = new Date().toISOString();
this.db.prepare(`
insert into tasks(id, org_id, title, description, status, assignee_agent_id, risk, created_at, updated_at)
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
insert into tasks(
id, org_id, title, description, status, assignee_agent_id, risk,
project_id, goal_id, workspace_id, priority, requires_filesystem, created_at, updated_at
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id,
input.orgId,
@@ -119,6 +143,11 @@ export class OrganizationService {
input.status,
input.assigneeAgentId,
input.risk,
null,
null,
null,
"normal",
0,
now,
now,
);
@@ -28,8 +28,19 @@ export class SnapshotService {
constructor(private readonly db: DatabaseSync) {}
load(): AppSnapshot {
const organizations = list<AppSnapshot["organizations"][number]>(this.db, "organizations", "created_at asc");
const organizations = list<AppSnapshot["organizations"][number]>(this.db, "organizations", "created_at asc")
.map((org) => ({
...org,
autoApproveHires: toBool(org.autoApproveHires),
reviewDeliverables: toBool(org.reviewDeliverables),
}));
const activeOrgId = organizations[0]?.id ?? null;
const agents = list<AppSnapshot["agents"][number]>(this.db, "agents", "created_at asc")
.map((agent) => ({ ...agent, heartbeatEnabled: toBool(agent.heartbeatEnabled) }));
const tasks = list<AppSnapshot["tasks"][number]>(this.db, "tasks", "created_at asc")
.map((task) => ({ ...task, requiresFilesystem: toBool(task.requiresFilesystem) }));
const workspaces = list<AppSnapshot["workspaces"][number]>(this.db, "workspaces", "created_at asc")
.map((workspace) => ({ ...workspace, isPrimary: toBool(workspace.isPrimary) }));
const providers = list<AppSnapshot["providers"][number]>(this.db, "providers", "label asc")
.map((provider) => ({ ...provider, enabled: toBool(provider.enabled) }));
const skillBindings = list<AppSnapshot["skillBindings"][number]>(this.db, "skill_bindings", "priority asc")
@@ -39,8 +50,12 @@ export class SnapshotService {
activeOrgId,
organizations,
employees: list(this.db, "employees", "created_at asc"),
agents: list(this.db, "agents", "created_at asc"),
tasks: list(this.db, "tasks", "created_at asc"),
agents,
tasks,
workspaces,
goals: list(this.db, "goals", "created_at asc"),
projects: list(this.db, "projects", "created_at asc"),
taskRuns: list(this.db, "task_runs", "created_at asc"),
approvals: list(this.db, "approvals", "created_at desc"),
chatSpaces: list(this.db, "chat_spaces", "created_at asc"),
chatMessages: list(this.db, "chat_messages", "created_at asc"),