update
This commit is contained in:
1 parent
0cd65521b8
commit
48daca9ba0
18 files changed
+192
-91
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agent-company",
|
||||
"version": "0.4.29",
|
||||
"version": "0.4.36",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Cross-platform Electron desktop control plane for local-first AI agent companies.",
|
||||
|
||||
@@ -41,8 +41,8 @@ function parseModelProvider(model: string | null): string | null {
|
||||
return trimmed.slice(0, trimmed.indexOf("/")).trim() || null;
|
||||
}
|
||||
|
||||
function claudeSkillsHome(): string {
|
||||
return path.join(os.homedir(), ".claude", "skills");
|
||||
function openCodeSkillsHome(): string {
|
||||
return path.join(os.homedir(), ".opencode", "skills");
|
||||
}
|
||||
|
||||
async function resolveAgentCompanySkillsDir(): Promise<string | null> {
|
||||
@@ -57,7 +57,7 @@ async function ensureOpenCodeSkillsInjected(onLog: AdapterExecutionContext["onLo
|
||||
const skillsDir = await resolveAgentCompanySkillsDir();
|
||||
if (!skillsDir) return;
|
||||
|
||||
const skillsHome = claudeSkillsHome();
|
||||
const skillsHome = openCodeSkillsHome();
|
||||
await fs.mkdir(skillsHome, { recursive: true });
|
||||
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
|
||||
@@ -781,16 +781,18 @@ export function createAutomationService({
|
||||
const isSuccess = event.status === "succeeded";
|
||||
const isFailed = event.status === "failed" || event.status === "timed_out";
|
||||
|
||||
if (isSuccess && task.status === "in_progress") {
|
||||
let effectiveStatus = task.status;
|
||||
|
||||
if (isSuccess && effectiveStatus === "in_progress") {
|
||||
db.advanceTaskStatus(task.id, "in_review", "Run completed successfully. Awaiting review.");
|
||||
handleTaskStatusChange(task.id, run.companyId, "in_progress", "in_review");
|
||||
effectiveStatus = "in_review";
|
||||
publishDomainChanged();
|
||||
}
|
||||
|
||||
if (isSuccess && task.title.startsWith("[Heartbeat]") && task.status !== "done") {
|
||||
const previousStatus = task.status;
|
||||
if (isSuccess && task.title.startsWith("[Heartbeat]") && effectiveStatus !== "done") {
|
||||
db.advanceTaskStatus(task.id, "done", "Heartbeat check completed.");
|
||||
handleTaskStatusChange(task.id, run.companyId, previousStatus, "done");
|
||||
handleTaskStatusChange(task.id, run.companyId, effectiveStatus, "done");
|
||||
}
|
||||
|
||||
const automationContext: AutomationEventContext = {
|
||||
@@ -811,8 +813,7 @@ export function createAutomationService({
|
||||
dispatchAutomation("run_completed", automationContext);
|
||||
if (task.parentId) {
|
||||
try {
|
||||
const freshSnapshot = db.listSnapshot();
|
||||
const siblings = freshSnapshot.tasks.filter((entry) => entry.parentId === task!.parentId && entry.companyId === run.companyId);
|
||||
const siblings = db.listChildTasks(task.parentId, run.companyId);
|
||||
const allSiblingsDoneOrReview = siblings.every((entry) => entry.status === "done" || entry.status === "in_review");
|
||||
if (allSiblingsDoneOrReview) {
|
||||
const parent = db.getTask(task.parentId);
|
||||
@@ -1017,8 +1018,7 @@ export function createAutomationService({
|
||||
|
||||
if (newStatus === "done" && task.parentId) {
|
||||
try {
|
||||
const snapshot = db.listSnapshot();
|
||||
const siblings = snapshot.tasks.filter((entry) => entry.parentId === task!.parentId && entry.companyId === companyId);
|
||||
const siblings = db.listChildTasks(task.parentId, companyId);
|
||||
const allDone = siblings.every((entry) => entry.id === taskId ? true : entry.status === "done");
|
||||
if (allDone) {
|
||||
const parent = db.getTask(task.parentId);
|
||||
|
||||
@@ -76,7 +76,18 @@ export async function openConnectorAuthTerminal(connectorId: ConnectorId, comman
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
const child = spawn("cmd.exe", ["/c", "start", "", "cmd.exe", "/k", script], {
|
||||
const spec = connectorAuthSpec(connectorId, command);
|
||||
const winScript = [
|
||||
`@echo off`,
|
||||
`echo ${spec.intro}`,
|
||||
`echo.`,
|
||||
`cd /d "${targetCwd}"`,
|
||||
spec.shellCommand,
|
||||
`echo.`,
|
||||
`echo You can close this terminal after the login flow completes.`,
|
||||
`pause`,
|
||||
].join(" & ");
|
||||
const child = spawn("cmd.exe", ["/c", "start", "", "cmd.exe", "/k", winScript], {
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
shell: false,
|
||||
|
||||
@@ -37,7 +37,7 @@ export function createConnectorHealthService({
|
||||
return new Promise((resolve) => {
|
||||
let resolved = false;
|
||||
const child = spawn(command, ["--version"], {
|
||||
env: process.env,
|
||||
env: buildSanitizedRuntimeEnv(process.env, {}),
|
||||
shell: false,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
@@ -125,7 +125,7 @@ const CONNECTORS: ConnectorDefinition[] = [
|
||||
gracefulCancel: true,
|
||||
modelDiscovery: true,
|
||||
structuredTranscript: true,
|
||||
costAttribution: false,
|
||||
costAttribution: true,
|
||||
},
|
||||
configurationDoc: opencodeDoc,
|
||||
module: {
|
||||
|
||||
@@ -1986,6 +1986,69 @@ export class AppDatabase {
|
||||
});
|
||||
}
|
||||
|
||||
getCompanyStatus(companyId: string): string | null {
|
||||
const row = this.db.prepare("select status from companies where id = ?").get(companyId) as { status: string } | undefined;
|
||||
return row?.status ?? null;
|
||||
}
|
||||
|
||||
getCompanyBasic(companyId: string): { name: string; description: string } | null {
|
||||
const row = this.db.prepare("select name, description from companies where id = ?").get(companyId) as { name: string; description: string } | undefined;
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
listActiveCompanyIds(): Array<{ id: string; name: string }> {
|
||||
return this.readAll<{ id: string; name: string }>(
|
||||
"select id, name from companies where status = 'active'",
|
||||
);
|
||||
}
|
||||
|
||||
countActiveTasksForCompany(companyId: string): number {
|
||||
const row = this.db.prepare(
|
||||
"select count(*) as cnt from tasks where company_id = ? and status not in ('done', 'cancelled') and title not like '[Heartbeat]%'",
|
||||
).get(companyId) as { cnt: number };
|
||||
return row.cnt;
|
||||
}
|
||||
|
||||
countActiveGoalsForCompany(companyId: string): number {
|
||||
const row = this.db.prepare(
|
||||
"select count(*) as cnt from goals where company_id = ? and status in ('active', 'planned')",
|
||||
).get(companyId) as { cnt: number };
|
||||
return row.cnt;
|
||||
}
|
||||
|
||||
getCeoForCompany(companyId: string): { id: string; name: string; lastHeartbeatAt: string | null } | null {
|
||||
const row = this.db.prepare(
|
||||
"select id, name, last_heartbeat_at as lastHeartbeatAt from agents where company_id = ? and reports_to is null and status != 'terminated' limit 1",
|
||||
).get(companyId) as { id: string; name: string; lastHeartbeatAt: string | null } | undefined;
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
listActiveAgentsForCompany(companyId: string): Array<{ id: string; name: string; department: string | null; lastHeartbeatAt: string | null }> {
|
||||
return this.readAll<{ id: string; name: string; department: string | null; lastHeartbeatAt: string | null }>(
|
||||
"select id, name, department, last_heartbeat_at as lastHeartbeatAt from agents where company_id = ? and status not in ('terminated', 'paused', 'pending_approval')",
|
||||
companyId,
|
||||
);
|
||||
}
|
||||
|
||||
hasAgentAssignedTasks(agentId: string, companyId: string): boolean {
|
||||
const row = this.db.prepare(
|
||||
"select 1 from tasks where assignee_agent_id = ? and company_id = ? and status in ('todo', 'in_progress', 'blocked') limit 1",
|
||||
).get(agentId, companyId);
|
||||
return !!row;
|
||||
}
|
||||
|
||||
listChildTasks(parentId: string, companyId: string): Array<{ id: string; status: string; assigneeAgentId: string | null }> {
|
||||
return this.readAll<{ id: string; status: string; assigneeAgentId: string | null }>(
|
||||
`select id, status, assignee_agent_id as assigneeAgentId from tasks where parent_id = ? and company_id = ?`,
|
||||
parentId, companyId,
|
||||
);
|
||||
}
|
||||
|
||||
updateAgentStatus(agentId: string, status: string) {
|
||||
const ts = new Date().toISOString();
|
||||
this.db.prepare("update agents set status = ?, updated_at = ? where id = ?").run(status, ts, agentId);
|
||||
}
|
||||
|
||||
private getSetting(key: string): string | null {
|
||||
const row = this.db.prepare("select value from settings where key = ?").get(key) as { value: string } | undefined;
|
||||
return row?.value ?? null;
|
||||
|
||||
@@ -24,10 +24,10 @@ export const DEFAULT_AUTOMATION_RULES: ReadonlyArray<{
|
||||
},
|
||||
{
|
||||
name: "Escalate completed work to manager",
|
||||
description: "When a run completes successfully, escalate to the assignee's manager so they review the work.",
|
||||
description: "When a run completes successfully, escalate to the assignee's manager so they can review the completed work.",
|
||||
trigger: "run_completed",
|
||||
action: "escalate_to_manager",
|
||||
actionConfigJson: JSON.stringify({ body: "A task run has completed successfully. Please review the work and approve or send back for changes." }),
|
||||
actionConfigJson: JSON.stringify({ body: "A task run has completed successfully. The task has moved to review status. Please review and either approve (mark as done) or send back with feedback." }),
|
||||
priority: 80,
|
||||
},
|
||||
{
|
||||
@@ -126,6 +126,14 @@ export const DEFAULT_AUTOMATION_RULES: ReadonlyArray<{
|
||||
actionConfigJson: JSON.stringify({}),
|
||||
priority: 78,
|
||||
},
|
||||
{
|
||||
name: "Wake approvers on new approval",
|
||||
description: "When a new approval request is created and not auto-approved, escalate to the requesting agent's manager for review.",
|
||||
trigger: "approval_created",
|
||||
action: "escalate_to_manager",
|
||||
actionConfigJson: JSON.stringify({ body: "A new approval request needs your review. Please check the Approvals section." }),
|
||||
priority: 88,
|
||||
},
|
||||
];
|
||||
|
||||
export function seedDefaultAutomationRules(
|
||||
|
||||
@@ -75,6 +75,12 @@ export function registerCoreOperationHandlers({
|
||||
publishDomainChanged();
|
||||
return ok(id);
|
||||
}
|
||||
// Approval was NOT auto-approved — wake decision-makers to review it
|
||||
dispatchAutomation("approval_created", {
|
||||
companyId: parsed.companyId,
|
||||
agentId: parsed.requestedByAgentId ?? undefined,
|
||||
taskId: parsed.relatedTaskId ?? undefined,
|
||||
});
|
||||
publishDomainChanged();
|
||||
return ok(id);
|
||||
});
|
||||
|
||||
@@ -286,11 +286,15 @@ export function registerCoreWorkGraphHandlers({
|
||||
}
|
||||
const isNew = !parsed.id;
|
||||
let previousStatus: string | null = null;
|
||||
let previousAssigneeAgentId: string | null = null;
|
||||
if (!isNew) {
|
||||
try {
|
||||
previousStatus = db.getTask(parsed.id!).status;
|
||||
const existingTask = db.getTask(parsed.id!);
|
||||
previousStatus = existingTask.status;
|
||||
previousAssigneeAgentId = existingTask.assigneeAgentId;
|
||||
} catch {
|
||||
previousStatus = null;
|
||||
previousAssigneeAgentId = null;
|
||||
}
|
||||
}
|
||||
let id: string;
|
||||
@@ -309,8 +313,14 @@ export function registerCoreWorkGraphHandlers({
|
||||
});
|
||||
if (isNew) {
|
||||
handleTaskCreated(id, parsed.companyId);
|
||||
} else if (previousStatus && previousStatus !== parsed.status) {
|
||||
handleTaskStatusChange(id, parsed.companyId, previousStatus, parsed.status);
|
||||
} else {
|
||||
if (previousStatus && previousStatus !== parsed.status) {
|
||||
handleTaskStatusChange(id, parsed.companyId, previousStatus, parsed.status);
|
||||
}
|
||||
// Wake new assignee when task is reassigned (matches api-server.ts behavior)
|
||||
if (parsed.assigneeAgentId && parsed.assigneeAgentId !== previousAssigneeAgentId) {
|
||||
wakeAgentIfPossible(parsed.assigneeAgentId, parsed.companyId, "assignment");
|
||||
}
|
||||
}
|
||||
publishDomainChanged();
|
||||
return ok(id);
|
||||
|
||||
@@ -117,6 +117,7 @@ export function createOrchestrationService({
|
||||
if (!(agent.budgetMonthlyUsd > 0) || agent.spentMonthlyUsd < agent.budgetMonthlyUsd) {
|
||||
return false;
|
||||
}
|
||||
db.updateAgentStatus(agent.id, "paused");
|
||||
db.addActivity({
|
||||
companyId: agent.companyId,
|
||||
actor: "system",
|
||||
@@ -294,9 +295,8 @@ export function createOrchestrationService({
|
||||
|
||||
function startAgentHeartbeatRun(agentId: string, companyId: string, trigger: string): string | null {
|
||||
if (!hasWorkerReady()) return null;
|
||||
const snapshot = db.listSnapshot();
|
||||
const company = snapshot.companies.find((entry) => entry.id === companyId) ?? null;
|
||||
if (!company || company.status !== "active") {
|
||||
const companyStatus = db.getCompanyStatus(companyId);
|
||||
if (companyStatus !== "active") {
|
||||
logger.info(`[heartbeat] Skipping ${agentId} because company ${companyId} is not active.`);
|
||||
return null;
|
||||
}
|
||||
@@ -335,10 +335,10 @@ export function createOrchestrationService({
|
||||
}
|
||||
|
||||
const isCeo = !agent.reportsTo;
|
||||
const isManager = snapshot.agents.some((entry) => entry.reportsTo === agent.id && entry.companyId === companyId);
|
||||
const activeGoals = snapshot.goals.filter((goal) => goal.companyId === companyId && (goal.status === "active" || goal.status === "planned"));
|
||||
const companyTasks = snapshot.tasks.filter((entry) => entry.companyId === companyId && entry.status !== "done" && entry.status !== "cancelled");
|
||||
const companyNeedsBootstrap = activeGoals.length > 0 && companyTasks.length < activeGoals.length;
|
||||
const isManager = db.getDirectReports(agent.id, companyId).length > 0;
|
||||
const goalCount = db.countActiveGoalsForCompany(companyId);
|
||||
const taskCount = db.countActiveTasksForCompany(companyId);
|
||||
const companyNeedsBootstrap = goalCount > 0 && taskCount < goalCount;
|
||||
|
||||
if (isTimer && !isCeo && !isManager && !companyNeedsBootstrap) {
|
||||
logger.info(`[heartbeat] Skipping ${agent.name} — no actionable tasks and trigger is timer.`);
|
||||
@@ -388,6 +388,7 @@ export function createOrchestrationService({
|
||||
connectorId: connector.id,
|
||||
});
|
||||
|
||||
const companyBasic = db.getCompanyBasic(companyId);
|
||||
dispatchRunToWorker({
|
||||
runId: run.id,
|
||||
companyId,
|
||||
@@ -396,9 +397,9 @@ export function createOrchestrationService({
|
||||
connector,
|
||||
workspace,
|
||||
workspaceMode: workspaceResolution.lockMode,
|
||||
companyName: company.name,
|
||||
companyDescription: company.description ?? "",
|
||||
snapshot,
|
||||
companyName: companyBasic?.name ?? "Unknown",
|
||||
companyDescription: companyBasic?.description ?? "",
|
||||
snapshot: db.listSnapshot(),
|
||||
trigger,
|
||||
});
|
||||
|
||||
@@ -446,9 +447,8 @@ export function createOrchestrationService({
|
||||
return fail("WORKER_UNAVAILABLE", "Orchestration worker is not ready.");
|
||||
}
|
||||
const task = db.getTask(taskId);
|
||||
const snapshot = db.listSnapshot();
|
||||
const company = snapshot.companies.find((entry) => entry.id === task.companyId) ?? null;
|
||||
if (!company || company.status !== "active") {
|
||||
const companyStatus = db.getCompanyStatus(task.companyId);
|
||||
if (companyStatus !== "active") {
|
||||
return fail("COMPANY_NOT_ACTIVE", "Runs can only start while the company is active.");
|
||||
}
|
||||
if (task.activeRunId || hasActiveRun({ taskId: task.id, companyId: task.companyId })) {
|
||||
@@ -498,6 +498,7 @@ export function createOrchestrationService({
|
||||
connectorId: connector.id,
|
||||
});
|
||||
|
||||
const companyBasic = db.getCompanyBasic(task.companyId);
|
||||
dispatchRunToWorker({
|
||||
runId: run.id,
|
||||
companyId: task.companyId,
|
||||
@@ -506,9 +507,9 @@ export function createOrchestrationService({
|
||||
connector,
|
||||
workspace,
|
||||
workspaceMode: workspaceResolution.lockMode,
|
||||
companyName: company.name,
|
||||
companyDescription: company.description,
|
||||
snapshot,
|
||||
companyName: companyBasic?.name ?? "Unknown",
|
||||
companyDescription: companyBasic?.description ?? "",
|
||||
snapshot: db.listSnapshot(),
|
||||
trigger,
|
||||
});
|
||||
db.addActivity({
|
||||
@@ -579,10 +580,10 @@ export function createOrchestrationService({
|
||||
heartbeatTimer = setInterval(() => {
|
||||
try {
|
||||
if (!hasWorkerReady()) return;
|
||||
const snapshot = db.listSnapshot();
|
||||
const activeCompanies = db.listActiveCompanyIds();
|
||||
const now = Date.now();
|
||||
|
||||
for (const company of snapshot.companies.filter((entry) => entry.status === "active")) {
|
||||
for (const company of activeCompanies) {
|
||||
try {
|
||||
const eligibleAgents = db.listHeartbeatEligibleAgents(company.id);
|
||||
for (const agent of eligibleAgents) {
|
||||
@@ -596,14 +597,14 @@ export function createOrchestrationService({
|
||||
startAgentHeartbeatRun(agent.id, company.id, "timer");
|
||||
}
|
||||
|
||||
const companyGoals = snapshot.goals.filter((goal) => goal.companyId === company.id && (goal.status === "active" || goal.status === "planned"));
|
||||
const companyActiveTasks = snapshot.tasks.filter((task) => task.companyId === company.id && !["done", "cancelled"].includes(task.status) && !task.title.startsWith("[Heartbeat]"));
|
||||
if (companyGoals.length > 0 && companyActiveTasks.length === 0) {
|
||||
const ceo = snapshot.agents.find((agent) => agent.companyId === company.id && !agent.reportsTo && agent.status !== "terminated");
|
||||
const goalCount = db.countActiveGoalsForCompany(company.id);
|
||||
const taskCount = db.countActiveTasksForCompany(company.id);
|
||||
if (goalCount > 0 && taskCount === 0) {
|
||||
const ceo = db.getCeoForCompany(company.id);
|
||||
const ceoLastWake = ceo?.lastHeartbeatAt ? new Date(ceo.lastHeartbeatAt).getTime() : 0;
|
||||
const bootstrapCooldown = 120_000;
|
||||
if (ceo && !hasActiveRun({ agentId: ceo.id, companyId: company.id }) && (now - ceoLastWake > bootstrapCooldown)) {
|
||||
logger.info(`[heartbeat] Company ${company.name} has ${companyGoals.length} goals but no tasks — bootstrapping CEO ${ceo.name}`);
|
||||
logger.info(`[heartbeat] Company ${company.name} has ${goalCount} goals but no tasks — bootstrapping CEO ${ceo.name}`);
|
||||
startAgentHeartbeatRun(ceo.id, company.id, "bootstrap");
|
||||
}
|
||||
}
|
||||
@@ -615,12 +616,9 @@ export function createOrchestrationService({
|
||||
scheduleLastFiredMap.set(scheduleKey, now);
|
||||
}
|
||||
|
||||
const companyAgents = snapshot.agents.filter((agent) => agent.companyId === company.id && agent.status !== "terminated" && agent.status !== "paused" && agent.status !== "pending_approval");
|
||||
const companyAgents = db.listActiveAgentsForCompany(company.id);
|
||||
for (const agent of companyAgents) {
|
||||
const hasAssigned = snapshot.tasks.some(
|
||||
(task) => task.assigneeAgentId === agent.id && task.companyId === company.id && ["todo", "in_progress", "blocked"].includes(task.status),
|
||||
);
|
||||
if (!hasAssigned) {
|
||||
if (!db.hasAgentAssignedTasks(agent.id, company.id)) {
|
||||
const lastActive = agent.lastHeartbeatAt ? new Date(agent.lastHeartbeatAt).getTime() : 0;
|
||||
const idleThreshold = 20 * 60 * 1000;
|
||||
if (now - lastActive > idleThreshold) {
|
||||
|
||||
@@ -321,6 +321,18 @@ export function createWorkerService({
|
||||
}
|
||||
|
||||
processRunCompletion(run, event as z.infer<typeof workerMessageSchema> & { type: "run-finished" });
|
||||
|
||||
// Self-wake: pick up next assigned task immediately instead of waiting for heartbeat timer
|
||||
if (pendingWakes.length === 0) {
|
||||
try {
|
||||
const actionableTasks = db.listAgentTasks(run.agentId, run.companyId, ["todo", "in_progress"]);
|
||||
if (actionableTasks.length > 0) {
|
||||
wakeAgentIfPossible(run.agentId, run.companyId, "task_continuation");
|
||||
}
|
||||
} catch {
|
||||
// Best-effort self-wake; agent will pick up work on the next heartbeat cycle.
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+6
-22
@@ -314,22 +314,6 @@ export function App() {
|
||||
const companyId = snapshot.currentCompanyId;
|
||||
let cancelled = false;
|
||||
|
||||
if (section === "inbox") {
|
||||
setInboxItems([]);
|
||||
}
|
||||
|
||||
if (section === "performance") {
|
||||
setCompanyMetrics(null);
|
||||
}
|
||||
|
||||
if (section === "standup") {
|
||||
setStandupReport(null);
|
||||
}
|
||||
|
||||
if (section === "social") {
|
||||
setSocialAccounts([]);
|
||||
}
|
||||
|
||||
if (section === "inbox") {
|
||||
void window.agentCompany.getInbox(companyId).then((result) => {
|
||||
if (!cancelled && result.ok) setInboxItems(result.data);
|
||||
@@ -1266,7 +1250,7 @@ export function App() {
|
||||
<SprintManager companyId={currentCompany.id} sprints={companySprints} onRefresh={refresh} />
|
||||
) : null}
|
||||
|
||||
{section === "workspaces" ? (
|
||||
{section === "workspaces" && currentCompany ? (
|
||||
<EntityWorkspace
|
||||
title="Workspaces"
|
||||
eyebrow={undefined}
|
||||
@@ -1304,7 +1288,7 @@ export function App() {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{section === "settings" ? (
|
||||
{section === "settings" && currentCompany ? (
|
||||
<div className="space-y-10">
|
||||
<Panel title="Profile and release">
|
||||
<SettingsPane snapshot={snapshot} secrets={companySecrets} companyId={currentCompany!.id} onSaved={refresh} />
|
||||
@@ -1340,10 +1324,10 @@ export function App() {
|
||||
compact
|
||||
value={selectedRun?.id ?? ""}
|
||||
onChange={setSelectedRunId}
|
||||
options={(runs.length > 0 ? runs : [{ id: "", summary: "No runs" } as RunRecord]).map((run) => ({
|
||||
value: run.id,
|
||||
label: run.summary || `Run ${run.id.slice(0, 8)}`,
|
||||
}))}
|
||||
options={runs.length > 0
|
||||
? runs.map((run) => ({ value: run.id, label: run.summary || `Run ${run.id.slice(0, 8)}` }))
|
||||
: [{ value: "", label: "No runs" }]
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -37,8 +37,10 @@ const triggerConfig: Record<AutomationTrigger, { label: string; description: str
|
||||
task_status_changed: { label: "Task Status Changed", description: "When a task transitions to a new status", icon: ArrowsClockwise },
|
||||
task_created: { label: "Task Created", description: "When a new task is added to the system", icon: Plus },
|
||||
task_blocked: { label: "Task Blocked", description: "When a task becomes blocked by a dependency", icon: Pause },
|
||||
task_reassigned: { label: "Task Reassigned", description: "When a task is reassigned to a different agent", icon: ArrowsClockwise },
|
||||
run_completed: { label: "Run Completed", description: "When an agent run finishes successfully", icon: Play },
|
||||
run_failed: { label: "Run Failed", description: "When an agent run encounters an error", icon: Trash },
|
||||
approval_created: { label: "Approval Created", description: "When a new approval request needs review", icon: Eye },
|
||||
approval_resolved: { label: "Approval Resolved", description: "When a pending approval is decided", icon: Eye },
|
||||
budget_threshold: { label: "Budget Threshold", description: "When spending exceeds a configured limit", icon: Funnel },
|
||||
agent_idle: { label: "Agent Idle", description: "When an agent has no tasks and becomes idle", icon: Robot },
|
||||
|
||||
@@ -501,10 +501,11 @@ export function DocumentCenter({
|
||||
title="Delete document"
|
||||
message="This removes the document from the company knowledge surface."
|
||||
onCancel={() => setDeleteId(null)}
|
||||
onConfirm={() => {
|
||||
onConfirm={async () => {
|
||||
if (!deleteId) return;
|
||||
void window.agentCompany.deleteDocument({ id: deleteId, companyId }).then(() => onRefresh());
|
||||
const result = await window.agentCompany.deleteDocument({ id: deleteId, companyId });
|
||||
setDeleteId(null);
|
||||
if (result.ok) await onRefresh();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -568,10 +569,11 @@ export function KnowledgeBaseManager({
|
||||
title="Delete knowledge entry"
|
||||
message="This removes the entry from the shared company knowledge base."
|
||||
onCancel={() => setDeleteId(null)}
|
||||
onConfirm={() => {
|
||||
onConfirm={async () => {
|
||||
if (!deleteId) return;
|
||||
void window.agentCompany.deleteKnowledgeEntry({ id: deleteId, companyId }).then(() => onRefresh());
|
||||
const result = await window.agentCompany.deleteKnowledgeEntry({ id: deleteId, companyId });
|
||||
setDeleteId(null);
|
||||
if (result.ok) await onRefresh();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -638,10 +640,11 @@ export function MeetingsManager({
|
||||
title="Delete meeting"
|
||||
message="This removes the meeting and its operational context from the company timeline."
|
||||
onCancel={() => setDeleteId(null)}
|
||||
onConfirm={() => {
|
||||
onConfirm={async () => {
|
||||
if (!deleteId) return;
|
||||
void window.agentCompany.deleteMeeting({ id: deleteId, companyId }).then(() => onRefresh());
|
||||
const result = await window.agentCompany.deleteMeeting({ id: deleteId, companyId });
|
||||
setDeleteId(null);
|
||||
if (result.ok) await onRefresh();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -705,10 +708,11 @@ export function SprintManager({
|
||||
title="Delete sprint"
|
||||
message="This removes the sprint board entry. Historical run and task records remain."
|
||||
onCancel={() => setDeleteId(null)}
|
||||
onConfirm={() => {
|
||||
onConfirm={async () => {
|
||||
if (!deleteId) return;
|
||||
void window.agentCompany.deleteSprint({ id: deleteId, companyId }).then(() => onRefresh());
|
||||
const result = await window.agentCompany.deleteSprint({ id: deleteId, companyId });
|
||||
setDeleteId(null);
|
||||
if (result.ok) await onRefresh();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "@phosphor-icons/react";
|
||||
import type { AgentRecord, ApprovalRecord } from "@shared/types";
|
||||
import { getAgentInitials } from "../lib/formatters";
|
||||
import { unwrap } from "../lib/desktop";
|
||||
import { ActionButton, EmptyState, Panel, StatusPill } from "./ui";
|
||||
|
||||
interface HiringPipelineProps {
|
||||
@@ -189,12 +190,12 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
|
||||
icon={Check}
|
||||
tone="accent"
|
||||
onClick={async () => {
|
||||
await window.agentCompany.decideApproval({
|
||||
unwrap(await window.agentCompany.decideApproval({
|
||||
companyId: a.companyId,
|
||||
approvalId: a.id,
|
||||
state: "approved",
|
||||
decisionNote: "Hire approved via hiring pipeline",
|
||||
});
|
||||
}));
|
||||
await onRefresh();
|
||||
}}
|
||||
/>
|
||||
@@ -203,12 +204,12 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent
|
||||
icon={X}
|
||||
tone="danger"
|
||||
onClick={async () => {
|
||||
await window.agentCompany.decideApproval({
|
||||
unwrap(await window.agentCompany.decideApproval({
|
||||
companyId: a.companyId,
|
||||
approvalId: a.id,
|
||||
state: "rejected",
|
||||
decisionNote: "Hire rejected via hiring pipeline",
|
||||
});
|
||||
}));
|
||||
await onRefresh();
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -271,7 +271,7 @@ export const heartbeatSettingsSchema = z.object({
|
||||
export const triggerHeartbeatSchema = z.object({
|
||||
agentId: z.string().uuid(),
|
||||
companyId: z.string().uuid(),
|
||||
trigger: z.enum(["manual", "timer", "assignment", "goal_activated", "approval_resolved", "subtask_completed", "comment", "report_blocked", "report_failed", "recovery", "message", "peer_request"]).default("manual"),
|
||||
trigger: z.enum(["manual", "timer", "assignment", "goal_activated", "approval_resolved", "subtask_completed", "comment", "report_blocked", "report_failed", "recovery", "message", "peer_request", "task_continuation"]).default("manual"),
|
||||
});
|
||||
|
||||
export const metricsQuerySchema = z.object({
|
||||
@@ -393,10 +393,10 @@ export const automationRuleInputSchema = z.object({
|
||||
name: z.string().min(2).max(120),
|
||||
description: z.string().max(500).default(""),
|
||||
trigger: z.enum([
|
||||
"task_status_changed", "task_created", "task_blocked", "run_completed",
|
||||
"run_failed", "approval_resolved", "budget_threshold", "agent_idle",
|
||||
"comment_posted", "document_created", "schedule", "hire_approved",
|
||||
"sprint_started", "sprint_ended",
|
||||
"task_status_changed", "task_created", "task_blocked", "task_reassigned",
|
||||
"run_completed", "run_failed", "approval_created", "approval_resolved",
|
||||
"budget_threshold", "agent_idle", "comment_posted", "document_created",
|
||||
"schedule", "hire_approved", "sprint_started", "sprint_ended",
|
||||
]),
|
||||
conditionsJson: z.string().default("{}"),
|
||||
action: z.enum([
|
||||
@@ -471,10 +471,10 @@ export const workflowInputSchema = z.object({
|
||||
description: z.string().max(500).default(""),
|
||||
stepsJson: z.string().default("[]"),
|
||||
triggerType: z.enum([
|
||||
"task_status_changed", "task_created", "task_blocked", "run_completed",
|
||||
"run_failed", "approval_resolved", "budget_threshold", "agent_idle",
|
||||
"comment_posted", "document_created", "schedule", "hire_approved",
|
||||
"sprint_started", "sprint_ended",
|
||||
"task_status_changed", "task_created", "task_blocked", "task_reassigned",
|
||||
"run_completed", "run_failed", "approval_created", "approval_resolved",
|
||||
"budget_threshold", "agent_idle", "comment_posted", "document_created",
|
||||
"schedule", "hire_approved", "sprint_started", "sprint_ended",
|
||||
]),
|
||||
triggerConfigJson: z.string().default("{}"),
|
||||
status: z.enum(["draft", "active", "paused", "completed", "failed"]).default("draft"),
|
||||
|
||||
@@ -639,8 +639,10 @@ export type AutomationTrigger =
|
||||
| "task_status_changed"
|
||||
| "task_created"
|
||||
| "task_blocked"
|
||||
| "task_reassigned"
|
||||
| "run_completed"
|
||||
| "run_failed"
|
||||
| "approval_created"
|
||||
| "approval_resolved"
|
||||
| "budget_threshold"
|
||||
| "agent_idle"
|
||||
|
||||
Reference in new issue
Block a user