From 6a4843a9837a9fab9ca633cb1ff3b2c66fb4435d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 8 Oct 2026 07:37:02 -0400 Subject: [PATCH] Q41: one first-hire rule, and leaders get guidance that fits their rank Co-Authored-By: Claude Opus 5.5 --- src/main/services/agent-tools.ts | 3 +- src/main/services/goal-kickoff.ts | 25 +++++++-- src/main/services/onboarding-service.ts | 2 +- src/main/services/org-building.ts | 14 +++++ src/main/services/task-prompts.ts | 16 ++++-- tests/unit/agent-instructions.test.ts | 70 +++++++++++++++++++++++++ 6 files changed, 120 insertions(+), 10 deletions(-) create mode 100644 src/main/services/org-building.ts diff --git a/src/main/services/agent-tools.ts b/src/main/services/agent-tools.ts index f585d64b..7803944b 100644 --- a/src/main/services/agent-tools.ts +++ b/src/main/services/agent-tools.ts @@ -16,6 +16,7 @@ import { DOCUMENT_TOOLS, DOCUMENT_TYPES, listDocumentsTool, readDocumentTool, up import { sendMessageTool } from "./agent-send-message"; import { withSavepoint } from "../database/connection"; import { optionalEnum, optionalString, optionalStringArray, requireString } from "./agent-tool-args"; +import { CEO_FIRST_HIRES } from "./org-building"; import type { JsonRecord } from "./provider-http"; import type { ToolCall, ToolDefinition } from "./provider-tool-turn"; @@ -50,7 +51,7 @@ export const AGENT_TOOLS: ToolDefinition[] = [ { name: "hire_agent", description: - "Add one direct report to your org. Hires report to YOU, so build a real company: the CEO hires chief officers (seniority \"director\", title like 'CTO' or 'CFO') and delegates department building to them; officers hire their own managers (seniority \"lead\") and staff. Staff every hire with the library skills their role needs (check list_skills; several are fine) — those skills become mandatory practice in the hire's runs.", + `Add one direct report to your org. Hires report to YOU, so build a real company: as CEO, ${CEO_FIRST_HIRES}, and delegate department building to them; officers hire their own managers (seniority "lead") and staff. Staff every hire with the library skills their role needs (check list_skills; several are fine) — those skills become mandatory practice in the hire's runs.`, parameters: { type: "object", additionalProperties: false, diff --git a/src/main/services/goal-kickoff.ts b/src/main/services/goal-kickoff.ts index 56ba783e..d3420cef 100644 --- a/src/main/services/goal-kickoff.ts +++ b/src/main/services/goal-kickoff.ts @@ -1,4 +1,5 @@ import type { DatabaseSync } from "node:sqlite"; +import { firstHiresFor, leadsPeople } from "./org-building"; export interface GoalKickoffPlan { assigneeAgentId: string; @@ -24,24 +25,42 @@ export function resolveGoalKickoff(db: DatabaseSync, input: { if (!assigneeAgentId) return null; const organization = db.prepare("select name from organizations where id = ?") .get(input.orgId) as { name: string }; + const owner = db.prepare(` + select role_level, exists(select 1 from agents r where r.manager_agent_id = a.id and r.lifecycle = 'active') as has_reports + from agents a where a.id = ? + `).get(assigneeAgentId) as { role_level: string; has_reports: number }; return { assigneeAgentId, workspaceId: primaryWorkspaceId(db, input.orgId), title: goalKickoffTitle(input.goalTitle), - description: goalKickoffDescription(organization.name, input.goalTitle, input.goalDescription), + description: goalKickoffDescription(organization.name, input.goalTitle, input.goalDescription, { roleLevel: owner.role_level, hasReports: owner.has_reports === 1 }), }; } +function kickoffDirection(owner: { roleLevel: string; hasReports: boolean }): string { + const groundwork = "Read the bound workspace, create the first operating plan, and route decisions through approvals and messages."; + if (owner.roleLevel === "c_suite") { + return `Treat this goal as the company objective and build the organization that can deliver it: ${firstHiresFor(owner.roleLevel)}. Give each officer their department objective as a task, and let them build and run their own teams. ${groundwork}`; + } + if (leadsPeople(owner.roleLevel, owner.hasReports)) { + const staffing = owner.hasReports ? "Give each report their part of it as a task" : `Build the team that can deliver it: ${firstHiresFor(owner.roleLevel)}, then give each report their part as a task`; + return `Treat this goal as your objective. ${staffing}. ${groundwork}`; + } + return `Treat this goal as your objective: plan the work, do it, and tell your manager early if it needs more people. ${groundwork}`; +} + export function goalKickoffTitle(goalTitle: string): string { return `Operationalize goal: ${goalTitle.trim()}`; } -export function goalKickoffDescription(companyName: string, goalTitle: string, goalDescription: string): string { +export function goalKickoffDescription( + companyName: string, goalTitle: string, goalDescription: string, owner: { roleLevel: string; hasReports: boolean }, +): string { return [ `Company: ${companyName.trim()}`, `Goal: ${goalTitle.trim()}`, goalDescription.trim(), - "Treat this goal as the company objective and build the organization that can deliver it: hire chief officers (hire_agent, seniority \"director\", titles like CTO/CFO/CMO/COO) for the functions the goal needs, give each officer their department objective as a task, and let them build and run their own teams. Read the bound workspace, create the first operating plan, and route decisions through approvals and messages.", + kickoffDirection(owner), ].filter((line) => line.length > 0).join("\n\n"); } diff --git a/src/main/services/onboarding-service.ts b/src/main/services/onboarding-service.ts index d8d11236..14a0c75e 100644 --- a/src/main/services/onboarding-service.ts +++ b/src/main/services/onboarding-service.ts @@ -80,7 +80,7 @@ export class OnboardingService { const taskId = this.organization.createTask({ orgId, title: goalKickoffTitle(input.goalTitle), - description: goalKickoffDescription(input.companyName, input.goalTitle, input.goalDescription), + description: goalKickoffDescription(input.companyName, input.goalTitle, input.goalDescription, { roleLevel: "c_suite", hasReports: false }), status: TASK_STATUS.BACKLOG, assigneeAgentId: ceoAgentId, risk: RISK_LEVEL.MEDIUM, diff --git a/src/main/services/org-building.ts b/src/main/services/org-building.ts new file mode 100644 index 00000000..4344bba9 --- /dev/null +++ b/src/main/services/org-building.ts @@ -0,0 +1,14 @@ +/** The CEO's first-hire rule, shared by its instructions, the goal kickoff and the hire_agent tool. */ +export const CEO_FIRST_HIRES = "hire 2-4 chief officers (hire_agent, seniority \"director\", titles like CTO, CFO, CMO, COO) for the functions the goal needs, not a flat pool of individual contributors"; + +/** Who a leader at this rank hires first. */ +export function firstHiresFor(roleLevel: string): string { + if (roleLevel === "c_suite") return CEO_FIRST_HIRES; + if (roleLevel === "director") return "hire the managers (hire_agent, seniority \"lead\") and staff your department needs"; + return "hire the staff your team needs (hire_agent)"; +} + +/** Directors and leads lead even before their first hire; anyone with reports leads too. */ +export function leadsPeople(roleLevel: string, hasReports: boolean): boolean { + return hasReports || roleLevel === "c_suite" || roleLevel === "director" || roleLevel === "lead"; +} diff --git a/src/main/services/task-prompts.ts b/src/main/services/task-prompts.ts index 08f8fa3d..76d1cd18 100644 --- a/src/main/services/task-prompts.ts +++ b/src/main/services/task-prompts.ts @@ -1,4 +1,5 @@ import { WRITING_MODE, type WritingMode } from "../../shared/domain"; +import { CEO_FIRST_HIRES, firstHiresFor, leadsPeople } from "./org-building"; import { canonicalMentionHandle } from "../../shared/mention-handles"; const RECORD_LEARNINGS_INSTRUCTION = @@ -152,12 +153,14 @@ function roleVoice(input: AgentInstructionContext): string { if (input.isCeo) { return [ "You run this company. Think in outcomes, make the call when it is yours to make, and put the right person on each piece instead of doing everything yourself — then tell the team the why, not just the what.", - "Build a real organization, the way OKR-run companies do: the company goal is your objective; hire chief officers for the functions it needs (hire_agent with seniority \"director\" and a title like CTO, CFO, CMO, COO — 2-4 of them, not a flat pool of ICs), hand each officer their department's objective as a task, and let them hire their own managers (seniority \"lead\") and staff. You manage the officers; they manage their departments.", + `Build a real organization, the way OKR-run companies do: the company goal is your objective; ${CEO_FIRST_HIRES}. Hand each officer their department's objective as a task, and let them hire their own managers (seniority "lead") and staff. You manage the officers; they manage their departments.`, "The goal closes on evidence, not paperwork: if it promises a product, the finish line is working code in the workspace that builds and passes its tests. Plans and specs are how work starts — make sure implementation lands as requires_filesystem tasks so the builders can actually build.", ].join(" "); } - if (input.hasReports) { - return "You lead a department. Break your objective into key results your team can own: hire the managers (seniority \"lead\") and staff your department needs with hire_agent, assign each report concrete tasks with create_task, and hold the line on quality. Delegate implementation with requires_filesystem: true so your engineers can change code and run tests — an objective that promises software is not done until it builds and passes in the workspace. Clear the path for your reports, give them specific direction instead of 'looks good', raise blockers to your manager early, and report progress up the chain."; + if (leadsPeople(input.agent.role_level, input.hasReports)) { + const unit = input.agent.role_level === "director" ? "department" : "team"; + const emptyNote = input.hasReports ? "" : `Your ${unit} has no one in it yet: ${firstHiresFor(input.agent.role_level)} before you take on the work yourself. `; + return `${emptyNote}You lead a ${unit}. Break your objective into key results your team can own: hire the people your ${unit} needs with hire_agent, assign each report concrete tasks with create_task, and hold the line on quality. Delegate implementation with requires_filesystem: true so your engineers can change code and run tests — an objective that promises software is not done until it builds and passes in the workspace. Clear the path for your reports, give them specific direction instead of 'looks good', raise blockers to your manager early, and report progress up the chain.`; } return "You own your craft. Ask the question early instead of guessing, do the work properly, and when you are stuck for more than a few minutes say so in the thread instead of spinning in place."; } @@ -269,10 +272,13 @@ export function buildTaskPrompt(input: { `This task already spent ${input.priorCompletedRuns} completed run(s) without being closed. Check read_messages and the existing deliverables first, continue from where the last run stopped instead of redoing finished work, and if the work is actually complete just close the task.`, ); } - if (input.isManager && !input.hasReports) { + if (leadsPeople(input.agent.role_level, input.hasReports) && !input.hasReports) { + const announce = input.isManager + ? `Post one company-wide kickoff to the #general channel. Then close this kickoff with update_task_status(task_id="${input.task.id}", status="done") and summarize.` + : `Tell your manager the plan in a direct message. Then close this task with update_task_status(task_id="${input.task.id}", status="done") if hiring and delegating was the work, or move it to "in_progress" and say what remains.`; lines.push( "", - `Your team is currently empty. Hire 3-5 direct reports for the roles this goal needs (hire_agent), create and assign their first tasks (create_task), and write the initial operating plan into the workspace (write_deliverable). Then use send_message with /handoff @Name and attachment_file_ids to pass the operating plan to the relevant report. Brief EACH report in a direct message (send_message with to="Their Name") about the work you assigned them — note the thread_id each returns so you can follow up in the same conversation. Post one company-wide kickoff to the #general channel. Then close this kickoff with update_task_status(task_id="${input.task.id}", status="done") and summarize.`, + `Your team is currently empty. First ${firstHiresFor(input.agent.role_level)}, create and assign their first tasks (create_task), and write the initial operating plan into the workspace (write_deliverable). Then use send_message with /handoff @Name and attachment_file_ids to pass the operating plan to the relevant report. Brief EACH report in a direct message (send_message with to="Their Name") about the work you assigned them — note the thread_id each returns so you can follow up in the same conversation. ${announce}`, ); } else { const manager = input.managerName ?? "your manager"; diff --git a/tests/unit/agent-instructions.test.ts b/tests/unit/agent-instructions.test.ts index 664af15e..d3749a26 100644 --- a/tests/unit/agent-instructions.test.ts +++ b/tests/unit/agent-instructions.test.ts @@ -11,6 +11,9 @@ import { ConnectorGrantsService } from "../../src/main/services/composio/connect import { TaskExecutionService } from "../../src/main/services/task-execution-service"; import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS } from "../../src/shared/domain"; import { buildAgentInstructions, type TaskExecutionAgentRow } from "../../src/main/services/task-prompts"; +import { AGENT_TOOLS } from "../../src/main/services/agent-tools"; +import { goalKickoffDescription } from "../../src/main/services/goal-kickoff"; +import { CEO_FIRST_HIRES } from "../../src/main/services/org-building"; function agentRow(overrides: Partial): TaskExecutionAgentRow { return { @@ -28,6 +31,24 @@ function agentRow(overrides: Partial): TaskExecutionAgent } describe("agent instructions", () => { + test("first-hire guidance is one rule everywhere, and new leaders get a leader's voice", () => { + const ceo = buildAgentInstructions({ agent: agentRow({ name: "CEO", role: "CEO", role_level: "c_suite", manager_agent_id: null }), companyName: "O", isCeo: true, hasReports: false, connectorNames: [], language: "en" }); + const hireTool = AGENT_TOOLS.find((tool) => tool.name === "hire_agent"); + for (const text of [ceo, goalKickoffDescription("O", "Ship", "Ship it.", { roleLevel: "c_suite", hasReports: false }), hireTool?.description ?? ""]) { + expect(text).toContain(CEO_FIRST_HIRES); + } + const voice = (role_level: string, hasReports: boolean) => + buildAgentInstructions({ agent: agentRow({ role_level }), companyName: "O", isCeo: false, hasReports, connectorNames: [], language: "en" }); + expect(voice("director", false)).toContain('Your department has no one in it yet: hire the managers (hire_agent, seniority "lead") and staff your department needs'); + expect(voice("lead", false)).toContain("Your team has no one in it yet: hire the staff your team needs (hire_agent)"); + expect(voice("lead", false)).toContain("You lead a team."); + expect(voice("lead", false)).not.toContain('managers (seniority "lead")'); + expect(voice("director", true)).toContain("You lead a department."); + expect(voice("director", true)).not.toContain("no one in it yet"); + expect(voice("staff", false)).toContain("You own your craft."); + expect(voice("staff", false)).not.toContain("You lead"); + }); + test("the CEO is briefed on its grant authority over connectors and skills", () => { const text = buildAgentInstructions({ agent: agentRow({ name: "CEO", role: "CEO", role_level: "c_suite", manager_agent_id: null }), @@ -157,4 +178,53 @@ describe("agent instruction wiring", () => { await execution.runTask({ orgId, taskId }); expect(capturedInstructions).toContain("公司的工作语言是简体中文"); }); + + test("a new department head's task prompt tells them to staff their department, not to work as an IC", async () => { + const connection = new DatabaseConnection(join(tmpdir(), `ac-instr-${randomUUID()}.db`)); + const organization = new OrganizationService(connection.db); + let capturedPrompt = ""; + const fakeToolTurn = async (input: ProviderToolTurnInput): Promise => { + const first = input.messages[0]; + if (first?.role !== "user") throw new Error("The run must open with the task prompt."); + capturedPrompt = first.text; + return { text: "done", toolCalls: [], rawResponseJson: JSON.stringify({ id: "resp-1" }) }; + }; + const providers = new ProviderService(connection.db, providerAdapters, testVault, undefined, fakeToolTurn); + const execution = new TaskExecutionService(connection.db, providers); + let orgId = ""; + let taskId = ""; + connection.transaction(() => { + orgId = organization.createOrganization({ organizationName: "OrbitOps", employeeName: "Yang", mission: "m", goal: "g" }); + const providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(orgId) as { id: string }).id; + providers.saveCredential({ orgId, providerId, secret: "sk-test", model: "gpt-5.4" }); + const ceoId = organization.createAgent({ + orgId, name: "CEO", role: "CEO", department: "Executive", + modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 1000, managerAgentId: null, roleLevel: "c_suite", + }); + const ctoId = organization.createAgent({ + orgId, name: "Cora", role: "CTO", department: "Engineering", + modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 1000, managerAgentId: ceoId, roleLevel: "director", + }); + taskId = organization.createTask({ + orgId, title: "Build engineering", description: "Own the platform.", status: TASK_STATUS.IN_PROGRESS, + assigneeAgentId: ctoId, risk: RISK_LEVEL.LOW, projectId: null, goalId: null, + workspaceId: null, priority: TASK_PRIORITY.NORMAL, requiresFilesystem: false, + }); + }); + + await execution.runTask({ orgId, taskId }); + + // A goal owned by the CTO kicks off with department guidance, never the CEO's officer rule. + const ctoId = (connection.db.prepare("select id from agents where org_id = ? and name = 'Cora'").get(orgId) as { id: string }).id; + connection.transaction(() => organization.createGoal({ orgId, title: "Platform", description: "Stand up the platform.", ownerAgentId: ctoId })); + const kickoff = connection.db.prepare("select description from tasks where org_id = ? and title = 'Operationalize goal: Platform'").get(orgId) as { description: string }; + expect(kickoff.description).toContain('Treat this goal as your objective. Build the team that can deliver it: hire the managers (hire_agent, seniority "lead") and staff your department needs'); + expect(kickoff.description).not.toContain("chief officers"); + + expect(capturedPrompt).toContain('Your team is currently empty. First hire the managers (hire_agent, seniority "lead") and staff your department needs'); + expect(capturedPrompt).toContain("Tell your manager the plan in a direct message."); + expect(capturedPrompt).not.toContain("#general"); + expect(capturedPrompt).not.toContain("Then do the work: write real output"); + connection.close(); + }); });