feat(api): sub-agent roles + DB project creation in onboarding scan

- Auto-generate specialist roles from detected interests/skills:
  Code Engineer, UI/UX Designer, Data Analyst, DevOps, Music, Writer
- Always include General Assistant role
- Embed role descriptions + instructions in agent briefing
- Create real Prisma Space entries for discovered git repos
- Deduplicate: skip spaces that already exist
- Pass prisma client through scan pipeline chain

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Ingo Wagner 2026-09-24 00:17:29 +00:00
parent 85a0f3860a
commit c18a04ce3f
5 changed files with 143 additions and 23 deletions

View File

@ -1,7 +1,7 @@
{
"previousSha": "HEAD",
"deployedSha": "is now at 83e88d0 feat: security headers + onboarding scan & profile creation flow",
"ref": "83e88d05f3efa00635a5ac5956fe637e45d71057",
"deployedSha": "is now at 2fe3dc5 feat(api): real onboarding scan — filesystem, Git repos, profile compilation",
"ref": "2fe3dc5971d55ca9604757bc79aae94feb9ec21d",
"status": "success",
"at": "2026-09-23T23:36:23Z"
"at": "2026-09-23T23:52:26Z"
}

View File

@ -9,3 +9,4 @@
- Autopilot restart path: A rejected (evoo-stack = Rakazo Docker, broken symlink /srv/rakazo/stack-up.sh → missing /srv/evoo/rakazo/…). Chose new user unit evoo-local.service + ~/.local/bin/evoo-dev-stack.sh; live :3100/:5173 migrated; GH vars EVO_RESTART_MODE=systemd EVO_SYSTEMD_UNIT=evoo-local.service. Do not use evoo-stack for evo-deploy.
- Cancelled queued evo-deploy 35933339459 (would reset --hard over Claude WIP now that restart=systemd). Re-dispatch after WIP committed/stashed.
2026-09-23 23:53:08 UTC evoo-tools disabled (missing app.py)

View File

