From 2f95fc2f2de75316abff75fd9a387c33ed1d26f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 6 Jul 2026 23:36:48 -0400 Subject: [PATCH] Make AI Co-Founder mode actually review what the company ships review_deliverables was written at onboarding and shown in Settings but nothing ever consumed it - Co-Founder mode silently behaved like AI CEO mode for output. Now every write_deliverable and create_document in a review_deliverables org files a pending deliverable_review approval tied to the file or document, and the tool tells the agent their work is queued for board review; AI CEO mode keeps shipping without review, which is that mode's contract. Kickoff and worker briefings also catch up with the conversation model: managers brief each report in a direct message with one company-wide post in #general, and workers raise blockers with their manager over DM instead of flooding department channels. Three execution-mode contract tests pin deliverable review, no-review autonomy, and the hire-approval split. --- .../src/main/services/agent-tools.ts | 46 ++++++- .../src/main/services/task-prompts.ts | 4 +- .../tests/unit/execution-modes.test.ts | 116 ++++++++++++++++++ 3 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 AgentCompany-New/tests/unit/execution-modes.test.ts diff --git a/AgentCompany-New/src/main/services/agent-tools.ts b/AgentCompany-New/src/main/services/agent-tools.ts index cc124c77..fb2653f7 100644 --- a/AgentCompany-New/src/main/services/agent-tools.ts +++ b/AgentCompany-New/src/main/services/agent-tools.ts @@ -3,6 +3,7 @@ import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS, type RiskLevel } from "../../sh import { ORGANIZATION_DEPARTMENTS } from "./org-governance-service"; import type { OrganizationService } from "./organization-service"; import type { ChatService } from "./chat-service"; +import { ApprovalService } from "./approval-service"; import { writeDeliverable } from "./deliverable-writer"; import { DocumentService } from "./document-service"; import { recordWorkspaceFile } from "./files/file-object-store"; @@ -281,7 +282,42 @@ function writeDeliverableTool(deps: AgentToolDeps, ctx: AgentToolContext, args: workspaceRoot: ctx.workspace.localPath, relativePath: written, }); - return `Wrote deliverable ${written} (file_id=${fileId}, chars=${content.length}).`; + const review = requestDeliverableReview(deps, ctx, { + title: `Review deliverable: ${written}`, + details: `Deliverable ${written} (${content.length} chars) produced by an agent run.`, + sourceEntityType: "file", + sourceEntityId: fileId, + }); + return `Wrote deliverable ${written} (file_id=${fileId}, chars=${content.length}).${review ? " It is queued for board review in Approvals." : ""}`; +} + +/** + * In AI Co-Founder mode the operator reviews what the company produces, so + * every deliverable and document lands in the Approvals queue. AI CEO mode + * ships without review — that autonomy is the mode's contract. + */ +function requestDeliverableReview( + deps: AgentToolDeps, + ctx: AgentToolContext, + input: { title: string; details: string; sourceEntityType: "file" | "document"; sourceEntityId: string }, +): boolean { + const org = deps.db.prepare("select review_deliverables from organizations where id = ?") + .get(ctx.orgId) as { review_deliverables: number } | undefined; + if (!org) throw new Error("Organization was not found."); + if (org.review_deliverables !== 1) return false; + new ApprovalService(deps.db).create( + { + orgId: ctx.orgId, + kind: "deliverable_review", + title: input.title, + details: input.details, + risk: RISK_LEVEL.LOW, + requestedByType: "agent", + requestedById: ctx.agent.id, + }, + { sourceEntityType: input.sourceEntityType, sourceEntityId: input.sourceEntityId }, + ); + return true; } function sendMessageTool(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord): string { @@ -384,7 +420,13 @@ function createDocumentTool(deps: AgentToolDeps, ctx: AgentToolContext, args: Js goalId: ctx.taskGoalId, taskId: null, }); - return `Created document "${title}" (doc_id=${docId}, type=${docType}, chars=${content.length}).`; + const review = requestDeliverableReview(deps, ctx, { + title: `Review ${docType}: ${title}`, + details: `Document "${title}" (${docType}, ${content.length} chars) authored by an agent run.`, + sourceEntityType: "document", + sourceEntityId: docId, + }); + return `Created document "${title}" (doc_id=${docId}, type=${docType}, chars=${content.length}).${review ? " It is queued for board review in Approvals." : ""}`; } interface RunnableAssignmentAgent { diff --git a/AgentCompany-New/src/main/services/task-prompts.ts b/AgentCompany-New/src/main/services/task-prompts.ts index 4a1f75bd..bd9ca00a 100644 --- a/AgentCompany-New/src/main/services/task-prompts.ts +++ b/AgentCompany-New/src/main/services/task-prompts.ts @@ -185,14 +185,14 @@ export function buildTaskPrompt(input: { if (input.isManager && !input.hasReports) { 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. For EACH report, send_message to that report's department channel (engineering, product, or general) using an @mention and the work you assigned them — start a new thread per report and note the thread_id it returns. Post the company-wide kickoff to #general. Then summarize.", + "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 summarize.", ); } else { const manager = input.managerName ?? "your manager"; const managerReference = input.managerName ? `${manager} (${agentMention(input.managerName)})` : manager; lines.push( "", - `First, read_messages to check if anyone has said something you should respond to — reply to them like a colleague, not a report. Then do the work: write real output with write_deliverable, discuss decisions and blockers with ${managerReference} and the team in the relevant channel, and when the work is genuinely done move the task to done. Hand files over with /handoff and attachment_file_ids. Keep conversation going — ask questions, share your thinking, push back if something seems wrong.`, + `First, read_messages to check if anyone has said something you should respond to — reply to them like a colleague, not a report. Then do the work: write real output with write_deliverable, raise decisions and blockers with ${managerReference} in a direct message (send_message with to=), keep team-wide topics in channels, and when the work is genuinely done move the task to done. Hand files over with /handoff and attachment_file_ids. Keep conversation going — ask questions, share your thinking, push back if something seems wrong.`, ); } return lines.join("\n"); diff --git a/AgentCompany-New/tests/unit/execution-modes.test.ts b/AgentCompany-New/tests/unit/execution-modes.test.ts new file mode 100644 index 00000000..af6503d8 --- /dev/null +++ b/AgentCompany-New/tests/unit/execution-modes.test.ts @@ -0,0 +1,116 @@ +import { randomUUID } from "node:crypto"; +import { mkdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, test } from "vitest"; +import { DatabaseConnection } from "../../src/main/database/connection"; +import { OrganizationService } from "../../src/main/services/organization-service"; +import { ChatService } from "../../src/main/services/chat-service"; +import { dispatchAgentTool, type AgentToolContext } from "../../src/main/services/agent-tools"; + +function call(name: string, args: Record) { + return { id: `call_${randomUUID()}`, name, arguments: args }; +} + +interface Harness { + connection: DatabaseConnection; + orgId: string; + ceoId: string; + ctx: AgentToolContext; + deps: { db: DatabaseConnection["db"]; organization: OrganizationService; chat: ChatService }; +} + +function harness(mode: "ai_ceo" | "ai_cofounder"): Harness { + const connection = new DatabaseConnection(join(tmpdir(), `ac-mode-${randomUUID()}.db`)); + const organization = new OrganizationService(connection.db); + const workspaceRoot = join(tmpdir(), `ac-mode-ws-${randomUUID()}`); + mkdirSync(workspaceRoot, { recursive: true }); + let orgId = ""; + let ceoId = ""; + let workspaceId = ""; + connection.transaction(() => { + orgId = organization.createOrganization({ + organizationName: "OrbitOps", + employeeName: "Yang", + mission: "m", + goal: "g", + executionMode: mode, + autoApproveHires: mode === "ai_ceo", + reviewDeliverables: mode === "ai_cofounder", + }); + const providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(orgId) as { id: string }).id; + ceoId = organization.createAgent({ + orgId, name: "CEO", role: "CEO", department: "Executive", + modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 25000, managerAgentId: null, roleLevel: "c_suite", + }); + workspaceId = organization.createWorkspace({ orgId, name: "HQ", localPath: workspaceRoot, projectId: null }); + }); + return { + connection, + orgId, + ceoId, + ctx: { + orgId, + agent: { id: ceoId, modelProviderId: null, modelId: null }, + taskProjectId: null, + taskGoalId: null, + workspace: { id: workspaceId, localPath: workspaceRoot }, + }, + deps: { db: connection.db, organization, chat: new ChatService(connection.db) }, + }; +} + +function pendingApprovals(h: Harness, kind: string): Array<{ state: string; source_entity_type: string | null }> { + return h.connection.db.prepare( + "select state, source_entity_type from approvals where org_id = ? and kind = ?", + ).all(h.orgId, kind) as Array<{ state: string; source_entity_type: string | null }>; +} + +describe("execution modes", () => { + test("AI Co-Founder mode queues every deliverable and document for board review", () => { + const h = harness("ai_cofounder"); + const wrote = dispatchAgentTool(h.deps, h.ctx, call("write_deliverable", { path: "plan/business-plan.md", content: "# Plan" })); + expect(wrote).toContain("queued for board review"); + const doc = dispatchAgentTool(h.deps, h.ctx, call("create_document", { title: "Launch memo", doc_type: "memo", content: "Ship it." })); + expect(doc).toContain("queued for board review"); + + const reviews = pendingApprovals(h, "deliverable_review"); + expect(reviews).toHaveLength(2); + expect(reviews.every((review) => review.state === "pending")).toBe(true); + expect(reviews.map((review) => review.source_entity_type).sort()).toEqual(["document", "file"]); + }); + + test("AI CEO mode ships deliverables without review", () => { + const h = harness("ai_ceo"); + const wrote = dispatchAgentTool(h.deps, h.ctx, call("write_deliverable", { path: "plan/business-plan.md", content: "# Plan" })); + expect(wrote).not.toContain("board review"); + dispatchAgentTool(h.deps, h.ctx, call("create_document", { title: "Launch memo", doc_type: "memo", content: "Ship it." })); + expect(pendingApprovals(h, "deliverable_review")).toHaveLength(0); + }); + + test("AI Co-Founder mode holds above-threshold hires; AI CEO mode clears them", () => { + // Hiring policies require review above $5,000/month; the mode decides + // whether that review waits for the operator or clears automatically. + const hireAva = (h: Harness) => { + const providerId = (h.connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(h.orgId) as { id: string }).id; + h.connection.transaction(() => h.deps.organization.createAgent({ + orgId: h.orgId, name: "Ava", role: "Designer", department: "Product", + modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 600000, managerAgentId: h.ceoId, + })); + }; + + const cofounder = harness("ai_cofounder"); + hireAva(cofounder); + const held = cofounder.connection.db.prepare( + "select state from approvals where org_id = ? and kind = 'agent_hire' and title like '%Ava%'", + ).get(cofounder.orgId) as { state: string }; + expect(held.state).toBe("pending"); + + const ceoMode = harness("ai_ceo"); + hireAva(ceoMode); + const cleared = ceoMode.connection.db.prepare( + "select state from approvals where org_id = ? and kind = 'agent_hire' and title like '%Ava%'", + ).get(ceoMode.orgId) as { state: string }; + expect(cleared.state).toBe("approved"); + }); +});