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.
This commit is contained in:
1 parent
6bf5b8afeb
commit
2f95fc2f2d
3 files changed
+162
-4
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<string, unknown>) {
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user