diff --git a/src/main/services/agent-tools.ts b/src/main/services/agent-tools.ts
index 43620cd8..a082c082 100644
--- a/src/main/services/agent-tools.ts
+++ b/src/main/services/agent-tools.ts
@@ -1,3 +1,4 @@
+import { COMPANY_PRESETS } from "../../shared/company-presets";
import type { DatabaseSync } from "node:sqlite";
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS, WRITING_MODE, type RiskLevel, type WorkLanguage, type WritingMode } from "../../shared/domain";
import { ORGANIZATION_DEPARTMENTS } from "./org-governance-service";
@@ -20,7 +21,6 @@ import { CEO_FIRST_HIRES } from "./org-building";
import type { JsonRecord } from "./provider-http";
import type { ToolCall, ToolDefinition } from "./provider-tool-turn";
-const HIRE_BUDGET_CENTS = 25000;
const HEARTBEAT_INTERVAL_SEC = 120;
export interface AgentToolContext {
@@ -327,7 +327,7 @@ function hireAgent(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord)
department,
modelProviderId: ctx.agent.modelProviderId,
modelId: model,
- budgetCents: HIRE_BUDGET_CENTS,
+ budgetCents: COMPANY_PRESETS.agentHireBudgetCents,
managerAgentId: ctx.agent.id,
roleLevel: seniority,
workspaceId: ctx.workspace?.id ?? null,
diff --git a/src/main/services/onboarding-service.ts b/src/main/services/onboarding-service.ts
index 14a0c75e..0cb9248c 100644
--- a/src/main/services/onboarding-service.ts
+++ b/src/main/services/onboarding-service.ts
@@ -1,3 +1,4 @@
+import { COMPANY_PRESETS } from "../../shared/company-presets";
import type { DatabaseSync } from "node:sqlite";
import type {
BootstrapOnboardingInput,
@@ -60,7 +61,7 @@ export class OnboardingService {
department: "Executive",
modelProviderId: providerId,
modelId: input.providerModel.trim(),
- budgetCents: 25000,
+ budgetCents: COMPANY_PRESETS.ceoBudgetCents,
managerAgentId: null,
roleLevel: "c_suite",
modelStrategy: "explicit",
diff --git a/src/main/services/org-governance-service.ts b/src/main/services/org-governance-service.ts
index 6a85f9d9..e42ed935 100644
--- a/src/main/services/org-governance-service.ts
+++ b/src/main/services/org-governance-service.ts
@@ -1,3 +1,4 @@
+import { COMPANY_PRESETS } from "../../shared/company-presets";
import type { DatabaseSync } from "node:sqlite";
import type { CreateAgentInput } from "../../shared/commands";
import { RISK_LEVEL, ROUTE_TO_TYPE, type RiskLevel } from "../../shared/domain";
@@ -127,14 +128,14 @@ export class OrgGovernanceService {
private seedDepartmentPolicy(orgId: string, departmentName: string, now: string): void {
this.db.prepare(`
insert into org_departments(id, org_id, name, lead_agent_id, headcount_limit, monthly_budget_limit_cents, created_at, updated_at)
- values (?, ?, ?, null, 12, 2500000, ?, ?)
+ values (?, ?, ?, null, ?, ?, ?, ?)
on conflict(org_id, name) do nothing
- `).run(createId("dep"), orgId, departmentName, now, now);
+ `).run(createId("dep"), orgId, departmentName, COMPANY_PRESETS.departmentHeadcount, COMPANY_PRESETS.departmentBudgetCents, now, now);
this.db.prepare(`
insert into hiring_policies(id, org_id, department_name, max_headcount, monthly_budget_limit_cents, require_approval_above_cents, created_at, updated_at)
- values (?, ?, ?, 12, 2500000, 500000, ?, ?)
+ values (?, ?, ?, ?, ?, ?, ?, ?)
on conflict(org_id, department_name) do nothing
- `).run(createId("hpl"), orgId, departmentName, now, now);
+ `).run(createId("hpl"), orgId, departmentName, COMPANY_PRESETS.departmentHeadcount, COMPANY_PRESETS.departmentBudgetCents, COMPANY_PRESETS.hireReviewAboveCents, now, now);
}
private resolveRoutingRule(orgId: string, kind: string, risk: RiskLevel): ApprovalRoutingRuleRow {
diff --git a/src/main/services/organization-seed.ts b/src/main/services/organization-seed.ts
index d4035a7c..ae21359c 100644
--- a/src/main/services/organization-seed.ts
+++ b/src/main/services/organization-seed.ts
@@ -1,3 +1,4 @@
+import { COMPANY_PRESETS } from "../../shared/company-presets";
import type { DatabaseSync } from "node:sqlite";
import type { CreateOrganizationInput } from "../../shared/commands";
import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
@@ -68,8 +69,8 @@ export function seedOrganization(db: DatabaseSync, input: CreateOrganizationInpu
for (const provider of PROVIDER_CATALOG) {
db.prepare(`
- insert into providers(id, org_id, provider_key, label, auth_type, model, credential_id, enabled, created_at)
- values (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ insert into providers(id, org_id, provider_key, label, auth_type, model, credential_id, enabled, monthly_request_limit, created_at)
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
createId("prv"),
orgId,
@@ -79,6 +80,7 @@ export function seedOrganization(db: DatabaseSync, input: CreateOrganizationInpu
provider.defaultModel,
null,
0,
+ COMPANY_PRESETS.providerMonthlyRequests,
now,
);
}
diff --git a/src/main/services/policy-service.ts b/src/main/services/policy-service.ts
index be98d877..e454c58d 100644
--- a/src/main/services/policy-service.ts
+++ b/src/main/services/policy-service.ts
@@ -1,3 +1,4 @@
+import { COMPANY_PRESETS } from "../../shared/company-presets";
import type { DatabaseSync } from "node:sqlite";
import type { RiskLevel } from "../../shared/domain";
import { RISK_LEVEL } from "../../shared/domain";
@@ -21,7 +22,7 @@ export class PolicyService {
this.db.prepare(`
insert into policy_rules(id, org_id, subject_type, subject_id, action, effect, risk_ceiling, created_at)
values (?, ?, ?, ?, ?, ?, ?, ?)
- `).run(createId("pol"), orgId, "organization", orgId, "task_create", "require_approval", RISK_LEVEL.MEDIUM, now);
+ `).run(createId("pol"), orgId, "organization", orgId, "task_create", "require_approval", COMPANY_PRESETS.taskRiskCeiling, now);
}
evaluateTaskCreation(orgId: string, risk: RiskLevel): PolicyDecision {
diff --git a/src/renderer/src/app/AutonomySummary.tsx b/src/renderer/src/app/AutonomySummary.tsx
new file mode 100644
index 00000000..326e66f9
--- /dev/null
+++ b/src/renderer/src/app/AutonomySummary.tsx
@@ -0,0 +1,27 @@
+import { COMPANY_PRESETS } from "../../../shared/company-presets";
+import { t } from "../i18n/i18n";
+
+const dollars = (cents: number) => `$${(cents / 100).toLocaleString("en-US")}`;
+const RISK_NAMES = { low: () => t("Low"), medium: () => t("Medium"), high: () => t("High") };
+
+/** Everything a mode decides and everything both modes preset, read from the same values the company is created with. */
+export function AutonomySummary({ mode }: { mode: "ai_ceo" | "ai_cofounder" }) {
+ const gated = { level: RISK_NAMES[COMPANY_PRESETS.taskRiskCeiling](), amount: dollars(COMPANY_PRESETS.hireReviewAboveCents) };
+ return (
+
+
+ - {mode === "ai_ceo"
+ ? t("Approved without you: tasks above {level} risk, tasks that run commands in a workspace, and hires over {amount} a month.", gated)
+ : t("Waiting for you: tasks above {level} risk, tasks that run commands in a workspace, and hires over {amount} a month.", gated)}
+ - {mode === "ai_ceo"
+ ? t("Deliverables are written without review.")
+ : t("Deliverables wait for your review before they are written.")}
+ - {t("The CEO starts with a {ceo} monthly budget, and each agent it hires gets {hire}.", { ceo: dollars(COMPANY_PRESETS.ceoBudgetCents), hire: dollars(COMPANY_PRESETS.agentHireBudgetCents) })}
+ - {t("Each department holds up to {count} agents and {amount} a month in agent budgets.", { count: COMPANY_PRESETS.departmentHeadcount, amount: dollars(COMPANY_PRESETS.departmentBudgetCents) })}
+ - {t("Each provider allows {count} model calls a month.", { count: COMPANY_PRESETS.providerMonthlyRequests.toLocaleString("en-US") })}
+ - {t("An approved workspace task runs commands with your user's file access; they are not confined to the workspace folder.")}
+ - {t("Later you can change the mode, hire approval and deliverable review in Settings, General; provider limits in Settings, Providers; and agent budgets on Team.")}
+
+
+ );
+}
diff --git a/src/renderer/src/app/Onboarding.tsx b/src/renderer/src/app/Onboarding.tsx
index 7017907a..f3cfb173 100644
--- a/src/renderer/src/app/Onboarding.tsx
+++ b/src/renderer/src/app/Onboarding.tsx
@@ -1,3 +1,4 @@
+import { AutonomySummary } from "./AutonomySummary";
import { useMemo, useState, type FormEvent } from "react";
import {
PROVIDER_AUTH_TYPE,
@@ -307,16 +308,17 @@ function ModeStep({ draft, onChange, onBack, onNext }: StepProps) {
{t("How autonomous should the CEO be?")}
onChange({ executionMode: "ai_ceo" })}
/>
onChange({ executionMode: "ai_cofounder" })}
/>
+
);
diff --git a/src/renderer/src/i18n/zh.ts b/src/renderer/src/i18n/zh.ts
index e28ae567..80c82971 100644
--- a/src/renderer/src/i18n/zh.ts
+++ b/src/renderer/src/i18n/zh.ts
@@ -227,7 +227,6 @@ export const zh: Record = {
"AgentCompany sets up your operating loop in six steps. Bring an API key, pick a runtime for the founding CEO, and we'll wire up the first goal, project, and task.":
"AgentCompany 用六步搭建你的运营循环:带上 API 密钥,为创始 CEO 选择运行时,我们会接好第一个目标、项目和任务。",
"Agents": "智能体",
- "Agents auto-approve hires up to the budget envelope and ship work that clears policy.": "智能体在预算范围内自动批准招聘,并交付通过策略检查的工作。",
"Agents only": "仅智能体",
"All": "全部",
"All agents, work, chats, and files are permanently removed.": "所有智能体、工作、聊天和文件将被永久删除。",
@@ -384,7 +383,6 @@ export const zh: Record = {
"High risk": "高风险",
"Hire": "招聘",
"Hire agent": "招聘智能体",
- "Hires need your stamp; deliverables route through review before shipping.": "招聘需要你签字确认;交付物先经审核再发布。",
"Hires require approval": "招聘需审批",
"Home": "首页",
"How autonomous should the CEO be?": "CEO 的自主程度?",
@@ -696,6 +694,18 @@ export const zh: Record = {
"Resets {date} (UTC)": "{date} 重置(UTC)",
"Unsaved": "未保存",
"Working language": "工作语言",
+ "Runs on its own: approves its risky work and large hires, and writes deliverables without review.": "自主运行:自行批准高风险工作和大额招聘,交付物不经审核直接写入。",
+ "Checks with you: risky work and large hires wait for you, and deliverables wait for your review.": "与你确认:高风险工作和大额招聘需要你批准,交付物需经你审核。",
+ "What this mode allows": "此模式允许的范围",
+ "Approved without you: tasks above {level} risk, tasks that run commands in a workspace, and hires over {amount} a month.": "无需你批准:风险高于“{level}”的任务、在工作区中运行命令的任务,以及月预算超过 {amount} 的招聘。",
+ "Waiting for you: tasks above {level} risk, tasks that run commands in a workspace, and hires over {amount} a month.": "需要你批准:风险高于“{level}”的任务、在工作区中运行命令的任务,以及月预算超过 {amount} 的招聘。",
+ "Deliverables are written without review.": "交付物不经审核直接写入。",
+ "Deliverables wait for your review before they are written.": "交付物在写入前需经你审核。",
+ "The CEO starts with a {ceo} monthly budget, and each agent it hires gets {hire}.": "CEO 初始月预算为 {ceo},其招聘的每个 Agent 获得 {hire}。",
+ "Each department holds up to {count} agents and {amount} a month in agent budgets.": "每个部门最多 {count} 个 Agent,Agent 月预算合计不超过 {amount}。",
+ "Each provider allows {count} model calls a month.": "每个 Provider 每月允许 {count} 次模型调用。",
+ "An approved workspace task runs commands with your user's file access; they are not confined to the workspace folder.": "获批的工作区任务以你的用户文件权限运行命令,不局限于工作区文件夹。",
+ "Later you can change the mode, hire approval and deliverable review in Settings, General; provider limits in Settings, Providers; and agent budgets on Team.": "之后可以在“设置 › 通用”中修改模式、招聘审批和交付物审核,在“设置 › 服务商”中修改服务商额度,在“团队”中修改 Agent 预算。",
"{language} applies after a restart.": "{language} 将在重启后生效。",
"Restart now": "立即重启",
"Restart to switch language?": "重启以切换语言?",
diff --git a/src/shared/company-presets.ts b/src/shared/company-presets.ts
new file mode 100644
index 00000000..f55c79e4
--- /dev/null
+++ b/src/shared/company-presets.ts
@@ -0,0 +1,13 @@
+/** What a new company starts with; the seeds use these and onboarding shows them, so the two cannot drift. */
+export const COMPANY_PRESETS = {
+ ceoBudgetCents: 25_000,
+ /** Monthly budget of each agent an agent hires. */
+ agentHireBudgetCents: 25_000,
+ departmentHeadcount: 12,
+ departmentBudgetCents: 2_500_000,
+ /** A hire with a larger monthly budget goes to review. */
+ hireReviewAboveCents: 500_000,
+ providerMonthlyRequests: 1_000,
+ /** Tasks riskier than this need approval. */
+ taskRiskCeiling: "medium",
+} as const;
diff --git a/tests/e2e/desktop.spec.ts b/tests/e2e/desktop.spec.ts
index 7429db5e..85578b87 100644
--- a/tests/e2e/desktop.spec.ts
+++ b/tests/e2e/desktop.spec.ts
@@ -40,6 +40,14 @@ test("onboards, hires, and creates work in the Electron shell", async () => {
await page.getByLabel("Your name").fill("Dana Lee");
await page.getByLabel("Description").fill("Coordinate agents with auditable approvals.");
await page.getByRole("button", { name: "Continue" }).click();
+ // The mode step spells out what each mode lets the company do, and the amounts it starts with.
+ const allows = page.getByRole("region", { name: "What this mode allows" });
+ await expect(allows).toContainText("Approved without you: tasks above Medium risk, tasks that run commands in a workspace, and hires over $5,000 a month.");
+ await expect(allows).toContainText("The CEO starts with a $250 monthly budget, and each agent it hires gets $250.");
+ await expect(allows).toContainText("Each provider allows 1,000 model calls a month.");
+ await page.getByRole("button", { name: /AI Co-Founder/ }).click();
+ await expect(allows).toContainText("Deliverables wait for your review before they are written.");
+ await page.getByRole("button", { name: /AI CEO/ }).click();
await page.getByRole("button", { name: "Continue" }).click();
await page.getByLabel("Goal").fill("Ship a verified control plane.");
await page.getByLabel("Acceptance criteria").fill("The CEO creates the kickoff operating loop.");
diff --git a/tests/unit/company-presets.test.ts b/tests/unit/company-presets.test.ts
new file mode 100644
index 00000000..9c315e04
--- /dev/null
+++ b/tests/unit/company-presets.test.ts
@@ -0,0 +1,44 @@
+import { randomUUID } from "node:crypto";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { describe, expect, test } from "vitest";
+import { DatabaseConnection } from "../../src/main/database/connection";
+import { OnboardingService } from "../../src/main/services/onboarding-service";
+import { OrganizationService } from "../../src/main/services/organization-service";
+import { ProviderService } from "../../src/main/services/provider-service";
+import { providerAdapters } from "../../src/main/services/provider-adapters";
+import { ChatService } from "../../src/main/services/chat-service";
+import { dispatchAgentTool } from "../../src/main/services/agent-tools";
+import { COMPANY_PRESETS } from "../../src/shared/company-presets";
+
+const vault = { encryptSecret: (secret: string) => Buffer.from(secret, "utf8"), decryptSecret: (ciphertext: Buffer) => ciphertext.toString("utf8") };
+
+describe("what onboarding shows is what a company gets", () => {
+ test("a new company is seeded with exactly the presets onboarding lists", () => {
+ const connection = new DatabaseConnection(join(tmpdir(), `ac-presets-${randomUUID()}.db`));
+ const db = connection.db;
+ const providers = new ProviderService(db, providerAdapters, vault);
+ const onboarding = new OnboardingService({ db, saveProviderCredential: (input) => providers.saveCredential(input) });
+ let orgId = "";
+ let ceoId = "";
+ connection.transaction(() => {
+ const summary = onboarding.bootstrap({
+ companyName: "FlowPilot", companyDescription: "d", employeeName: "Yang", workspacePath: "", executionMode: "ai_cofounder", autoApproveHires: false,
+ reviewDeliverables: true, goalTitle: "Ship.", goalDescription: "d", providerKey: "openai", providerSecret: "sk-test", providerModel: "gpt-5.4",
+ });
+ orgId = summary.orgId;
+ ceoId = summary.ceoAgentId;
+ });
+ expect(db.prepare("select budget_cents from agents where id = ?").get(ceoId)).toEqual({ budget_cents: COMPANY_PRESETS.ceoBudgetCents });
+ expect(db.prepare("select distinct max_headcount, monthly_budget_limit_cents, require_approval_above_cents from hiring_policies where org_id = ?").all(orgId))
+ .toEqual([{ max_headcount: COMPANY_PRESETS.departmentHeadcount, monthly_budget_limit_cents: COMPANY_PRESETS.departmentBudgetCents, require_approval_above_cents: COMPANY_PRESETS.hireReviewAboveCents }]);
+ expect(db.prepare("select distinct monthly_request_limit from providers where org_id = ?").all(orgId)).toEqual([{ monthly_request_limit: COMPANY_PRESETS.providerMonthlyRequests }]);
+ expect(db.prepare("select risk_ceiling from policy_rules where org_id = ? and action = 'task_create'").all(orgId)).toEqual([{ risk_ceiling: COMPANY_PRESETS.taskRiskCeiling }]);
+
+ const hired = dispatchAgentTool({ db, organization: new OrganizationService(db), chat: new ChatService(db) }, {
+ orgId, agent: { id: ceoId, modelProviderId: (db.prepare("select model_provider_id from agents where id = ?").get(ceoId) as { model_provider_id: string }).model_provider_id, modelId: null }, taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null,
+ }, { id: "call_1", name: "hire_agent", arguments: { name: "Ada", role: "Engineer", title: "Engineer", department: "Engineering", seniority: "staff" } });
+ expect(hired).toContain("Ada");
+ expect(db.prepare("select budget_cents from agents where org_id = ? and name = 'Ada'").get(orgId)).toEqual({ budget_cents: COMPANY_PRESETS.agentHireBudgetCents });
+ });
+});