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",
|
"previousSha": "HEAD",
|
||||||
"deployedSha": "is now at 83e88d0 feat: security headers + onboarding scan & profile creation flow",
|
"deployedSha": "is now at 2fe3dc5 feat(api): real onboarding scan — filesystem, Git repos, profile compilation",
|
||||||
"ref": "83e88d05f3efa00635a5ac5956fe637e45d71057",
|
"ref": "2fe3dc5971d55ca9604757bc79aae94feb9ec21d",
|
||||||
"status": "success",
|
"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.
|
- 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.
|
- 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;
|
profileSummary: ProfileSummary;
|
||||||
agentBriefing: string;
|
agentBriefing: string;
|
||||||
createdProjects: CreatedProject[];
|
createdProjects: CreatedProject[];
|
||||||
|
subAgentRoles: SubAgentRole[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RepoInfo {
|
interface RepoInfo {
|
||||||
@ -75,7 +76,7 @@ interface ScanRecord {
|
|||||||
|
|
||||||
const scanRegistry = new Map<string, 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 scanId = `scan_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||||
const record: ScanRecord = {
|
const record: ScanRecord = {
|
||||||
id: scanId, botId, status: "scanning", progress: 0, message: "Scanning your computer...",
|
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 () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const homeDir = process.env.HOME ?? "/home/cynar";
|
const homeDir = process.env.HOME ?? "/home/cynar";
|
||||||
const result = await executeFullScan(homeDir, botId);
|
const result = await executeFullScan(homeDir, botId, prisma);
|
||||||
record.status = "complete";
|
record.status = "complete";
|
||||||
record.progress = 100;
|
record.progress = 100;
|
||||||
record.message = "Scan complete — profile ready";
|
record.message = "Scan complete — profile ready";
|
||||||
@ -116,7 +117,7 @@ export async function getScanStatus(scanId: string): Promise<{
|
|||||||
// Full scan pipeline
|
// 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
|
// Phase 1: Local file scan
|
||||||
const [fileSummary, projectFolders] = await scanFiles(homeDir);
|
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
|
// Phase 4: Profile compilation from findings
|
||||||
const profileSummary = compileProfile(localRepos, fileSummary, projectFolders);
|
const profileSummary = compileProfile(localRepos, fileSummary, projectFolders);
|
||||||
|
|
||||||
// Phase 5: Agent briefing
|
|
||||||
const agentBriefing = generateBriefing(localRepos, profileSummary, fileSummary);
|
|
||||||
|
|
||||||
// Phase 6: Project folder creation
|
// 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
|
// 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[] = [];
|
const lines: string[] = [];
|
||||||
lines.push(`# User Profile Briefing`);
|
lines.push(`# User Profile Briefing`);
|
||||||
lines.push(``);
|
lines.push(``);
|
||||||
@ -418,6 +427,15 @@ function generateBriefing(repos: RepoInfo[], profile: ProfileSummary, fileSummar
|
|||||||
lines.push(`## User Skills`);
|
lines.push(`## User Skills`);
|
||||||
lines.push(`- ${profile.skills.slice(0, 5).join("\n- ")}`);
|
lines.push(`- ${profile.skills.slice(0, 5).join("\n- ")}`);
|
||||||
lines.push(``);
|
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(`---`);
|
||||||
lines.push(`This profile should be used to tailor all future interactions, project recommendations, and agent assignments.`);
|
lines.push(`This profile should be used to tailor all future interactions, project recommendations, and agent assignments.`);
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
@ -427,24 +445,124 @@ function generateBriefing(repos: RepoInfo[], profile: ProfileSummary, fileSummar
|
|||||||
// Phase 5: Project folder creation
|
// Phase 5: Project folder creation
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function createProjectFolders(repos: RepoInfo[]): Promise<CreatedProject[]> {
|
async function createProjectFolders(repos: RepoInfo[], prisma: any): Promise<CreatedProject[]> {
|
||||||
const created: CreatedProject[] = [];
|
const created: CreatedProject[] = [];
|
||||||
|
|
||||||
for (const repo of repos) {
|
for (const repo of repos) {
|
||||||
// Each repo becomes a project space
|
try {
|
||||||
// In production, this would call the database API to create a space
|
// Check if space already exists
|
||||||
// For now, we return the info for the caller to persist
|
const existing = await prisma.space.findFirst({ where: { title: repo.name } });
|
||||||
created.push({
|
if (existing) {
|
||||||
id: `proj_${repo.name}_${Date.now()}`,
|
created.push({ id: existing.id, name: repo.name, spaceId: existing.id, description: repo.remote || repo.path });
|
||||||
name: repo.name,
|
continue;
|
||||||
spaceId: `space_${repo.name.toLowerCase().replace(/[^a-z0-9]/g, "_")}`,
|
}
|
||||||
description: repo.remote || `Local project: ${repo.path}`,
|
// 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;
|
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
|
// Utility
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
type Prisma,
|
type Prisma,
|
||||||
type PrismaClient,
|
type PrismaClient,
|
||||||
type ThreadEvents,
|
type ThreadEvents,
|
||||||
|
createPool,
|
||||||
} from "@rakazo/db";
|
} from "@rakazo/db";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -3318,7 +3318,7 @@ export function createRouter(deps: RouterDeps) {
|
|||||||
return { ok: true as const };
|
return { ok: true as const };
|
||||||
}),
|
}),
|
||||||
scan: authed.onboarding.scan.handler(async ({ context, input }) => {
|
scan: authed.onboarding.scan.handler(async ({ context, input }) => {
|
||||||
const { scanId } = await startOnboardingScan(input.botId);
|
const { scanId } = await startOnboardingScan(input.botId, deps.prisma);
|
||||||
return { scanId };
|
return { scanId };
|
||||||
}),
|
}),
|
||||||
scanStatus: authed.onboarding.scanStatus.handler(async ({ input }) => {
|
scanStatus: authed.onboarding.scanStatus.handler(async ({ input }) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user