From 08e0e91af05f89acd5c7beb2e8de17b144e9a714 Mon Sep 17 00:00:00 2001 From: scotttong Date: Fri, 20 Mar 2026 14:05:57 -0700 Subject: [PATCH] experiment: board concierge skill + web UI chat surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a board-member skill that teaches Claude how to manage a Paperclip company via chat — covering onboarding, hiring plans, approvals, task monitoring, cost oversight, and agent system prompt management. Phase 1 (Claude Code surface): - Board skill at skills/paperclip-board/SKILL.md with full API reference - CLI bootstrap command `paperclipai board setup` that installs the skill and prints env exports Phase 2 (Web UI surface): - New /board/chat/stream endpoint that spawns Claude with the board skill as system prompt, passing PAPERCLIP_API_URL and PAPERCLIP_COMPANY_ID - BoardChat page with streaming responses, status indicators, and conversation persistence via Board Operations issue - Sidebar nav link and route registration The skill is a portable knowledge layer — same document powers Claude Code (Surface 1), web UI chat (Surface 2), and future MCP server (Surface 3). Co-Authored-By: Claude Opus 4.6 (1M context) --- cli/src/commands/client/board.ts | 208 +++++++++++ cli/src/index.ts | 2 + server/src/routes/agent-chat.ts | 234 ++++++++++++ skills/paperclip-board/SKILL.md | 620 +++++++++++++++++++++++++++++++ ui/src/App.tsx | 2 + ui/src/components/Sidebar.tsx | 1 + ui/src/lib/company-routes.ts | 4 + ui/src/pages/BoardChat.tsx | 315 ++++++++++++++++ 8 files changed, 1386 insertions(+) create mode 100644 cli/src/commands/client/board.ts create mode 100644 skills/paperclip-board/SKILL.md create mode 100644 ui/src/pages/BoardChat.tsx diff --git a/cli/src/commands/client/board.ts b/cli/src/commands/client/board.ts new file mode 100644 index 00000000..459a3375 --- /dev/null +++ b/cli/src/commands/client/board.ts @@ -0,0 +1,208 @@ +import { Command } from "commander"; +import { + removeMaintainerOnlySkillSymlinks, + resolvePaperclipSkillsDir, +} from "@paperclipai/adapter-utils/server-utils"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + addCommonClientOptions, + handleCommandError, + resolveCommandContext, + type BaseClientOptions, +} from "./common.js"; + +interface BoardSetupOptions extends BaseClientOptions { + companyId?: string; + installSkills?: boolean; +} + +interface SkillsInstallSummary { + tool: string; + target: string; + linked: string[]; + removed: string[]; + skipped: string[]; + failed: Array<{ name: string; error: string }>; +} + +const __moduleDir = path.dirname(fileURLToPath(import.meta.url)); + +function claudeSkillsHome(): string { + const fromEnv = process.env.CLAUDE_HOME?.trim(); + const base = fromEnv && fromEnv.length > 0 ? fromEnv : path.join(os.homedir(), ".claude"); + return path.join(base, "skills"); +} + +async function installSkillsForTarget( + sourceSkillsDir: string, + targetSkillsDir: string, + tool: string, +): Promise { + const summary: SkillsInstallSummary = { + tool, + target: targetSkillsDir, + linked: [], + removed: [], + skipped: [], + failed: [], + }; + + await fs.mkdir(targetSkillsDir, { recursive: true }); + const entries = await fs.readdir(sourceSkillsDir, { withFileTypes: true }); + summary.removed = await removeMaintainerOnlySkillSymlinks( + targetSkillsDir, + entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name), + ); + + // Only install the board skill + const boardEntry = entries.find((e) => e.isDirectory() && e.name === "paperclip-board"); + if (!boardEntry) { + summary.failed.push({ name: "paperclip-board", error: "Skill directory not found" }); + return summary; + } + + const source = path.join(sourceSkillsDir, boardEntry.name); + const target = path.join(targetSkillsDir, boardEntry.name); + const existing = await fs.lstat(target).catch(() => null); + + if (existing) { + if (existing.isSymbolicLink()) { + await fs.unlink(target); + } else { + summary.skipped.push(boardEntry.name); + return summary; + } + } + + try { + await fs.symlink(source, target); + summary.linked.push(boardEntry.name); + } catch (err) { + summary.failed.push({ + name: boardEntry.name, + error: err instanceof Error ? err.message : String(err), + }); + } + + return summary; +} + +function buildBoardEnvExports(input: { apiBase: string; companyId?: string }): string { + const escaped = (value: string) => value.replace(/'/g, "'\"'\"'"); + const lines = [`export PAPERCLIP_API_URL='${escaped(input.apiBase)}'`]; + if (input.companyId) { + lines.push(`export PAPERCLIP_COMPANY_ID='${escaped(input.companyId)}'`); + } + return lines.join("\n"); +} + +export function registerBoardCommands(program: Command): void { + const board = program.command("board").description("Board member operations"); + + addCommonClientOptions( + board + .command("setup") + .description( + "Install the board-member skill for Claude Code and print shell exports for managing your Paperclip company", + ) + .option("-C, --company-id ", "Company ID (if you already have one)") + .option("--no-install-skills", "Skip installing the board skill into ~/.claude/skills") + .action(async (opts: BoardSetupOptions) => { + try { + const ctx = resolveCommandContext(opts); + + // Attempt to auto-detect company if not provided + let companyId = opts.companyId?.trim() || ctx.companyId; + if (!companyId) { + try { + const companies = await ctx.api.get>( + "/api/companies", + ); + if (companies && companies.length === 1) { + companyId = companies[0].id; + console.log(`Auto-detected company: ${companies[0].name} (${companyId})`); + } else if (companies && companies.length > 1) { + console.log( + "Multiple companies found. Pass --company-id or set PAPERCLIP_COMPANY_ID:", + ); + for (const c of companies) { + console.log(` ${c.id} ${c.name}`); + } + } + } catch { + // Server might not be running yet — that's OK + } + } + + // Install skills + const installSummaries: SkillsInstallSummary[] = []; + if (opts.installSkills !== false) { + const skillsDir = await resolvePaperclipSkillsDir(__moduleDir, [ + path.resolve(process.cwd(), "skills"), + ]); + if (!skillsDir) { + console.log( + "Warning: Could not locate skills directory. Skipping skill installation.", + ); + } else { + installSummaries.push( + await installSkillsForTarget(skillsDir, claudeSkillsHome(), "claude"), + ); + } + } + + const exportsText = buildBoardEnvExports({ + apiBase: ctx.api.apiBase, + companyId, + }); + + if (ctx.json) { + const output = { + companyId, + skills: installSummaries, + exports: exportsText, + }; + console.log(JSON.stringify(output, null, 2)); + return; + } + + // Print summary + console.log(""); + console.log("Board setup complete!"); + console.log(""); + + if (installSummaries.length > 0) { + for (const summary of installSummaries) { + if (summary.linked.length > 0) { + console.log( + `Skill installed: ${summary.linked.join(", ")} → ${summary.target}`, + ); + } + for (const failed of summary.failed) { + console.log(` Failed: ${failed.name}: ${failed.error}`); + } + } + console.log(""); + } + + console.log("# Run this in your shell before launching Claude Code:"); + console.log(exportsText); + console.log(""); + console.log("# Then start Claude Code:"); + console.log("claude"); + if (!companyId) { + console.log(""); + console.log( + "Note: No company detected. Claude Code will guide you through creating one.", + ); + } + } catch (err) { + handleCommandError(err); + } + }), + { includeCompany: false }, + ); +} diff --git a/cli/src/index.ts b/cli/src/index.ts index 628cd7e7..fa4acf69 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -19,6 +19,7 @@ import { applyDataDirOverride, type DataDirOptionLike } from "./config/data-dir. import { loadPaperclipEnvFile } from "./config/env.js"; import { registerWorktreeCommands } from "./commands/worktree.js"; import { registerPluginCommands } from "./commands/client/plugin.js"; +import { registerBoardCommands } from "./commands/client/board.js"; const program = new Command(); const DATA_DIR_OPTION_HELP = @@ -138,6 +139,7 @@ registerActivityCommands(program); registerDashboardCommands(program); registerWorktreeCommands(program); registerPluginCommands(program); +registerBoardCommands(program); const auth = program.command("auth").description("Authentication and bootstrap utilities"); diff --git a/server/src/routes/agent-chat.ts b/server/src/routes/agent-chat.ts index d57565fe..af4158ce 100644 --- a/server/src/routes/agent-chat.ts +++ b/server/src/routes/agent-chat.ts @@ -2,6 +2,8 @@ import { Router } from "express"; import { randomUUID } from "node:crypto"; import { spawn } from "node:child_process"; import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import type { Db } from "@paperclipai/db"; import { agents as agentsTable, heartbeatRuns, issueWorkProducts } from "@paperclipai/db"; import { eq } from "drizzle-orm"; @@ -616,5 +618,237 @@ If nothing to create, output empty arrays. ALWAYS include this signal line.`; proc.stdin.end(); }); + // ── Board Concierge Chat ────────────────────────────────────────────── + // Same streaming pattern as /agents/:id/chat/stream but uses the + // board-member skill as the system prompt instead of the CEO agent's + // prompt. Allows the board to manage their company from the web UI chat. + + let _boardSkillCache: string | null = null; + + function loadBoardSkill(): string { + if (_boardSkillCache) return _boardSkillCache; + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const skillPath = path.resolve(__dirname, "../../../skills/paperclip-board/SKILL.md"); + try { + let content = fs.readFileSync(skillPath, "utf-8"); + // Strip YAML frontmatter + content = content.replace(/^---[\s\S]*?---\s*\n/, ""); + _boardSkillCache = content; + return content; + } catch { + return "You are a board-level assistant helping a human manage their AI-agent company through Paperclip. Help them create companies, hire agents, approve tasks, and monitor their organization."; + } + } + + router.post("/board/chat/stream", async (req, res) => { + const { companyId, message, taskId } = req.body as { + companyId: string; + message: string; + taskId?: string; + }; + + if (!companyId || !message) { + res.status(400).json({ error: "companyId and message are required" }); + return; + } + + const issueSvc = issueService(db); + let issueId = taskId; + + // Find or create the Board Operations issue + if (!issueId) { + const companyIssues = await issueSvc.list(companyId, { + q: "Board Operations", + }); + const boardIssue = companyIssues.find( + (i: any) => i.title === "Board Operations" && i.status !== "done" && i.status !== "cancelled", + ); + if (boardIssue) { + issueId = boardIssue.id; + } else { + const created = await issueSvc.create(companyId, { + title: "Board Operations", + description: "Standing issue for board concierge conversations and decision log", + status: "in_progress", + priority: "medium", + }); + issueId = created.id; + } + } + + const resolvedIssueId = issueId!; + + // Save user message as comment + await issueSvc.addComment(resolvedIssueId, message, { + userId: (req as any).actor?.userId ?? "local-board", + }); + + // Build conversation history from recent comments + const comments = await issueSvc.listComments(resolvedIssueId); + const sorted = [...comments].sort( + (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(), + ); + const recent = sorted.slice(-20); + const history = recent + .map((c) => { + const role = c.authorAgentId ? "ASSISTANT" : "USER"; + return `${role}: ${c.body}`; + }) + .join("\n\n"); + + // Load board skill as system prompt + const systemPrompt = loadBoardSkill(); + + // Compose prompt with conversation history + const prompt = history + ? `Here is the conversation so far:\n\n${history}\n\nRespond to the latest message from the user.` + : message; + + // Set up SSE + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + "X-Accel-Buffering": "no", + }); + res.flushHeaders(); + res.write(`data: ${JSON.stringify({ type: "start", issueId: resolvedIssueId })}\n\n`); + + // Spawn claude CLI with board skill + const args = [ + "-p", + "-", + "--output-format", + "stream-json", + "--verbose", + "--append-system-prompt", + systemPrompt, + "--model", + "sonnet", + "--dangerously-skip-permissions", + ]; + + // Determine the API URL for the spawned process + const serverAddr = (req as any).socket?.localAddress ?? "127.0.0.1"; + const serverPort = (req as any).socket?.localPort ?? 3000; + const apiUrl = `http://${serverAddr === "::" || serverAddr === "::1" ? "127.0.0.1" : serverAddr}:${serverPort}`; + + const proc = spawn("claude", args, { + stdio: ["pipe", "pipe", "pipe"], + cwd: "/tmp", + env: { + ...process.env, + PAPERCLIP_API_URL: apiUrl, + PAPERCLIP_COMPANY_ID: companyId, + }, + }); + + let fullResponse = ""; + const startTime = Date.now(); + let killed = false; + + // 120s timeout (board conversations can involve multiple API calls) + const timeout = setTimeout(() => { + killed = true; + proc.kill("SIGTERM"); + }, 120000); + + // Stream stdout — parse stream-json events + let stdoutBuf = ""; + proc.stdout.on("data", (data: Buffer) => { + stdoutBuf += data.toString(); + const lines = stdoutBuf.split("\n"); + stdoutBuf = lines.pop() ?? ""; + + for (const line of lines) { + if (!line.trim()) continue; + try { + const event = JSON.parse(line); + if (event.type === "assistant" && event.message?.content) { + for (const block of event.message.content) { + if (block.type === "text" && block.text && res.writable) { + fullResponse += block.text; + res.write(`data: ${JSON.stringify({ type: "chunk", text: block.text })}\n\n`); + } + } + } else if (event.type === "content_block_delta" && event.delta?.text) { + fullResponse += event.delta.text; + if (res.writable) { + res.write(`data: ${JSON.stringify({ type: "chunk", text: event.delta.text })}\n\n`); + } + } else if (event.type === "content_block_start" && event.content_block?.type === "tool_use" && res.writable) { + // Forward tool-use status so UI can show activity + const toolName = event.content_block.name ?? "working"; + let statusText = "Working..."; + if (toolName === "Bash" || toolName === "bash") { + statusText = "Running a command..."; + } else if (toolName === "Read" || toolName === "read") { + statusText = "Reading a file..."; + } else if (toolName === "Grep" || toolName === "grep") { + statusText = "Searching..."; + } else { + statusText = `Using ${toolName}...`; + } + res.write(`data: ${JSON.stringify({ type: "status", text: statusText })}\n\n`); + } else if (event.type === "result" && event.result && !fullResponse) { + fullResponse = event.result; + if (res.writable) { + res.write(`data: ${JSON.stringify({ type: "chunk", text: event.result })}\n\n`); + } + } + } catch { + // Not JSON or unknown format — skip + } + } + }); + + proc.stderr.on("data", (data: Buffer) => { + console.error("[board/chat/stream stderr]", data.toString()); + }); + + proc.on("close", async (exitCode) => { + clearTimeout(timeout); + + // Save response as a comment (strip any action signals) + const cleanedResponse = stripActionSignals(fullResponse).trim(); + if (cleanedResponse) { + try { + // Save as a system/board comment (no agentId) + await issueSvc.addComment(resolvedIssueId, cleanedResponse, { + userId: "board-concierge", + }); + } catch { + /* best effort */ + } + } + + const duration = Date.now() - startTime; + if (res.writable) { + res.write( + `data: ${JSON.stringify({ + type: "done", + issueId: resolvedIssueId, + duration, + exitCode: exitCode ?? 0, + timedOut: killed, + })}\n\n`, + ); + } + if (res.writable) res.end(); + }); + + proc.on("error", (err) => { + clearTimeout(timeout); + if (res.writable) { + res.write(`data: ${JSON.stringify({ type: "error", message: err.message })}\n\n`); + res.end(); + } + }); + + // Pipe the prompt to stdin + proc.stdin.write(prompt); + proc.stdin.end(); + }); + return router; } diff --git a/skills/paperclip-board/SKILL.md b/skills/paperclip-board/SKILL.md new file mode 100644 index 00000000..55fa03b4 --- /dev/null +++ b/skills/paperclip-board/SKILL.md @@ -0,0 +1,620 @@ +--- +name: paperclip-board +description: > + Manage a Paperclip company as a board member via chat. Covers onboarding + (company creation, CEO setup, hiring plans), agent management, approvals, + task monitoring, cost oversight, and work product review. Use this skill + whenever the user wants to interact with their Paperclip control plane. +--- + +# Paperclip Board Skill + +You are a board-level assistant helping a human manage their AI-agent company through Paperclip. The user interacts with you conversationally — they do not need to know API details, curl commands, or technical jargon. Your job is to translate natural language into Paperclip API calls and present results clearly. + +## Authentication & Environment + +**Environment variables** (set by `paperclipai board setup`): +- `PAPERCLIP_API_URL` — base URL of the Paperclip server (e.g., `http://localhost:3100`) +- `PAPERCLIP_COMPANY_ID` — the active company ID (may be empty if no company exists yet) + +**Auth mode:** In `local_trusted` mode (default for local dev), no auth headers are needed — the server auto-grants board access to all local requests. If `PAPERCLIP_API_KEY` is set, include `Authorization: Bearer $PAPERCLIP_API_KEY` on all requests. + +**Making API calls:** Use `curl -sS` via bash. All endpoints are under `/api`. All request/response bodies are JSON. Always use `Content-Type: application/json` on POST/PATCH/PUT requests. + +**Critical rules:** +- Always re-read a document or config from the API before modifying it (write-path freshness) +- Never hard-code the API URL — always use `$PAPERCLIP_API_URL` +- Always include web UI links in responses: `$PAPERCLIP_API_URL/{companyPrefix}/...` +- Present results conversationally — summarize, don't dump JSON + +## Session Startup + +Every time you begin a new conversation with the user: + +1. Check if `PAPERCLIP_API_URL` is set. If not, tell the user to run `pnpm paperclipai board setup`. +2. Check if `PAPERCLIP_COMPANY_ID` is set. + - If set: fetch the dashboard to understand current state. + - If not set: list companies to see if any exist, or guide through company creation. +3. Check if a decision log exists: `GET $PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/issues?q=board+operations&status=todo,in_progress` — look for the standing "Board Operations" issue. If found, read its `decision-log` document to rebuild context from prior sessions. +4. Greet the user with a brief status summary. + +```bash +# Fetch dashboard +curl -sS "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/dashboard" +``` + +Present the dashboard as: +``` +{Company Name} Dashboard +──────────────────────── +Agents: {active} active, {paused} paused +Tasks: {open} open ({inProgress} in progress, {blocked} blocked) +Budget: ${monthSpendCents/100} / ${monthBudgetCents/100} this month ({utilization}%) +Pending approvals: {pendingApprovals} + +{If pendingApprovals > 0: list them briefly} +{If blocked > 0: mention blocked tasks} +``` + +## Onboarding Flow + +Guide the user through these steps when they're setting up for the first time. + +### Step 1: Create or Select a Company + +```bash +# List existing companies +curl -sS "$PAPERCLIP_API_URL/api/companies" + +# Create a new company +curl -sS -X POST "$PAPERCLIP_API_URL/api/companies" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Company Name", + "description": "Company mission / description", + "budgetMonthlyCents": 50000 + }' +``` + +Ask the user for: +- Company name +- Mission / description (store in `description` field) +- Monthly budget (suggest a reasonable default like $500 = 50000 cents) + +The response includes the company `id` and auto-generated `issuePrefix`. Tell the user both. + +After creating, set `PAPERCLIP_COMPANY_ID` for subsequent calls. Also set `requireBoardApprovalForNewAgents: true` so all hires go through governance: + +```bash +curl -sS -X PATCH "$PAPERCLIP_API_URL/api/companies/{companyId}" \ + -H "Content-Type: application/json" \ + -d '{"requireBoardApprovalForNewAgents": true}' +``` + +### Step 2: Create the CEO Agent + +The CEO is the first agent. Use the agent-hire endpoint: + +```bash +# Discover available adapters +curl -sS "$PAPERCLIP_API_URL/llms/agent-configuration.txt" + +# Read adapter-specific docs (e.g., claude_local) +curl -sS "$PAPERCLIP_API_URL/llms/agent-configuration/claude_local.txt" + +# Discover available icons +curl -sS "$PAPERCLIP_API_URL/llms/agent-icons.txt" + +# Submit hire request +curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/agent-hires" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "CEO Name", + "role": "ceo", + "title": "Chief Executive Officer", + "icon": "crown", + "capabilities": "Strategic planning, team management, task delegation", + "adapterType": "claude_local", + "adapterConfig": { + "cwd": "/path/to/working/directory", + "model": "sonnet" + }, + "runtimeConfig": { + "heartbeat": {"enabled": true, "intervalSec": 300, "wakeOnDemand": true} + }, + "permissions": {"canCreateAgents": true}, + "budgetMonthlyCents": 10000 + }' +``` + +Guide the user through: +- CEO name and icon (show available icons) +- Working directory (where the CEO will operate) +- Adapter type (default: `claude_local`) +- Budget + +Generate the CEO's system prompt using the Agent System Prompt Template (Section D below). + +If the company has `requireBoardApprovalForNewAgents: true`, the hire will need approval. Check if an approval was created and auto-approve it for the CEO (since the user just asked to create it): + +```bash +# Check pending approvals +curl -sS "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/approvals?status=pending" + +# Approve the CEO hire +curl -sS -X POST "$PAPERCLIP_API_URL/api/approvals/{approvalId}/approve" \ + -H "Content-Type: application/json" \ + -d '{"decisionNote": "CEO hire approved by board during onboarding"}' +``` + +### Step 3: Create the Board Operations Issue + +Create a standing issue for decision logging and board operations: + +```bash +curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/issues" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "Board Operations", + "description": "Standing issue for board decision log and operations tracking", + "status": "in_progress", + "priority": "medium" + }' +``` + +Then create the decision log document: + +```bash +curl -sS -X PUT "$PAPERCLIP_API_URL/api/issues/{boardIssueId}/documents/decision-log" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "Decision Log", + "format": "markdown", + "body": "# Decision Log — {Company Name}\n\n## {today date}\n- Created company {name} with mission: {description}\n- Hired CEO agent \"{ceo name}\"\n" + }' +``` + +Also write this to a local file at `./artifacts/decision-log.md` so the user can view it directly. + +### Step 4: Launch the Company + +Start the CEO's first heartbeat: + +```bash +curl -sS -X POST "$PAPERCLIP_API_URL/api/agents/{ceoId}/heartbeat/invoke" \ + -H "Content-Type: application/json" +``` + +## Hiring Plan Loop + +When the user wants to build a hiring plan: + +1. **Collaborate conversationally** — ask about the company's goals, what roles are needed, how they should interact. Use your judgment to suggest roles. + +2. **Store as a document artifact** — create an issue for the hiring plan, then attach the plan as a document: + +```bash +# Create the hiring plan issue +curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/issues" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "Hiring Plan", + "description": "Develop and execute the team hiring plan", + "status": "in_progress", + "priority": "high" + }' + +# Attach the plan document +curl -sS -X PUT "$PAPERCLIP_API_URL/api/issues/{issueId}/documents/hiring-plan" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "Hiring Plan", + "format": "markdown", + "body": "# Hiring Plan\n\n## Roles\n\n### 1. Role Name\n- Focus: ...\n- Reports to: ...\n- Budget: ...\n" + }' +``` + +3. **Also write a local file** at `./artifacts/hiring-plan.md` so the user can open and edit it directly. + +4. **Iterate** — when the user suggests changes: + - In chat: update both the API document and local file + - If user says they edited the file: re-read `./artifacts/hiring-plan.md` and sync to API + - If user says they edited in web UI: re-fetch from API with `GET /api/issues/{id}/documents/hiring-plan` + +5. **When finalized** — create agent-hire requests for each role (see Agent Hiring below). + +## Agent System Prompt Template + +Every new agent's system prompt MUST include these sections by default (unless the board explicitly overrides): + +```markdown +# {Agent Name} + +## Description +{One-line role summary} + +## Expertise +{Core expertise — what this agent knows, how it thinks, what it does} + +## Priorities +{Ordered list of what matters most for this agent's work} + +## Boundaries +{What this agent should NOT do, scope limits, guardrails} + +## Tool Permissions +{Which tools/APIs this agent can use, and any exclusions} + +## Communication Guidelines +{How this agent reports status, asks for help, formats output} + +## Collaboration & Escalation +{Which agents this one works with, when to escalate, to whom} +``` + +Present each agent's draft system prompt to the user for review before submitting the hire. + +## Agent Hiring + +For each agent to hire: + +```bash +# Compare existing agent configurations +curl -sS "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/agent-configurations" + +# Submit hire request +curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/agent-hires" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Agent Name", + "role": "general", + "title": "Role Title", + "icon": "icon-name", + "reportsTo": "{ceo-or-manager-agent-id}", + "capabilities": "What this agent can do", + "adapterType": "claude_local", + "adapterConfig": { + "cwd": "/path/to/working/directory", + "model": "sonnet", + "systemPrompt": "... the full system prompt from the template ..." + }, + "runtimeConfig": { + "heartbeat": {"enabled": true, "intervalSec": 300, "wakeOnDemand": true} + }, + "budgetMonthlyCents": 5000 + }' +``` + +### Cross-Agent Escalation Path Updates + +When a new agent is hired, update existing agents' Collaboration & Escalation sections: + +1. **Org-based (deterministic):** Identify agents in the same reporting chain (same `reportsTo` or the CEO). These always need to know about the new hire. + +2. **Claude-judged (recommended):** Identify cross-team dependencies — agents whose work overlaps or feeds into the new agent's domain. Include your reasoning. + +3. **Present all proposed changes for board approval** — distinguish the two categories: + +``` +Hiring @designer — proposed escalation path updates: + +Org-based (same reporting chain): + @ceo — add: "@designer handles brand assets, visual design, UX research. + Route design reviews through @designer." + @frontend-engineer — add: "Escalate visual design decisions to @designer. + Request mockups before building new UI components." + +Additionally recommended: + @content-strategist — add: "Request visual assets (headers, social images) + from @designer. Coordinate brand voice with design." + Reason: Content pipeline will need visual assets for blog posts and social. + +Approve these updates? (approve all / review individually / edit) +``` + +4. Only after board approval, update each affected agent: + +```bash +# Fetch current config first (write-path freshness) +curl -sS "$PAPERCLIP_API_URL/api/agents/{agentId}" + +# Update the agent's config with new escalation paths +curl -sS -X PATCH "$PAPERCLIP_API_URL/api/agents/{agentId}" \ + -H "Content-Type: application/json" \ + -d '{ + "adapterConfig": { ... updated config with new Collaboration section ... } + }' +``` + +5. Log the changes and reasoning in the decision log. + +## Approvals + +```bash +# List pending approvals +curl -sS "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/approvals?status=pending" + +# Approve +curl -sS -X POST "$PAPERCLIP_API_URL/api/approvals/{id}/approve" \ + -H "Content-Type: application/json" \ + -d '{"decisionNote": "Approved by board"}' + +# Reject +curl -sS -X POST "$PAPERCLIP_API_URL/api/approvals/{id}/reject" \ + -H "Content-Type: application/json" \ + -d '{"decisionNote": "Reason for rejection"}' + +# Request revision +curl -sS -X POST "$PAPERCLIP_API_URL/api/approvals/{id}/request-revision" \ + -H "Content-Type: application/json" \ + -d '{"decisionNote": "Please adjust X, Y, Z"}' +``` + +Present approvals as: +``` +Pending Approvals +───────────────── +1. [hire] Designer — submitted by @ceo + View: {baseUrl}/{prefix}/approvals/{id} + → approve / reject / request revision + +2. [tool] Icon library ($12/mo) — requested by @designer + → approve / reject +``` + +For batch approval: list all pending, let the user approve all or review individually. + +## Task Management + +```bash +# List open tasks +curl -sS "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/issues?status=todo,in_progress,blocked" + +# Get task detail +curl -sS "$PAPERCLIP_API_URL/api/issues/{issueId}" + +# Get task comments +curl -sS "$PAPERCLIP_API_URL/api/issues/{issueId}/comments" + +# Create a task +curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/issues" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "Task title", + "description": "What needs to be done", + "status": "todo", + "priority": "medium", + "assigneeAgentId": "{agent-id}", + "projectId": "{project-id}", + "parentId": "{parent-issue-id}" + }' + +# Update a task +curl -sS -X PATCH "$PAPERCLIP_API_URL/api/issues/{issueId}" \ + -H "Content-Type: application/json" \ + -d '{"status": "done", "comment": "Completed"}' + +# Add a comment +curl -sS -X POST "$PAPERCLIP_API_URL/api/issues/{issueId}/comments" \ + -H "Content-Type: application/json" \ + -d '{"body": "Comment text in markdown"}' + +# Search issues +curl -sS "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/issues?q=search+term" +``` + +Present tasks as: +``` +{PREFIX}-{number}: {title} [{status}] → @{assignee} + Priority: {priority} + Latest: "{last comment snippet...}" + View: {baseUrl}/{prefix}/issues/{identifier} +``` + +## Agent Monitoring + +```bash +# List all agents +curl -sS "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/agents" + +# Get agent detail +curl -sS "$PAPERCLIP_API_URL/api/agents/{id}" + +# Get agent config revisions (change history) +curl -sS "$PAPERCLIP_API_URL/api/agents/{id}/config-revisions" +``` + +Present agents as: +``` +Team Overview +───────────── +@ceo (Atlas) — active, last heartbeat 5m ago + Budget: $45 / $100 (45%) + Working on: PAP-12 Homepage redesign + +@frontend-engineer — active, last heartbeat 2m ago + Budget: $30 / $50 (60%) + Working on: PAP-15 Blog template +``` + +## Cost Monitoring + +```bash +# Overall summary +curl -sS "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/costs/summary" + +# Breakdown by agent +curl -sS "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/costs/by-agent" + +# Breakdown by project +curl -sS "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/costs/by-project" + +# Optional date range +curl -sS "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/costs/summary?from=2026-03-01&to=2026-03-31" +``` + +Present costs as: +``` +Costs This Month +──────────────── +Total: $145.23 / $500.00 (29%) + +By Agent: + @ceo $45.12 (31%) + @frontend-eng $62.30 (43%) + @content-strat $37.81 (26%) +``` + +## Work Products + +```bash +# List work products for an issue +curl -sS "$PAPERCLIP_API_URL/api/issues/{issueId}/work-products" + +# View a document +curl -sS "$PAPERCLIP_API_URL/api/issues/{issueId}/documents/{key}" + +# View document revisions +curl -sS "$PAPERCLIP_API_URL/api/issues/{issueId}/documents/{key}/revisions" +``` + +Present work products with status and links: +``` +Work Products — PAP-12 +────────────────────── +1. Homepage mockup [ready_for_review] — artifact + View: {baseUrl}/{prefix}/issues/PAP-12#document-mockup + +2. Feature branch [active] — branch + URL: https://github.com/... +``` + +## Editing Agent System Prompts + +Three ways the user can edit system prompts: + +**In chat:** User describes changes, you update via API: +```bash +# Always re-fetch before modifying +curl -sS "$PAPERCLIP_API_URL/api/agents/{id}" + +# Then update +curl -sS -X PATCH "$PAPERCLIP_API_URL/api/agents/{id}" \ + -H "Content-Type: application/json" \ + -d '{"adapterConfig": { ... updated config ... }}' +``` + +**Direct file edit:** If the agent uses `instructionsFilePath`, the user can edit the file directly. When they tell you they're done, re-read the file and confirm changes. + +**Web UI edit:** User edits at `{baseUrl}/{prefix}/agents/{agentUrlKey}`. When they say "sync up," re-fetch from the API. + +**Viewing change history:** +```bash +curl -sS "$PAPERCLIP_API_URL/api/agents/{id}/config-revisions" +``` + +Present as a changelog: +``` +Config History — @designer +────────────────────────── +Rev 3 (2026-03-21 14:30) — changed: systemPrompt + Added UX research to expertise section + +Rev 2 (2026-03-21 10:15) — changed: budgetMonthlyCents + Budget increased from $50 to $100 + +Rev 1 (2026-03-20 16:00) — initial configuration +``` + +## Decision Log + +Maintain a decision log for session continuity. Log major decisions — not every interaction. + +**What to log:** +- Company creation and configuration changes +- Agents hired, modified, or removed +- Budget changes +- Strategic decisions (what was prioritized, what was cut and why) +- Approvals granted or rejected with reasoning + +**When to log:** +- After completing a significant action (hiring, approving, budget change) +- At the end of a session if notable decisions were made + +**How to log:** +1. Update the API document: +```bash +# Fetch current log +curl -sS "$PAPERCLIP_API_URL/api/issues/{boardIssueId}/documents/decision-log" + +# Update with new entries appended +curl -sS -X PUT "$PAPERCLIP_API_URL/api/issues/{boardIssueId}/documents/decision-log" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "Decision Log", + "format": "markdown", + "body": "... existing content ... \n\n## {date}\n- New decision\n", + "baseRevisionId": "{current revision id}" + }' +``` +2. Also update the local file at `./artifacts/decision-log.md`. + +## Presentation Rules + +- Use markdown tables for lists (agents, tasks, costs) +- Use bold for status values: **in_progress**, **blocked**, **completed** +- Always include web UI links: `View: {PAPERCLIP_API_URL}/{prefix}/issues/{identifier}` +- For org charts: generate mermaid diagrams or ASCII art +- Smart summaries: surface what needs attention first, then the rest +- Task format: `PAP-123: Build landing page [in_progress] → @engineer` +- Keep responses concise — the user can ask to drill deeper +- When presenting multiple items for action (approvals, hires), number them for easy reference +- Derive the company's URL prefix from any issue identifier (e.g., `PAP-315` → prefix is `PAP`) + +## Link Format + +All web UI links must include the company prefix: +- Issues: `/{prefix}/issues/{identifier}` (e.g., `/PAP/issues/PAP-12`) +- Agents: `/{prefix}/agents/{agent-url-key}` +- Approvals: `/{prefix}/approvals/{approval-id}` +- Projects: `/{prefix}/projects/{project-url-key}` +- Documents: `/{prefix}/issues/{identifier}#document-{key}` + +## Key Endpoints Reference + +| Action | Method | Endpoint | +|--------|--------|----------| +| List companies | GET | `/api/companies` | +| Create company | POST | `/api/companies` | +| Update company | PATCH | `/api/companies/:id` | +| Get company | GET | `/api/companies/:id` | +| Dashboard | GET | `/api/companies/:companyId/dashboard` | +| List agents | GET | `/api/companies/:companyId/agents` | +| Get agent | GET | `/api/agents/:id` | +| Update agent | PATCH | `/api/agents/:id` | +| Agent configs | GET | `/api/companies/:companyId/agent-configurations` | +| Config revisions | GET | `/api/agents/:id/config-revisions` | +| Hire agent | POST | `/api/companies/:companyId/agent-hires` | +| Invoke heartbeat | POST | `/api/agents/:id/heartbeat/invoke` | +| List issues | GET | `/api/companies/:companyId/issues` | +| Create issue | POST | `/api/companies/:companyId/issues` | +| Get issue | GET | `/api/issues/:id` | +| Update issue | PATCH | `/api/issues/:id` | +| Issue comments | GET | `/api/issues/:id/comments` | +| Add comment | POST | `/api/issues/:id/comments` | +| Issue documents | GET | `/api/issues/:id/documents` | +| Get document | GET | `/api/issues/:id/documents/:key` | +| Create/update doc | PUT | `/api/issues/:id/documents/:key` | +| Work products | GET | `/api/issues/:id/work-products` | +| List approvals | GET | `/api/companies/:companyId/approvals` | +| Approve | POST | `/api/approvals/:id/approve` | +| Reject | POST | `/api/approvals/:id/reject` | +| Request revision | POST | `/api/approvals/:id/request-revision` | +| Cost summary | GET | `/api/companies/:companyId/costs/summary` | +| Costs by agent | GET | `/api/companies/:companyId/costs/by-agent` | +| Costs by project | GET | `/api/companies/:companyId/costs/by-project` | +| Adapter docs | GET | `/llms/agent-configuration.txt` | +| Adapter detail | GET | `/llms/agent-configuration/:adapterType.txt` | +| Agent icons | GET | `/llms/agent-icons.txt` | +| Set instructions | PATCH | `/api/agents/:id/instructions-path` | +| Search issues | GET | `/api/companies/:companyId/issues?q=term` | diff --git a/ui/src/App.tsx b/ui/src/App.tsx index e97d3d68..b9d3769d 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -8,6 +8,7 @@ import { authApi } from "./api/auth"; import { healthApi } from "./api/health"; import { Artifacts } from "./pages/Artifacts"; import { Chat } from "./pages/Chat"; +import { BoardChat } from "./pages/BoardChat"; import { Dashboard } from "./pages/Dashboard"; import { Companies } from "./pages/Companies"; import { Agents } from "./pages/Agents"; @@ -116,6 +117,7 @@ function boardRoutes() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/ui/src/components/Sidebar.tsx b/ui/src/components/Sidebar.tsx index 20909af9..c867be88 100644 --- a/ui/src/components/Sidebar.tsx +++ b/ui/src/components/Sidebar.tsx @@ -84,6 +84,7 @@ export function Sidebar() {