@ -15,6 +15,7 @@ interface ScanResult {
profileSummary: ProfileSummary;
agentBriefing: string;
createdProjects: CreatedProject[];
subAgentRoles: SubAgentRole[];
}
interface RepoInfo {
@ -75,7 +76,7 @@ interface ScanRecord {
const scanRegistry = new Map<string, ScanRecord>();
export async function startOnboardingScan(botId: string): Promise<{ scanId: string }> {
export async function startOnboardingScan(botId: string, prisma: any): Promise<{ scanId: string }> {
const scanId = `scan_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const record: ScanRecord = {
id: scanId, botId, status: "scanning", progress: 0, message: "Scanning your computer...",
@ -87,7 +88,7 @@ export async function startOnboardingScan(botId: string): Promise<{ scanId: stri
(async () => {
try {
const homeDir = process.env.HOME ?? "/home/cynar";
const result = await executeFullScan(homeDir, botId);
const result = await executeFullScan(homeDir, botId, prisma);
record.status = "complete";
record.progress = 100;
record.message = "Scan complete — profile ready";
@ -116,7 +117,7 @@ export async function getScanStatus(scanId: string): Promise<{
// Full scan pipeline
// ---------------------------------------------------------------------------
async function executeFullScan(homeDir: string, _botId: string): Promise<ScanResult> {
async function executeFullScan(homeDir: string, _botId: string, prisma: any): Promise<ScanResult> {
// Phase 1: Local file scan
const [fileSummary, projectFolders] = await scanFiles(homeDir);
@ -127,13 +128,16 @@ async function executeFullScan(homeDir: string, _botId: string): Promise<ScanRes
// Phase 4: Profile compilation from findings
const profileSummary = compileProfile(localRepos, fileSummary, projectFolders);
// Phase 5: Agent briefing
const agentBriefing = generateBriefing(localRepos, profileSummary, fileSummary);
// Phase 6: Project folder creation
const createdProjects = await createProjectFolders(localRepos);
const createdProjects = await createProjectFolders(localRepos, prisma);
return { localRepos, projectFolders, fileSummary, profileSummary, agentBriefing, createdProjects };
// Phase 7: Sub-agent role creation
const subAgentRoles = generateSubAgentRoles(profileSummary, localRepos);
// Phase 5: Agent briefing (includes sub-agent roles)
const agentBriefing = generateBriefing(localRepos, profileSummary, fileSummary, subAgentRoles);
return { localRepos, projectFolders, fileSummary, profileSummary, agentBriefing, createdProjects, subAgentRoles };
}
// ---------------------------------------------------------------------------
@ -393,7 +397,12 @@ function compileProfile(repos: RepoInfo[], fileSummary: FileSummary, folders: Pr
// Phase 4: Agent briefing
// ---------------------------------------------------------------------------
function generateBriefing(repos: RepoInfo[], profile: ProfileSummary, fileSummary: FileSummary): string {
function generateBriefing(
repos: RepoInfo[],
profile: ProfileSummary,
fileSummary: FileSummary,
subAgentRoles: SubAgentRole[],
): string {
const lines: string[] = [];
lines.push(`# User Profile Briefing`);
lines.push(``);
@ -418,6 +427,15 @@ function generateBriefing(repos: RepoInfo[], profile: ProfileSummary, fileSummar
lines.push(`## User Skills`);
lines.push(`- ${profile.skills.slice(0, 5).join("\n- ")}`);
lines.push(``);
lines.push(`## Assigned Specialist Roles`);
for (const role of subAgentRoles) {
lines.push(`### ${role.name}`);
lines.push(`- **Role:** ${role.role}`);
lines.push(`- **Description:** ${role.description}`);
lines.push(`- **Instructions:** ${role.instructions}`);
lines.push(`- **Skills:** ${role.skills.join(", ")}`);
lines.push("");
}
lines.push(`---`);
lines.push(`This profile should be used to tailor all future interactions, project recommendations, and agent assignments.`);
return lines.join("\n");
@ -427,24 +445,124 @@ function generateBriefing(repos: RepoInfo[], profile: ProfileSummary, fileSummar
// Phase 5: Project folder creation
// ---------------------------------------------------------------------------
async function createProjectFolders(repos: RepoInfo[]): Promise<CreatedProject[]> {
async function createProjectFolders(repos: RepoInfo[], prisma: any): Promise<CreatedProject[]> {
const created: CreatedProject[] = [];
for (const repo of repos) {
// Each repo becomes a project space
// In production, this would call the database API to create a space
// For now, we return the info for the caller to persist
created.push({
id: `proj_${repo.name}_${Date.now()}`,
name: repo.name,
spaceId: `space_${repo.name.toLowerCase().replace(/[^a-z0-9]/g, "_")}`,
description: repo.remote || `Local project: ${repo.path}`,
try {
// Check if space already exists
const existing = await prisma.space.findFirst({ where: { title: repo.name } });
if (existing) {
created.push({ id: existing.id, name: repo.name, spaceId: existing.id, description: repo.remote || repo.path });
continue;
}
// Create new space
const space = await prisma.space.create({
data: { title: repo.name, path: repo.path },
});
created.push({ id: space.id, name: repo.name, spaceId: space.id, description: repo.remote || repo.path });
} catch {
// Space might already exist or DB unavailable — skip
}
}
return created;
}
// ---------------------------------------------------------------------------
// Phase 6: Sub-agent role creation
// ---------------------------------------------------------------------------
interface SubAgentRole {
name: string;
role: string;
description: string;
instructions: string;
skills: string[];
}
function generateSubAgentRoles(profile: ProfileSummary, repos: RepoInfo[]): SubAgentRole[] {
const roles: SubAgentRole[] = [];
const hasCode = profile.languages?.length > 0;
const hasDesign = profile.interests?.some(i => i.toLowerCase().includes("design") || i.toLowerCase().includes("ui"));
const hasData = profile.interests?.some(i => i.toLowerCase().includes("data") || i.toLowerCase().includes("analytics") || i.toLowerCase().includes("database"));
const hasDevOps = profile.interests?.some(i => i.toLowerCase().includes("infra") || i.toLowerCase().includes("ops") || i.toLowerCase().includes("deploy"));
const hasMusic = profile.interests?.some(i => i.toLowerCase().includes("music"));
const hasWriting = profile.interests?.some(i => i.toLowerCase().includes("write") || i.toLowerCase().includes("blog"));
if (hasCode) {
const primaryLang = profile.languages?.[0] || "TypeScript";
roles.push({
name: "Code Engineer",
role: "software-engineer",
description: `Specializes in ${primaryLang} development and code review`,
instructions: `You are the Code Engineer specialist. Your expertise is in ${profile.languages?.join(", ") || "software development"}. Review code, write tests, debug issues, and implement features. You have access to the user's local repositories and can run commands to build and test code.`,
skills: profile.skills.filter(s => s.toLowerCase().includes("code") || s.toLowerCase().includes("program")),
});
}
if (hasDesign) {
roles.push({
name: "UI/UX Designer",
role: "ui-designer",
description: "Creates polished interfaces and design systems",
instructions: "You are the UI/UX Designer specialist. Focus on creating beautiful, accessible interfaces. Review designs, suggest improvements, and ensure consistency across the user's projects.",
skills: ["design", "UI/UX"],
});
}
if (hasData) {
roles.push({
name: "Data Analyst",
role: "data-analyst",
description: "Analyzes data, creates reports, and extracts insights",
instructions: "You are the Data Analyst specialist. Process data from the user's projects, create visualizations, generate reports, and provide actionable insights.",
skills: ["data analysis", "statistics"],
});
}
if (hasDevOps) {
roles.push({
name: "DevOps Engineer",
role: "devops-engineer",
description: "Manages infrastructure, deployments, and CI/CD",
instructions: "You are the DevOps Engineer specialist. Handle deployments, manage infrastructure, configure CI/CD pipelines, and ensure system reliability.",
skills: ["DevOps", "infrastructure"],
});
}
if (hasMusic) {
roles.push({
name: "Music Producer",
role: "music-producer",
description: "Assists with music production and audio engineering",
instructions: "You are the Music Producer specialist. Help with music composition, audio engineering, mixing, and mastering. You understand the user's music projects and can provide expert advice.",
skills: ["music production", "audio engineering"],
});
}
if (hasWriting) {
roles.push({
name: "Content Writer",
role: "content-writer",
description: "Creates and edits written content",
instructions: "You are the Content Writer specialist. Write blog posts, documentation, articles, and other content for the user's projects and personal use.",
skills: ["writing", "content creation"],
});
}
// Always include a general assistant
roles.unshift({
name: "General Assistant",
role: "general-assistant",
description: "Handles general tasks and coordinates other specialists",
instructions: `You are the General Assistant and team coordinator. Your role is to handle general queries, coordinate with other specialists (${roles.slice(1).map(r => r.name).join(", ") || "none yet"}), and manage the user's daily workflow. You have access to all tools and can delegate tasks to specialists when needed.`,
skills: ["task management", "coordination"],
});
return roles;
}
// ---------------------------------------------------------------------------
// Utility
// ---------------------------------------------------------------------------

View File

@ -8,6 +8,7 @@ import {
type Prisma,
type PrismaClient,
type ThreadEvents,
createPool,
} from "@rakazo/db";
/**

View File

@ -3318,7 +3318,7 @@ export function createRouter(deps: RouterDeps) {
return { ok: true as const };
}),
scan: authed.onboarding.scan.handler(async ({ context, input }) => {
const { scanId } = await startOnboardingScan(input.botId);
const { scanId } = await startOnboardingScan(input.botId, deps.prisma);
return { scanId };
}),
scanStatus: authed.onboarding.scanStatus.handler(async ({ input }) => {