Q175: onboarding shows what each mode allows and the amounts a company starts with

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 05:02:32 -04:00
1 parent dc3127c26f
commit 43dcb1e7d3
11 files changed
+123 -14

No files matched your search

+2 -2
View File
@@ -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,
+2 -1
View File
@@ -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",
+5 -4
View File
@@ -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 {
+4 -2
View File
@@ -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,
);
}
+2 -1
View File
@@ -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 {
+27
View File
@@ -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 (
<section aria-label={t("What this mode allows")} style={{ fontSize: 13, color: "var(--text-2)", lineHeight: 1.6 }}>
<ul style={{ margin: 0, paddingLeft: 18 }}>
<li>{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)}</li>
<li>{mode === "ai_ceo"
? t("Deliverables are written without review.")
: t("Deliverables wait for your review before they are written.")}</li>
<li>{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) })}</li>
<li>{t("Each department holds up to {count} agents and {amount} a month in agent budgets.", { count: COMPANY_PRESETS.departmentHeadcount, amount: dollars(COMPANY_PRESETS.departmentBudgetCents) })}</li>
<li>{t("Each provider allows {count} model calls a month.", { count: COMPANY_PRESETS.providerMonthlyRequests.toLocaleString("en-US") })}</li>
<li>{t("An approved workspace task runs commands with your user's file access; they are not confined to the workspace folder.")}</li>
<li>{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.")}</li>
</ul>
</section>
);
}
+4 -2
View File
@@ -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) {
<p style={{ color: "var(--text-3)", fontSize: 14 }}>{t("How autonomous should the CEO be?")}</p>
<ModeCard
title={t("AI CEO")}
description={t("Agents auto-approve hires up to the budget envelope and ship work that clears policy.")}
description={t("Runs on its own: approves its risky work and large hires, and writes deliverables without review.")}
active={draft.executionMode === "ai_ceo"}
onClick={() => onChange({ executionMode: "ai_ceo" })}
/>
<ModeCard
title={t("AI Co-Founder")}
description={t("Hires need your stamp; deliverables route through review before shipping.")}
description={t("Checks with you: risky work and large hires wait for you, and deliverables wait for your review.")}
active={draft.executionMode === "ai_cofounder"}
onClick={() => onChange({ executionMode: "ai_cofounder" })}
/>
<AutonomySummary mode={draft.executionMode} />
<Actions onBack={onBack} onNext={onNext} />
</div>
);
+12 -2
View File
@@ -227,7 +227,6 @@ export const zh: Record<string, string> = {
"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<string, string> = {
"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<string, string> = {
"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?": "重启以切换语言?",
+13
View File
@@ -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;
+8
View File
@@ -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.");
+44
View File
@@ -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 });
});
});