From b60fcd8d0665e28a487a9c657e4197379b1e2f34 Mon Sep 17 00:00:00 2001 From: scotttong Date: Tue, 17 Mar 2026 16:41:27 -0700 Subject: [PATCH] experiment: add hiring plan review (step 5) and hire tasks (step 6) Step 5 - Review Hiring Plan: - Parses CEO's markdown plan into structured role cards - Each role has a checkbox (include/exclude), edit button, delete button - Inline editing for role name and description - "Add role" button to create new roles manually - "Revise with CEO" button returns to chat (step 4) - Collapsible raw plan view for reference - "Approve" button only enabled when at least one role is selected Step 6 - Make Your First Hires: - Summary showing company, CEO, and all approved roles - "Make your first hires" creates one task per approved role assigned to the CEO (e.g., "Hire: Content Strategist") - Navigates to the task list after completion Plan parsing handles "- **Role Name**: description" markdown format. Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/components/OnboardingWizard.tsx | 350 ++++++++++++++++++++++--- 1 file changed, 307 insertions(+), 43 deletions(-) diff --git a/ui/src/components/OnboardingWizard.tsx b/ui/src/components/OnboardingWizard.tsx index 62b5bfe2..a9041ebf 100644 --- a/ui/src/components/OnboardingWizard.tsx +++ b/ui/src/components/OnboardingWizard.tsx @@ -51,7 +51,11 @@ import { Loader2, FolderOpen, ChevronDown, - X + X, + Plus, + Pencil, + Trash2, + MessageSquare } from "lucide-react"; type Step = 1 | 2 | 3 | 4 | 5 | 6; @@ -81,6 +85,45 @@ function buildMissionFromQuestionnaire(q1: string, q2: string, q3: string, q4: s return parts.join(" "); } +interface HiringRole { + id: string; + name: string; + description: string; + enabled: boolean; + editing: boolean; +} + +let roleIdCounter = 0; +function nextRoleId(): string { + return `role-${++roleIdCounter}`; +} + +/** + * Parse a markdown hiring plan into structured roles. + * Looks for bullet points with bold role names: "- **Role Name**: description" + * Falls back to any bold text in bullet points. + */ +function parseHiringPlan(markdown: string): HiringRole[] { + const roles: HiringRole[] = []; + const lines = markdown.split("\n"); + for (const line of lines) { + // Match "- **Role Name**: description" or "- **Role Name** - description" + const match = line.match( + /^\s*[-*]\s+\*\*([^*]+)\*\*[:\s-]*(.*)$/ + ); + if (match) { + roles.push({ + id: nextRoleId(), + name: match[1].trim(), + description: match[2].trim(), + enabled: true, + editing: false, + }); + } + } + return roles; +} + const DEFAULT_TASK_DESCRIPTION = `Setup yourself as the CEO. Use the ceo persona found here: https://github.com/paperclipai/companies/blob/main/default/ceo/AGENTS.md @@ -150,6 +193,8 @@ export function OnboardingWizard() { // Planning task + hiring plan const [planningTaskId, setPlanningTaskId] = useState(null); const [planContent, setPlanContent] = useState(null); + const [hiringRoles, setHiringRoles] = useState([]); + const [showRawPlan, setShowRawPlan] = useState(false); // Created entity IDs — pre-populate from existing company when skipping step 1 const [createdCompanyId, setCreatedCompanyId] = useState( @@ -284,6 +329,8 @@ export function OnboardingWizard() { setQ4(""); setPlanningTaskId(null); setPlanContent(null); + setHiringRoles([]); + setShowRawPlan(false); setAgentName("CEO"); setAdapterType("claude_local"); setCwd(""); @@ -551,33 +598,35 @@ export function OnboardingWizard() { setLoading(true); setError(null); try { - let issueRef = createdIssueRef; - if (!issueRef) { - const issue = await issuesApi.create(createdCompanyId, { - title: taskTitle.trim(), - ...(taskDescription.trim() - ? { description: taskDescription.trim() } - : {}), + // Create a hire task for each approved role + const approvedRoles = hiringRoles.filter( + (r) => r.enabled && r.name.trim() + ); + for (const role of approvedRoles) { + await issuesApi.create(createdCompanyId, { + title: `Hire: ${role.name}`, + description: role.description + ? `Hire a ${role.name} for the company.\n\n${role.description}` + : `Hire a ${role.name} for the company.`, assigneeAgentId: createdAgentId, status: "todo" }); - issueRef = issue.identifier ?? issue.id; - setCreatedIssueRef(issueRef); - queryClient.invalidateQueries({ - queryKey: queryKeys.issues.list(createdCompanyId) - }); } + queryClient.invalidateQueries({ + queryKey: queryKeys.issues.list(createdCompanyId) + }); + setSelectedCompanyId(createdCompanyId); reset(); closeOnboarding(); navigate( createdCompanyPrefix - ? `/${createdCompanyPrefix}/issues/${issueRef}` - : `/issues/${issueRef}` + ? `/${createdCompanyPrefix}/issues` + : `/issues` ); } catch (err) { - setError(err instanceof Error ? err.message : "Failed to create task"); + setError(err instanceof Error ? err.message : "Failed to create hire tasks"); } finally { setLoading(false); } @@ -1394,9 +1443,9 @@ export function OnboardingWizard() { )} - {/* Step 5: Review hiring plan — placeholder */} + {/* Step 5: Review hiring plan */} {step === 5 && ( -
+
@@ -1404,16 +1453,209 @@ export function OnboardingWizard() {

Review your hiring plan

- Select which roles to hire. You can edit, add, or remove - roles before approving. + Select which roles to hire. Edit, add, or remove roles + before approving.

-
-

- Hiring plan review component coming soon. -

-
+ + {hiringRoles.length === 0 ? ( +
+

+ No roles parsed from the hiring plan yet. +

+ +
+ ) : ( +
+ {hiringRoles.map((role) => ( +
+ {role.editing ? ( +
+ + setHiringRoles((prev) => + prev.map((r) => + r.id === role.id + ? { ...r, name: e.target.value } + : r + ) + ) + } + autoFocus + /> +