This commit is contained in:
ZacharyZhang-NY committed 2026-03-15 23:49:46 -04:00
1 parent 53607f142d
commit d067589276
6 files changed
+1152 -347

No files matched your search

+101
View File
@@ -835,5 +835,106 @@ export class AgentApiServer {
const projects = snapshot.projects.filter((p) => p.companyId === params.companyId); const projects = snapshot.projects.filter((p) => p.companyId === params.companyId);
this.json(res, 200, projects); 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,
};
}),
});
});
} }
} }
+201 -111
View File
@@ -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 // API instructions
sections.push(`# AgentCompany API sections.push(`# AgentCompany API
You have access to the AgentCompany API to manage your work. Base URL: ${ctx.apiUrl} 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 GET /api/agents/me
Returns your agent record with chain of command. 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 ### List Your Assigned Tasks
GET /api/companies/{companyId}/issues?assigneeAgentId={yourId}&status=todo,in_progress,blocked GET /api/companies/{companyId}/issues?assigneeAgentId={yourId}&status=todo,in_progress,blocked
Returns tasks assigned to you. Returns tasks assigned to you.
@@ -237,22 +305,37 @@ GET /api/companies/{companyId}/projects
// Phase 0 — Team Building // Phase 0 — Team Building
sections.push(`# Operating Instructions (CEO — Team Building Phase) 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 ## Step 1: Analyze the Mission
- Check Active Goals to understand the company direction. - Read ALL Active Goals carefully to understand the company direction.
- Identify what departments are needed to achieve these goals.
Step 2: Design Organization ## Step 2: Design the Organization Structure
- Based on goals, determine what C-suite roles are needed (CTO/CMO/CPO etc.) Based on goals, design a complete org chart. A mature company needs:
- Use POST /api/companies/{companyId}/agents to submit hire requests - **CTO / VP Engineering** — owns all technical execution (if goals involve code/product)
- Each role needs clear capabilities, connector, workspace - **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 ## Step 3: Submit Hire Requests
- Create an approve_ceo_strategy approval with your org structure plan For EACH role, use POST /api/companies/{companyId}/agents with:
- Use GET /api/approvals/{id} to check approval status - A clear, professional name
- Do NOT delegate work or create projects until the Board approves - 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) { } else if (hasPendingOrgApproval) {
// Phase 1 — Awaiting Org Approval // 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 // Phase 2 — Normal Operations
sections.push(`# Operating Instructions (Executive / CEO) 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. ## Priority 1: Standup Check (EVERY wake)
- Create projects with clear scope and assign a lead from your direct reports. Before doing anything else, run a mental standup:
- Each project should have a goal linked to it. 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. ## Priority 2: Review Completed Work (SOP-004)
- Match tasks to the agent whose role fits best. When tasks from your reports have status "in_review":
- CTO/Engineering Lead → technical tasks - Read ALL comments to understand what was done
- CMO/Marketing Lead → marketing, growth, content tasks - If satisfactory: update status to "done" + approving comment
- CPO/Product Lead → product design, UX, feature scoping - 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 - 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: ## Priority 4: Unblock & Coordinate
- Check which tasks/projects from your reports have status "in_review" or "done". - Read comments from reports on blocked tasks
- Read comments on those tasks to understand what was accomplished. - Create secret_access approvals for credential needs
- If satisfactory: update the task status to "done" and leave an approving comment. - Escalate to Board for anything requiring human judgment
- If changes are needed: post a comment with feedback and set the task back to "in_progress".
4. **Coordination**: Check on project progress. Read comments from your reports. ## Priority 5: Grow the Team
If a report is blocked or needs credentials, either: When workload exceeds capacity or new capabilities are needed:
a. Resolve it yourself if you can - Use POST /api/companies/{companyId}/agents to propose hires
b. Escalate to the Board by creating an approval request - 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: ## Priority 6: Strategic Reporting
- Create a secret_access approval for the Board After each run, leave a concise status update on your primary task covering:
- After approval, notify the requesting agent via comment - What was reviewed/delegated/unblocked
- Key decisions made
- Any items needing Board attention
6. **Hiring**: When the team needs additional members: CRITICAL: You are the CEO. DELEGATE work — do NOT try to code, write content, or design yourself. Your job is to ORCHESTRATE.
- 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.
`); `);
} }
} else if (isManager) { } else if (isManager) {
// Step 2d: Manager with credential escalation instruction // Step 2d: Manager with credential escalation instruction
sections.push(`# Operating Instructions (Manager) 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. ## Priority 1: Standup Check (EVERY wake — SOP-001)
- Use POST /api/companies/{companyId}/issues to create subtasks. 1. Check tasks with status "in_review" from your reports — these MUST be reviewed first
- Set assigneeAgentId to one of your direct reports. 2. Check for "blocked" tasks from your reports — second priority
- Set parentId to link subtasks to the parent task. 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. ## Priority 2: Review Completed Work (SOP-004)
- Match each report's capabilities to the right subtask. This is your CORE DUTY — reviews unblock your team's progress:
- Each subtask should have a clear title and description explaining what needs to be done. 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: ## Priority 3: Task Breakdown & Delegation (SOP-003)
- Check which subtasks have status "in_review" or "done". When assigned a project or high-level task:
- Read comments on those tasks to understand what was done. 1. Analyze the work and break it into 3-7 specific subtasks
- If the work is satisfactory: update the subtask status to "done" and leave an approving comment. 2. Each subtask needs: clear title, detailed description with acceptance criteria, priority
- If changes are needed: post a comment explaining what needs to change, and set the subtask status back to "in_progress". 3. Assign each to the best-fit report based on their capabilities
- This is your core management duty — do NOT skip reviews. 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. ## Priority 4: Unblock Your Reports
- POST /api/issues/{issueId}/comments to ask a question or give feedback on any task. When reports post comments about blockers:
- The task assignee will be automatically woken and notified. - 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. ## Priority 5: Progress Reporting
- GET /api/companies/{companyId}/issues to see all tasks and their statuses. After each run, update your task status and leave a summary for your manager:
- GET /api/issues/{issueId}/comments to read discussion on specific tasks. - 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: CRITICAL: You are a manager. Break down work and delegate it. Do NOT try to do IC work yourself unless you have no reports.
- 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.
`); `);
} else { } else {
// Step 2d: IC with credential escalation instruction // Step 2d: IC with credential escalation instruction
sections.push(`# Operating Instructions (Individual Contributor) 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. ## Priority 1: Check for Feedback (EVERY wake — SOP-001)
- Focus on tasks that match your role and capabilities. 1. Read ALL new comments on your assigned tasks
- Use the tools available in your workspace (CLI, code editors, etc.) to produce deliverables. 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. ## Priority 2: Execute Your Highest-Priority Task
- POST /api/issues/{issueId}/checkout to claim the 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: ## Task Execution Workflow (SOP-002)
- Create subtasks for yourself using POST /api/companies/{companyId}/issues. - ALWAYS checkout before working
- Set assigneeAgentId to your own ID and parentId to the original task. - For large tasks: create subtasks assigned to yourself (parentId = original task)
- This helps you track progress on multi-step work. - 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. ## Handling Blockers (SOP-006)
- POST /api/issues/{issueId}/comments with a summary of what you did. If you're stuck:
- Your manager will be automatically notified of your comments. 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". ## Handling Review Feedback
- PATCH /api/issues/{issueId} with status "in_review" and a comment describing what was done. When your task is set back to "in_progress" with feedback:
- Your manager will be automatically notified to review your work. 1. Read ALL feedback comments carefully
- Wait for your manager's feedback before marking as "done". 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: ## Quality Standards
- Post a comment on your task with your question. - Always test your work before submitting for review
- Your manager will be notified and can respond. - Leave clear comments so your manager can understand what was done
- Check comments on your task for any feedback or answers. - If requirements are ambiguous, ask first rather than guessing
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.
`); `);
} }
+52 -214
View File
@@ -36,6 +36,7 @@ import type {
import type { SidebarBadges } from "@shared/contracts"; import type { SidebarBadges } from "@shared/contracts";
import { CompanyOnboarding } from "./components/CompanyOnboarding"; import { CompanyOnboarding } from "./components/CompanyOnboarding";
import { ConnectorEditor } from "./components/connectors"; import { ConnectorEditor } from "./components/connectors";
import { Dashboard, KanbanBoard } from "./components/dashboard";
import { EntityWorkspace } from "./components/EntityWorkspace"; import { EntityWorkspace } from "./components/EntityWorkspace";
import { import {
ActivityList, ActivityList,
@@ -128,6 +129,7 @@ export function App() {
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null); const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const [selectedGoalId, setSelectedGoalId] = useState<string | null>(null); const [selectedGoalId, setSelectedGoalId] = useState<string | null>(null);
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null); const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
const [taskViewMode, setTaskViewMode] = useState<"list" | "kanban">("list");
const logBufferRef = useRef<Record<string, string>>({}); const logBufferRef = useRef<Record<string, string>>({});
const [, setLogRevision] = useState(0); const [, setLogRevision] = useState(0);
const [badges, setBadges] = useState<SidebarBadges | null>(null); const [badges, setBadges] = useState<SidebarBadges | null>(null);
@@ -530,218 +532,27 @@ export function App() {
className="space-y-14" className="space-y-14"
> >
{section === "overview" ? ( {section === "overview" ? (
<> <Dashboard
{/* Status metrics */} agents={companyAgents}
<section className="space-y-3"> tasks={tasks}
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Current status</div> runs={runs}
<div className="grid grid-cols-2 border-y border-[color:var(--line)] lg:grid-cols-4"> approvals={approvals}
{overviewRows.map((row, index) => ( goals={companyGoals}
<div projects={companyProjects}
key={row.label} activity={filteredActivity}
className={`px-4 py-4 lg:px-5 lg:first:pl-0 lg:last:pr-0 ${ costs={companyCosts}
index > 0 ? "border-l border-[color:var(--line)]" : "" connectors={snapshot.connectors}
} ${index >= 2 ? "border-t border-[color:var(--line)] lg:border-t-0" : ""}`} onNavigate={(targetSection, entityId) => {
> setSection(targetSection);
<div className="text-[12px] font-medium text-[color:var(--muted)]">{row.label}</div> if (entityId) {
<div className={`mono text-[18px] font-semibold tracking-[-0.01em] sm:mt-2 ${row.tone || "text-[color:var(--text)]"}`}>{row.value}</div> if (targetSection === "agents") setSelectedAgentId(entityId);
</div> else if (targetSection === "tasks") setSelectedTaskId(entityId);
))} else if (targetSection === "approvals") setSelectedApprovalId(entityId);
</div> else if (targetSection === "goals") setSelectedGoalId(entityId);
</section> else if (targetSection === "projects") setSelectedProjectId(entityId);
}
{/* Live runs */} }}
{liveRuns.length > 0 ? ( />
<section className="space-y-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--success)]">Live runs</div>
<ListFrame>
{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 (
<ListRow key={run.id} onClick={() => { setSelectedRunId(run.id); setConsoleOpen(true); }}>
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="truncate text-[14px] font-medium text-[color:var(--text)]">{task?.title ?? run.id.slice(0, 8)}</div>
<div className="mt-1 flex items-center gap-3 text-[11px] uppercase tracking-[0.12em] text-[color:var(--muted)]">
{agent ? <span>{agent.name}</span> : null}
<span>{run.connectorId.replace("_local", "")}</span>
{run.model ? <span className="mono text-[10px] normal-case tracking-normal">{run.model}</span> : null}
</div>
</div>
<StatusPill status={run.status} />
</div>
</ListRow>
);
})}
</ListFrame>
</section>
) : null}
{/* Needs attention: pending approvals + failed runs */}
{(pendingApprovals.length > 0 || failedRuns.length > 0) ? (
<section className="space-y-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">Needs attention</div>
<ListFrame>
{pendingApprovals.slice(0, 4).map((approval) => {
const requester = companyAgents.find((a) => a.id === approval.requestedByAgentId);
return (
<ListRow key={approval.id} onClick={() => { setSelectedApprovalId(approval.id); setSection("approvals"); }}>
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="truncate text-[14px] font-medium text-[color:var(--text)]">{approval.payloadSummary}</div>
<div className="mt-1 flex items-center gap-3 text-[11px] uppercase tracking-[0.12em] text-[color:var(--muted)]">
<span>{approval.type.replaceAll("_", " ")}</span>
{requester ? <span>by {requester.name}</span> : null}
</div>
</div>
<StatusPill status={approval.state} />
</div>
</ListRow>
);
})}
{failedRuns.slice(0, 3).map((run) => {
const agent = companyAgents.find((a) => a.id === run.agentId);
return (
<ListRow key={run.id} onClick={() => { setSelectedRunId(run.id); setConsoleOpen(true); }}>
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="truncate text-[14px] font-medium text-[color:var(--text)]">{run.summary || "Run ended with an error"}</div>
<div className="mt-1 flex items-center gap-3 text-[11px] text-[color:var(--danger)]">
{agent ? <span className="uppercase tracking-[0.12em]">{agent.name}</span> : null}
{run.errorMessage ? <span className="truncate">{run.errorMessage}</span> : null}
</div>
</div>
<StatusPill status={run.status} />
</div>
</ListRow>
);
})}
</ListFrame>
</section>
) : null}
{/* Budget alerts */}
{(() => {
const budgetWarningAgents = companyAgents.filter(
(a) => a.budgetMonthlyUsd > 0 && (a.spentMonthlyUsd / a.budgetMonthlyUsd) >= 0.8,
);
if (budgetWarningAgents.length === 0) return null;
return (
<section className="space-y-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--danger)]">Budget alerts</div>
<ListFrame>
{budgetWarningAgents.map((agent) => {
const pct = agent.budgetMonthlyUsd > 0 ? Math.round((agent.spentMonthlyUsd / agent.budgetMonthlyUsd) * 100) : 0;
const overBudget = agent.spentMonthlyUsd >= agent.budgetMonthlyUsd;
return (
<ListRow key={agent.id} onClick={() => { setSelectedAgentId(agent.id); setSection("agents"); }}>
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="text-[14px] font-medium text-[color:var(--text)]">{agent.name}</div>
<div className="mt-1 flex items-center gap-3 text-[11px] text-[color:var(--muted)]">
<span className="uppercase tracking-[0.12em]">{agent.role}</span>
<span className={overBudget ? "text-[color:var(--danger)]" : "text-[color:var(--warn)]"}>
{formatMoney(agent.spentMonthlyUsd)} / {formatMoney(agent.budgetMonthlyUsd)} ({pct}%)
</span>
</div>
</div>
<StatusPill status={overBudget ? "error" : "degraded"} />
</div>
</ListRow>
);
})}
</ListFrame>
</section>
);
})()}
{/* Connector health */}
<section className="space-y-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Connector health</div>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
{snapshot.connectors.map((connector) => {
const isReady = connector.status === "ready" || connector.status === "degraded";
const isError = connector.status === "error" || connector.status === "auth_required";
return (
<button
key={connector.id}
type="button"
onClick={() => { setSelectedConnectorId(connector.id); setSection("connectors"); }}
className={`focus-ring flex items-center gap-3 rounded-[10px] border px-4 py-3 text-left transition duration-150 hover:bg-[color:var(--panel-soft)] ${
isError ? "border-[color:var(--danger)] border-opacity-30" : "border-[color:var(--line)]"
}`}
>
<div className={`h-2 w-2 shrink-0 rounded-full ${
isReady ? "bg-[color:var(--success)]" : isError ? "bg-[color:var(--danger)]" : "bg-[color:var(--muted)]"
}`} />
<div className="min-w-0">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{connector.label}</div>
<div className="truncate text-[11px] text-[color:var(--muted)]">{connector.status.replaceAll("_", " ")}</div>
</div>
{!connector.capabilityMatrix.costAttribution ? (
<span className="ml-auto shrink-0 rounded bg-[color:var(--panel-soft)] px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-[0.12em] text-[color:var(--muted)]">no cost</span>
) : null}
</button>
);
})}
</div>
</section>
{/* Open work */}
<section className="space-y-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Open work</div>
<ListFrame>
{tasks.length === 0 ? (
<div className="py-8 text-center text-[13px] text-[color:var(--muted)]">No work queued</div>
) : (
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 (
<ListRow key={task.id} onClick={() => { setSection("tasks"); setSelectedTaskId(task.id); }}>
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="truncate text-[14px] font-medium text-[color:var(--text)]">{task.title}</div>
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] uppercase tracking-[0.12em] text-[color:var(--muted)]">
<span className={task.priority === "critical" ? "text-[color:var(--danger)]" : task.priority === "high" ? "text-[color:var(--warn)]" : ""}>{task.priority}</span>
{assignee ? <span>{assignee.name}</span> : null}
</div>
</div>
<StatusPill status={run?.status ?? task.status} />
</div>
</ListRow>
);
})
)}
</ListFrame>
</section>
{/* Workspace issues */}
{(() => {
const problemWorkspaces = companyWorkspaces.filter(
(w) => w.state !== "ready",
);
if (problemWorkspaces.length === 0) return null;
return (
<section className="space-y-3">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">Workspace issues</div>
<ListFrame>
{problemWorkspaces.map((ws) => (
<ListRow key={ws.id} onClick={() => setSection("workspaces")}>
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="text-[14px] font-medium text-[color:var(--text)]">{ws.name}</div>
<div className="mt-1 truncate text-[11px] text-[color:var(--muted)] mono">{ws.localPath}</div>
</div>
<StatusPill status={ws.state} />
</div>
</ListRow>
))}
</ListFrame>
</section>
);
})()}
</>
) : null} ) : null}
{section === "connectors" && selectedConnector ? ( {section === "connectors" && selectedConnector ? (
@@ -864,8 +675,35 @@ export function App() {
{section === "tasks" ? ( {section === "tasks" ? (
<> <>
<Panel title="Task queue" action={<ActionButton label="New task" icon={ListChecks} tone="accent" onClick={() => setSelectedTaskId("__new__")} />}> <Panel
<TaskQueue tasks={tasks} runs={runs} onSelect={setSelectedTaskId} selectedTaskId={selectedTask?.id ?? null} /> title="Task queue"
action={
<div className="flex items-center gap-2">
<div className="flex rounded-[6px] border border-[color:var(--line)] bg-[color:var(--panel)]">
<button
type="button"
onClick={() => setTaskViewMode("list")}
className={`px-2.5 py-1.5 text-[11px] font-medium transition ${taskViewMode === "list" ? "bg-[color:var(--panel-soft)] text-[color:var(--text)]" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}
>
<ListChecks size={14} />
</button>
<button
type="button"
onClick={() => setTaskViewMode("kanban")}
className={`px-2.5 py-1.5 text-[11px] font-medium transition ${taskViewMode === "kanban" ? "bg-[color:var(--panel-soft)] text-[color:var(--text)]" : "text-[color:var(--muted)] hover:text-[color:var(--text)]"}`}
>
<Kanban size={14} />
</button>
</div>
<ActionButton label="New task" icon={ListChecks} tone="accent" onClick={() => setSelectedTaskId("__new__")} />
</div>
}
>
{taskViewMode === "kanban" ? (
<KanbanBoard tasks={tasks} agents={companyAgents} runs={runs} onSelectTask={setSelectedTaskId} />
) : (
<TaskQueue tasks={tasks} runs={runs} onSelect={setSelectedTaskId} selectedTaskId={selectedTask?.id ?? null} />
)}
</Panel> </Panel>
<Modal open={selectedTaskId === "__new__"} title="New task" onClose={() => setSelectedTaskId(null)}> <Modal open={selectedTaskId === "__new__"} title="New task" onClose={() => setSelectedTaskId(null)}>
<TaskCreator companyId={currentCompany!.id} goals={companyGoals} projects={companyProjects} agents={companyAgents} workspaces={companyWorkspaces} onSaved={async () => { setSelectedTaskId(null); await refresh(); }} /> <TaskCreator companyId={currentCompany!.id} goals={companyGoals} projects={companyProjects} agents={companyAgents} workspaces={companyWorkspaces} onSaved={async () => { setSelectedTaskId(null); await refresh(); }} />
+740
View File
@@ -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<DashboardProps, "agents" | "tasks" | "runs" | "approvals" | "connectors">): 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 (
<div className="flex items-center gap-5">
<div className="relative h-[90px] w-[90px] shrink-0">
<svg viewBox="0 0 88 88" className="h-full w-full -rotate-90">
<circle cx="44" cy="44" r={radius} fill="none" stroke="var(--line)" strokeWidth="6" />
<motion.circle
cx="44"
cy="44"
r={radius}
fill="none"
stroke={strokeColor}
strokeWidth="6"
strokeLinecap="round"
strokeDasharray={circumference}
initial={{ strokeDashoffset: circumference }}
animate={{ strokeDashoffset: offset }}
transition={{ duration: 1.2, ease: "easeOut" }}
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className={`text-[22px] font-bold ${tone}`}>{score}</span>
</div>
</div>
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Company Health</div>
<div className={`text-[16px] font-semibold ${tone}`}>{label}</div>
</div>
</div>
);
}
/* ------------------------------------------------------------------ */
/* 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 (
<button
type="button"
onClick={onClick}
className="focus-ring flex items-center gap-3.5 rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-3.5 text-left transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm"
>
<div className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-[8px] ${bgMap[tone]}`}>
<Icon size={20} weight="fill" className={toneMap[tone]} />
</div>
<div className="min-w-0 flex-1">
<div className="text-[11px] font-medium uppercase tracking-[0.06em] text-[color:var(--muted)]">{label}</div>
<div className={`text-[20px] font-bold tracking-[-0.02em] leading-tight ${toneMap[tone]}`}>{value}</div>
{subtitle ? <div className="mt-0.5 truncate text-[11px] text-[color:var(--muted)]">{subtitle}</div> : null}
</div>
</button>
);
}
/* ------------------------------------------------------------------ */
/* Agent Workforce Grid */
/* ------------------------------------------------------------------ */
const statusGlow: Record<string, string> = {
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 (
<button
type="button"
onClick={onClick}
className={`focus-ring group relative flex flex-col items-center gap-2 rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-3 text-center transition duration-150 hover:border-[color:var(--line-strong)] hover:shadow-sm ${statusGlow[agent.status] ?? ""}`}
>
<div className="relative">
<div className={`flex h-11 w-11 items-center justify-center rounded-full text-[14px] font-bold ${
isActive ? "bg-[color:var(--success-soft)] text-[color:var(--success)]" : "bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
}`}>
{initials}
</div>
{agent.status === "running" ? (
<span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5 items-center justify-center">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[color:var(--success)] opacity-40" />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-[color:var(--success)]" />
</span>
) : (
<span
className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[color:var(--panel)]"
style={{
backgroundColor: agent.status === "error"
? "var(--danger)"
: agent.status === "active"
? "var(--success)"
: agent.status === "pending_approval"
? "var(--warn)"
: agent.status === "paused" || agent.status === "terminated"
? "var(--muted)"
: "var(--muted)",
}}
/>
)}
</div>
<div className="w-full min-w-0">
<div className="truncate text-[12px] font-semibold text-[color:var(--text)]">{agent.name}</div>
<div className="truncate text-[10px] text-[color:var(--muted)]">{agent.title || agent.role}</div>
</div>
</button>
);
}
function WorkforceGrid({ agents, onSelectAgent }: { agents: AgentRecord[]; onSelectAgent: (id: string) => void }) {
if (agents.length === 0) {
return (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-10 text-center">
<Robot size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No agents hired yet</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">Create a CEO agent to start building your company</div>
</div>
);
}
return (
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{agents.map((agent) => (
<AgentCard key={agent.id} agent={agent} onClick={() => onSelectAgent(agent.id)} />
))}
</div>
);
}
/* ------------------------------------------------------------------ */
/* Activity Stream */
/* ------------------------------------------------------------------ */
const actionIcons: Record<string, React.ElementType> = {
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 (
<div className="flex h-full items-center justify-center py-8 text-center">
<div className="text-[13px] text-[color:var(--muted)]">No activity yet. Start by creating agents and tasks.</div>
</div>
);
}
return (
<div className="space-y-0.5">
{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 (
<motion.div
key={item.id}
initial={index === 0 ? { opacity: 0, y: -8 } : false}
animate={{ opacity: 1, y: 0 }}
className="flex items-start gap-3 rounded-[6px] px-3 py-2 transition hover:bg-[color:var(--panel-soft)]"
>
<div className="mt-0.5 shrink-0">
<Icon size={14} className="text-[color:var(--muted)]" />
</div>
<div className="min-w-0 flex-1">
<div className="text-[12px] leading-relaxed text-[color:var(--text)]">
<span className="font-medium">{actorName}</span>
{" "}
<span className="text-[color:var(--muted)]">{item.action}</span>
{" "}
<span className="text-[color:var(--muted-strong)]">{item.entityType}</span>
{item.detail ? (
<span className="text-[color:var(--muted)]"> — {item.detail.slice(0, 60)}{item.detail.length > 60 ? "..." : ""}</span>
) : null}
</div>
<div className="mt-0.5 text-[10px] text-[color:var(--muted)]">{timeAgo(item.createdAt)}</div>
</div>
</motion.div>
);
})}
</div>
);
}
/* ------------------------------------------------------------------ */
/* 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 (
<div className="flex items-center gap-1.5">
{stages.map((stage, i) => (
<div key={stage.label} className="flex items-center gap-1.5">
<div className={`flex items-center gap-2 rounded-[8px] px-3 py-2 ${stage.bg}`}>
<span className={`text-[18px] font-bold ${stage.tone}`}>{stage.count}</span>
<span className="text-[10px] font-medium uppercase tracking-[0.06em] text-[color:var(--muted)]">{stage.label}</span>
</div>
{i < stages.length - 1 ? (
<ArrowRight size={12} className="text-[color:var(--muted)]" />
) : null}
</div>
))}
</div>
);
}
/* ------------------------------------------------------------------ */
/* 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 (
<div className="space-y-8">
{/* Row 1: Health + Metrics */}
<div className="flex flex-col gap-6 lg:flex-row lg:items-start">
{/* Health Ring */}
<div className="shrink-0 rounded-[12px] border border-[color:var(--line)] bg-[color:var(--panel)] p-5">
<HealthRing score={healthScore} />
<div className="mt-3 flex gap-3">
<div className="text-center">
<div className="text-[16px] font-bold text-[color:var(--text)]">{activeAgents.length}</div>
<div className="text-[10px] text-[color:var(--muted)]">Agents</div>
</div>
<div className="h-8 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className="text-[16px] font-bold text-[color:var(--text)]">{readyConnectors.length}/{connectors.length}</div>
<div className="text-[10px] text-[color:var(--muted)]">Connectors</div>
</div>
<div className="h-8 w-px bg-[color:var(--line)]" />
<div className="text-center">
<div className="text-[16px] font-bold text-[color:var(--text)]">{activeGoals.length}</div>
<div className="text-[10px] text-[color:var(--muted)]">Goals</div>
</div>
</div>
</div>
{/* Metric Cards */}
<div className="grid flex-1 grid-cols-2 gap-2 lg:grid-cols-3">
<MetricCard
label="Live Runs"
value={liveRuns.length}
icon={Heartbeat}
tone={liveRuns.length > 0 ? "success" : "default"}
onClick={() => onNavigate("tasks")}
subtitle={liveRuns.length > 0 ? `${liveRuns.filter((r) => r.status === "running").length} running` : "All quiet"}
/>
<MetricCard
label="Active Tasks"
value={activeTasks.length}
icon={Briefcase}
tone={activeTasks.length > 0 ? "accent" : "default"}
onClick={() => onNavigate("tasks")}
subtitle={`${tasks.filter((t) => t.status === "todo").length} queued`}
/>
<MetricCard
label="Approvals"
value={pendingApprovals.length}
icon={ShieldCheck}
tone={pendingApprovals.length > 0 ? "warn" : "default"}
onClick={() => onNavigate("approvals")}
subtitle={pendingApprovals.length > 0 ? "Needs your attention" : "All clear"}
/>
<MetricCard
label="Failed Runs"
value={failedRuns.length}
icon={Warning}
tone={failedRuns.length > 0 ? "danger" : "default"}
onClick={() => onNavigate("tasks")}
/>
<MetricCard
label="Total Spend"
value={formatMoney(totalSpend)}
icon={CurrencyCircleDollar}
onClick={() => onNavigate("costs")}
/>
<MetricCard
label="Projects"
value={projects.filter((p) => p.status === "in_progress").length}
icon={Target}
onClick={() => onNavigate("projects")}
subtitle={`${projects.length} total`}
/>
</div>
</div>
{/* Row 2: Task Pipeline */}
<section>
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Task Pipeline</div>
<button
type="button"
onClick={() => onNavigate("tasks")}
className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline"
>
View all <ArrowRight size={12} />
</button>
</div>
<div className="overflow-x-auto rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] p-4">
<TaskPipeline tasks={tasks} />
</div>
</section>
{/* Row 3: Workforce + Activity */}
<div className="grid gap-6 lg:grid-cols-5">
{/* Workforce */}
<section className="lg:col-span-3">
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Workforce</div>
<button
type="button"
onClick={() => onNavigate("orgchart")}
className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline"
>
Org chart <TreeStructure size={12} />
</button>
</div>
<WorkforceGrid agents={agents} onSelectAgent={(id) => onNavigate("agents", id)} />
</section>
{/* Activity Feed */}
<section className="lg:col-span-2">
<div className="mb-3 flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Recent Activity</div>
<button
type="button"
onClick={() => onNavigate("activity")}
className="flex items-center gap-1 text-[11px] font-medium text-[color:var(--accent)] transition hover:underline"
>
View all <ArrowRight size={12} />
</button>
</div>
<div className="max-h-[400px] overflow-y-auto rounded-[10px] border border-[color:var(--line)] bg-[color:var(--panel)] py-2">
<ActivityFeed activity={activity} agents={agents} />
</div>
</section>
</div>
{/* Row 4: Connector health (compact) */}
<section>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">Infrastructure</div>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-5">
{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 (
<button
key={connector.id}
type="button"
onClick={() => onNavigate("connectors")}
className={`focus-ring flex items-center gap-2.5 rounded-[8px] border px-3 py-2.5 text-left transition duration-150 hover:bg-[color:var(--panel-soft)] ${
isError ? "border-[color:var(--danger)] border-opacity-30" : "border-[color:var(--line)]"
}`}
>
<Cpu size={16} className={isReady ? "text-[color:var(--success)]" : isError ? "text-[color:var(--danger)]" : "text-[color:var(--muted)]"} />
<div className="min-w-0 flex-1">
<div className="truncate text-[12px] font-medium text-[color:var(--text)]">{connector.label}</div>
<div className="flex items-center gap-2 text-[10px] text-[color:var(--muted)]">
<StatusPill status={connector.status} />
{agentsUsing > 0 ? <span>{agentsUsing} agent{agentsUsing > 1 ? "s" : ""}</span> : null}
</div>
</div>
</button>
);
})}
</div>
</section>
{/* Row 5: Quick Actions / Needs Attention */}
{(pendingApprovals.length > 0 || failedRuns.length > 0) ? (
<section>
<div className="mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--warn)]">Needs Attention</div>
<div className="space-y-1.5">
{pendingApprovals.slice(0, 5).map((approval) => {
const requester = agents.find((a) => a.id === approval.requestedByAgentId);
return (
<button
key={approval.id}
type="button"
onClick={() => onNavigate("approvals", approval.id)}
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--warn)] border-opacity-20 bg-[color:var(--warn-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
>
<ShieldCheck size={18} className="shrink-0 text-[color:var(--warn)]" />
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{approval.payloadSummary}</div>
<div className="mt-0.5 flex items-center gap-2 text-[11px] text-[color:var(--muted)]">
<span className="uppercase tracking-[0.1em]">{approval.type.replaceAll("_", " ")}</span>
{requester ? <span>by {requester.name}</span> : null}
</div>
</div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
</button>
);
})}
{failedRuns.slice(0, 3).map((run) => {
const agent = agents.find((a) => a.id === run.agentId);
return (
<button
key={run.id}
type="button"
onClick={() => onNavigate("tasks")}
className="focus-ring flex w-full items-center gap-3 rounded-[8px] border border-[color:var(--danger)] border-opacity-20 bg-[color:var(--danger-soft)] px-4 py-3 text-left transition hover:border-opacity-40"
>
<XCircle size={18} className="shrink-0 text-[color:var(--danger)]" />
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-medium text-[color:var(--text)]">{run.summary || "Run failed"}</div>
<div className="mt-0.5 text-[11px] text-[color:var(--muted)]">
{agent?.name} — {run.errorMessage?.slice(0, 80) ?? "Unknown error"}
</div>
</div>
<ArrowRight size={14} className="shrink-0 text-[color:var(--muted)]" />
</button>
);
})}
</div>
</section>
) : null}
</div>
);
}
/* ------------------------------------------------------------------ */
/* 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<string, string> = {
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<string, TaskRecord[]> = {};
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 (
<div className="flex flex-col items-center justify-center rounded-[10px] border border-dashed border-[color:var(--line)] py-16 text-center">
<Briefcase size={32} className="mb-3 text-[color:var(--muted)]" />
<div className="text-[14px] font-medium text-[color:var(--muted-strong)]">No tasks yet</div>
<div className="mt-1 text-[12px] text-[color:var(--muted)]">Tasks created by agents will appear here on the board</div>
</div>
);
}
return (
<div className="flex gap-3 overflow-x-auto pb-2">
{KANBAN_COLUMNS.map((col) => {
const colTasks = tasksByStatus[col.status] ?? [];
return (
<div key={col.status} className="flex w-[220px] shrink-0 flex-col">
{/* Column header */}
<div className="mb-2 flex items-center gap-2 px-1">
<span className={`h-2 w-2 rounded-full ${col.dotColor}`} />
<span className={`text-[11px] font-semibold uppercase tracking-[0.08em] ${col.tone}`}>{col.label}</span>
<span className="ml-auto rounded-full bg-[color:var(--panel-soft)] px-1.5 py-0.5 text-[10px] font-bold text-[color:var(--muted)]">
{colTasks.length}
</span>
</div>
{/* Cards */}
<div className="flex flex-1 flex-col gap-1.5 rounded-[8px] bg-[color:var(--panel-soft)] p-1.5" style={{ minHeight: 120 }}>
{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 (
<button
key={task.id}
type="button"
onClick={() => onSelectTask(task.id)}
className={`focus-ring rounded-[6px] border border-l-[3px] border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-left transition hover:shadow-sm ${priorityColors[task.priority] ?? ""}`}
>
<div className="text-[12px] font-medium leading-snug text-[color:var(--text)]">{task.title}</div>
<div className="mt-1.5 flex items-center gap-2">
{assignee ? (
<span className="flex items-center gap-1 text-[10px] text-[color:var(--muted)]">
<Robot size={10} />
{assignee.name}
</span>
) : (
<span className="text-[10px] italic text-[color:var(--muted)]">Unassigned</span>
)}
{isRunning ? (
<span className="flex items-center gap-1 text-[10px] text-[color:var(--success)]">
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-[color:var(--success)]" />
Running
</span>
) : null}
</div>
{task.priority === "critical" || task.priority === "high" ? (
<div className={`mt-1 text-[9px] font-bold uppercase tracking-[0.12em] ${task.priority === "critical" ? "text-[color:var(--danger)]" : "text-[color:var(--warn)]"}`}>
{task.priority}
</div>
) : null}
</button>
);
})}
</div>
</div>
);
})}
</div>
);
}
+51 -15
View File
@@ -1028,8 +1028,8 @@ export function ProjectEditor({
/* OrgChart – tree layout with pan & zoom */ /* OrgChart – tree layout with pan & zoom */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
const CARD_W = 200; const CARD_W = 220;
const CARD_H = 100; const CARD_H = 110;
const GAP_X = 32; const GAP_X = 32;
const GAP_Y = 80; const GAP_Y = 80;
const CHART_PADDING = 60; const CHART_PADDING = 60;
@@ -1252,13 +1252,16 @@ export function OrgChart({
const x2 = child.x + CARD_W / 2; const x2 = child.x + CARD_W / 2;
const y2 = child.y; const y2 = child.y;
const midY = (y1 + y2) / 2; const midY = (y1 + y2) / 2;
const isChildRunning = child.agent.status === "running";
return ( return (
<path <path
key={`${parent.agent.id}-${child.agent.id}`} key={`${parent.agent.id}-${child.agent.id}`}
d={`M ${x1} ${y1} L ${x1} ${midY} L ${x2} ${midY} L ${x2} ${y2}`} d={`M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`}
fill="none" fill="none"
stroke="var(--line-strong)" stroke={isChildRunning ? "var(--success)" : "var(--line-strong)"}
strokeWidth={1.5} strokeWidth={isChildRunning ? 2 : 1.5}
strokeDasharray={isChildRunning ? "6 3" : "none"}
opacity={isChildRunning ? 0.8 : 0.5}
/> />
); );
})} })}
@@ -1273,32 +1276,65 @@ export function OrgChart({
{allNodes.map((node) => { {allNodes.map((node) => {
const dotColor = statusDotColor[node.agent.status] ?? "#a3a3a3"; const dotColor = statusDotColor[node.agent.status] ?? "#a3a3a3";
const connectorLabel = adapterLabels[node.agent.connectorId] ?? node.agent.connectorId; 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 ( return (
<div <div
key={node.agent.id} key={node.agent.id}
data-org-card data-org-card
className="absolute cursor-pointer select-none rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] shadow-sm transition-[box-shadow,border-color] duration-150 hover:border-[color:var(--line-strong)] hover:shadow-md" className={`absolute cursor-pointer select-none rounded-[10px] border bg-[color:var(--panel)] shadow-sm transition-all duration-200 hover:shadow-lg ${
isRunning ? "border-[color:var(--success)] shadow-[0_0_12px_rgba(74,222,128,0.15)]" :
isError ? "border-[color:var(--danger)] border-opacity-50" :
isPending ? "border-[color:var(--warn)] border-dashed" :
"border-[color:var(--line)] hover:border-[color:var(--line-strong)]"
}`}
style={{ left: node.x, top: node.y, width: CARD_W, minHeight: CARD_H }} style={{ left: node.x, top: node.y, width: CARD_W, minHeight: CARD_H }}
onClick={() => onSelectAgent?.(node.agent.id)} onClick={() => onSelectAgent?.(node.agent.id)}
> >
{/* Top accent bar for role hierarchy */}
<div className={`h-[3px] rounded-t-[9px] ${
isCEO ? "bg-[color:var(--accent)]" :
node.children.length > 0 ? "bg-[color:var(--warn)]" :
"bg-[color:var(--muted)] opacity-30"
}`} />
<div className="flex items-center gap-3 px-4 py-3"> <div className="flex items-center gap-3 px-4 py-3">
<div className="relative shrink-0"> <div className="relative shrink-0">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-[color:var(--panel-soft)]"> <div className={`flex h-10 w-10 items-center justify-center rounded-full text-[13px] font-bold ${
<Robot size={18} className="text-[color:var(--muted-strong)]" /> isActive ? "bg-[color:var(--success-soft)] text-[color:var(--success)]" :
isError ? "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" :
"bg-[color:var(--panel-soft)] text-[color:var(--muted-strong)]"
}`}>
{initials}
</div> </div>
<span {isRunning ? (
className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[color:var(--panel)]" <span className="absolute -bottom-0.5 -right-0.5 flex h-3.5 w-3.5">
style={{ backgroundColor: dotColor }} <span className="absolute inline-flex h-full w-full animate-ping rounded-full opacity-40" style={{ backgroundColor: dotColor }} />
/> <span className="relative inline-flex h-2.5 w-2.5 rounded-full border-2 border-[color:var(--panel)]" style={{ backgroundColor: dotColor }} />
</span>
) : (
<span
className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[color:var(--panel)]"
style={{ backgroundColor: dotColor }}
/>
)}
</div> </div>
<div className="flex min-w-0 flex-1 flex-col items-start"> <div className="flex min-w-0 flex-1 flex-col items-start">
<span className="text-[13px] font-semibold leading-tight text-[color:var(--text)]">{node.agent.name}</span> <span className="text-[13px] font-semibold leading-tight text-[color:var(--text)]">{node.agent.name}</span>
<span className="mt-0.5 text-[11px] leading-tight text-[color:var(--muted)]"> <span className="mt-0.5 text-[11px] leading-tight text-[color:var(--muted)]">
{node.agent.title || node.agent.role} {node.agent.title || node.agent.role}
</span> </span>
<span className="mono mt-1 text-[10px] leading-tight text-[color:var(--muted)]"> <div className="mt-1.5 flex items-center gap-2">
{connectorLabel} <span className="rounded bg-[color:var(--panel-soft)] px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]">
</span> {connectorLabel}
</span>
{node.agent.heartbeatEnabled ? (
<Heartbeat size={10} className="text-[color:var(--success)]" />
) : null}
</div>
</div> </div>
</div> </div>
</div> </div>
+7 -7
View File
@@ -168,8 +168,8 @@ describe("Step 2: CEO phased prompts", () => {
const prompt = buildSystemPrompt(ctx); const prompt = buildSystemPrompt(ctx);
expect(prompt).toContain("Team Building Phase"); expect(prompt).toContain("Team Building Phase");
expect(prompt).toContain("Design Organization"); expect(prompt).toContain("Design the Organization Structure");
expect(prompt).toContain("Submit Org Plan for Approval"); expect(prompt).toContain("Submit Org Strategy for Approval");
expect(prompt).not.toContain("Awaiting Org Approval"); expect(prompt).not.toContain("Awaiting Org Approval");
expect(prompt).not.toContain("Goal Decomposition"); expect(prompt).not.toContain("Goal Decomposition");
}); });
@@ -239,7 +239,7 @@ describe("Step 2: CEO phased prompts", () => {
expect(prompt).toContain("Executive / CEO"); expect(prompt).toContain("Executive / CEO");
expect(prompt).toContain("Goal Decomposition"); expect(prompt).toContain("Goal Decomposition");
expect(prompt).toContain("Delegation"); 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("Team Building Phase");
expect(prompt).not.toContain("Awaiting Org Approval"); expect(prompt).not.toContain("Awaiting Org Approval");
}); });
@@ -289,8 +289,8 @@ describe("Step 2: CEO phased prompts", () => {
}); });
const prompt = buildSystemPrompt(ctx); const prompt = buildSystemPrompt(ctx);
expect(prompt).toContain("Do NOT create secret_access approvals directly"); expect(prompt).toContain("escalate by commenting on YOUR parent task");
expect(prompt).toContain("escalate upward"); expect(prompt).toContain("Break down work and delegate it");
}); });
it("IC prompt includes credential escalation guidance", () => { it("IC prompt includes credential escalation guidance", () => {
@@ -305,8 +305,8 @@ describe("Step 2: CEO phased prompts", () => {
}); });
const prompt = buildSystemPrompt(ctx); const prompt = buildSystemPrompt(ctx);
expect(prompt).toContain("Your manager will be notified and help resolve it"); expect(prompt).toContain("Your manager is auto-notified and will help");
expect(prompt).toContain("Only create a secret_access approval directly if you have NO manager"); expect(prompt).toContain("comment explaining what API key/account you need");
}); });
}); });