Assign default skills when a hire is approved

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.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-19 19:18:01 -04:00
1 parent 5dd74a477d
commit a3006f0b38
3 files changed
+119 -17

No files matched your search

@@ -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",
@@ -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": "<id>"}
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": "<id>"}
- 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:
+89
View File
@@ -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<DepartmentId, readonly string[]> = {
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<string, unknown>;
const value = parsed?.builtinCategory;
return typeof value === "string" ? value : null;
} catch {
return null;
}
}
if (typeof metadata === "object") {
const value = (metadata as Record<string, unknown>).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 };
}