Back office for the /company skill: every /company run is its own company (state file per company, several open at once), hires project specialists as agent…

Formerly
ai-prompts. Old links and clones redirect to this repo.
Skills, mods, subagents, hooks, slash commands and guides for Claude Code — installable by your agent (INSTALL.md).
Project-agnostic guides and executable agents for setting up Claude Code with 70-90% token reduction.
This library contains reusable prompts for implementing global Claude Code optimization across any project. Share with other developers or use to recreate your setup.
Point your coding agent at INSTALL.md:
Read https://github.com/escapeboy/claude-code-kit/blob/master/INSTALL.md and install what fits my setup.
The agent inspects your environment, proposes a selection (starter, code intelligence, delivery, multi-agent, mods, security, unattended, stack-specific), installs it from the latest release tag after your yes, without overwriting your files, and verifies the result. llms.txt is the index agents use to find their way around.
Set up global optimization (ONE-TIME, applies to ALL projects)
~/.claude/skills/)optimize/ - /optimize — max token efficiency mode (multi-file: thin core + references/)context/ - /ctx — memory management (multi-file: thin core + references/)cache-inspector/ - /cache-inspector — cache monitoring (multi-file: thin core + references/)update-docs/ - /update-docs — documentation refresh (multi-file: thin core + references/)init-project/ - /init-project — new project setup (multi-file: thin core + references/)agent-ready/ - /agent-ready — AI-agent-readiness audit + selective remediation (multi-file: scanner script + ROI/implementation references)continuity/ - /continuity — repo-local resume→work→finalize lifecycle + evidence-weighted .continuity/STATE.md (multi-file: lint/scaffold script + format reference)self-improve/ - /self-improve — closing-the-loop for the skill library: mine recurring feedback → bounded edit → tiered eval gate (deterministic skill-lint.py + trigger accuracy + LLM judge + with/without-skill behavioral eval via claude plugin eval) → converge (multi-file: linter + behavior-stats.py + deepeval_tier3.py scripts, rubric/behavior-eval/integration/deepeval-setup references)video-digest/ - /video-digest — video or recording → short digest, related notes, fact-checked memory candidatescode-research/ - /code-research — multi-agent grounded audit of a local or git codebase into a cross-linked knowledge base (multi-file: per-phase references/)agent-team/ - /agent-team — Agent Teams presets: pr-review, debug, feature, customcodebase-memory/ - codebase-memory-mcp knowledge-graph queries: callers, call chains, dead code, Cypherconfidence-check/ - /confidence-check — ≥90% readiness gate before implementation (+ confidence.ts reference implementation)decision-classify/ - /decision-classify — Mechanical / Taste / User Challenge classification to cut interruptionssprint-orchestrate/ - /sprint-orchestrate — Think → Plan → Build → Review → Test → Ship → Reflect with decision gates (--from-design, --no-merge for callers like /company)company/ - /company — an IT company for one task: clarify → research → split into parts → staff teams with fitting models → deliver through sprint-orchestrate; two stops for the user (multi-file: org-design, roster, briefs, workflows, memory references; back office in the company-hq mod)sync-features/ - /sync-features — sync a project's feature inventory into Serena memories and auto-memoryui-ux-review/ - /ui-ux-review — audit UI code for design consistency and accessibility (multi-file: examples + sample report)global-optimization.md - Core optimization rulessymbol-first-protocol.md - Symbol-first exploration protocolTime: 2-3 hours (one-time) Benefit: 70-90% token reduction on ALL future projects ROI: Pays for itself in 2-3 sessions
Activate optimization for a specific project (10-15 min per project)
architecture-template.md - Project structure templateconventions-template.md - Coding patterns templateTime: 10-15 minutes per project Benefit: Project-specific memories for 60-70% session savings Required: After global setup, before starting work
Create custom slash commands (skills)
example-simple-skill.md - Simple single-action skillexample-complex-skill.md - Multi-action skill with integrationpwa.md - PWA features skill (service worker, manifest, offline, push notifications)/module:mcp — Add a Laravel MCP server to any project (dual transport, domain tools, auth)/module:assistant — Add an AI assistant chat panel (Livewire, PrismPHP tools, local agent support)Use when: You want custom commands like /deploy or /migrate, or full modules like /module:mcp and /module:assistant Benefit: Encapsulate common workflows, reduce repetition; module skills bootstrap entire features
Research and integrate new Claude API features
Use when: New Claude API features released, quarterly reviews Benefit: Keep documentation current, adopt new optimizations
Deep dive into token reduction techniques
Use when: Analyzing token usage, optimizing specific workflows Benefit: Understand and maximize savings, track ROI
Advanced techniques for complex scenarios
/agent-team skill with pr-review, debug, feature, custom presetsUse when: Complex projects, team coordination, critical decisions, cost visibility Benefit: Handle advanced scenarios with proven patterns; understand where tokens go
Custom slash commands for specialized workflows
Use when: Specialized workflows, testing, debugging, sprint retrospectives, content quality assurance Benefit: Encapsulate complex workflows into simple commands
Production-ready UI/UX implementation with Claude Code
Use when: Building dashboards, admin panels, landing pages, SaaS interfaces Benefit: 40-60% token savings, production-ready code with security and accessibility Time: 60-90 minutes per dashboard (vs 3-4 hours manual)
Connect Claude Code with Laravel's MCP ecosystem
Use when: Working on Laravel 11.x/12.x projects with Claude Code Benefit: Project-aware assistance via Laravel Boost + package discovery via LaraPlugins.io Setup: 5 minutes per project (composer install + MCP registration)
Create custom AI agents with specialized knowledge and tools
plan-challenger (Opus) — adversarial plan review across 5 dimensions with refutation checkoutput-evaluator (Haiku) — LLM-as-Judge: APPROVE/NEEDS_REVIEW/REJECT before commitloop-monitor (Haiku) — watchdog for autonomous sessions: stall/runaway/loop detectioncode-reviewer.md - Read-only code review agentlaravel-specialist.md - Laravel development agentdebugger.md - Debugging specialisttest-generator.md - Test generation agentUse when: Repetitive specialized tasks, team standardization, cost optimization Benefit: Reusable agents with controlled tool access and model selection Setup: 5-10 minutes per agent definition
Mobile development with Claude Code across all major platforms
Use when: Developing iOS (Swift/SwiftUI), Android (Kotlin/Compose), React Native, or Flutter apps Benefit: Platform-specific MCP integration, build/test automation, device control Setup: 5-10 minutes per platform (MCP installation + CLAUDE.md template)
Desktop development with Claude Code for macOS, Tauri, and Electron
Use when: Building macOS native (SwiftUI/AppKit), Tauri (Rust + Web), or Electron (Node.js + Chromium) desktop apps Benefit: Platform-specific MCP integration, build/package automation, code signing and notarization workflows Setup: 5-10 minutes per platform (MCP installation + CLAUDE.md template)
WebMCP — structured browser tools for AI agents (W3C Draft, Chrome 146 Canary)
navigator.modelContext API reference with code examplesUse when: Building web applications that AI agents will interact with Benefit: Structured tool access instead of DOM scraping; 89% token reduction Status: Early Preview — Chrome 146 Canary only, spec actively changing
Protect Claude Code workflows from MCP attacks, prompt injection, and accidental data loss
settings.json + hook implementationspermissions.deny hardening templates (global + project-level)dangerous-actions-blocker.py — blocks rm -rf /-class commands in any spelling, force-push to main, DROP TABLE, over-broad pkill/killall, edits to key filespre-commit-secrets.py — scans staged content for API keys / private keys / DB URLs before every git commitblock-interactive-sudo.py — refuses sudo that would hang on a password prompttests/ — two-sided matrices (must-block AND must-pass) · WHY.md — the failure behind each hooksettings.json wiring and the PreToolUse hook contractUse when: Team environments, production codebases, regulated industries, before adding new MCP servers Benefit: Prevent data exfiltration, block destructive operations, audit MCP supply chain Time: 15-30 minutes for initial hardening; 5 minutes per new MCP added
Run Claude Code unattended — cron agents, heartbeat watchdogs, and session journaling
/loop, and hooks — and when each appliesHEARTBEAT_OK token, 200-token failure budget, probe allowlist — born from sessions killed for drifting into investigationsUse when: Scheduled health checks, automated journaling, any unattended Claude Code run Benefit: Agents that complete within budget instead of drifting; a daily work journal nobody has to write Time: 30-60 minutes for the first cron agent
Claude Code mods (v2.1.287+) — TypeScript hooks inside the engine
ai-prompts-mods: 13 mods with tests — context-meter, cache-guard, spend-ledger, subagent-models, secret-redactor, ssh-guard, deploy-verify, ci-watch, cleanup-tracker, aside, fleet-status, lang-guard, company-hqUse when: A policy must hold on every tool call or subagent spawn, output must be rewritten before the model sees it, or you want live UI (meters, panes, status) Benefit: Secrets out of transcripts, model routing that plugin updates cannot undo, cache and spend visible while you work Time: 10 minutes to install the set; 1-2 hours to write your first mod
Option 1: Automated (Recommended)
# Navigate to Claude Code
cd ~/.claude
# Use the setup agent
# Copy contents of 01-global-optimization/setup-agent.md
# Paste into Claude Code conversation
# Agent will create all files automatically
Option 2: Manual
# Follow the step-by-step guide
# Read: 01-global-optimization/guide.md
# Create files as instructed
# Verify with: 01-global-optimization/checklist.md
# Navigate to your project
cd ~/projects/your-project
# Use the activation agent
# Copy contents of 02-project-activation/activation-agent.md
# Paste into Claude Code conversation
# Agent will activate Serena and create memories
# In your project directory
/optimize "Your task here"
# Or use /init-project for new projects
/init-project --full
| Scenario | Baseline | Optimized | Savings |
|---|---|---|---|
| Simple task (bug fix) | 22,000 tokens | 1,600 tokens | 93% |
| Medium task (new feature) | 31,000 tokens | 8,500 tokens | 73% |
| Complex task (module creation) | 85,000 tokens | 18,000 tokens | 79% |
Conservative target: 30-50% overall reduction Aggressive target: 50-70% with full optimization Maximum achieved: 80-90% with prompt caching on large contexts
Per session (average medium task):
Monthly (30 sessions):
Annual (360 sessions):
Multiple projects (3 projects, 60 sessions/month):
~/.claude/)Agents (orchestration):
agents/pm-orchestrator.md - Central coordinatoragents/plan-challenger.md - Adversarial plan review (Opus)agents/output-evaluator.md - Code quality judge before commit (Haiku)agents/loop-monitor.md - Autonomous session watchdog (Haiku)Hooks (automation):
hooks/dangerous-actions-blocker.py - Blocks destructive commands and protected fileshooks/pre-commit-secrets.py - Scans staged files for API keys before commithooks/register.tsx 683 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { AgentRun, Budget, Company, Dashboard, Decision, Hire, Question, Specialist } from '../types'
5
6// Back office of the /company skill (~/.claude/skills/company). The skill
7// decides; this mod keeps the books:
8// - every /company run is its own company with its own id, org, cap and spend;
9// several can be open at once, in one session or in several;
10// - each company is a file, ~/.claude/company-hq/companies/<id>/state.json,
11// written only by the session that owns it (the dashboard reads these);
12// - hire: registers a specialist as agent type `company-hq:<name>`;
13// - every subagent spawn (Workflow agents included) is checked against the
14// active company's cap and refused past it;
15// - a pane shows the active company and the other open ones.
16
17const PANE = 'company'
18const companies = atom({ plugin: 'company-hq', key: 'companies' } as const, [])
19const activeId = atom({ plugin: 'company-hq', key: 'activeId' } as const, null)
20
21const MODELS = new Set(['haiku', 'sonnet', 'opus', 'inherit'])
22const DASHBOARDS = new Set(['off', 'local', 'fleetq', 'local+fleetq'])
23const NAME = /^[a-z][a-z0-9-]{1,40}$/
24const ID = /^[a-z0-9][a-z0-9-]{0,80}$/
25const PART = /^([a-z0-9][a-z0-9-]{0,40}):\s/
26const MAX_AGENTS = 200
27
28export const inProject = (cwd: string, dir: string) => cwd === dir || cwd.startsWith(dir.endsWith('/') ? dir : dir + '/')
29export const overCap = (b: Budget, liveUsd: number) => b.capUsd > 0 && b.spentUsd + liveUsd >= b.capUsd
30export const nearCap = (b: Budget, liveUsd: number) => b.capUsd > 0 && b.spentUsd + liveUsd >= b.capUsd * 0.8
31// Agents are tied to a part of the plan by a `<part>: ` prefix in their
32// description (a Workflow agent's label, an Agent call's description).
33export const partOf = (description: string) => PART.exec(description)?.[1] ?? null
34// 2026-10-08T14:30:12.345Z -> <slug>-20261008-143012
35export const companyId = (slug: string, at: Date) =>
36 `${slug}-${at.toISOString().slice(0, 19).replace(/[-:]/g, '').replace('T', '-')}`
37
38export const validateHire = (a: Record<string, unknown>): string | undefined => {
39 if (typeof a.name !== 'string' || !NAME.test(a.name)) return 'name must be kebab-case, 2-41 chars'
40 if (typeof a.prompt !== 'string' || a.prompt.length < 40) return 'prompt (the system prompt) is required, at least 40 chars'
41 if (typeof a.description !== 'string' || !a.description) return 'description (when to delegate to it) is required'
42 if (typeof a.model !== 'string' || !MODELS.has(a.model)) return 'model must be haiku, sonnet, opus or inherit'
43 return undefined
44}
45
46export const addMember = (c: Company, s: Specialist): Company => {
47 const m = { name: s.name, agent: s.agent, model: s.model, description: s.description }
48 const teams = c.teams.map(t => ({ ...t, members: t.members.filter(x => x.name !== s.name) }))
49 const t = teams.find(x => x.name === s.team)
50 return {
51 ...c,
52 teams: (t ? teams.map(x => (x === t ? { ...x, members: [...x.members, m] } : x)) : [...teams, { name: s.team, members: [m] }])
53 .filter(x => x.members.length > 0),
54 }
55}
56
57const TOOLS = [
58 {
59 name: 'open_project',
60 description: 'company-hq: open a new /company company (every call is a separate company with its own id, org, cap and spend), or resume one by id. Sets the budget cap every subagent spawn is checked against and opens the status pane.',
61 inputSchema: {
62 type: 'object',
63 properties: {
64 slug: { type: 'string', description: 'kebab-case short name' },
65 title: { type: 'string' },
66 capUsd: { type: 'number', description: 'optional budget cap in USD for all agents of this company; omit or 0 = no cap' },
67 dir: { type: 'string', description: 'working directory of the project' },
68 task: { type: 'string', description: 'the task as the user gave it' },
69 kind: { type: 'string', enum: ['code', 'new-project', 'audit', 'ops'] },
70 dashboard: { type: 'string', enum: ['off', 'local', 'fleetq', 'local+fleetq'], description: 'omit to use ~/.claude/company-hq/config.json, else off' },
71 resume: { type: 'string', description: 'id of an open company to take over in this session; the other fields are ignored' },
72 },
73 required: ['slug', 'title'],
74 },
75 },
76 {
77 name: 'hire',
78 description: 'company-hq: hire a specialist for the active company: registers agent type company-hq:<name> with its own system prompt and model. Usable by Agent/Workflow agentType from the NEXT turn.',
79 inputSchema: {
80 type: 'object',
81 properties: {
82 name: { type: 'string' },
83 team: { type: 'string' },
84 description: { type: 'string', description: 'when to delegate to it' },
85 prompt: { type: 'string', description: 'its system prompt: role, expertise, owned files, rules, deliverable' },
86 model: { type: 'string', enum: ['haiku', 'sonnet', 'opus', 'inherit'] },
87 tools: { type: 'array', items: { type: 'string' } },
88 skills: { type: 'array', items: { type: 'string' } },
89 },
90 required: ['name', 'team', 'description', 'prompt', 'model'],
91 },
92 },
93 {
94 name: 'set_phase',
95 description: 'company-hq: record the current /company phase and a one-line note for the active company (or the one named by id).',
96 inputSchema: { type: 'object', properties: { phase: { type: 'string' }, note: { type: 'string' }, company: { type: 'string', description: 'company id; default the active one' } }, required: ['phase'] },
97 },
98 {
99 name: 'ask_user',
100 description: 'company-hq: record a question only the user can decide (decision-classify: User; or scope, budget, data, security, deploy, deletion). Returns its id. Then notify the user (PushNotification) and keep working on parts that do not depend on it; never guess the answer.',
101 inputSchema: {
102 type: 'object',
103 properties: {
104 text: { type: 'string' },
105 part: { type: 'string', description: 'the part it blocks; omit when it blocks the whole company' },
106 options: { type: 'array', items: { type: 'string' } },
107 class: { type: 'string', description: 'decision-classify class, default user' },
108 company: { type: 'string', description: 'company id; default the active one' },
109 },
110 required: ['text'],
111 },
112 },
113 {
114 name: 'answer_question',
115 description: "company-hq: record the user's answer to an open question (asked with ask_user).",
116 inputSchema: { type: 'object', properties: { id: { type: 'string' }, answer: { type: 'string' }, company: { type: 'string' } }, required: ['id', 'answer'] },
117 },
118 {
119 name: 'record_decision',
120 description: 'company-hq: log a non-critical choice (decision-classify: Mechanical/Taste) made without the user, with the options, the choice, who chose (agent or an external scorer) and the scorer\'s scores when there were any, so the user can review and reverse it.',
121 inputSchema: {
122 type: 'object',
123 properties: {
124 text: { type: 'string' },
125 options: { type: 'array', items: { type: 'string' } },
126 chosen: { type: 'string' },
127 by: { type: 'string', enum: ['agent', 'scorer'] },
128 part: { type: 'string' },
129 class: { type: 'string', description: 'mechanical or taste' },
130 scorer: { type: 'object', description: 'an external ranker of the options, when one was asked', properties: { name: { type: 'string' }, scores: { type: 'array', items: { type: 'number' } }, mode: { type: 'string', enum: ['shadow', 'decide'] } } },
131 company: { type: 'string' },
132 },
133 required: ['text', 'options', 'chosen', 'by'],
134 },
135 },
136 {
137 name: 'close_project',
138 description: 'company-hq: close the active company (or the one named by id): returns its final spend and roster (with prompts) so the skill can record it and offer to keep specialists. Other open companies are not touched.',
139 inputSchema: {
140 type: 'object',
141 properties: {
142 company: { type: 'string', description: 'company id; default the active one' },
143 pr: { type: 'string', description: 'PR URL, when there is one' },
144 report: { type: 'string', description: 'path of the final report, when there is one' },
145 },
146 },
147 },
148] as const
149
150// Module state; a reload starts it over and session.start fills it again.
151// baseUsd: session cost at the last flush, the base for "spent since".
152const S = { baseUsd: 0, home: '', machine: 'local', sessionId: '' }
153const warned = new Set<string>()
154const hires = new Map<string, Record<string, Hire>>()
155
156function stateFile(id: string) { return `${S.home}/companies/${id}/state.json` }
157function hiresFile(id: string) { return `${S.home}/companies/${id}/private.json` }
158
159async function readJson<T>($: EngineInterface, path: string): Promise<T | undefined> {
160 if (!S.home) return undefined
161 return $.fs.read(path).then(t => JSON.parse(t as string) as T).catch(() => undefined)
162}
163
164async function write($: EngineInterface, c: Company) {
165 if (S.home) await $.fs.write(stateFile(c.id), JSON.stringify(c, null, 2)).catch(() => undefined)
166}
167
168async function writeHires($: EngineInterface, id: string) {
169 if (S.home) await $.fs.write(hiresFile(id), JSON.stringify({ hires: hires.get(id) ?? {} }, null, 2)).catch(() => undefined)
170}
171
172// Applies fn to the open company `id`, bumps seq, writes the file.
173async function change($: EngineInterface, id: string, fn: (c: Company) => Company): Promise<Company | undefined> {
174 const cur = (await read($, companies)).find(c => c.id === id)
175 if (!cur) return undefined
176 const next = { ...fn(cur), seq: cur.seq + 1, updatedAt: new Date().toISOString() }
177 await update($, companies, list => list.map(c => (c.id === id ? next : c)))
178 await write($, next)
179 if (wantsFleet(next.dashboard)) scheduleFleet($, id)
180 return next
181}
182
183async function active($: EngineInterface): Promise<Company | undefined> {
184 const id = await read($, activeId)
185 return id ? (await read($, companies)).find(c => c.id === id) : undefined
186}
187
188async function pick($: EngineInterface, id?: string) {
189 return id ? (await read($, companies)).find(c => c.id === id) : active($)
190}
191
192async function liveUsd($: EngineInterface) {
193 return Math.max(0, ((await $.session.usage()).cost?.usd ?? S.baseUsd) - S.baseUsd)
194}
195
196// Books the session's cost since the last flush to the active company.
197async function flush($: EngineInterface) {
198 const now = (await $.session.usage()).cost?.usd ?? S.baseUsd
199 const delta = Math.max(0, now - S.baseUsd)
200 S.baseUsd = now
201 const a = await active($)
202 if (a && delta > 0) await change($, a.id, c => ({ ...c, budget: { ...c.budget, spentUsd: c.budget.spentUsd + delta } }))
203}
204
205async function registerHire($: EngineInterface, team: string, name: string, model: string, description: string, h: Hire) {
206 await $.agent.register({
207 name, description: `[company/${team}] ${description}`, prompt: h.prompt, model,
208 ...(h.tools?.length ? { tools: h.tools } : {}),
209 ...(h.skills?.length ? { skills: h.skills } : {}),
210 })
211}
212
213// Hired agents come back only inside the company's folder, so a hire never
214// follows the user into other repos.
215async function restoreHires($: EngineInterface, c: Company, cwd: string) {
216 const saved = (await readJson<{ hires: Record<string, Hire> }>($, hiresFile(c.id)))?.hires ?? {}
217 hires.set(c.id, saved)
218 if (!inProject(cwd, c.dir)) return
219 for (const t of c.teams) for (const m of t.members) {
220 const h = saved[m.name]
221 if (h) await registerHire($, t.name, m.name, m.model, m.description, h).catch(() => undefined)
222 }
223}
224
225// v0.2 kept one project in $.store; turn it into a company of this session.
226async function migrate($: EngineInterface, now: Date): Promise<Company | undefined> {
227 const old = (await $.store.get('project')) as { slug: string; title: string; capUsd: number; spentUsd: number; phase: string; note: string; dir: string } | null | undefined
228 if (!old) return undefined
229 const roster = ((await $.store.get('roster')) as Specialist[] | undefined) ?? []
230 const prompts = ((await $.store.get('prompts')) as Record<string, Hire> | undefined) ?? {}
231 let c: Company = {
232 ...blank(now, old.slug, old.title, old.dir, 'off'),
233 phase: old.phase, note: old.note, budget: { capUsd: old.capUsd, spentUsd: old.spentUsd },
234 }
235 for (const s of roster) c = addMember(c, s)
236 hires.set(c.id, prompts)
237 await write($, c)
238 await writeHires($, c.id)
239 for (const k of ['project', 'roster', 'prompts', 'spawns']) await $.store.delete(k).catch(() => undefined)
240 return c
241}
242
243function blank(now: Date, slug: string, title: string, dir: string, dashboard: Dashboard): Company {
244 const at = now.toISOString()
245 return {
246 schema: 1, id: companyId(slug, now), slug, title, task: '', kind: 'code', machine: S.machine, sessionId: S.sessionId, dir,
247 docsDir: `claudedocs/company/${slug}`, dashboard, status: 'open', phase: 'intake', note: '',
248 budget: { capUsd: 0, spentUsd: 0 }, teams: [], agents: [], questions: [], decisions: [], result: {}, seq: 0,
249 openedAt: at, updatedAt: at, closedAt: null,
250 }
251}
252
253// ---- FleetQ adapter (dashboard fleetq / local+fleetq). Sends the full
254// snapshot plus the plan documents (redacted) to FleetQ; contract in
255// server/INGEST-CONTRACT.md. Never blocks a hook: sends
256// run on timers, failures retry with backoff, and the snapshot is always whole,
257// so only the latest one matters.
258
259// The client redacts what FleetQ would reject (the same list as the server).
260const SECRETS: RegExp[] = [
261 /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
262 /\bops_[A-Za-z0-9_-]{20,}/g,
263 /\bsk-ant-[A-Za-z0-9_-]{20,}/g,
264 /\bsk-[A-Za-z0-9]{20,}/g,
265 /\bgh[pousr]_[A-Za-z0-9]{30,}/g,
266 /\bgithub_pat_[A-Za-z0-9_]{40,}/g,
267 /\bglpat-[A-Za-z0-9_-]{20,}/g,
268 /\bxox[abprs]-[A-Za-z0-9-]{10,}/g,
269 /\bAKIA[0-9A-Z]{16}\b/g,
270 /\bsntry[su]_[A-Za-z0-9+/=_-]{20,}/g,
271 /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
272 /Bearer\s+[A-Za-z0-9._~+/=-]{20,}/g,
273]
274export const redact = (t: string) => SECRETS.reduce((s, re) => s.replace(re, '[REDACTED]'), t)
275const DOC = /^docs\/(design|architecture|test-plan)-[a-z0-9][a-z0-9-]*\.md$/
276// docsDir comes from a state file: only a relative path without '..' is read.
277export const safeDocsDir = (d: string) => {
278 const s = d.replace(/^\/+|\/+$/g, '')
279 return /^[A-Za-z0-9_][A-Za-z0-9_.-]*(\/[A-Za-z0-9_][A-Za-z0-9_.-]*)*$/.test(s) && !s.split('/').includes('..') ? s : ''
280}
281// The token goes only over https (plain http only to this machine).
282export const fleetUrlOk = (u: string) => /^https:\/\/[^/\s]+/.test(u) || /^http:\/\/(127\.0\.0\.1|localhost)(:\d+)?(\/|$)/.test(u)
283const wantsFleet = (d: Dashboard) => d === 'fleetq' || d === 'local+fleetq'
284
285export async function sha256Hex(text: string) {
286 const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text))
287 return [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('')
288}
289
290// `git worktree list --porcelain` -> roots, the company dir first.
291export const parseWorktrees = (out: string, dir: string) => {
292 const roots: { path: string; branch: string | null }[] = []
293 for (const line of out.split('\n')) {
294 if (line.startsWith('worktree ')) roots.push({ path: line.slice(9), branch: null })
295 else if (line.startsWith('branch ') && roots.length) roots[roots.length - 1]!.branch = line.slice(7).replace(/^refs\/heads\//, '')
296 }
297 const i = roots.findIndex(r => r.path === dir)
298 const main = i >= 0 ? roots.splice(i, 1)[0]! : { path: dir, branch: null }
299 return [main, ...roots]
300}
301
302type Doc = { key: string; path: string; branch: string | null; content: string }
303
304async function collectDocs($: EngineInterface, c: Company): Promise<Doc[]> {
305 const real = (await $.fs.stat(c.dir, { resolve: true }).catch(() => undefined))?.realPath ?? c.dir
306 const wt = await $.process.run(['git', '-C', c.dir, 'worktree', 'list', '--porcelain']).catch(() => undefined)
307 const roots = parseWorktrees(wt?.exitCode === 0 ? wt.stdout : '', real)
308 const docsDir = safeDocsDir(c.docsDir)
309 const subs = docsDir && docsDir !== 'docs' ? ['docs', docsDir] : ['docs']
310 const out: Doc[] = []
311 for (const [i, r] of roots.entries()) {
312 for (const sub of subs) {
313 const names = await $.fs.list(`${r.path}/${sub}`).catch(() => [])
314 for (const n of names) {
315 const path = `${sub}/${n.name}`
316 if (n.kind !== 'file' || !n.name.endsWith('.md') || (sub === 'docs' && !DOC.test(path))) continue
317 const text = await $.fs.read(`${r.path}/${path}`).catch(() => undefined)
318 if (typeof text === 'string') out.push({ key: `${i}:${path}`, path, branch: r.branch, content: redact(text) })
319 }
320 }
321 }
322 return out
323}
324
325// company id -> doc key -> sha256 the server is known to have. Absent after a
326// (re)load: nothing is known, so the first send carries no content and the
327// server's missingDocs says what to send.
328const fleetHas = new Map<string, Map<string, string>>()
329const fleetTimer = new Map<string, { cancel: () => void }>()
330const fleetDelay = new Map<string, number>()
331const fleetWarned = new Set<string>()
332
333function scheduleFleet($: EngineInterface, id: string, ms = 2000) {
334 fleetTimer.get(id)?.cancel()
335 fleetTimer.set(id, $.clock.after(ms, () => { void sendFleet($, id) }))
336}
337
338async function outbox($: EngineInterface, id: string, v: Record<string, unknown>) {
339 await $.fs.write(`${S.home}/companies/${id}/outbox.json`, JSON.stringify({ ...v, at: new Date().toISOString() })).catch(() => undefined)
340}
341
342async function sendFleet($: EngineInterface, id: string) {
343 fleetTimer.delete(id)
344 const c = (await read($, companies)).find(x => x.id === id) ?? (await readJson<Company>($, stateFile(id)))
345 if (!c || !wantsFleet(c.dashboard)) return
346 const cfg = (await readJson<{ fleetq?: { url?: string } }>($, `${S.home}/config.json`)) ?? {}
347 const url = cfg.fleetq?.url?.replace(/\/+$/, '')
348 const token = await $.env.get('COMPANY_HQ_FLEETQ_TOKEN')
349 if (url && !fleetUrlOk(url)) {
350 if (!fleetWarned.has('url')) {
351 fleetWarned.add('url')
352 $.ui.toast('company: fleetq.url must be https:// (http only for localhost); nothing was sent')
353 }
354 return
355 }
356 if (!url || !token) {
357 if (!fleetWarned.has('config')) {
358 fleetWarned.add('config')
359 $.ui.toast('company: dashboard fleetq needs fleetq.url in ~/.claude/company-hq/config.json and COMPANY_HQ_FLEETQ_TOKEN')
360 }
361 return
362 }
363 const has = fleetHas.get(id)
364 const sent: { key: string; path: string; branch: string | null; sha256: string; content?: string }[] = []
365 for (const d of await collectDocs($, c)) {
366 const sha = await sha256Hex(d.content)
367 const withContent = has !== undefined && has.get(d.key) !== sha
368 sent.push({ key: d.key, path: d.path, branch: d.branch, sha256: sha, ...(withContent ? { content: d.content } : {}) })
369 }
370 const r = await $.http.fetch(`${url}/api/company-hq/ingest`, {
371 method: 'POST',
372 headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
373 body: JSON.stringify({ machine: c.machine, snapshot: c, docs: sent }),
374 }).catch(() => undefined)
375 if (r && (r.status === 401 || r.status === 413 || r.status === 422)) {
376 // Retrying does not fix these; the next change tries again.
377 if (!fleetWarned.has(`${id}:${r.status}`)) {
378 fleetWarned.add(`${id}:${r.status}`)
379 $.ui.toast(`company: FleetQ refused ${c.slug} (${r.status}); see ~/.claude/company-hq/companies/${id}/outbox.json`)
380 }
381 await outbox($, id, { pending: false, refused: r.status, body: r.text.slice(0, 500) })
382 return
383 }
384 if (!r || !r.ok) {
385 const delay = Math.min(300_000, (fleetDelay.get(id) ?? 2500) * 2)
386 fleetDelay.set(id, delay)
387 await outbox($, id, { pending: true, status: r?.status ?? null, retryInMs: delay })
388 scheduleFleet($, id, delay)
389 return
390 }
391 fleetDelay.delete(id)
392 let body: { missingDocs?: string[]; rejectedDocs?: string[] } = {}
393 try { body = JSON.parse(r.text) } catch { /* an empty 200 is fine */ }
394 const missing = new Set(body.missingDocs ?? [])
395 const rejected = new Set(body.rejectedDocs ?? [])
396 // Accepted: sent with content, or sent without and the server had it.
397 fleetHas.set(id, new Map(sent.filter(d => !missing.has(d.key) && !rejected.has(d.key)).map(d => [d.key, d.sha256])))
398 await outbox($, id, { pending: false, rejectedDocs: [...rejected] })
399 if (missing.size) scheduleFleet($, id, 200)
400}
401
402// Starts the local dashboard (server/company-dashboard.py) unless one runs,
403// detached in its own session so it outlives this one; it stops itself when
404// no company is open. Paths reach Python as argv, never inside the code.
405const LAUNCH = "import subprocess,sys;f=open(sys.argv[3],'ab');p=subprocess.Popen([sys.executable,sys.argv[1],'--home',sys.argv[2]],stdin=subprocess.DEVNULL,stdout=f,stderr=subprocess.STDOUT,start_new_session=True);print(p.pid)"
406
407async function ensureServer($: EngineInterface): Promise<string | undefined> {
408 if (!S.home) return undefined
409 const info = `${S.home}/server.json`
410 const alive = async () => {
411 const i = await readJson<{ app?: string; port?: number; url?: string }>($, info)
412 if (!i?.port) return undefined
413 const r = await $.http.fetch(`http://127.0.0.1:${i.port}/health`).catch(() => undefined)
414 return r?.ok && r.text.includes('company-dashboard') ? i.url ?? `http://127.0.0.1:${i.port}/` : undefined
415 }
416 const up = await alive()
417 if (up) return up
418 const r = await $.process.run(
419 ['python3', '-c', LAUNCH, `${$.plugin.root}/server/company-dashboard.py`, S.home, `${S.home}/server.log`],
420 { timeoutMs: 10000 },
421 ).catch(() => undefined)
422 if (!r || r.exitCode !== 0) return undefined
423 for (let i = 0; i < 30; i++) {
424 await $.clock.sleep(100)
425 const url = await alive()
426 if (url) return url
427 }
428 return undefined
429}
430
431const wantsLocal = (d: Dashboard) => d === 'local' || d === 'local+fleetq'
432export const dashLine = (d: Dashboard, url: string | undefined) =>
433 !wantsLocal(d) ? '' : url ? ` Dashboard: ${url}` : ' Dashboard could not start (is python3 on PATH? see ~/.claude/company-hq/server.log); the company runs without it.'
434
435export const register: Register = on => {
436 warned.clear()
437 hires.clear()
438
439 on('session.start', async ($, e, next) => {
440 S.baseUsd = (await $.session.usage()).cost?.usd ?? 0
441 for (const t of TOOLS) await $.tool.register({ ...t, inputSchema: t.inputSchema as never })
442 await $.command.register({ name: 'company-status', description: 'Show the /company pane (active company, other open ones, spend)', immediate: true })
443
444 const h = await $.env.get('HOME')
445 S.home = h ? `${h}/.claude/company-hq` : ''
446 S.sessionId = await $.session.id().catch(() => '')
447 const cfg = (await readJson<{ machine?: string }>($, `${S.home}/config.json`)) ?? {}
448 S.machine = cfg.machine
449 ?? (await $.process.run(['hostname', '-s']).then(r => r.stdout.trim()).catch(() => ''))
450 ?? 'local'
451 if (!S.machine) S.machine = 'local'
452
453 // This session's open companies (a resumed session keeps its id).
454 const mine: Company[] = []
455 const dirs = S.home ? await $.fs.list(`${S.home}/companies`).catch(() => []) : []
456 for (const d of dirs) {
457 if (d.kind !== 'dir') continue
458 const c = await readJson<Company>($, stateFile(d.name))
459 if (c && c.schema === 1 && c.status === 'open' && c.sessionId === S.sessionId) mine.push({ ...c, questions: c.questions ?? [], decisions: c.decisions ?? [] })
460 }
461 const migrated = await migrate($, new Date()).catch(() => undefined)
462 if (migrated) mine.push(migrated)
463 mine.sort((a, b) => a.openedAt.localeCompare(b.openedAt))
464 await update($, companies, () => mine)
465 await update($, activeId, () => mine.at(-1)?.id ?? null)
466 for (const c of mine) await restoreHires($, c, e.cwd)
467 for (const c of mine) if (wantsFleet(c.dashboard)) scheduleFleet($, c.id, 1000)
468 if (mine.some(c => wantsLocal(c.dashboard))) {
469 void ensureServer($).then(url => { if (url) $.ui.toast(`company dashboard: ${url}`) })
470 }
471 return next(e)
472 })
473
474 on('tool.call', { tool: 'mcp__company-hq__open_project' }, async ($, e) => {
475 const a = e as unknown as { slug: string; title: string; capUsd?: number; dir?: string; task?: string; kind?: string; dashboard?: string; resume?: string }
476 await flush($)
477 const cwd = await $.session.cwd()
478
479 if (a.resume) {
480 if (!ID.test(a.resume)) return { result: 'resume refused: not a company id.' }
481 const c = (await read($, companies)).find(x => x.id === a.resume) ?? (await readJson<Company>($, stateFile(a.resume)))
482 if (!c || c.status !== 'open' || c.id !== a.resume) return { result: `resume refused: no open company ${a.resume}.` }
483 const taken = { ...c, questions: c.questions ?? [], decisions: c.decisions ?? [], sessionId: S.sessionId, seq: c.seq + 1, updatedAt: new Date().toISOString() }
484 await update($, companies, list => [...list.filter(x => x.id !== c.id), taken])
485 await update($, activeId, () => taken.id)
486 await write($, taken)
487 await restoreHires($, taken, cwd)
488 if (wantsFleet(taken.dashboard)) scheduleFleet($, taken.id)
489 void $.ui.open({ id: PANE, title: `Company: ${taken.title}` })
490 const url = wantsLocal(taken.dashboard) ? await ensureServer($) : undefined
491 return { result: `Company ${taken.id} resumed; cap $${taken.budget.capUsd || '∞'}; spent so far $${taken.budget.spentUsd.toFixed(2)}.${dashLine(taken.dashboard, url)}` }
492 }
493
494 if (typeof a.slug !== 'string' || !NAME.test(a.slug)) return { result: 'open_project refused: slug must be kebab-case, 2-41 chars.' }
495 const cfg = (await readJson<{ dashboard?: string }>($, `${S.home}/config.json`)) ?? {}
496 const dashboard = (DASHBOARDS.has(a.dashboard ?? '') ? a.dashboard : DASHBOARDS.has(cfg.dashboard ?? '') ? cfg.dashboard : 'off') as Dashboard
497 let id = companyId(a.slug, new Date())
498 const taken = new Set((await read($, companies)).map(c => c.id))
499 for (let n = 2; taken.has(id); n++) id = `${companyId(a.slug, new Date())}-${n}`
500 const c: Company = {
501 ...blank(new Date(), a.slug, a.title, a.dir ?? cwd, dashboard),
502 id, task: a.task ?? '', kind: a.kind ?? 'code', budget: { capUsd: Math.max(0, a.capUsd ?? 0), spentUsd: 0 },
503 }
504 hires.set(id, {})
505 await update($, companies, list => [...list, c])
506 await update($, activeId, () => id)
507 await write($, c)
508 await writeHires($, id)
509 if (wantsFleet(dashboard)) scheduleFleet($, id)
510 void $.ui.open({ id: PANE, title: `Company: ${c.title}` })
511 const url = wantsLocal(dashboard) ? await ensureServer($) : undefined
512 return { result: `Company ${id} open; cap $${c.budget.capUsd || '∞'}; dashboard ${dashboard}.${dashLine(dashboard, url)}` }
513 }).catch(() => ({ result: 'company-hq: open_project failed; see claude --debug.' }))
514
515 on('tool.call', { tool: 'mcp__company-hq__hire' }, async ($, e) => {
516 const a = e as unknown as { name: string; team: string; description: string; prompt: string; model: string; tools?: string[]; skills?: string[] }
517 const bad = validateHire(a as unknown as Record<string, unknown>)
518 if (bad) return { result: `hire refused: ${bad}` }
519 const c = await active($)
520 if (!c) return { result: 'hire refused: open_project first.' }
521 const s: Specialist = { name: a.name, agent: `company-hq:${a.name}`, team: a.team, model: a.model, description: a.description }
522 const h: Hire = { prompt: a.prompt, tools: a.tools, skills: a.skills }
523 await registerHire($, s.team, s.name, s.model, s.description, h)
524 hires.set(c.id, { ...(hires.get(c.id) ?? {}), [s.name]: h })
525 await writeHires($, c.id)
526 await change($, c.id, x => addMember(x, s))
527 return { result: `Hired ${s.agent} (${s.model}) for team ${s.team} in ${c.id}. Usable as subagent_type / Workflow agentType from the next turn.` }
528 }).catch(() => ({ result: 'company-hq: hire failed; see claude --debug.' }))
529
530 on('tool.call', { tool: 'mcp__company-hq__set_phase' }, async ($, e) => {
531 const a = e as unknown as { phase: string; note?: string; company?: string }
532 const c = await pick($, a.company)
533 if (!c) return { result: 'No open company.' }
534 await change($, c.id, x => ({ ...x, phase: a.phase, note: a.note ?? '' }))
535 return { result: `Phase of ${c.id}: ${a.phase}` }
536 }).catch(() => ({ result: 'company-hq: set_phase failed; see claude --debug.' }))
537
538 on('tool.call', { tool: 'mcp__company-hq__ask_user' }, async ($, e) => {
539 const a = e as unknown as { text: string; part?: string; options?: string[]; class?: string; company?: string }
540 const c = await pick($, a.company)
541 if (!c) return { result: 'No open company.' }
542 const q: Question = {
543 id: `q${c.questions.length + 1}`, part: a.part ?? null, class: a.class ?? 'user', text: a.text, options: a.options ?? [],
544 askedAt: new Date().toISOString(), status: 'open', answer: null, answeredAt: null,
545 }
546 await change($, c.id, x => ({ ...x, questions: [...x.questions, q] }))
547 $.ui.toast(`company: question ${q.id} waits for you${q.part ? ` (blocks ${q.part})` : ''}`)
548 return { result: `Question ${q.id} recorded for ${c.id}. Notify the user (PushNotification), then continue with the parts that do not depend on it. Do not guess the answer.` }
549 }).catch(() => ({ result: 'company-hq: ask_user failed; see claude --debug.' }))
550
551 on('tool.call', { tool: 'mcp__company-hq__answer_question' }, async ($, e) => {
552 const a = e as unknown as { id: string; answer: string; company?: string }
553 const c = await pick($, a.company)
554 const q = c?.questions.find(x => x.id === a.id)
555 if (!c || !q) return { result: `No question ${a.id}.` }
556 const at = new Date().toISOString()
557 await change($, c.id, x => ({ ...x, questions: x.questions.map(y => (y.id === a.id ? { ...y, status: 'answered' as const, answer: a.answer, answeredAt: at } : y)) }))
558 return { result: `Question ${a.id} answered.` }
559 }).catch(() => ({ result: 'company-hq: answer_question failed; see claude --debug.' }))
560
561 on('tool.call', { tool: 'mcp__company-hq__record_decision' }, async ($, e) => {
562 const a = e as unknown as { text: string; options: string[]; chosen: string; by: string; part?: string; class?: string; scorer?: Decision['scorer']; company?: string }
563 const c = await pick($, a.company)
564 if (!c) return { result: 'No open company.' }
565 if (a.by !== 'agent' && a.by !== 'scorer') return { result: 'record_decision refused: by must be agent or scorer.' }
566 const d: Decision = {
567 id: `d${c.decisions.length + 1}`, part: a.part ?? null, class: a.class ?? 'taste', text: a.text, options: a.options ?? [],
568 ...(a.scorer ? { scorer: a.scorer } : {}), chosen: a.chosen, by: a.by, at: new Date().toISOString(),
569 }
570 await change($, c.id, x => ({ ...x, decisions: [...x.decisions, d] }))
571 return { result: `Decision ${d.id} logged.` }
572 }).catch(() => ({ result: 'company-hq: record_decision failed; see claude --debug.' }))
573
574 on('tool.call', { tool: 'mcp__company-hq__close_project' }, async ($, e) => {
575 const a = e as unknown as { company?: string; pr?: string; report?: string }
576 await flush($)
577 const c = await pick($, a.company)
578 if (!c) return { result: 'No open company.' }
579 const closed = await change($, c.id, x => ({
580 ...x, status: 'closed', closedAt: new Date().toISOString(),
581 result: { ...x.result, ...(a.pr ? { pr: a.pr } : {}), ...(a.report ? { report: a.report } : {}) },
582 }))
583 const h = hires.get(c.id) ?? {}
584 const out = {
585 id: c.id, project: c.slug, title: c.title, capUsd: c.budget.capUsd,
586 spentUsd: Number((closed ?? c).budget.spentUsd.toFixed(2)),
587 roster: c.teams.flatMap(t => t.members.map(m => ({ ...m, team: t.name, ...(h[m.name] ?? {}) }))),
588 spawns: c.agents.length,
589 }
590 hires.delete(c.id)
591 warned.delete(c.id)
592 await update($, companies, list => list.filter(x => x.id !== c.id))
593 if ((await read($, activeId)) === c.id) await update($, activeId, () => (null as string | null))
594 const rest = await read($, companies)
595 if (!(await read($, activeId)) && rest.length) await update($, activeId, () => rest.at(-1)!.id)
596 return { result: JSON.stringify(out) }
597 }).catch(() => ({ result: 'company-hq: close_project failed; see claude --debug.' }))
598
599 // Budget cap on every spawn, Workflow agents included.
600 on('agent.spawn', async ($, e, next) => {
601 // The cap belongs to the company's folder: an abandoned company must not
602 // throttle work in other repos.
603 const a = await active($)
604 const c = a && inProject(await $.session.cwd(), a.dir) ? a : undefined
605 const row = (status: AgentRun['status'], agentId?: string, model?: string): AgentRun => ({
606 ...(agentId ? { agentId } : {}), type: e.subagentType, ...(model ? { model } : {}),
607 part: partOf(e.description), description: e.description.slice(0, 200), status, startedAt: new Date().toISOString(),
608 })
609 const log = (r: AgentRun) => change($, c!.id, x => ({ ...x, agents: [...x.agents, r].slice(-MAX_AGENTS) }))
610 if (c) {
611 const live = await liveUsd($)
612 if (overCap(c.budget, live)) {
613 await log(row('denied'))
614 return { deny: `company-hq: company "${c.id}" reached its budget cap ($${c.budget.capUsd}). Stop, report progress to the user and ask before spending more.` }
615 }
616 if (!warned.has(c.id) && nearCap(c.budget, live)) {
617 warned.add(c.id)
618 $.ui.toast(`company: ${c.slug} at 80% of its $${c.budget.capUsd} cap`)
619 }
620 }
621 const result = await next(e)
622 if (c) await log(result.deny ? row('denied') : row('running', result.agentId, result.model))
623 return result
624 }).catch(($, e, next) => next(e))
625
626 on('turn.complete', async ($, e, next) => {
627 const result = await next(e)
628 if (e.agentId !== undefined) {
629 const id = e.agentId
630 const owner = (await read($, companies)).find(c => c.agents.some(r => r.agentId === id && r.status === 'running'))
631 if (owner) {
632 const at = new Date().toISOString()
633 await change($, owner.id, x => ({ ...x, agents: x.agents.map(r => (r.agentId === id ? { ...r, status: 'done' as const, endedAt: at } : r)) }))
634 }
635 return result
636 }
637 await flush($)
638 return result
639 }).catch(($, e, next) => next(e))
640
641 on('command.run', { command: 'company-status' }, async $ => {
642 await $.ui.open({ id: PANE, title: 'Company' })
643 return {}
644 })
645
646 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
647 const { Box, Text } = $.ui.resolve(e)
648 const list = await read($, companies)
649 const id = await read($, activeId)
650 const p = list.find(c => c.id === id)
651 if (!p) return <Text dimColor>No open /company company.</Text>
652 const s = p.agents
653 const running = s.filter(x => x.status === 'running').length
654 const pct = p.budget.capUsd ? Math.round((p.budget.spentUsd / p.budget.capUsd) * 100) : 0
655 const others = list.filter(c => c.id !== id)
656 return (
657 <Box flexDirection="column">
658 <Text bold>{p.title}</Text>
659 <Text dimColor>{p.id} · dashboard {p.dashboard}</Text>
660 <Text>phase: {p.phase}{p.note ? ` · ${p.note}` : ''}</Text>
661 {p.questions.some(q => q.status === 'open') && <Text color="red">questions waiting: {p.questions.filter(q => q.status === 'open').map(q => q.id).join(', ')}</Text>}
662 <Text color={pct >= 80 ? 'red' : undefined}>spent ${p.budget.spentUsd.toFixed(2)}{p.budget.capUsd ? ` of $${p.budget.capUsd} (${pct}%)` : ''} · agents {s.length} ({running} running, {s.filter(x => x.status === 'denied').length} denied)</Text>
663 {p.teams.map(t => (
664 <Box flexDirection="column" marginTop={1}>
665 <Text bold>{t.name}</Text>
666 {t.members.map(x => <Text> {x.name} · {x.model} · {x.description.slice(0, 60)}</Text>)}
667 </Box>
668 ))}
669 <Box flexDirection="column" marginTop={1}>
670 <Text bold>recent agents</Text>
671 {s.slice(-8).map(x => <Text dimColor={x.status !== 'running'}> {x.status === 'running' ? '▶' : x.status === 'done' ? '✓' : '✗'} {x.part ? `[${x.part}] ` : ''}{x.type} {x.model ? `(${x.model})` : ''} {x.description.slice(0, 40)}</Text>)}
672 </Box>
673 {others.length > 0 && (
674 <Box flexDirection="column" marginTop={1}>
675 <Text bold>other open companies</Text>
676 {others.map(c => <Text dimColor> {c.title} · {c.phase} · ${c.budget.spentUsd.toFixed(2)}</Text>)}
677 </Box>
678 )}
679 </Box>
680 )
681 })
682}
683types/index.d.ts 38 lines1export type Specialist = { name: string; agent: string; team: string; model: string; description: string }
2export type Member = Omit<Specialist, 'team'>
3export type Team = { name: string; members: Member[] }
4export type AgentRun = {
5 agentId?: string; type: string; model?: string; part: string | null; description: string
6 status: 'running' | 'done' | 'denied'; startedAt: string; endedAt?: string
7}
8export type Dashboard = 'off' | 'local' | 'fleetq' | 'local+fleetq'
9export type Budget = { capUsd: number; spentUsd: number }
10// A question for the user that blocks a part; decided only by the user.
11export type Question = {
12 id: string; part: string | null; class: string; text: string; options: string[]
13 askedAt: string; status: 'open' | 'answered'; answer: string | null; answeredAt: string | null
14}
15// A non-critical choice made without the user, kept so it can be reviewed and reversed.
16export type Decision = {
17 id: string; part: string | null; class: string; text: string; options: string[]
18 // scorer: an optional external ranker of the options; it never decides User-class questions.
19 scorer?: { name: string; scores: number[]; mode: 'shadow' | 'decide' }; chosen: string; by: 'agent' | 'scorer'; at: string
20}
21// One /company run. Written to ~/.claude/company-hq/companies/<id>/state.json by
22// the session that owns it; the dashboard server and the FleetQ adapter read it.
23export type Company = {
24 schema: 1; id: string; slug: string; title: string; task: string; kind: string
25 machine: string; sessionId: string; dir: string; docsDir: string; dashboard: Dashboard
26 status: 'open' | 'closed'; phase: string; note: string; budget: Budget
27 teams: Team[]; agents: AgentRun[]; questions: Question[]; decisions: Decision[]
28 result: { pr?: string; report?: string }
29 seq: number; openedAt: string; updatedAt: string; closedAt: string | null
30}
31export type Hire = { prompt: string; tools?: string[]; skills?: string[] }
32
33declare module 'claude-code' {
34 interface PluginState {
35 'company-hq': { companies: Company[]; activeId: string | null }
36 }
37}
38