Package release 0.4.167 from the current workspace snapshot

Constraint: Windows and Linux artifacts are built on native GitHub runners from a temporary snapshot branch
Rejected: Build Windows/Linux locally on Apple Silicon | docker/wine packaging is not stable on this host
Confidence: medium
Scope-risk: moderate
Directive: This snapshot branch is transport-only and should be deleted after the release completes
Tested: local verify/build stages before snapshot creation
Not-tested: remote packaging before workflow execution
This commit is contained in:
ZacharyZhang-NY committed 2026-04-11 02:11:07 -04:00
1 parent 9779c4d1ca
commit 237db05e37
29 files changed
+1193 -85

No files matched your search

+1
View File
@@ -71,3 +71,4 @@ pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
local-release/
.omx/
+1
View File
@@ -13,6 +13,7 @@ export default tseslint.config(
"dist/**",
"oldreference/**",
"Reference/**",
"claude-code-source-reference/**",
"boss-build/**",
"local-release/**",
"builtin-skills/**",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "agent-company",
"version": "0.4.165",
"version": "0.4.167",
"private": true,
"type": "module",
"description": "Cross-platform Electron desktop control plane for local-first AI agent companies.",
@@ -35,6 +35,8 @@ interface SpawnTarget {
args: string[];
}
const WINDOWS_CMD_SCRIPT_EXTENSIONS = new Set([".bat", ".cmd"]);
type ChildProcessWithEvents = ChildProcess & {
on(event: "error", listener: (err: Error) => void): ChildProcess;
on(
@@ -105,7 +107,23 @@ function quoteForCmd(arg: string) {
return /[\s"&<>|^()]/.test(escaped) ? `"${escaped}"` : escaped;
}
async function resolveSpawnTarget(
export function buildWindowsSpawnTarget(
executable: string,
args: string[],
shell: string,
): SpawnTarget {
const extension = path.extname(executable).toLowerCase();
if (!WINDOWS_CMD_SCRIPT_EXTENSIONS.has(extension)) {
return { command: executable, args };
}
const commandLine = [quoteForCmd(executable), ...args.map(quoteForCmd)].join(" ");
return {
command: shell,
args: ["/d", "/c", `chcp 65001 >nul && ${commandLine}`],
};
}
export async function resolveSpawnTarget(
command: string,
args: string[],
cwd: string,
@@ -123,11 +141,7 @@ async function resolveSpawnTarget(
// Chinese/CJK character corruption. The default Windows code page (e.g. 936/GBK)
// corrupts multi-byte UTF-8 characters passed through cmd.exe, curl, etc.
const shell = env.ComSpec || process.env.ComSpec || "cmd.exe";
const commandLine = [quoteForCmd(executable), ...args.map(quoteForCmd)].join(" ");
return {
command: shell,
args: ["/d", "/s", "/c", `chcp 65001 >nul && ${commandLine}`],
};
return buildWindowsSpawnTarget(executable, args, shell);
}
export function ensurePathInEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
+154 -2
View File
@@ -1,7 +1,10 @@
import type { AppDatabase } from "./database";
import type { TaskRecord } from "@shared/types";
function safeGetAgent(db: AppDatabase, agentId: string) {
function safeGetAgent(db: AppDatabase, agentId: string | null | undefined) {
if (!agentId) {
return null;
}
try {
return db.getAgent(agentId);
} catch {
@@ -23,7 +26,7 @@ const MANAGEMENT_KEYWORDS = [
"lead",
];
function hasManagementAuthority(agent: { role: string; title: string; department: string | null }) {
export function hasManagementAuthority(agent: { role: string; title: string; department: string | null }) {
if (agent.department === "hr") {
return true;
}
@@ -89,6 +92,155 @@ export function canRequestHire(db: AppDatabase, companyId: string, agentId: stri
return hasManagementAuthority(agent);
}
function getDepartmentLeadCandidateId(
db: AppDatabase,
companyId: string,
department: string,
excludeAgentId: string | null,
): string | null {
const candidates = db.queryAll<{
id: string;
role: string;
title: string;
department: string | null;
createdAt: string;
}>(
`select id, role, title, department, created_at as createdAt
from agents
where company_id = ?
and department = ?
and status not in ('terminated', 'pending_approval')`,
companyId,
department,
);
const ranked = candidates
.filter((candidate) => candidate.id !== excludeAgentId)
.map((candidate) => ({
id: candidate.id,
hasReports: db.getDirectReports(candidate.id, companyId).length > 0,
leadershipScore: hasManagementAuthority(candidate) ? 1 : 0,
chainDepth: db.getChainOfCommand(candidate.id).length,
createdAt: candidate.createdAt,
}))
.filter((candidate) => candidate.hasReports || candidate.leadershipScore > 0)
.sort((left, right) => {
if (left.hasReports !== right.hasReports) return left.hasReports ? -1 : 1;
if (left.leadershipScore !== right.leadershipScore) return right.leadershipScore - left.leadershipScore;
if (left.chainDepth !== right.chainDepth) return left.chainDepth - right.chainDepth;
return left.createdAt.localeCompare(right.createdAt);
});
return ranked[0]?.id ?? null;
}
export function resolveHireReportsTo(
db: AppDatabase,
companyId: string,
requestedByAgentId: string | null | undefined,
explicitReportsTo: string | null | undefined,
department: string | null | undefined,
): string | null {
if (explicitReportsTo !== undefined) {
return explicitReportsTo;
}
const requester = requestedByAgentId ? safeGetAgent(db, requestedByAgentId) : null;
if (requester && requester.companyId !== companyId) {
return null;
}
if (!department) {
return requester?.id ?? db.getCeoForCompany(companyId)?.id ?? null;
}
if (requester && (requester.department === department || !requester.reportsTo)) {
return requester.id;
}
const departmentLeadId = getDepartmentLeadCandidateId(db, companyId, department, requester?.id ?? null);
if (departmentLeadId) {
return departmentLeadId;
}
if (requester) {
return requester.id;
}
return db.getCeoForCompany(companyId)?.id ?? null;
}
function normalizeComparableText(value: string): string {
return value
.trim()
.toLowerCase()
.replace(/[\s_\-()[\]{}]+/g, " ")
.replace(/\s+/g, " ");
}
function isAllowedRoleAcronym(value: string): boolean {
return /^[A-Z]{2,5}$/.test(value.trim());
}
export function getHireNameValidationError(input: {
name: string;
role: string;
title: string;
}): string | null {
const normalizedName = normalizeComparableText(input.name);
if (!normalizedName) {
return "Hire requests require a real display name.";
}
const normalizedRole = normalizeComparableText(input.role);
const normalizedTitle = normalizeComparableText(input.title);
if (normalizedName === normalizedRole && !isAllowedRoleAcronym(input.name)) {
return "Hire name must be a real display name, not just the role.";
}
if (
normalizedTitle.length > 0 &&
normalizedName === normalizedTitle &&
!isAllowedRoleAcronym(input.name)
) {
return "Hire name must be a real display name, not just the title.";
}
return null;
}
export function getHireRoutingValidationError(
db: AppDatabase,
companyId: string,
requestedByAgentId: string | null | undefined,
reportsTo: string | null | undefined,
department: string | null | undefined,
): string | null {
const requester = requestedByAgentId ? safeGetAgent(db, requestedByAgentId) : null;
const manager = reportsTo ? safeGetAgent(db, reportsTo) : null;
const requestedDepartment = department ?? null;
if (requester && requester.companyId !== companyId) {
return "Requesting agent does not belong to the current company.";
}
if (!requestedDepartment) {
return null;
}
if (reportsTo != null && !manager) {
return "Selected manager not found.";
}
if (manager && manager.companyId !== companyId) {
return "Selected manager does not belong to the current company.";
}
if (
manager &&
manager.reportsTo != null &&
manager.department !== requestedDepartment
) {
return "Selected manager must belong to the same department as the new hire, unless they are a top-level executive.";
}
return null;
}
export function canAssignTask(db: AppDatabase, companyId: string, actorAgentId: string, targetAgentId: string | null): boolean {
if (!targetAgentId || targetAgentId === actorAgentId) {
return true;
+50 -14
View File
@@ -1,7 +1,13 @@
import log from "electron-log/main.js";
import { hireRequestSchema } from "@shared/contracts";
import { getConnectorExecutionReadinessIssue } from "@shared/connector-policy";
import { canManageOrganization, canRequestHire } from "../agent-permissions";
import {
canManageOrganization,
canRequestHire,
getHireNameValidationError,
getHireRoutingValidationError,
resolveHireReportsTo,
} from "../agent-permissions";
import { resolveApprovalDecision, shouldAutoApproveApproval } from "../approval-policy";
import { requireParam, type AgentApiServer } from "./server-core";
@@ -236,32 +242,59 @@ export function registerAgentRoutes(server: AgentApiServer) {
server.json(res, 403, { error: "Current agent is not allowed to request hires" });
return;
}
const reportsToProvided =
typeof body === "object" &&
body !== null &&
Object.prototype.hasOwnProperty.call(body, "reportsTo");
const parsed = hireRequestSchema.safeParse({
companyId,
requestedByAgentId: token.agentId,
...(typeof body === "object" && body ? body : {}),
reportsTo:
typeof body === "object" && body && "reportsTo" in body
? (body as { reportsTo?: string | null }).reportsTo
: token.agentId,
});
if (!parsed.success) {
server.json(res, 400, { error: "Invalid hire payload" });
return;
}
const input = parsed.data;
const resolvedReportsTo = resolveHireReportsTo(
db,
companyId,
token.agentId,
reportsToProvided ? input.reportsTo : undefined,
input.department ?? null,
);
const nameError = getHireNameValidationError({
name: input.name,
role: input.role,
title: input.title,
});
if (nameError) {
server.json(res, 422, { error: nameError });
return;
}
const routingError = getHireRoutingValidationError(
db,
companyId,
token.agentId,
resolvedReportsTo,
input.department ?? null,
);
if (routingError) {
server.json(res, 422, { error: routingError });
return;
}
if (server.companyRefError(res, companyId, [
{ id: input.requestedByAgentId, table: "agents", label: "requesting agent" },
{ id: input.reportsTo, table: "agents", label: "manager" },
{ id: resolvedReportsTo, table: "agents", label: "manager" },
{ id: input.workspaceId, table: "workspaces", label: "workspace" },
])) {
return;
}
if (
input.reportsTo &&
input.reportsTo !== token.agentId &&
resolvedReportsTo &&
resolvedReportsTo !== token.agentId &&
!canManageOrganization(db, companyId, token.agentId) &&
!db.getChainOfCommand(input.reportsTo).some((agent) => agent.id === token.agentId)
!db.getChainOfCommand(resolvedReportsTo).some((agent) => agent.id === token.agentId)
) {
server.json(res, 403, { error: "Requested hire must report to you or a manager in your reporting chain." });
return;
@@ -310,12 +343,12 @@ export function registerAgentRoutes(server: AgentApiServer) {
} catch { /* invalid payloadJson, skip */ }
}
const managerLabel = input.reportsTo
const managerLabel = resolvedReportsTo
? (() => {
try {
return db.getAgent(input.reportsTo).name;
return db.getAgent(resolvedReportsTo).name;
} catch {
return input.reportsTo;
return resolvedReportsTo;
}
})()
: "the company board";
@@ -329,7 +362,7 @@ export function registerAgentRoutes(server: AgentApiServer) {
role: input.role,
title: input.title,
department: input.department ?? null,
reportsTo: input.reportsTo ?? token.agentId,
reportsTo: resolvedReportsTo,
connectorId: input.connectorId,
workspaceId: input.workspaceId ?? null,
model: input.model ?? null,
@@ -339,6 +372,9 @@ export function registerAgentRoutes(server: AgentApiServer) {
payloadSummary: `Hire ${input.name} as ${input.role}`,
impactSummary: input.capabilities || `${input.name} will report to ${managerLabel}.`,
payloadJson: JSON.stringify({
name: input.name,
role: input.role,
title: input.title,
requestedByAgentId: input.requestedByAgentId ?? token.agentId,
requestedConfigurationSnapshot: {
connectorId: input.connectorId,
@@ -347,7 +383,7 @@ export function registerAgentRoutes(server: AgentApiServer) {
metadataJson: input.metadataJson ?? "{}",
capabilities: input.capabilities,
budgetMonthlyUsd: input.budgetMonthlyUsd,
reportsTo: input.reportsTo ?? token.agentId,
reportsTo: resolvedReportsTo,
department: input.department ?? null,
title: input.title,
},
+31 -3
View File
@@ -50,8 +50,30 @@ export function buildBudgetMethods(
notifyEnabled?: boolean;
isActive?: boolean;
}): string {
const id = input.id || randomUUID();
const ts = nowIso();
const metric = input.metric ?? "billed_cents";
const windowKind = input.windowKind ?? "calendar_month_utc";
const scopedExisting = input.id
? []
: readAll<{ id: string; createdAt: string }>(
`SELECT id, created_at as createdAt
FROM budget_policies
WHERE company_id = ? AND scope_type = ? AND scope_id = ? AND metric = ? AND window_kind = ?
ORDER BY created_at ASC`,
input.companyId,
input.scopeType,
input.scopeId,
metric,
windowKind,
);
const existing = input.id
? getDb()
.prepare("SELECT id, created_at as createdAt FROM budget_policies WHERE id = ?")
.get(input.id) as { id: string; createdAt: string } | undefined
: scopedExisting[0];
const staleIds = input.id ? [] : scopedExisting.slice(1).map((policy) => policy.id);
const id = existing?.id ?? input.id ?? randomUUID();
const createdAt = existing?.createdAt ?? ts;
getDb()
.prepare(
`INSERT OR REPLACE INTO budget_policies
@@ -61,12 +83,18 @@ export function buildBudgetMethods(
)
.run(
id, input.companyId, input.scopeType, input.scopeId,
input.metric ?? "billed_cents", input.windowKind ?? "calendar_month_utc",
metric, windowKind,
input.amount, input.warnPercent ?? 80,
(input.hardStopEnabled ?? true) ? 1 : 0,
(input.notifyEnabled ?? true) ? 1 : 0,
(input.isActive ?? true) ? 1 : 0, ts, ts,
(input.isActive ?? true) ? 1 : 0, createdAt, ts,
);
if (staleIds.length > 0) {
const placeholders = staleIds.map(() => "?").join(", ");
getDb()
.prepare(`UPDATE budget_policies SET is_active = 0, updated_at = ? WHERE id IN (${placeholders})`)
.run(ts, ...staleIds);
}
return id;
},
+1
View File
@@ -33,6 +33,7 @@ export function buildEntityCrudMethods(
)
.get(id) as AgentRecord | undefined;
if (!row) throw new Error(`Agent not found: ${id}`);
row.heartbeatEnabled = Boolean(row.heartbeatEnabled);
return row;
},
+160
View File
@@ -0,0 +1,160 @@
import { resolveApprovalDecision } from "./approval-policy";
import type { AppDatabase } from "./database";
type ReconcileExecutionModeChangeInput = {
db: AppDatabase;
companyId: string;
previousExecutionMode: string;
nextExecutionMode: string;
previousReviewDeliverables: boolean;
nextReviewDeliverables: boolean;
handleApprovalResolved: (approvalId: string, companyId: string, decision: string) => void;
};
const COFOUNDER_TRANSITION_APPROVAL_TYPES = [
"deliverable_review",
"document_review",
"plan_review",
"scope_review",
"draft_review",
"strategy_review",
"exception_review",
"approve_ceo_strategy",
] as const;
const REVIEW_WAIT_STATE_TYPES = new Set(["user_reply", "user_review", "user_approval"]);
function collectImpactedAgentIdsFromWaitStates(db: AppDatabase, companyId: string): Set<string> {
const agentIds = new Set<string>();
const waitStates = db.listThreadWaitStates(companyId);
for (const waitState of waitStates) {
const thread = db.getThread(waitState.threadId, companyId);
if (!thread || thread.relatedEntityType !== "task" || !thread.relatedEntityId) continue;
try {
const task = db.getTask(thread.relatedEntityId);
if (task.assigneeAgentId) {
agentIds.add(task.assigneeAgentId);
}
} catch {
continue;
}
}
return agentIds;
}
function collectImpactedAgentIdsFromReviewState(db: AppDatabase, companyId: string): Set<string> {
const agentIds = new Set<string>();
const flaggedTasks = db.queryAll<{ assigneeAgentId: string | null }>(
"select assignee_agent_id as assigneeAgentId from tasks where company_id = ? and requires_user_review = 1",
companyId,
);
for (const task of flaggedTasks) {
if (task.assigneeAgentId) {
agentIds.add(task.assigneeAgentId);
}
}
const pendingApprovals = db.queryAll<{ requestedByAgentId: string | null; relatedTaskId: string | null }>(
`select requested_by_agent_id as requestedByAgentId, related_task_id as relatedTaskId
from approvals
where company_id = ?
and state = 'pending'
and type = 'deliverable_review'`,
companyId,
);
for (const approval of pendingApprovals) {
if (approval.requestedByAgentId) {
agentIds.add(approval.requestedByAgentId);
}
if (!approval.relatedTaskId) continue;
try {
const task = db.getTask(approval.relatedTaskId);
if (task.assigneeAgentId) {
agentIds.add(task.assigneeAgentId);
}
} catch {
continue;
}
}
return agentIds;
}
function resumeAgents(db: AppDatabase, companyId: string, agentIds: Iterable<string>): void {
for (const agentId of agentIds) {
try {
const agent = db.getAgent(agentId);
if (agent.companyId !== companyId) continue;
if (["terminated", "pending_approval"].includes(agent.status)) continue;
if (!agent.heartbeatEnabled) {
db.setHeartbeat(agent.id, true, agent.heartbeatIntervalSec || 120);
}
} catch {
continue;
}
}
}
export function reconcileExecutionModeChange({
db,
companyId,
previousExecutionMode,
nextExecutionMode,
previousReviewDeliverables,
nextReviewDeliverables,
handleApprovalResolved,
}: ReconcileExecutionModeChangeInput): void {
const leavingCofounderMode =
previousExecutionMode === "ai_cofounder" && nextExecutionMode === "ai_ceo";
const disablingDeliverableReviews =
previousReviewDeliverables && !nextReviewDeliverables;
if (!leavingCofounderMode && !disablingDeliverableReviews) {
return;
}
const impactedAgents = new Set<string>([
...collectImpactedAgentIdsFromWaitStates(db, companyId),
...collectImpactedAgentIdsFromReviewState(db, companyId),
]);
if (leavingCofounderMode) {
const pendingApprovals = db.queryAll<{ id: string; type: string }>(
`select id, type
from approvals
where company_id = ?
and state = 'pending'
and type in (${COFOUNDER_TRANSITION_APPROVAL_TYPES.map(() => "?").join(", ")})`,
companyId,
...COFOUNDER_TRANSITION_APPROVAL_TYPES,
);
for (const approval of pendingApprovals) {
resolveApprovalDecision({
db,
handleApprovalResolved,
companyId,
approvalId: approval.id,
state: "approved",
decisionNote: "Auto-approved because the company switched to AI CEO mode.",
detail: `Mode transition auto-approved ${approval.type}.`,
actor: "system",
});
}
const waitStates = db.listThreadWaitStates(companyId);
for (const waitState of waitStates) {
if (!REVIEW_WAIT_STATE_TYPES.has(waitState.waitingFor)) continue;
db.resolveThreadWaitState(waitState.id, companyId);
}
}
if (disablingDeliverableReviews || leavingCofounderMode) {
const tasks = db.queryAll<{ id: string }>(
"select id from tasks where company_id = ? and requires_user_review = 1",
companyId,
);
for (const task of tasks) {
db.setTaskRequiresUserReview(task.id, false);
}
}
resumeAgents(db, companyId, impactedAgents);
}
+16 -1
View File
@@ -8,6 +8,7 @@ import {
secretInputSchema,
} from "@shared/contracts";
import { validateConnectorConfigurationModel, validateConnectorModelSelection } from "./connector-model-policy";
import { reconcileExecutionModeChange } from "./execution-mode-reconciler";
import { fail, ok, type RegisterCoreHandlersDependencies } from "./ipc-core-common";
export function registerCoreAdminHandlers({
@@ -15,16 +16,30 @@ export function registerCoreAdminHandlers({
db,
ensureCompanyExists,
publishDomainChanged,
handleApprovalResolved,
checkConnector,
openConnectorAuthTerminal,
openGenericTerminal,
}: Pick<
RegisterCoreHandlersDependencies,
"registerHandle" | "db" | "ensureCompanyExists" | "publishDomainChanged" | "checkConnector" | "openConnectorAuthTerminal" | "openGenericTerminal"
"registerHandle" | "db" | "ensureCompanyExists" | "publishDomainChanged" | "handleApprovalResolved" | "checkConnector" | "openConnectorAuthTerminal" | "openGenericTerminal"
>) {
registerHandle(IPC_CHANNELS.saveCompany, async (_event, payload) => {
const parsed = companyInputSchema.parse(payload);
const previousMode = parsed.id ? db.getCompanyExecutionMode(parsed.id) : parsed.executionMode;
const previousApprovalSettings = parsed.id
? db.getCompanyApprovalSettings(parsed.id)
: { autoApproveHires: parsed.autoApproveHires, reviewDeliverables: parsed.reviewDeliverables };
const id = db.saveCompany(parsed);
reconcileExecutionModeChange({
db,
companyId: id,
previousExecutionMode: previousMode,
nextExecutionMode: parsed.executionMode,
previousReviewDeliverables: previousApprovalSettings?.reviewDeliverables ?? false,
nextReviewDeliverables: parsed.reviewDeliverables,
handleApprovalResolved,
});
db.addActivity({
companyId: id,
actor: "board",
+35 -6
View File
@@ -7,6 +7,7 @@ import {
} from "@shared/contracts";
import { getConnectorExecutionReadinessIssue } from "@shared/connector-policy";
import { resolveApprovalDecision, shouldAutoApproveApproval } from "./approval-policy";
import { getHireNameValidationError, getHireRoutingValidationError, resolveHireReportsTo } from "./agent-permissions";
import { validateConnectorModelSelection } from "./connector-model-policy";
import { fail, ok, type RegisterCoreHandlersDependencies } from "./ipc-core-common";
@@ -146,9 +147,16 @@ export function registerCoreAgentGraphHandlers({
const parsed = hireRequestSchema.parse(payload);
const companyError = ensureCompanyExists(parsed.companyId);
if (companyError) return companyError;
const resolvedReportsTo = resolveHireReportsTo(
db,
parsed.companyId,
parsed.requestedByAgentId ?? null,
parsed.reportsTo,
parsed.department ?? null,
);
const invalidReference = findInvalidReference(parsed.companyId, [
{ id: parsed.requestedByAgentId, table: "agents", label: "requesting agent" },
{ id: parsed.reportsTo, table: "agents", label: "manager" },
{ id: resolvedReportsTo, table: "agents", label: "manager" },
{ id: parsed.workspaceId, table: "workspaces", label: "workspace" },
]);
if (invalidReference) {
@@ -168,16 +176,34 @@ export function registerCoreAgentGraphHandlers({
if (connectorIssue) {
return fail("CONNECTOR_NOT_READY", connectorIssue);
}
const nameError = getHireNameValidationError({
name: parsed.name,
role: parsed.role,
title: parsed.title,
});
if (nameError) {
return fail("INVALID_HIRE_NAME", nameError);
}
const routingError = getHireRoutingValidationError(
db,
parsed.companyId,
parsed.requestedByAgentId ?? null,
parsed.reportsTo,
parsed.department ?? null,
);
if (routingError) {
return fail("INVALID_HIRE_ROUTING", routingError);
}
const modelSelection = validateConnectorModelSelection(db, parsed.connectorId, parsed.model ?? null);
if (!modelSelection.ok) {
return fail("CONNECTOR_MODEL_NOT_ALLOWED", modelSelection.error);
}
const managerLabel = parsed.reportsTo
const managerLabel = resolvedReportsTo
? (() => {
try {
return db.getAgent(parsed.reportsTo).name;
return db.getAgent(resolvedReportsTo).name;
} catch {
return parsed.reportsTo;
return resolvedReportsTo;
}
})()
: "the company board";
@@ -192,7 +218,7 @@ export function registerCoreAgentGraphHandlers({
role: parsed.role,
title: parsed.title,
department: parsed.department ?? null,
reportsTo: parsed.reportsTo ?? null,
reportsTo: resolvedReportsTo,
connectorId: parsed.connectorId,
workspaceId: parsed.workspaceId ?? null,
model: parsed.model ?? null,
@@ -202,6 +228,9 @@ export function registerCoreAgentGraphHandlers({
payloadSummary,
impactSummary,
payloadJson: JSON.stringify({
name: parsed.name,
role: parsed.role,
title: parsed.title,
requestedByAgentId: parsed.requestedByAgentId ?? null,
requestedConfigurationSnapshot: {
connectorId: parsed.connectorId,
@@ -210,7 +239,7 @@ export function registerCoreAgentGraphHandlers({
metadataJson: parsed.metadataJson ?? "{}",
capabilities: parsed.capabilities,
budgetMonthlyUsd: parsed.budgetMonthlyUsd,
reportsTo: parsed.reportsTo ?? null,
reportsTo: resolvedReportsTo,
department: parsed.department ?? null,
title: parsed.title,
},
+16 -2
View File
@@ -80,14 +80,28 @@ export function createHeartbeat(
return null;
}
const companyMode = db.getCompanyExecutionMode(companyId);
const approvalSettings = db.getCompanyApprovalSettings(companyId);
const shouldEnforceHumanReview =
companyMode === "ai_cofounder" || approvalSettings?.reviewDeliverables === true;
// Block heartbeat when agent has tasks awaiting user deliverable review
if (!isMessageWake && (db.hasAgentPendingDeliverableReview(agentId, companyId) || db.hasAgentTaskAwaitingReview(agentId, companyId))) {
if (
shouldEnforceHumanReview &&
!isMessageWake &&
(db.hasAgentPendingDeliverableReview(agentId, companyId) || db.hasAgentTaskAwaitingReview(agentId, companyId))
) {
logger.debug(`[heartbeat] Skipping ${agent.name} — awaiting user deliverable review.`);
return null;
}
// Block CEO heartbeat when company has any pending deliverable reviews
if (!isMessageWake && !agent.reportsTo && db.hasPendingDeliverableReviews(companyId)) {
if (
shouldEnforceHumanReview &&
!isMessageWake &&
!agent.reportsTo &&
db.hasPendingDeliverableReviews(companyId)
) {
logger.debug(`[heartbeat] Skipping CEO ${agent.name} — company has pending deliverable reviews.`);
return null;
}
+1 -1
View File
@@ -313,7 +313,7 @@ Body: {
### Propose a Hire
POST /api/companies/${ctx.agent.companyId}/agents
Body: {
"name": "New hire name",
"name": "New hire display name (must not repeat role/title)",
"role": "role_title",
"title": "Descriptive title",
"department": "engineering|product|marketing|sales|hr|finance|legal|operations|customer_support|research|design|executive",
@@ -43,10 +43,10 @@ Start with C-suite and VPs who report to you (3-5 max), then let them hire their
## Step 3: Submit Hire Requests
For EACH role, use POST /api/companies/{companyId}/agents with:
- A clear, professional name
- A clear, professional name that is not just the role or title repeated back
- Specific role and title
- Detailed capabilities describing what they should own
- reportsTo: YOUR agent ID (they report to you)
- reportsTo: an explicit manager agent ID (do not omit this field)
- connectorId: Use the connector recommended in the "Available Connectors" section above. If no section is shown, use claude_local.
- budgetMonthlyUsd: Appropriate budget (recommend $50-250/agent)
- model: Best model for the role's complexity
@@ -159,7 +159,7 @@ When workload exceeds capacity or new capabilities are needed:
- Assess: count open tasks vs active agents per department
- If a department is overloaded: propose hiring via POST /api/companies/${ctx.agent.companyId}/agents
- Consider hiring specialists under your VPs (reportsTo = VP, not you)
- Each hire needs: clear role, specific capabilities, budget justification, department assignment
- Each hire needs: a distinct agent name, clear role/title, specific capabilities, budget justification, department assignment, and the correct manager (reportsTo)
- After hire approval, create onboarding tasks for the new agent
## Priority 7: Strategic Communication & Reporting
+1 -1
View File
@@ -41,7 +41,7 @@ When reviewing work from reports:
## SOP-005: Hiring Protocol
When the team needs new members:
1. Identify the capability gap (what work can't be done with current team?)
2. Define the role clearly: name, title, capabilities, which manager they report to
2. Define the role clearly: a distinct agent name, title, capabilities, and which manager they report to
3. Choose the best connector/model for the role
4. Submit via POST /api/companies/{companyId}/agents
5. The hire enters pending_approval — the Board must approve
@@ -215,7 +215,7 @@ export function ApprovalInspector({ approval, agents, tasks, documents, companyI
</div>
{approval.type === "hire_agent" ? (() => { const payload = parseApprovalPayloadJson(approval); const payloadAgentId = typeof payload?.agentId === "string" ? payload.agentId : null; const reqSnap = payload?.requestedConfigurationSnapshot && typeof payload.requestedConfigurationSnapshot === "object" && !Array.isArray(payload.requestedConfigurationSnapshot) ? payload.requestedConfigurationSnapshot as Record<string, unknown> : null; const proposedAgent = (approval.relatedAgentId ? (agents.find((a) => a.id === approval.relatedAgentId) ?? null) : null) ?? (payloadAgentId ? (agents.find((a) => a.id === payloadAgentId) ?? null) : null); const mgrId = proposedAgent?.reportsTo ?? (typeof reqSnap?.reportsTo === "string" ? reqSnap.reportsTo : null); const mgr = mgrId ? (agents.find((a) => a.id === mgrId) ?? null) : null; if (!proposedAgent && !reqSnap) return null; return (
<div className="rounded-lg border border-[color:var(--primary)] border-opacity-30 bg-accent px-4 py-3"><div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-primary">Proposed Hire</div><div className="grid gap-1.5 text-[12px]">
<div><span className="text-muted-foreground">Name:</span> <span className="font-medium text-foreground">{proposedAgent?.name ?? approval.payloadSummary.replace(/^Hire\s+/i, "").split(/\s+as\s+/i)[0] ?? "Pending hire"}</span></div>
<div><span className="text-muted-foreground">Name:</span> <span className="font-medium text-foreground">{proposedAgent?.name ?? (typeof reqSnap?.name === "string" ? reqSnap.name : "Unspecified")}</span></div>
<div><span className="text-muted-foreground">Role:</span> <span className="text-foreground">{proposedAgent?.role ?? (typeof reqSnap?.role === "string" ? reqSnap.role : "Unspecified")}</span></div>
{(proposedAgent?.title || typeof reqSnap?.title === "string") ? <div><span className="text-muted-foreground">Title:</span> <span className="text-foreground">{proposedAgent?.title || reqSnap?.title as string}</span></div> : null}
{(proposedAgent?.capabilities || typeof reqSnap?.capabilities === "string") ? <div><span className="text-muted-foreground">Capabilities:</span> <span className="text-foreground">{proposedAgent?.capabilities || reqSnap?.capabilities as string}</span></div> : null}
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import type { BudgetPolicyRecord } from "@shared/types";
import { ActionButton, Input, Modal, Select } from "../ui";
import { buildBudgetPolicyInput, getBudgetPolicyAmountDollars } from "../../lib/budget-policy-input";
interface PolicyFormModalProps {
open: boolean;
@@ -25,7 +26,7 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
if (editingPolicy) {
setScopeType(editingPolicy.scopeType);
setScopeId(editingPolicy.scopeId);
setAmountDollars((editingPolicy.amount / 100).toFixed(2));
setAmountDollars(getBudgetPolicyAmountDollars(editingPolicy));
setWarnPercent(String(editingPolicy.warnPercent));
setHardStopEnabled(editingPolicy.hardStopEnabled);
setNotifyEnabled(editingPolicy.notifyEnabled);
@@ -52,24 +53,19 @@ export function PolicyFormModal({ open, onClose, onSaved, companyId, editingPoli
async function handleSave() {
setSaving(true);
try {
const amountCents = Math.round(parseFloat(amountDollars) * 100);
if (Number.isNaN(amountCents) || amountCents <= 0) return;
const wp = parseInt(warnPercent, 10);
if (Number.isNaN(wp) || wp < 1 || wp > 100) return;
const result = await window.agentCompany.saveBudgetPolicy({
const payload = buildBudgetPolicyInput({
policyId: editingPolicy?.id ?? null,
companyId,
scopeType,
scopeId,
metric: "billed_cents",
windowKind: "calendar_month_utc",
amount: amountCents,
warnPercent: wp,
amountDollars,
warnPercent,
hardStopEnabled,
notifyEnabled,
isActive: true,
});
if (!payload) return;
const result = await window.agentCompany.saveBudgetPolicy(payload);
if (result.ok) {
onSaved();
onClose();
@@ -1,7 +1,8 @@
import { useEffect, useMemo, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Robot } from "@phosphor-icons/react";
import type { AgentRecord, ConnectorRecord, WorkspaceRecord } from "@shared/types";
import { DEPARTMENT_OPTIONS } from "@shared/constants";
import type { AgentRecord, ConnectorRecord, DepartmentId, WorkspaceRecord } from "@shared/types";
import { isConnectorExecutionReady } from "@shared/connector-policy";
import { unwrap } from "../../lib/desktop";
import { ActionButton, Input, Select, TextArea } from "../ui";
@@ -27,6 +28,7 @@ export function AgentCreator({
const [name, setName] = useState("");
const [role, setRole] = useState("");
const [title, setTitle] = useState("");
const [department, setDepartment] = useState<DepartmentId | "">("");
const [connectorId, setConnectorId] = useState(executionReadyConnectors[0]?.id ?? "");
const [model, setModel] = useState("");
const [availableModels, setAvailableModels] = useState<Array<{ id: string; label: string }>>([]);
@@ -69,6 +71,7 @@ export function AgentCreator({
const t = useT();
const [showAdvanced, setShowAdvanced] = useState(false);
const selectedDepartment: DepartmentId | null = department === "" ? null : department;
return (
<div className="grid min-w-0 gap-4">
@@ -114,6 +117,15 @@ export function AgentCreator({
<AnimatePresence>
{showAdvanced ? (
<motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }} className="grid gap-4 overflow-hidden">
<Select
label={t("comm.department")}
value={department}
onChange={(value) => setDepartment(value as DepartmentId | "")}
options={DEPARTMENT_OPTIONS.map((option) => ({
value: option.value,
label: option.value === "" ? t("comm.selectDepartment") : option.label,
}))}
/>
<Select
label={t("agent.workspace")}
value={workspaceId}
@@ -162,21 +174,22 @@ export function AgentCreator({
unwrap(await window.agentCompany.requestHire({
companyId,
requestedByAgentId: null,
name,
role,
title,
department: null,
name: name.trim(),
role: role.trim(),
title: title.trim(),
department: selectedDepartment,
connectorId: connectorId as ConnectorRecord["id"],
workspaceId: workspaceId || null,
reportsTo: reportsTo || null,
model: model || null,
metadataJson: JSON.stringify(meta),
capabilities,
capabilities: capabilities.trim(),
budgetMonthlyUsd: Number(budget || 0),
}));
setName("");
setRole("");
setTitle("");
setDepartment("");
setCapabilities("");
setReportsTo("");
setRequestError(null);
@@ -1,8 +1,10 @@
import { useEffect, useMemo, useState } from "react";
import { Pencil, Trash } from "@phosphor-icons/react";
import type { AgentRecord, BudgetPolicyRecord, ConnectorRecord, WorkspaceRecord } from "@shared/types";
import { DEPARTMENT_OPTIONS } from "@shared/constants";
import type { AgentRecord, BudgetPolicyRecord, ConnectorRecord, DepartmentId, WorkspaceRecord } from "@shared/types";
import { getConnectorExecutionReadinessIssue, isConnectorExecutionReady } from "@shared/connector-policy";
import { unwrap } from "../../lib/desktop";
import { buildBudgetPolicyInput, getBudgetPolicyAmountDollars } from "../../lib/budget-policy-input";
import { useAppStore } from "../../state/store";
import { useT } from "../../i18n";
import { ActionButton, Input, Select, TextArea } from "../ui";
@@ -30,6 +32,7 @@ export function AgentEditor({
const [name, setName] = useState(agent.name);
const [role, setRole] = useState(agent.role);
const [title, setTitle] = useState(agent.title);
const [department, setDepartment] = useState<DepartmentId | "">(agent.department ?? "");
const [status, setStatus] = useState<AgentRecord["status"]>(agent.status);
const [connectorId, setConnectorId] = useState<ConnectorRecord["id"]>(agent.connectorId);
const [model, setModel] = useState(agent.model ?? "");
@@ -108,18 +111,22 @@ export function AgentEditor({
void (async () => {
const result = await window.agentCompany.listBudgetPolicies(companyId);
if (result.ok) {
const policy = result.data.find(
(p) => p.scopeType === "agent" && p.scopeId === agent.id
);
const policy = result.data
.filter((p) => p.scopeType === "agent" && p.scopeId === agent.id)
.sort((left, right) => {
if (left.isActive !== right.isActive) return left.isActive ? -1 : 1;
return right.updatedAt.localeCompare(left.updatedAt);
})[0];
if (policy) {
setAgentPolicy(policy);
setBudget(String((policy.amount / 100).toFixed(2)));
setBudget(getBudgetPolicyAmountDollars(policy));
}
}
})();
}, [companyId, agent.id]);
const managerCandidates = agents.filter((a) => a.id !== agent.id);
const selectedDepartment: DepartmentId | null = department === "" ? null : department;
async function persistAgentDraft() {
const meta = { ...existingMeta };
@@ -130,9 +137,10 @@ export function AgentEditor({
unwrap(await window.agentCompany.saveAgent({
id: agent.id,
companyId,
name,
role,
title,
name: name.trim(),
role: role.trim(),
title: title.trim(),
department: selectedDepartment,
status,
connectorId,
workspaceId: workspaceId || null,
@@ -140,22 +148,24 @@ export function AgentEditor({
model: model || null,
metadataJson: JSON.stringify(meta),
budgetMonthlyUsd: budgetDollars,
capabilities,
capabilities: capabilities.trim(),
}));
if (budgetDollars > 0 && agent.id) {
await window.agentCompany.saveBudgetPolicy({
const payload = buildBudgetPolicyInput({
policyId: agentPolicy?.id ?? null,
companyId,
scopeType: "agent",
scopeId: agent.id,
metric: "billed_cents",
windowKind: "calendar_month_utc",
amount: Math.round(budgetDollars * 100),
warnPercent: agentPolicy?.warnPercent ?? 80,
amountDollars: String(budgetDollars),
warnPercent: String(agentPolicy?.warnPercent ?? 80),
hardStopEnabled: agentPolicy?.hardStopEnabled ?? true,
notifyEnabled: agentPolicy?.notifyEnabled ?? true,
isActive: true,
});
if (!payload) {
throw new Error("Agent budget policy is invalid.");
}
unwrap(await window.agentCompany.saveBudgetPolicy(payload));
}
}
@@ -175,6 +185,16 @@ export function AgentEditor({
<Input label={t("agent.name")} value={name} onChange={setName} placeholder={t("agent.name")} readOnly={pendingApproval} disabled={pendingApproval} />
<Input label={t("agent.role")} value={role} onChange={setRole} placeholder={t("agent.role")} readOnly={pendingApproval} disabled={pendingApproval} />
<Input label={t("agent.title")} value={title} onChange={setTitle} placeholder={t("agent.title")} readOnly={pendingApproval} disabled={pendingApproval} />
<Select
label={t("comm.department")}
value={department}
onChange={(value) => setDepartment(value as DepartmentId | "")}
disabled={pendingApproval}
options={DEPARTMENT_OPTIONS.map((option) => ({
value: option.value,
label: option.value === "" ? t("comm.selectDepartment") : option.label,
}))}
/>
<Select
label={t("agent.status")}
value={status}
+45
View File
@@ -0,0 +1,45 @@
import type { BudgetPolicyRecord } from "@shared/types";
interface BuildBudgetPolicyInputArgs {
policyId?: string | null;
companyId: string;
scopeType: "company" | "agent" | "project";
scopeId: string;
amountDollars: string;
warnPercent: string;
hardStopEnabled: boolean;
notifyEnabled: boolean;
}
export function buildBudgetPolicyInput(args: BuildBudgetPolicyInputArgs) {
const amount = Math.round(Number.parseFloat(args.amountDollars) * 100);
if (!Number.isFinite(amount) || amount <= 0) {
return null;
}
const warnPercent = Number.parseInt(args.warnPercent, 10);
if (!Number.isFinite(warnPercent) || warnPercent < 1 || warnPercent > 100) {
return null;
}
return {
id: args.policyId ?? null,
companyId: args.companyId,
scopeType: args.scopeType,
scopeId: args.scopeId,
metric: "billed_cents" as const,
windowKind: "calendar_month_utc" as const,
amount,
warnPercent,
hardStopEnabled: args.hardStopEnabled,
notifyEnabled: args.notifyEnabled,
isActive: true,
};
}
export function getBudgetPolicyAmountDollars(policy: BudgetPolicyRecord | null): string {
if (!policy) {
return "";
}
return (policy.amount / 100).toFixed(2);
}
+10 -6
View File
@@ -2,12 +2,16 @@ import { z } from "zod";
import { DEPARTMENT_IDS, optionalId } from "./helpers";
import { connectorInputSchema } from "./schemas-company";
const agentNameSchema = z.string().trim().min(2).max(80);
const agentRoleSchema = z.string().trim().min(2).max(80);
const agentTitleSchema = z.string().trim().max(80).default("");
export const agentInputSchema = z.object({
id: optionalId,
companyId: z.string().uuid(),
name: z.string().min(2).max(80),
role: z.string().min(2).max(80),
title: z.string().default(""),
name: agentNameSchema,
role: agentRoleSchema,
title: agentTitleSchema,
department: DEPARTMENT_IDS.nullable().optional(),
reportsTo: optionalId,
connectorId: connectorInputSchema.shape.id,
@@ -22,9 +26,9 @@ export const agentInputSchema = z.object({
export const hireRequestSchema = z.object({
companyId: z.string().uuid(),
requestedByAgentId: optionalId,
name: z.string().min(2).max(80),
role: z.string().min(2).max(80),
title: z.string().default(""),
name: agentNameSchema,
role: agentRoleSchema,
title: agentTitleSchema,
department: DEPARTMENT_IDS.nullable().optional(),
reportsTo: optionalId,
connectorId: connectorInputSchema.shape.id,
@@ -110,7 +110,7 @@ describe("recursive org execution", () => {
method: "POST",
headers: harness.headers,
body: JSON.stringify({
name: "Engineering IC",
name: "Avery Chen",
role: "Engineer",
title: "Frontend Engineer",
department: "engineering",
@@ -166,6 +166,100 @@ describe("recursive org execution", () => {
}
});
it("routes cross-department hires to the relevant department lead when reportsTo is omitted", async () => {
const harness = await createHarness();
try {
const hrId = harness.db.saveAgent({
companyId: harness.companyId,
name: "HR Director",
role: "HR Director",
title: "Director of People",
status: "active",
reportsTo: harness.ceoId,
connectorId: "codex_local",
workspaceId: harness.workspaceId,
model: null,
capabilities: "Coordinate staffing",
budgetMonthlyUsd: 180,
department: "hr",
});
const hrTaskId = harness.db.saveTask({
companyId: harness.companyId,
title: "Fill engineering gap",
description: "Request a backend engineer for the engineering team.",
assigneeAgentId: hrId,
workspaceId: harness.workspaceId,
priority: "high",
status: "in_progress",
});
const hrRun = harness.db.createRun({
companyId: harness.companyId,
taskId: hrTaskId,
agentId: hrId,
workspaceId: harness.workspaceId,
connectorId: "codex_local",
});
const hrToken = createAgentToken(hrId, harness.companyId, hrRun.id);
const response = await fetch(`${harness.baseUrl}/api/companies/${harness.companyId}/agents`, {
method: "POST",
headers: {
Authorization: `Bearer ${hrToken}`,
"X-Agent-Company-Run-Id": hrRun.id,
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({
name: "Avery Chen",
role: "Backend Engineer",
title: "Backend Engineer Golang",
department: "engineering",
connectorId: "codex_local",
workspaceId: harness.workspaceId,
model: null,
capabilities: "Go, PostgreSQL, distributed systems",
budgetMonthlyUsd: 180,
}),
});
expect(response.status).toBe(201);
const body = await response.json() as { agentId: string };
const pendingHire = harness.db.getAgent(body.agentId);
expect(pendingHire.department).toBe("engineering");
expect(pendingHire.reportsTo).toBe(harness.ctoId);
} finally {
harness.server.stop();
harness.db.close();
}
});
it("rejects multi-word hire names that only repeat the job title", async () => {
const harness = await createHarness();
try {
const response = await fetch(`${harness.baseUrl}/api/companies/${harness.companyId}/agents`, {
method: "POST",
headers: harness.headers,
body: JSON.stringify({
name: "Backend Engineer Golang",
role: "Backend Engineer",
title: "Backend Engineer Golang",
department: "engineering",
connectorId: "codex_local",
workspaceId: harness.workspaceId,
model: null,
capabilities: "Go, APIs",
budgetMonthlyUsd: 180,
}),
});
expect(response.status).toBe(422);
const body = await response.json() as { error: string };
expect(body.error).toContain("Hire name");
} finally {
harness.server.stop();
harness.db.close();
}
});
it("manager cannot route a hire outside their reporting branch", async () => {
const harness = await createHarness();
try {
@@ -201,9 +295,9 @@ describe("recursive org execution", () => {
}),
});
expect(response.status).toBe(403);
expect(response.status).toBe(422);
const body = await response.json() as { error: string };
expect(body.error).toContain("must report to you or a manager in your reporting chain");
expect(body.error).toContain("same department");
} finally {
harness.server.stop();
harness.db.close();
+112
View File
@@ -0,0 +1,112 @@
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { afterEach, describe, expect, it } from "vitest";
import { AppDatabase } from "../../src/main/database";
const tempDirs: string[] = [];
async function createTempDir(prefix: string) {
const dir = await mkdtemp(join(tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
});
describe("agent budget policy synchronization", () => {
it("updates the canonical agent-scope policy instead of creating duplicate active policies", async () => {
const profileDir = await createTempDir("agentcompany-budget-sync-");
const db = new AppDatabase(profileDir);
db.init();
const companyId = db.saveCompany({
name: "Budget Sync Co",
description: "Budget sync regression coverage",
status: "active",
});
db.updateConnectorHealth({
id: "codex_local",
status: "ready",
authState: "authenticated",
version: "test",
lastError: null,
});
const workspaceId = db.saveWorkspace({
companyId,
name: "Primary",
localPath: profileDir,
repoUrl: "",
repoRef: "",
isPrimary: true,
});
const agentId = db.saveAgent({
companyId,
name: "Engineer",
role: "Engineer",
title: "Backend Engineer",
status: "active",
reportsTo: null,
connectorId: "codex_local",
workspaceId,
model: null,
capabilities: "Build backend services",
budgetMonthlyUsd: 100,
department: "engineering",
});
const firstPolicyId = db.saveBudgetPolicy({
companyId,
scopeType: "agent",
scopeId: agentId,
metric: "billed_cents",
windowKind: "calendar_month_utc",
amount: 10_000,
warnPercent: 80,
hardStopEnabled: true,
notifyEnabled: true,
isActive: true,
});
db.saveAgent({
id: agentId,
companyId,
name: "Engineer",
role: "Engineer",
title: "Backend Engineer",
status: "active",
reportsTo: null,
connectorId: "codex_local",
workspaceId,
model: null,
capabilities: "Build backend services",
budgetMonthlyUsd: 250,
department: "engineering",
});
const secondPolicyId = db.saveBudgetPolicy({
companyId,
scopeType: "agent",
scopeId: agentId,
metric: "billed_cents",
windowKind: "calendar_month_utc",
amount: 25_000,
warnPercent: 80,
hardStopEnabled: true,
notifyEnabled: true,
isActive: true,
});
const agentPolicies = db.listBudgetPolicies(companyId).filter(
(policy) => policy.scopeType === "agent" && policy.scopeId === agentId,
);
expect(secondPolicyId).toBe(firstPolicyId);
expect(agentPolicies).toHaveLength(1);
expect(agentPolicies[0]?.amount).toBe(25_000);
expect(db.getActivePoliciesForRun(companyId, agentId, null).map((policy) => policy.amount)).toEqual([25_000]);
db.close();
});
});
+98
View File
@@ -0,0 +1,98 @@
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { afterEach, describe, expect, it } from "vitest";
import { AppDatabase } from "../../src/main/database";
import { getHireRoutingValidationError, resolveHireReportsTo } from "../../src/main/agent-permissions";
const tempDirs: string[] = [];
async function createTempDir(prefix: string) {
const dir = await mkdtemp(join(tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
});
describe("hire routing guards", () => {
it("defaults same-department hires to the requester and infers department leads for cross-department hires", async () => {
const profileDir = await createTempDir("agentcompany-hire-manager-");
const db = new AppDatabase(profileDir);
db.init();
const companyId = db.saveCompany({
name: "Routing Co",
description: "Hire routing regression coverage",
status: "active",
});
db.updateConnectorHealth({
id: "codex_local",
status: "ready",
authState: "authenticated",
version: "test",
lastError: null,
});
const workspaceId = db.saveWorkspace({
companyId,
name: "Primary",
localPath: profileDir,
repoUrl: "",
repoRef: "",
isPrimary: true,
});
const ceoId = db.saveAgent({
companyId,
name: "CEO",
role: "CEO",
title: "Chief Executive Officer",
status: "active",
reportsTo: null,
connectorId: "codex_local",
workspaceId,
model: null,
capabilities: "Lead the company",
budgetMonthlyUsd: 250,
department: "executive",
});
const hrId = db.saveAgent({
companyId,
name: "HR Lead",
role: "HR Lead",
title: "Head of HR",
status: "active",
reportsTo: ceoId,
connectorId: "codex_local",
workspaceId,
model: null,
capabilities: "Run hiring operations",
budgetMonthlyUsd: 180,
department: "hr",
});
const ctoId = db.saveAgent({
companyId,
name: "CTO",
role: "CTO",
title: "Chief Technology Officer",
status: "active",
reportsTo: ceoId,
connectorId: "codex_local",
workspaceId,
model: null,
capabilities: "Lead engineering",
budgetMonthlyUsd: 220,
department: "engineering",
});
expect(resolveHireReportsTo(db, companyId, hrId, undefined, "hr")).toBe(hrId);
expect(resolveHireReportsTo(db, companyId, hrId, undefined, "engineering")).toBe(ctoId);
expect(getHireRoutingValidationError(db, companyId, hrId, hrId, "engineering")).toBe(
"Selected manager must belong to the same department as the new hire, unless they are a top-level executive.",
);
expect(getHireRoutingValidationError(db, companyId, ctoId, ctoId, "engineering")).toBeNull();
db.close();
});
});
@@ -0,0 +1,59 @@
import { describe, expect, it } from "vitest";
import { hireRequestSchema } from "../../src/shared/contracts/schemas-agents";
import { getHireNameValidationError } from "../../src/main/agent-permissions";
const baseHireRequest = {
companyId: "00000000-0000-0000-0000-000000000000",
requestedByAgentId: null,
name: "Alice",
role: "Engineer",
title: "Backend Engineer",
department: "engineering" as const,
reportsTo: null,
connectorId: "codex_local" as const,
workspaceId: null,
model: null,
metadataJson: "{}",
capabilities: "Build backend services",
budgetMonthlyUsd: 120,
};
describe("agent input schemas", () => {
it("rejects whitespace-only hire names", () => {
expect(() =>
hireRequestSchema.parse({
...baseHireRequest,
name: " ",
}),
).toThrow();
});
it("trims semantic hire fields before persistence", () => {
const parsed = hireRequestSchema.parse({
...baseHireRequest,
name: " Alice ",
role: " Engineer ",
title: " Backend Engineer ",
});
expect(parsed.name).toBe("Alice");
expect(parsed.role).toBe("Engineer");
expect(parsed.title).toBe("Backend Engineer");
});
it("rejects role-like names for non-executive hires", () => {
expect(getHireNameValidationError({
name: "Backend Engineer",
role: "Backend Engineer",
title: "Backend Engineer",
})).toBe("Hire name must be a real display name, not just the role.");
});
it("allows executive acronyms when they are the intended display name", () => {
expect(getHireNameValidationError({
name: "CTO",
role: "CTO",
title: "Chief Technology Officer",
})).toBeNull();
});
});
+41
View File
@@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import { buildBudgetPolicyInput, getBudgetPolicyAmountDollars } from "../../src/renderer/lib/budget-policy-input";
describe("budget-policy-input", () => {
it("preserves the existing policy id on edit", () => {
const payload = buildBudgetPolicyInput({
policyId: "policy-123",
companyId: "00000000-0000-0000-0000-000000000001",
scopeType: "agent",
scopeId: "00000000-0000-0000-0000-000000000002",
amountDollars: "250.00",
warnPercent: "85",
hardStopEnabled: true,
notifyEnabled: true,
});
expect(payload?.id).toBe("policy-123");
expect(payload?.amount).toBe(25000);
expect(payload?.warnPercent).toBe(85);
});
it("formats stored cents back to dollars for edit forms", () => {
expect(
getBudgetPolicyAmountDollars({
id: "policy-123",
companyId: "company-1",
scopeType: "agent",
scopeId: "agent-1",
metric: "billed_cents",
windowKind: "calendar_month_utc",
amount: 12550,
warnPercent: 80,
hardStopEnabled: true,
notifyEnabled: true,
isActive: true,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
}),
).toBe("125.50");
});
});
+24 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { normalizeCommandValue } from "@agentcompany/adapter-utils/server-utils";
import { buildWindowsSpawnTarget, normalizeCommandValue } from "@agentcompany/adapter-utils/server-utils";
import { commandLooksLike } from "@agentcompany/adapter-utils/test-utils";
describe("command path normalization", () => {
@@ -21,4 +21,27 @@ describe("command path normalization", () => {
expect(commandLooksLike('"C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe"', "claude")).toBe(true);
expect(commandLooksLike("'C:\\Users\\First Last\\AppData\\Local\\bin\\codex.exe'", "codex")).toBe(true);
});
it("spawns direct exe paths on Windows instead of wrapping them through cmd.exe", () => {
const target = buildWindowsSpawnTarget(
"C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe",
["--version"],
"cmd.exe",
);
expect(target).toEqual({
command: "C:\\Users\\First Last\\AppData\\Local\\bin\\claude.exe",
args: ["--version"],
});
});
it("still wraps shell scripts through cmd.exe on Windows", () => {
const target = buildWindowsSpawnTarget(
"C:\\Tools\\agentcompany\\launch.cmd",
["--version"],
"cmd.exe",
);
expect(target.command).toBe("cmd.exe");
expect(target.args[0]).toBe("/d");
expect(target.args.at(-1)).toContain("launch.cmd");
});
});
+151
View File
@@ -0,0 +1,151 @@
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { afterEach, describe, expect, it, vi } from "vitest";
import { IPC_CHANNELS } from "@shared/contracts";
import { AppDatabase } from "../../src/main/database";
import { registerCoreAdminHandlers } from "../../src/main/ipc-core-admin-handlers";
import { createThreadIntegration } from "../../src/main/thread-integration";
const tempDirs: string[] = [];
async function createTempDir(prefix: string) {
const dir = await mkdtemp(join(tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
});
describe("company mode switching", () => {
it("clears cofounder review artifacts when switching back to AI CEO", async () => {
const profileDir = await createTempDir("agentcompany-mode-switch-");
const db = new AppDatabase(profileDir);
db.init();
db.updateConnectorHealth({
id: "codex_local",
status: "ready",
authState: "authenticated",
version: "test",
lastError: null,
});
const companyId = db.saveCompany({
name: "Mode Switch Co",
description: "Execution mode cleanup",
status: "active",
executionMode: "ai_cofounder",
autoApproveHires: false,
reviewDeliverables: true,
});
const workspaceId = db.saveWorkspace({
companyId,
name: "Primary",
localPath: profileDir,
repoUrl: "",
repoRef: "",
isPrimary: true,
});
const ceoId = db.saveAgent({
companyId,
name: "CEO",
role: "CEO",
title: "Chief Executive Officer",
status: "active",
reportsTo: null,
connectorId: "codex_local",
workspaceId,
model: null,
capabilities: "Lead the company",
budgetMonthlyUsd: 250,
});
const engineerId = db.saveAgent({
companyId,
name: "Engineer",
role: "Engineer",
title: "Lead Engineer",
status: "active",
reportsTo: ceoId,
connectorId: "codex_local",
workspaceId,
model: null,
capabilities: "Ship features",
budgetMonthlyUsd: 180,
department: "engineering",
});
const taskId = db.saveTask({
companyId,
title: "Ship feature",
description: "Waits for cofounder review.",
assigneeAgentId: engineerId,
workspaceId,
priority: "high",
status: "in_progress",
});
const threadIntegration = createThreadIntegration({
db,
logger: { info: () => {}, error: () => {} },
});
threadIntegration.onTaskCreated(taskId, companyId);
threadIntegration.onRunFinished("run-1", taskId, engineerId, companyId, "succeeded", null);
db.setTaskRequiresUserReview(taskId, true);
const approvalId = db.requestApproval({
companyId,
relatedTaskId: taskId,
requestedByAgentId: engineerId,
type: "deliverable_review",
payloadSummary: "Review deliverable",
impactSummary: "Needs review",
payloadJson: "{}",
});
threadIntegration.onApprovalCreated(approvalId, companyId, taskId, "deliverable_review", "Review deliverable");
expect(db.listThreadWaitStates(companyId).length).toBeGreaterThan(0);
expect(db.getTask(taskId).requiresUserReview).toBe(true);
expect(db.getApprovalRecord(approvalId).state).toBe("pending");
const handlers = new Map<string, (...args: any[]) => any>();
registerCoreAdminHandlers({
registerHandle: (channel, handler) => {
handlers.set(channel, handler);
},
db,
ensureCompanyExists: (id) => (db.hasCompany(id) ? null : { ok: false, error: { code: "COMPANY_NOT_FOUND", message: "missing" } }),
publishDomainChanged: vi.fn(),
handleApprovalResolved: (resolvedApprovalId, resolvedCompanyId, decision) => {
threadIntegration.onApprovalResolved(resolvedApprovalId, resolvedCompanyId, decision);
},
checkConnector: vi.fn(),
openConnectorAuthTerminal: vi.fn(),
openGenericTerminal: vi.fn(),
});
const saveCompany = handlers.get(IPC_CHANNELS.saveCompany);
if (!saveCompany) {
throw new Error("saveCompany handler was not registered.");
}
const result = await saveCompany(null, {
id: companyId,
name: "Mode Switch Co",
description: "Execution mode cleanup",
status: "active",
autoApproveHires: true,
reviewDeliverables: false,
executionMode: "ai_ceo",
}) as { ok: boolean };
expect(result.ok).toBe(true);
expect(db.getCompanyExecutionMode(companyId)).toBe("ai_ceo");
expect(db.listThreadWaitStates(companyId)).toHaveLength(0);
expect(db.getTask(taskId).requiresUserReview).toBe(false);
expect(db.getApprovalRecord(approvalId).state).toBe("approved");
db.close();
});
});
@@ -32,6 +32,7 @@ describe("ipc-core-admin-handlers connector saves", () => {
db,
ensureCompanyExists: () => null,
publishDomainChanged: vi.fn(),
handleApprovalResolved: vi.fn(),
checkConnector: vi.fn(),
openConnectorAuthTerminal: vi.fn(),
openGenericTerminal: vi.fn(),