Implement original onboarding bootstrap
This commit is contained in:
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"),
|
||||
|
||||
Reference in new issue
Block a user