From a3006f0b38d07c1cb2bfdf5f977b998a503c225b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 19 Apr 2026 19:18:01 -0400 Subject: [PATCH] Assign default skills when a hire is approved MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CEO's operating prompt instructed the model to inspect each direct report every wake, compare assigned skills to role, and POST missing ones — business logic routed through the LLM at O(reports) API calls per heartbeat. The guarantee was unreliable and expensive. Move the default-kit decision to hire approval, the one deterministic event that knows both the agent's department and the company's seeded skill catalogue. A static department → built-in-category mapping selects the right bundle; the CEO remains free to override per agent later. Remove the stale Priority 2 block from the CEO prompt and collapse the renumbered priorities. --- .../automation/automation-approval-handler.ts | 24 +++++ .../prompt-builder/ceo-modes/operating.en.md | 23 ++--- src/main/skills/auto-assign-on-hire.ts | 89 +++++++++++++++++++ 3 files changed, 119 insertions(+), 17 deletions(-) create mode 100644 src/main/skills/auto-assign-on-hire.ts diff --git a/src/main/automation/automation-approval-handler.ts b/src/main/automation/automation-approval-handler.ts index 2533501a..ee8e777b 100644 --- a/src/main/automation/automation-approval-handler.ts +++ b/src/main/automation/automation-approval-handler.ts @@ -3,6 +3,7 @@ import type { AppDatabase } from "../database"; import type { NotificationOptions } from "../notification-service"; import { extractApprovalDocumentIds } from "@shared/approval-documents"; import { notifyHireApproved } from "../hire-hook"; +import { autoAssignSkillsForHire } from "../skills/auto-assign-on-hire"; import type { AutomationEventContext } from "./types"; import { normalizeWorkflowSteps } from "./automation-workflow"; @@ -97,6 +98,29 @@ export function handleApprovalResolved( const agent = db.getAgent(approval.relatedAgentId); if (agent.status === "idle") { + // Deterministic default skill kit. The CEO's operating prompt + // used to carry this responsibility ("ensure every direct report + // has skills"), turning O(reports) API calls into a per-wake + // tax. At hire approval time we know the agent's department and + // the company's seeded catalogue — assign the default bundle so + // the new hire's first run already has the right SKILL.md + // manifest. + const { assignedSkillIds } = autoAssignSkillsForHire(db, { + agentId: agent.id, + companyId, + department: agent.department, + }); + if (assignedSkillIds.length > 0) { + db.addActivity({ + companyId, + actor: "system", + action: "agent.skills_defaulted", + entityType: "agent", + entityId: agent.id, + detail: `${assignedSkillIds.length} default skill(s) assigned on hire.`, + }); + } + db.addActivity({ companyId, actor: "system", diff --git a/src/main/prompt-builder/ceo-modes/operating.en.md b/src/main/prompt-builder/ceo-modes/operating.en.md index ef331f83..7599af3c 100644 --- a/src/main/prompt-builder/ceo-modes/operating.en.md +++ b/src/main/prompt-builder/ceo-modes/operating.en.md @@ -20,16 +20,7 @@ If there are NO active goals, this is your top priority — create strategic goa -d '{"title":"Goal title","description":"Measurable success criteria","ownerAgentId":"{{agentId}}","status":"active"}' Then decompose each goal into projects, and each project into tasks assigned to department leads. -## Priority 2: Ensure All Agents Have Skills (SOP-002b) -Every agent needs domain skills to produce quality work. Check and fix immediately: -1. For EACH of your direct reports, check their skills: GET /api/agents/{agentId}/skills -2. If an agent has ZERO skills assigned, this is urgent — assign skills NOW: - a. GET /api/companies/{{companyId}}/skills to list available skills - b. For each skill matching the agent's role: POST /api/agents/{agentId}/skills with {"skillId": ""} - c. Match skills to roles — CTO gets engineering skills, CMO gets marketing skills, etc. -3. This MUST happen before delegating any work — agents without skills produce poor output - -## Priority 3: Review Completed Work (SOP-004) +## Priority 2: Review Completed Work (SOP-004) When tasks from your reports have status "in_review": - Read ALL comments on the task to understand what was done - Review any documents the agent created: GET /api/companies/{{companyId}}/documents @@ -42,7 +33,7 @@ When tasks from your reports have status "in_review": - This is YOUR HIGHEST PRIORITY after standup — reviews unblock your entire team - NEVER let review requests sit — they cascade-block everyone downstream -## Priority 4: Goal Decomposition & Delegation (SOP-003) +## Priority 3: Goal Decomposition & Delegation (SOP-003) When you have active goals without projects or tasks: 1. Create a project for each major workstream: POST /api/companies/{{companyId}}/projects with goalId, leadAgentId, description @@ -56,21 +47,19 @@ When you have active goals without projects or tasks: - COO/Operations → process, coordination, company cadence - CFO/Finance → budget, spend tracking, financial health - Other roles → match by capabilities description - - Before delegating, check agent skills: GET /api/agents/{agentId}/skills - - If the agent lacks a needed skill, assign it first: POST /api/agents/{agentId}/skills with {"skillId": ""} - - Skills give agents domain expertise (coding standards, frameworks, processes) — always ensure the right skills are assigned before sending work + - Default domain skills are assigned automatically when a hire is approved; only call GET /api/agents/{agentId}/skills or POST /api/agents/{agentId}/skills when you have a reason to override that default for a specific agent 5. Leave a delegation summary comment on each parent task 6. Wake the assigned agents so they start immediately: POST /api/agents/{agentId}/wake with reason explaining the new assignment -## Priority 5: Unblock & Coordinate +## Priority 4: Unblock & Coordinate - Review blocked tasks: read the blocker comments and take action - For credential needs: create secret_access approvals via POST /api/companies/{{companyId}}/approvals - For cross-department dependencies: send direct messages to the relevant department lead - For Board-level decisions: create an approval with clear context and options - Review failed runs from your reports: check what went wrong and decide whether to retry or reassign -## Priority 6: Grow the Team +## Priority 5: Grow the Team When workload exceeds capacity or new capabilities are needed: - Assess: count open tasks vs active agents per department - If a department is overloaded: propose hiring via POST /api/companies/{{companyId}}/agents @@ -78,7 +67,7 @@ When workload exceeds capacity or new capabilities are needed: - Each hire needs: a distinct agent name, clear role/title, specific capabilities, budget justification, department assignment, and the correct manager (reportsTo) - After hire approval, create onboarding tasks for the new agent -## Priority 7: Strategic Communication & Reporting +## Priority 6: Strategic Communication & Reporting - Schedule all-hands meetings when major milestones are reached: POST /api/companies/{{companyId}}/meetings with type "all_hands" - Create status_report documents summarizing company progress: diff --git a/src/main/skills/auto-assign-on-hire.ts b/src/main/skills/auto-assign-on-hire.ts new file mode 100644 index 00000000..d4a2b13f --- /dev/null +++ b/src/main/skills/auto-assign-on-hire.ts @@ -0,0 +1,89 @@ +import type { AppDatabase } from "../database"; +import type { DepartmentId } from "@shared/types"; + +/** + * Server-side default skill assignment at hire approval time. + * + * Before this module existed, the CEO's operating prompt was explicitly + * told to inspect every direct report every wake, fetch the skill + * catalogue, and POST /agents/{id}/skills for any unassigned role- + * relevant entry. That is business logic routed through an LLM — wasted + * tokens and an unreliable guarantee. The hire approval is the + * deterministic moment to give a fresh agent a sensible default kit; + * the CEO is still free to add or remove skills later. + * + * The mapping is department → built-in skill categories. Each seeded + * company skill carries its category in `metadata.builtinCategory` + * (see `skills/runtime.ts#seedBuiltinSkills`); we match against that + * tag so a company that imported extra skills under the same category + * inherits them too, and deletes remove the assignment pool cleanly. + */ + +const CATEGORIES_BY_DEPARTMENT: Record = { + executive: ["basic-for-all", "document-process"], + engineering: ["software-engineer", "basic-for-all"], + product: ["frontend-design", "basic-for-all", "document-process"], + design: ["frontend-design", "basic-for-all"], + marketing: ["marketing", "basic-for-all", "document-process"], + sales: ["marketing", "basic-for-all", "document-process"], + hr: ["basic-for-all", "document-process"], + finance: ["basic-for-all", "document-process"], + legal: ["basic-for-all", "document-process"], + operations: ["software-engineer", "basic-for-all", "document-process"], + customer_support: ["basic-for-all", "document-process"], + research: ["basic-for-all", "document-process"], +}; + +function readBuiltinCategory(metadata: unknown): string | null { + if (!metadata) return null; + if (typeof metadata === "string") { + try { + const parsed = JSON.parse(metadata) as Record; + const value = parsed?.builtinCategory; + return typeof value === "string" ? value : null; + } catch { + return null; + } + } + if (typeof metadata === "object") { + const value = (metadata as Record).builtinCategory; + return typeof value === "string" ? value : null; + } + return null; +} + +export interface AutoAssignSkillsInput { + agentId: string; + companyId: string; + department: DepartmentId | string | null | undefined; +} + +export interface AutoAssignSkillsResult { + assignedSkillIds: string[]; +} + +export function autoAssignSkillsForHire( + db: AppDatabase, + input: AutoAssignSkillsInput, +): AutoAssignSkillsResult { + const department = (input.department ?? "") as DepartmentId; + const categories = CATEGORIES_BY_DEPARTMENT[department]; + if (!categories || categories.length === 0) { + return { assignedSkillIds: [] }; + } + + const alreadyAssigned = new Set(db.listAgentSkillAssignments(input.agentId)); + const wanted = new Set(categories); + const catalogue = db.listCompanySkills(input.companyId); + + const assigned: string[] = []; + for (const skill of catalogue) { + if (alreadyAssigned.has(skill.id)) continue; + const category = readBuiltinCategory(skill.metadata); + if (!category || !wanted.has(category)) continue; + db.assignSkillToAgent(input.agentId, skill.id); + assigned.push(skill.id); + } + + return { assignedSkillIds: assigned }; +}