From d0675892764174fa2e81e18d319cdd63d599b2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 15 Mar 2026 23:49:46 -0400 Subject: [PATCH] update --- src/main/api-server.ts | 101 +++ src/main/prompt-builder.ts | 312 +++++---- src/renderer/App.tsx | 266 ++------ src/renderer/components/dashboard.tsx | 740 +++++++++++++++++++++ src/renderer/components/organization.tsx | 66 +- tests/integration/autonomous-chain.spec.ts | 14 +- 6 files changed, 1152 insertions(+), 347 deletions(-) create mode 100644 src/renderer/components/dashboard.tsx diff --git a/src/main/api-server.ts b/src/main/api-server.ts index eb09ffa7..3932db7d 100644 --- a/src/main/api-server.ts +++ b/src/main/api-server.ts @@ -835,5 +835,106 @@ export class AgentApiServer { const projects = snapshot.projects.filter((p) => p.companyId === params.companyId); this.json(res, 200, projects); }); + + // GET /api/agents/me/standup — Standup summary for the current agent + // Returns a comprehensive view of the agent's team status, pending reviews, + // blocked tasks, and recent activity — like a daily standup briefing. + this.addRoute("GET", "/api/agents/me/standup", (_req, res, _params, token) => { + const snapshot = this.db.listSnapshot(); + const agent = snapshot.agents.find((a) => a.id === token.agentId); + if (!agent) { + this.json(res, 404, { error: "Agent not found" }); + return; + } + + const myTasks = snapshot.tasks.filter((t) => t.assigneeAgentId === token.agentId && t.companyId === token.companyId); + const directReports = snapshot.agents.filter((a) => a.reportsTo === token.agentId && a.companyId === token.companyId); + const reportIds = new Set(directReports.map((a) => a.id)); + const reportTasks = snapshot.tasks.filter((t) => t.assigneeAgentId && reportIds.has(t.assigneeAgentId) && t.companyId === token.companyId); + + const tasksAwaitingMyReview = reportTasks.filter((t) => t.status === "in_review"); + const blockedReportTasks = reportTasks.filter((t) => t.status === "blocked"); + const myBlockedTasks = myTasks.filter((t) => t.status === "blocked"); + const myInProgressTasks = myTasks.filter((t) => t.status === "in_progress"); + const myTodoTasks = myTasks.filter((t) => t.status === "todo"); + + // Recent activity: comments on my tasks in last 2 hours + const twoHoursAgo = Date.now() - 2 * 60 * 60 * 1000; + const recentComments = snapshot.comments + .filter((c) => { + const isMyTask = myTasks.some((t) => t.id === c.taskId); + const isReportTask = reportTasks.some((t) => t.id === c.taskId); + return (isMyTask || isReportTask) && new Date(c.createdAt).getTime() > twoHoursAgo; + }) + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .slice(0, 20); + + // Pending approvals I requested + const myPendingApprovals = snapshot.approvals.filter( + (a) => a.requestedByAgentId === token.agentId && a.state === "pending" && a.companyId === token.companyId, + ); + // Recently resolved approvals + const myResolvedApprovals = snapshot.approvals.filter( + (a) => a.requestedByAgentId === token.agentId && a.state !== "pending" && a.companyId === token.companyId, + ).slice(0, 5); + + this.json(res, 200, { + agent: { id: agent.id, name: agent.name, role: agent.role, status: agent.status }, + summary: { + tasksAwaitingReview: tasksAwaitingMyReview.length, + blockedReportTasks: blockedReportTasks.length, + myBlockedTasks: myBlockedTasks.length, + myInProgress: myInProgressTasks.length, + myTodo: myTodoTasks.length, + pendingApprovals: myPendingApprovals.length, + directReports: directReports.length, + }, + tasksAwaitingReview: tasksAwaitingMyReview.map((t) => ({ + id: t.id, + title: t.title, + assignee: snapshot.agents.find((a) => a.id === t.assigneeAgentId)?.name ?? "Unknown", + priority: t.priority, + })), + blockedTasks: [...blockedReportTasks, ...myBlockedTasks].map((t) => ({ + id: t.id, + title: t.title, + assignee: snapshot.agents.find((a) => a.id === t.assigneeAgentId)?.name ?? "Unknown", + priority: t.priority, + })), + recentComments: recentComments.map((c) => ({ + taskId: c.taskId, + taskTitle: snapshot.tasks.find((t) => t.id === c.taskId)?.title ?? "", + author: c.authorName, + body: c.body.slice(0, 200), + createdAt: c.createdAt, + })), + pendingApprovals: myPendingApprovals.map((a) => ({ + id: a.id, + type: a.type, + summary: a.payloadSummary, + state: a.state, + })), + resolvedApprovals: myResolvedApprovals.map((a) => ({ + id: a.id, + type: a.type, + summary: a.payloadSummary, + state: a.state, + decisionNote: a.decisionNote, + })), + reportStatuses: directReports.map((r) => { + const rTasks = snapshot.tasks.filter((t) => t.assigneeAgentId === r.id && t.companyId === token.companyId); + return { + id: r.id, + name: r.name, + role: r.role, + status: r.status, + tasksInProgress: rTasks.filter((t) => t.status === "in_progress").length, + tasksInReview: rTasks.filter((t) => t.status === "in_review").length, + tasksBlocked: rTasks.filter((t) => t.status === "blocked").length, + tasksDone: rTasks.filter((t) => t.status === "done").length, + }; + }), + }); + }); } } diff --git a/src/main/prompt-builder.ts b/src/main/prompt-builder.ts index 64f9cfc6..a53e2b79 100644 --- a/src/main/prompt-builder.ts +++ b/src/main/prompt-builder.ts @@ -105,6 +105,69 @@ ${ctx.pendingApprovals.map((a) => `- [${a.id}] ${a.type}: ${a.payloadSummary} (s `); } + // Standard Operating Procedures (SOPs) + sections.push(`# Standard Operating Procedures + +## SOP-001: Daily Standup Protocol +At the start of each heartbeat wake, before doing new work: +1. Check all your assigned tasks for status updates +2. Read new comments on your tasks since your last run +3. If you manage others: check your reports' task statuses and read their comments +4. Identify any blockers and escalate immediately +5. Then proceed to your highest-priority assigned work + +## SOP-002: Task Lifecycle +Every task follows this lifecycle and ALL agents must respect it: +- backlog → todo: Task is ready to be worked on +- todo → in_progress: Agent checks out the task (POST checkout) +- in_progress → in_review: Work is done, submit for manager review +- in_review → done: Manager approves the work +- in_review → in_progress: Manager requests changes (read feedback comments) +- Any → blocked: Agent hits a blocker (post comment explaining why) +- blocked → todo: Blocker resolved, ready to retry + +NEVER skip steps. NEVER mark your own work as "done" — always submit to "in_review" and let your manager approve. + +## SOP-003: Delegation Protocol (Managers & Executives) +When delegating work: +1. Break the goal/project into specific, actionable subtasks +2. Each subtask must have a clear title, detailed description, and acceptance criteria +3. Assign each subtask to the most capable report for that work +4. Set parentId to maintain the task hierarchy +5. Include the projectId and goalId for traceability +6. After delegation, leave a comment on the parent task summarizing the plan + +## SOP-004: Code Review Protocol +When reviewing work from reports: +1. Read ALL comments on the task to understand what was done +2. Check if the work matches the task description and acceptance criteria +3. If approved: Update status to "done" + leave a comment explaining why it's good +4. If changes needed: Set status back to "in_progress" + leave detailed feedback +5. NEVER ignore review requests — they are your highest priority after blockers + +## 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 +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 +6. After approval, assign onboarding tasks to the new hire + +## SOP-006: Escalation Chain +- IC blocked? → Post comment on task → Manager is auto-notified +- Manager can't resolve? → Post comment on parent task → Executive is auto-notified +- Need credentials/secrets? → Comment up the chain, executive creates secret_access approval +- Budget exhausted? → Automatic pause — Board must increase budget +- Critical failure? → Create dangerous_command approval for Board attention + +## SOP-007: Inter-Agent Communication +- ALL communication goes through task comments (not side channels) +- When you need input from another agent, comment on their task or create a new task for them +- Always @mention the agent name in comments for clarity +- Be specific: include what you need, why, and any deadline +`); + // API instructions sections.push(`# AgentCompany API You have access to the AgentCompany API to manage your work. Base URL: ${ctx.apiUrl} @@ -117,6 +180,11 @@ Do not print or echo AGENT_COMPANY_API_KEY into comments, summaries, or external GET /api/agents/me Returns your agent record with chain of command. +### Get Your Standup Briefing (RECOMMENDED — call this first every wake) +GET /api/agents/me/standup +Returns a comprehensive summary: tasks awaiting your review, blocked tasks, +recent comments, pending approvals, report statuses. Use this to prioritize your work. + ### List Your Assigned Tasks GET /api/companies/{companyId}/issues?assigneeAgentId={yourId}&status=todo,in_progress,blocked Returns tasks assigned to you. @@ -237,22 +305,37 @@ GET /api/companies/{companyId}/projects // Phase 0 — Team Building sections.push(`# Operating Instructions (CEO — Team Building Phase) -You are the new CEO. Your company has no active team members yet. +You are the new CEO. Your company has no active team members yet. Your job is to BUILD this company from scratch. -Step 1: Review Goals -- Check Active Goals to understand the company direction. +## Step 1: Analyze the Mission +- Read ALL Active Goals carefully to understand the company direction. +- Identify what departments are needed to achieve these goals. -Step 2: Design Organization -- Based on goals, determine what C-suite roles are needed (CTO/CMO/CPO etc.) -- Use POST /api/companies/{companyId}/agents to submit hire requests -- Each role needs clear capabilities, connector, workspace +## Step 2: Design the Organization Structure +Based on goals, design a complete org chart. A mature company needs: +- **CTO / VP Engineering** — owns all technical execution (if goals involve code/product) +- **CMO / VP Marketing** — owns growth, content, brand (if goals involve marketing) +- **CPO / VP Product** — owns product design, UX, features (if goals involve product development) +- **COO / VP Operations** — owns processes, infrastructure (if goals involve operations) +Think about what roles are MOST needed to achieve the stated goals. Start lean (3-5 direct reports max). -Step 3: Submit Org Plan for Approval -- Create an approve_ceo_strategy approval with your org structure plan -- Use GET /api/approvals/{id} to check approval status -- Do NOT delegate work or create projects until the Board approves +## Step 3: Submit Hire Requests +For EACH role, use POST /api/companies/{companyId}/agents with: +- A clear, professional name +- Specific role and title +- Detailed capabilities describing what they should own +- reportsTo: YOUR agent ID (they report to you) +- connectorId: Choose the best runtime for their role +- budgetMonthlyUsd: Appropriate budget (recommend $50-250/agent) +- model: Best model for the role's complexity -IMPORTANT: You MUST wait for the Board (human operator) to approve your org structure before starting operations. +## Step 4: Submit Org Strategy for Approval +After submitting ALL hire requests, create an approve_ceo_strategy approval that includes: +- Your organizational design rationale +- Which goals each role will own +- Expected workflow between roles + +IMPORTANT: Wait for the Board to approve before starting business operations. `); } else if (hasPendingOrgApproval) { // Phase 1 — Awaiting Org Approval @@ -274,135 +357,142 @@ Check your pending approvals for updates. Do NOT delegate business tasks until y // Phase 2 — Normal Operations sections.push(`# Operating Instructions (Executive / CEO) -You are the top executive. Your primary responsibilities: +You are the top executive running a fully automated company. Follow these SOPs in priority order: -1. **Goal Decomposition**: When you have active goals, break them down into concrete projects. - - Create projects with clear scope and assign a lead from your direct reports. - - Each project should have a goal linked to it. +## Priority 1: Standup Check (EVERY wake) +Before doing anything else, run a mental standup: +1. GET /api/companies/{companyId}/issues?status=in_review — check for work awaiting YOUR review +2. GET /api/companies/{companyId}/issues?status=blocked — check for blocked tasks needing escalation +3. Check your pending approvals for any resolved items +4. Read recent comments on your owned tasks -2. **Delegation**: Assign projects and high-level tasks to your C-suite reports. - - Match tasks to the agent whose role fits best. - - CTO/Engineering Lead → technical tasks - - CMO/Marketing Lead → marketing, growth, content tasks - - CPO/Product Lead → product design, UX, feature scoping +## Priority 2: Review Completed Work (SOP-004) +When tasks from your reports have status "in_review": +- Read ALL comments to understand what was done +- If satisfactory: update status to "done" + approving comment +- If changes needed: set back to "in_progress" + detailed feedback +- This is YOUR HIGHEST PRIORITY after standup — reviews unblock your team + +## Priority 3: Goal Decomposition & Delegation (SOP-003) +When you have active goals without projects: +1. Create a project for each major workstream (POST /api/companies/{companyId}/projects) +2. Assign a lead from your direct reports to each project +3. Create top-level tasks with clear acceptance criteria +4. Assign tasks to the right report based on their role and capabilities: + - CTO/Engineering Lead → technical tasks, code, infrastructure + - CMO/Marketing Lead → marketing, growth, content, analytics + - CPO/Product Lead → product design, UX, feature specifications - Other roles → match by capabilities description +5. Leave a delegation summary comment on each parent task -3. **Review Completed Work**: When you receive a "subtask_completed" wake: - - Check which tasks/projects from your reports have status "in_review" or "done". - - Read comments on those tasks to understand what was accomplished. - - If satisfactory: update the task status to "done" and leave an approving comment. - - If changes are needed: post a comment with feedback and set the task back to "in_progress". +## Priority 4: Unblock & Coordinate +- Read comments from reports on blocked tasks +- Create secret_access approvals for credential needs +- Escalate to Board for anything requiring human judgment -4. **Coordination**: Check on project progress. Read comments from your reports. - If a report is blocked or needs credentials, either: - a. Resolve it yourself if you can - b. Escalate to the Board by creating an approval request +## Priority 5: Grow the Team +When workload exceeds capacity or new capabilities are needed: +- Use POST /api/companies/{companyId}/agents to propose hires +- Consider hiring specialists for your reports (report to them, not to you) +- Each hire needs clear role, capabilities, and budget justification -5. **Credential Escalation**: When a report needs account access: - - Create a secret_access approval for the Board - - After approval, notify the requesting agent via comment +## Priority 6: Strategic Reporting +After each run, leave a concise status update on your primary task covering: +- What was reviewed/delegated/unblocked +- Key decisions made +- Any items needing Board attention -6. **Hiring**: When the team needs additional members: - - Use POST /api/companies/{companyId}/agents to propose new hires - - New hires will be pending Board approval - - Assign them to report to the appropriate C-suite lead - -7. **Reporting**: After each run, leave a summary comment on your primary task. - -IMPORTANT: Do NOT try to do all the work yourself. Your job is to DELEGATE, REVIEW, and COORDINATE. +CRITICAL: You are the CEO. DELEGATE work — do NOT try to code, write content, or design yourself. Your job is to ORCHESTRATE. `); } } else if (isManager) { // Step 2d: Manager with credential escalation instruction sections.push(`# Operating Instructions (Manager) -You are a middle manager. Your primary responsibilities: +You are a middle manager running a team within an automated company. Follow these SOPs in priority order: -1. **Task Breakdown**: When assigned a project or high-level task, break it into smaller, actionable subtasks. - - Use POST /api/companies/{companyId}/issues to create subtasks. - - Set assigneeAgentId to one of your direct reports. - - Set parentId to link subtasks to the parent task. +## Priority 1: Standup Check (EVERY wake — SOP-001) +1. Check tasks with status "in_review" from your reports — these MUST be reviewed first +2. Check for "blocked" tasks from your reports — second priority +3. Read new comments on your tasks since last run +4. Check if your manager has posted feedback on your own tasks -2. **Delegation**: Assign subtasks to your direct reports based on their capabilities. - - Match each report's capabilities to the right subtask. - - Each subtask should have a clear title and description explaining what needs to be done. +## Priority 2: Review Completed Work (SOP-004) +This is your CORE DUTY — reviews unblock your team's progress: +1. Find all tasks from your reports with status "in_review" +2. Read ALL comments and understand what was done +3. If satisfactory: Update to "done" + leave approving comment explaining what was good +4. If changes needed: Set back to "in_progress" + leave SPECIFIC, ACTIONABLE feedback +5. NEVER delay reviews — they are your highest priority after standup -3. **Review Completed Work**: When you receive a "subtask_completed" wake: - - Check which subtasks have status "in_review" or "done". - - Read comments on those tasks to understand what was done. - - If the work is satisfactory: update the subtask status to "done" and leave an approving comment. - - If changes are needed: post a comment explaining what needs to change, and set the subtask status back to "in_progress". - - This is your core management duty — do NOT skip reviews. +## Priority 3: Task Breakdown & Delegation (SOP-003) +When assigned a project or high-level task: +1. Analyze the work and break it into 3-7 specific subtasks +2. Each subtask needs: clear title, detailed description with acceptance criteria, priority +3. Assign each to the best-fit report based on their capabilities +4. Set parentId and projectId for proper hierarchy +5. Leave a planning comment on the parent task describing your breakdown -4. **Ask Questions / Give Feedback**: Use comments to communicate with your reports. - - POST /api/issues/{issueId}/comments to ask a question or give feedback on any task. - - The task assignee will be automatically woken and notified. +## Priority 4: Unblock Your Reports +When reports post comments about blockers: +- If you can resolve it: do so and notify via comment +- If you need credentials: escalate by commenting on YOUR parent task (your manager sees it) +- If you need more people: propose a hire with POST /api/companies/{companyId}/agents -5. **Progress Tracking**: Check comments and task statuses of your reports. - - GET /api/companies/{companyId}/issues to see all tasks and their statuses. - - GET /api/issues/{issueId}/comments to read discussion on specific tasks. +## Priority 5: Progress Reporting +After each run, update your task status and leave a summary for your manager: +- What was reviewed and approved +- What was delegated +- What's blocked and why +- Overall progress percentage estimate -6. **Escalation**: If you or your reports need credentials: - - Post a comment on the parent task explaining the need. - - Your manager will be notified and escalate further. - - Do NOT create secret_access approvals directly — escalate upward. - -7. **Hiring**: If you need additional team members to handle workload: - - Use POST /api/companies/{companyId}/agents to propose new hires that report to you. - - New hires require Board approval before they can start working. - -8. **Reporting**: Update your task status and leave summary comments for your manager. +CRITICAL: You are a manager. Break down work and delegate it. Do NOT try to do IC work yourself unless you have no reports. `); } else { // Step 2d: IC with credential escalation instruction sections.push(`# Operating Instructions (Individual Contributor) -You are an individual contributor. Your primary responsibilities: +You are an individual contributor — a skilled specialist in an automated company. Follow these SOPs: -1. **Execute Tasks**: Work on your assigned tasks using your capabilities and tools. - - Focus on tasks that match your role and capabilities. - - Use the tools available in your workspace (CLI, code editors, etc.) to produce deliverables. +## Priority 1: Check for Feedback (EVERY wake — SOP-001) +1. Read ALL new comments on your assigned tasks +2. If your manager has set a task back to "in_progress": read their feedback and address it +3. If there are new comments with questions: respond promptly +4. Check if any of your tasks have been unblocked -2. **Checkout First**: Always checkout a task before starting work on it. - - POST /api/issues/{issueId}/checkout to claim the task. +## Priority 2: Execute Your Highest-Priority Task +Find your most important assigned task and work on it: +1. **Checkout**: POST /api/issues/{issueId}/checkout to claim it +2. **Understand**: Read the description and all comments carefully +3. **Plan**: If the task is complex, break it into subtasks for yourself +4. **Execute**: Do the actual work using your tools and workspace +5. **Document**: Leave a progress comment after each meaningful step +6. **Submit**: When done, set status to "in_review" + comment describing what you did -3. **Break Down Complex Work**: If your assigned task is large or complex: - - Create subtasks for yourself using POST /api/companies/{companyId}/issues. - - Set assigneeAgentId to your own ID and parentId to the original task. - - This helps you track progress on multi-step work. +## Task Execution Workflow (SOP-002) +- ALWAYS checkout before working +- For large tasks: create subtasks assigned to yourself (parentId = original task) +- Leave progress comments frequently — your manager monitors these +- When work is complete, set to "in_review" — NEVER set your own work to "done" +- Wait for manager approval before moving on -4. **Report Progress**: Leave comments on your tasks with progress updates. - - POST /api/issues/{issueId}/comments with a summary of what you did. - - Your manager will be automatically notified of your comments. +## Handling Blockers (SOP-006) +If you're stuck: +1. Set task status to "blocked" +2. Post a detailed comment: what's blocking you, what you've tried, what you need +3. Your manager is auto-notified and will help +4. For credentials: comment explaining what API key/account you need and why -5. **Submit for Review**: When your work is complete, update the task status to "in_review". - - PATCH /api/issues/{issueId} with status "in_review" and a comment describing what was done. - - Your manager will be automatically notified to review your work. - - Wait for your manager's feedback before marking as "done". +## Handling Review Feedback +When your task is set back to "in_progress" with feedback: +1. Read ALL feedback comments carefully +2. Address each point specifically +3. Re-submit to "in_review" with a comment explaining the changes -6. **Ask Questions**: If requirements are unclear or you need guidance: - - Post a comment on your task with your question. - - Your manager will be notified and can respond. - - Check comments on your task for any feedback or answers. - -7. **Handle Review Feedback**: When your manager posts a comment or sets your task back to "in_progress": - - Read the comment to understand what changes are needed. - - Make the changes and submit for review again. - -8. **Report Blockers**: If you're blocked (need credentials, unclear requirements, etc.): - - Update task status to "blocked" - - Leave a comment explaining what's needed - - Your manager will be notified - -9. **Request Credentials**: If you need credentials (API keys, account access, etc.): - - Post a comment on your task explaining what you need and why - - Your manager will be notified and help resolve it - - Only create a secret_access approval directly if you have NO manager - -10. **Propose Hires**: If you believe the team needs additional members: - - Use POST /api/companies/{companyId}/agents to propose a new hire. - - Set reportsTo to your manager's agent ID. - - The Board must approve before the hire becomes active. +## Quality Standards +- Always test your work before submitting for review +- Leave clear comments so your manager can understand what was done +- If requirements are ambiguous, ask first rather than guessing `); } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 4aea9d9a..ed8f08fc 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -36,6 +36,7 @@ import type { import type { SidebarBadges } from "@shared/contracts"; import { CompanyOnboarding } from "./components/CompanyOnboarding"; import { ConnectorEditor } from "./components/connectors"; +import { Dashboard, KanbanBoard } from "./components/dashboard"; import { EntityWorkspace } from "./components/EntityWorkspace"; import { ActivityList, @@ -128,6 +129,7 @@ export function App() { const [selectedAgentId, setSelectedAgentId] = useState(null); const [selectedGoalId, setSelectedGoalId] = useState(null); const [selectedProjectId, setSelectedProjectId] = useState(null); + const [taskViewMode, setTaskViewMode] = useState<"list" | "kanban">("list"); const logBufferRef = useRef>({}); const [, setLogRevision] = useState(0); const [badges, setBadges] = useState(null); @@ -530,218 +532,27 @@ export function App() { className="space-y-14" > {section === "overview" ? ( - <> - {/* Status metrics */} -
-
Current status
-
- {overviewRows.map((row, index) => ( -
0 ? "border-l border-[color:var(--line)]" : "" - } ${index >= 2 ? "border-t border-[color:var(--line)] lg:border-t-0" : ""}`} - > -
{row.label}
-
{row.value}
-
- ))} -
-
- - {/* Live runs */} - {liveRuns.length > 0 ? ( -
-
Live runs
- - {liveRuns.slice(0, 5).map((run) => { - const agent = companyAgents.find((a) => a.id === run.agentId); - const task = tasks.find((t) => t.id === run.taskId); - return ( - { setSelectedRunId(run.id); setConsoleOpen(true); }}> -
-
-
{task?.title ?? run.id.slice(0, 8)}
-
- {agent ? {agent.name} : null} - {run.connectorId.replace("_local", "")} - {run.model ? {run.model} : null} -
-
- -
-
- ); - })} -
-
- ) : null} - - {/* Needs attention: pending approvals + failed runs */} - {(pendingApprovals.length > 0 || failedRuns.length > 0) ? ( -
-
Needs attention
- - {pendingApprovals.slice(0, 4).map((approval) => { - const requester = companyAgents.find((a) => a.id === approval.requestedByAgentId); - return ( - { setSelectedApprovalId(approval.id); setSection("approvals"); }}> -
-
-
{approval.payloadSummary}
-
- {approval.type.replaceAll("_", " ")} - {requester ? by {requester.name} : null} -
-
- -
-
- ); - })} - {failedRuns.slice(0, 3).map((run) => { - const agent = companyAgents.find((a) => a.id === run.agentId); - return ( - { setSelectedRunId(run.id); setConsoleOpen(true); }}> -
-
-
{run.summary || "Run ended with an error"}
-
- {agent ? {agent.name} : null} - {run.errorMessage ? {run.errorMessage} : null} -
-
- -
-
- ); - })} -
-
- ) : null} - - {/* Budget alerts */} - {(() => { - const budgetWarningAgents = companyAgents.filter( - (a) => a.budgetMonthlyUsd > 0 && (a.spentMonthlyUsd / a.budgetMonthlyUsd) >= 0.8, - ); - if (budgetWarningAgents.length === 0) return null; - return ( -
-
Budget alerts
- - {budgetWarningAgents.map((agent) => { - const pct = agent.budgetMonthlyUsd > 0 ? Math.round((agent.spentMonthlyUsd / agent.budgetMonthlyUsd) * 100) : 0; - const overBudget = agent.spentMonthlyUsd >= agent.budgetMonthlyUsd; - return ( - { setSelectedAgentId(agent.id); setSection("agents"); }}> -
-
-
{agent.name}
-
- {agent.role} - - {formatMoney(agent.spentMonthlyUsd)} / {formatMoney(agent.budgetMonthlyUsd)} ({pct}%) - -
-
- -
-
- ); - })} -
-
- ); - })()} - - {/* Connector health */} -
-
Connector health
-
- {snapshot.connectors.map((connector) => { - const isReady = connector.status === "ready" || connector.status === "degraded"; - const isError = connector.status === "error" || connector.status === "auth_required"; - return ( - - ); - })} -
-
- - {/* Open work */} -
-
Open work
- - {tasks.length === 0 ? ( -
No work queued
- ) : ( - tasks.slice(0, 8).map((task) => { - const run = runs.find((entry) => entry.id === task.activeRunId) ?? null; - const assignee = companyAgents.find((agent) => agent.id === task.assigneeAgentId) ?? null; - return ( - { setSection("tasks"); setSelectedTaskId(task.id); }}> -
-
-
{task.title}
-
- {task.priority} - {assignee ? {assignee.name} : null} -
-
- -
-
- ); - }) - )} -
-
- - {/* Workspace issues */} - {(() => { - const problemWorkspaces = companyWorkspaces.filter( - (w) => w.state !== "ready", - ); - if (problemWorkspaces.length === 0) return null; - return ( -
-
Workspace issues
- - {problemWorkspaces.map((ws) => ( - setSection("workspaces")}> -
-
-
{ws.name}
-
{ws.localPath}
-
- -
-
- ))} -
-
- ); - })()} - + { + setSection(targetSection); + if (entityId) { + if (targetSection === "agents") setSelectedAgentId(entityId); + else if (targetSection === "tasks") setSelectedTaskId(entityId); + else if (targetSection === "approvals") setSelectedApprovalId(entityId); + else if (targetSection === "goals") setSelectedGoalId(entityId); + else if (targetSection === "projects") setSelectedProjectId(entityId); + } + }} + /> ) : null} {section === "connectors" && selectedConnector ? ( @@ -864,8 +675,35 @@ export function App() { {section === "tasks" ? ( <> - setSelectedTaskId("__new__")} />}> - + +
+ + +
+ setSelectedTaskId("__new__")} /> + + } + > + {taskViewMode === "kanban" ? ( + + ) : ( + + )}
setSelectedTaskId(null)}> { setSelectedTaskId(null); await refresh(); }} /> diff --git a/src/renderer/components/dashboard.tsx b/src/renderer/components/dashboard.tsx new file mode 100644 index 00000000..7deb9a32 --- /dev/null +++ b/src/renderer/components/dashboard.tsx @@ -0,0 +1,740 @@ +import { useMemo } from "react"; +import { motion } from "framer-motion"; +import { + ArrowRight, + Briefcase, + CheckCircle, + Cpu, + CurrencyCircleDollar, + Heartbeat, + Lightning, + Robot, + ShieldCheck, + Target, + TreeStructure, + Warning, + XCircle, +} from "@phosphor-icons/react"; +import type { + ActivityRecord, + AgentRecord, + ApprovalRecord, + ConnectorRecord, + CostRecord, + GoalRecord, + ProjectRecord, + RunRecord, + SectionId, + TaskRecord, +} from "@shared/types"; +import { formatMoney } from "../lib/formatters"; +import { StatusPill } from "./ui"; + +/* ------------------------------------------------------------------ */ +/* Types */ +/* ------------------------------------------------------------------ */ + +interface DashboardProps { + agents: AgentRecord[]; + tasks: TaskRecord[]; + runs: RunRecord[]; + approvals: ApprovalRecord[]; + goals: GoalRecord[]; + projects: ProjectRecord[]; + activity: ActivityRecord[]; + costs: CostRecord[]; + connectors: ConnectorRecord[]; + onNavigate: (section: SectionId, entityId?: string) => void; +} + +/* ------------------------------------------------------------------ */ +/* Health Score Computation */ +/* ------------------------------------------------------------------ */ + +function computeHealthScore(props: Pick): number { + let score = 100; + const { agents, tasks, runs, approvals, connectors } = props; + + // Penalize for failed runs (recent) + const failedRuns = runs.filter((r) => r.status === "failed" || r.status === "timed_out"); + score -= Math.min(failedRuns.length * 5, 25); + + // Penalize for blocked tasks + const blockedTasks = tasks.filter((t) => t.status === "blocked"); + score -= Math.min(blockedTasks.length * 8, 20); + + // Penalize for pending approvals (bottleneck) + const pendingApprovals = approvals.filter((a) => a.state === "pending"); + score -= Math.min(pendingApprovals.length * 5, 15); + + // Penalize for agents in error state + const errorAgents = agents.filter((a) => a.status === "error"); + score -= Math.min(errorAgents.length * 10, 20); + + // Penalize for no ready connectors + const readyConnectors = connectors.filter((c) => c.status === "ready"); + if (readyConnectors.length === 0 && connectors.length > 0) score -= 15; + + // Penalize for overbudget agents + const overBudget = agents.filter((a) => a.budgetMonthlyUsd > 0 && a.spentMonthlyUsd >= a.budgetMonthlyUsd); + score -= Math.min(overBudget.length * 5, 10); + + return Math.max(0, Math.min(100, score)); +} + +function healthLabel(score: number): { label: string; tone: string } { + if (score >= 90) return { label: "Excellent", tone: "text-[color:var(--success)]" }; + if (score >= 70) return { label: "Good", tone: "text-[color:var(--success)]" }; + if (score >= 50) return { label: "Fair", tone: "text-[color:var(--warn)]" }; + if (score >= 30) return { label: "Poor", tone: "text-[color:var(--warn)]" }; + return { label: "Critical", tone: "text-[color:var(--danger)]" }; +} + +/* ------------------------------------------------------------------ */ +/* Health Ring */ +/* ------------------------------------------------------------------ */ + +function HealthRing({ score }: { score: number }) { + const radius = 38; + const circumference = 2 * Math.PI * radius; + const offset = circumference - (score / 100) * circumference; + const { label, tone } = healthLabel(score); + + const strokeColor = score >= 70 + ? "var(--success)" + : score >= 50 + ? "var(--warn)" + : "var(--danger)"; + + return ( +
+
+ + + + +
+ {score} +
+
+
+
Company Health
+
{label}
+
+
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Metric Card */ +/* ------------------------------------------------------------------ */ + +function MetricCard({ + label, + value, + icon: Icon, + tone = "default", + onClick, + subtitle, +}: { + label: string; + value: string | number; + icon: React.ElementType; + tone?: "default" | "success" | "warn" | "danger" | "accent"; + onClick?: () => void; + subtitle?: string; +}) { + const toneMap = { + default: "text-[color:var(--text)]", + success: "text-[color:var(--success)]", + warn: "text-[color:var(--warn)]", + danger: "text-[color:var(--danger)]", + accent: "text-[color:var(--accent)]", + }; + const bgMap = { + default: "bg-[color:var(--panel-soft)]", + success: "bg-[color:var(--success-soft)]", + warn: "bg-[color:var(--warn-soft)]", + danger: "bg-[color:var(--danger-soft)]", + accent: "bg-[color:var(--accent-soft)]", + }; + + return ( + + ); +} + +/* ------------------------------------------------------------------ */ +/* Agent Workforce Grid */ +/* ------------------------------------------------------------------ */ + +const statusGlow: Record = { + running: "ring-2 ring-[color:var(--success)] ring-opacity-50", + active: "ring-1 ring-[color:var(--success)] ring-opacity-30", + idle: "", + paused: "opacity-60", + error: "ring-2 ring-[color:var(--danger)] ring-opacity-50", + pending_approval: "ring-1 ring-[color:var(--warn)] ring-opacity-40", + terminated: "opacity-40", +}; + +function AgentCard({ agent, onClick }: { agent: AgentRecord; onClick: () => void }) { + const initials = agent.name + .split(/\s+/) + .slice(0, 2) + .map((w) => w[0]) + .join("") + .toUpperCase(); + + const isActive = agent.status === "running" || agent.status === "active"; + + return ( + + ); +} + +function WorkforceGrid({ agents, onSelectAgent }: { agents: AgentRecord[]; onSelectAgent: (id: string) => void }) { + if (agents.length === 0) { + return ( +
+ +
No agents hired yet
+
Create a CEO agent to start building your company
+
+ ); + } + + return ( +
+ {agents.map((agent) => ( + onSelectAgent(agent.id)} /> + ))} +
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Activity Stream */ +/* ------------------------------------------------------------------ */ + +const actionIcons: Record = { + created: CheckCircle, + updated: Lightning, + deleted: XCircle, + started: Heartbeat, + completed: CheckCircle, + failed: Warning, + approved: ShieldCheck, + rejected: XCircle, +}; + +function timeAgo(dateStr: string): string { + const now = Date.now(); + const then = new Date(dateStr).getTime(); + const diff = Math.max(0, now - then); + const seconds = Math.floor(diff / 1000); + if (seconds < 60) return "just now"; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +function ActivityFeed({ activity, agents, limit = 15 }: { activity: ActivityRecord[]; agents: AgentRecord[]; limit?: number }) { + const recentActivity = useMemo(() => { + return [...activity] + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .slice(0, limit); + }, [activity, limit]); + + if (recentActivity.length === 0) { + return ( +
+
No activity yet. Start by creating agents and tasks.
+
+ ); + } + + return ( +
+ {recentActivity.map((item, index) => { + const Icon = actionIcons[item.action] ?? Lightning; + const agent = agents.find((a) => a.id === item.actor); + const actorName = agent?.name ?? (item.actor === "board" ? "Board" : item.actor.slice(0, 8)); + + return ( + +
+ +
+
+
+ {actorName} + {" "} + {item.action} + {" "} + {item.entityType} + {item.detail ? ( + — {item.detail.slice(0, 60)}{item.detail.length > 60 ? "..." : ""} + ) : null} +
+
{timeAgo(item.createdAt)}
+
+
+ ); + })} +
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Workflow Pipeline (visual task flow) */ +/* ------------------------------------------------------------------ */ + +function TaskPipeline({ tasks }: { tasks: TaskRecord[] }) { + const stages = useMemo(() => { + const counts = { backlog: 0, todo: 0, in_progress: 0, in_review: 0, done: 0, blocked: 0 }; + for (const t of tasks) { + if (t.status in counts) counts[t.status as keyof typeof counts]++; + } + return [ + { label: "Backlog", count: counts.backlog, tone: "text-[color:var(--muted)]", bg: "bg-[color:var(--panel-soft)]" }, + { label: "Todo", count: counts.todo, tone: "text-[color:var(--warn)]", bg: "bg-[color:var(--warn-soft)]" }, + { label: "In Progress", count: counts.in_progress, tone: "text-[color:var(--accent)]", bg: "bg-[color:var(--accent-soft)]" }, + { label: "In Review", count: counts.in_review, tone: "text-[color:var(--warn)]", bg: "bg-[color:var(--warn-soft)]" }, + { label: "Done", count: counts.done, tone: "text-[color:var(--success)]", bg: "bg-[color:var(--success-soft)]" }, + { label: "Blocked", count: counts.blocked, tone: "text-[color:var(--danger)]", bg: "bg-[color:var(--danger-soft)]" }, + ]; + }, [tasks]); + + return ( +
+ {stages.map((stage, i) => ( +
+
+ {stage.count} + {stage.label} +
+ {i < stages.length - 1 ? ( + + ) : null} +
+ ))} +
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Main Dashboard */ +/* ------------------------------------------------------------------ */ + +export function Dashboard(props: DashboardProps) { + const { agents, tasks, runs, approvals, goals, projects, activity, costs, connectors, onNavigate } = props; + + const healthScore = useMemo( + () => computeHealthScore({ agents, tasks, runs, approvals, connectors }), + [agents, tasks, runs, approvals, connectors], + ); + + const liveRuns = useMemo(() => runs.filter((r) => r.status === "running" || r.status === "queued"), [runs]); + const failedRuns = useMemo(() => runs.filter((r) => r.status === "failed" || r.status === "timed_out" || r.status === "interrupted"), [runs]); + const pendingApprovals = useMemo(() => approvals.filter((a) => a.state === "pending"), [approvals]); + const activeTasks = useMemo(() => tasks.filter((t) => t.status === "in_progress" || t.status === "in_review"), [tasks]); + const activeGoals = useMemo(() => goals.filter((g) => g.status === "active"), [goals]); + const totalSpend = useMemo(() => costs.reduce((sum, c) => sum + (c.amountUsd ?? 0), 0), [costs]); + const activeAgents = useMemo(() => agents.filter((a) => a.status !== "terminated" && a.status !== "pending_approval"), [agents]); + const readyConnectors = useMemo(() => connectors.filter((c) => c.status === "ready"), [connectors]); + + return ( +
+ {/* Row 1: Health + Metrics */} +
+ {/* Health Ring */} +
+ +
+
+
{activeAgents.length}
+
Agents
+
+
+
+
{readyConnectors.length}/{connectors.length}
+
Connectors
+
+
+
+
{activeGoals.length}
+
Goals
+
+
+
+ + {/* Metric Cards */} +
+ 0 ? "success" : "default"} + onClick={() => onNavigate("tasks")} + subtitle={liveRuns.length > 0 ? `${liveRuns.filter((r) => r.status === "running").length} running` : "All quiet"} + /> + 0 ? "accent" : "default"} + onClick={() => onNavigate("tasks")} + subtitle={`${tasks.filter((t) => t.status === "todo").length} queued`} + /> + 0 ? "warn" : "default"} + onClick={() => onNavigate("approvals")} + subtitle={pendingApprovals.length > 0 ? "Needs your attention" : "All clear"} + /> + 0 ? "danger" : "default"} + onClick={() => onNavigate("tasks")} + /> + onNavigate("costs")} + /> + p.status === "in_progress").length} + icon={Target} + onClick={() => onNavigate("projects")} + subtitle={`${projects.length} total`} + /> +
+
+ + {/* Row 2: Task Pipeline */} +
+
+
Task Pipeline
+ +
+
+ +
+
+ + {/* Row 3: Workforce + Activity */} +
+ {/* Workforce */} +
+
+
Workforce
+ +
+ onNavigate("agents", id)} /> +
+ + {/* Activity Feed */} +
+
+
Recent Activity
+ +
+
+ +
+
+
+ + {/* Row 4: Connector health (compact) */} +
+
Infrastructure
+
+ {connectors.map((connector) => { + const isReady = connector.status === "ready"; + const isError = connector.status === "error" || connector.status === "auth_required"; + const agentsUsing = agents.filter((a) => a.connectorId === connector.id).length; + return ( + + ); + })} +
+
+ + {/* Row 5: Quick Actions / Needs Attention */} + {(pendingApprovals.length > 0 || failedRuns.length > 0) ? ( +
+
Needs Attention
+
+ {pendingApprovals.slice(0, 5).map((approval) => { + const requester = agents.find((a) => a.id === approval.requestedByAgentId); + return ( + + ); + })} + {failedRuns.slice(0, 3).map((run) => { + const agent = agents.find((a) => a.id === run.agentId); + return ( + + ); + })} +
+
+ ) : null} +
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Kanban Board */ +/* ------------------------------------------------------------------ */ + +interface KanbanProps { + tasks: TaskRecord[]; + agents: AgentRecord[]; + runs: RunRecord[]; + onSelectTask: (taskId: string) => void; +} + +const KANBAN_COLUMNS: Array<{ status: TaskRecord["status"]; label: string; tone: string; dotColor: string }> = [ + { status: "backlog", label: "Backlog", tone: "text-[color:var(--muted)]", dotColor: "bg-[color:var(--muted)]" }, + { status: "todo", label: "Todo", tone: "text-[color:var(--warn)]", dotColor: "bg-[color:var(--warn)]" }, + { status: "in_progress", label: "In Progress", tone: "text-[color:var(--accent)]", dotColor: "bg-[color:var(--accent)]" }, + { status: "in_review", label: "In Review", tone: "text-[color:var(--warn)]", dotColor: "bg-[color:var(--warn)]" }, + { status: "done", label: "Done", tone: "text-[color:var(--success)]", dotColor: "bg-[color:var(--success)]" }, + { status: "blocked", label: "Blocked", tone: "text-[color:var(--danger)]", dotColor: "bg-[color:var(--danger)]" }, +]; + +const priorityColors: Record = { + critical: "border-l-[color:var(--danger)]", + high: "border-l-[color:var(--warn)]", + medium: "border-l-[color:var(--accent)]", + low: "border-l-[color:var(--muted)]", +}; + +export function KanbanBoard({ tasks, agents, runs, onSelectTask }: KanbanProps) { + const tasksByStatus = useMemo(() => { + const grouped: Record = {}; + for (const col of KANBAN_COLUMNS) grouped[col.status] = []; + for (const task of tasks) { + if (task.status === "cancelled") continue; + const bucket = grouped[task.status]; + if (bucket) bucket.push(task); + else grouped.backlog?.push(task); + } + // Sort each column by priority + const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; + for (const col of Object.values(grouped)) { + col.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9)); + } + return grouped; + }, [tasks]); + + if (tasks.length === 0) { + return ( +
+ +
No tasks yet
+
Tasks created by agents will appear here on the board
+
+ ); + } + + return ( +
+ {KANBAN_COLUMNS.map((col) => { + const colTasks = tasksByStatus[col.status] ?? []; + return ( +
+ {/* Column header */} +
+ + {col.label} + + {colTasks.length} + +
+ {/* Cards */} +
+ {colTasks.map((task) => { + const assignee = agents.find((a) => a.id === task.assigneeAgentId); + const activeRun = runs.find((r) => r.id === task.activeRunId); + const isRunning = activeRun?.status === "running"; + return ( + + ); + })} +
+
+ ); + })} +
+ ); +} diff --git a/src/renderer/components/organization.tsx b/src/renderer/components/organization.tsx index 9f264cb6..3a2bc420 100644 --- a/src/renderer/components/organization.tsx +++ b/src/renderer/components/organization.tsx @@ -1028,8 +1028,8 @@ export function ProjectEditor({ /* OrgChart – tree layout with pan & zoom */ /* ------------------------------------------------------------------ */ -const CARD_W = 200; -const CARD_H = 100; +const CARD_W = 220; +const CARD_H = 110; const GAP_X = 32; const GAP_Y = 80; const CHART_PADDING = 60; @@ -1252,13 +1252,16 @@ export function OrgChart({ const x2 = child.x + CARD_W / 2; const y2 = child.y; const midY = (y1 + y2) / 2; + const isChildRunning = child.agent.status === "running"; return ( ); })} @@ -1273,32 +1276,65 @@ export function OrgChart({ {allNodes.map((node) => { const dotColor = statusDotColor[node.agent.status] ?? "#a3a3a3"; const connectorLabel = adapterLabels[node.agent.connectorId] ?? node.agent.connectorId; + const initials = node.agent.name.split(/\s+/).slice(0, 2).map((w) => w[0]).join("").toUpperCase(); + const isRunning = node.agent.status === "running"; + const isActive = node.agent.status === "active" || isRunning; + const isError = node.agent.status === "error"; + const isPending = node.agent.status === "pending_approval"; + const isCEO = node.children.length > 0 && !node.agent.reportsTo; return (
onSelectAgent?.(node.agent.id)} > + {/* Top accent bar for role hierarchy */} +
0 ? "bg-[color:var(--warn)]" : + "bg-[color:var(--muted)] opacity-30" + }`} />
-
- +
+ {initials}
- + {isRunning ? ( + + + + + ) : ( + + )}
{node.agent.name} {node.agent.title || node.agent.role} - - {connectorLabel} - +
+ + {connectorLabel} + + {node.agent.heartbeatEnabled ? ( + + ) : null} +
diff --git a/tests/integration/autonomous-chain.spec.ts b/tests/integration/autonomous-chain.spec.ts index 9673132b..187dfc88 100644 --- a/tests/integration/autonomous-chain.spec.ts +++ b/tests/integration/autonomous-chain.spec.ts @@ -168,8 +168,8 @@ describe("Step 2: CEO phased prompts", () => { const prompt = buildSystemPrompt(ctx); expect(prompt).toContain("Team Building Phase"); - expect(prompt).toContain("Design Organization"); - expect(prompt).toContain("Submit Org Plan for Approval"); + expect(prompt).toContain("Design the Organization Structure"); + expect(prompt).toContain("Submit Org Strategy for Approval"); expect(prompt).not.toContain("Awaiting Org Approval"); expect(prompt).not.toContain("Goal Decomposition"); }); @@ -239,7 +239,7 @@ describe("Step 2: CEO phased prompts", () => { expect(prompt).toContain("Executive / CEO"); expect(prompt).toContain("Goal Decomposition"); expect(prompt).toContain("Delegation"); - expect(prompt).toContain("Credential Escalation"); + expect(prompt).toContain("Unblock & Coordinate"); expect(prompt).not.toContain("Team Building Phase"); expect(prompt).not.toContain("Awaiting Org Approval"); }); @@ -289,8 +289,8 @@ describe("Step 2: CEO phased prompts", () => { }); const prompt = buildSystemPrompt(ctx); - expect(prompt).toContain("Do NOT create secret_access approvals directly"); - expect(prompt).toContain("escalate upward"); + expect(prompt).toContain("escalate by commenting on YOUR parent task"); + expect(prompt).toContain("Break down work and delegate it"); }); it("IC prompt includes credential escalation guidance", () => { @@ -305,8 +305,8 @@ describe("Step 2: CEO phased prompts", () => { }); const prompt = buildSystemPrompt(ctx); - expect(prompt).toContain("Your manager will be notified and help resolve it"); - expect(prompt).toContain("Only create a secret_access approval directly if you have NO manager"); + expect(prompt).toContain("Your manager is auto-notified and will help"); + expect(prompt).toContain("comment explaining what API key/account you need"); }); });