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:
parent
85a0f3860a
commit
c18a04ce3f
@ -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"
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
type Prisma,
|
||||
type PrismaClient,
|
||||
type ThreadEvents,
|
||||
createPool,
|
||||
} from "@rakazo/db";
|
||||
|
||||
/**
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user