OpenExecutive
CLAUDE.md5.8k starsChanged 50 days agoApache-2.0View on GitHub
Open Executive is a multi-agent AI system acting as a virtual corporate executive. Python backend (FastAPI) + Next.js 15 frontend. The "Executive" is a single coherent persona backed by 9 specialist sub-agents, all powered by the Anthropic Claude API. Uses uv for package management. The system is designed around Anthropic prompt caching. Breaking caching = 10x cost increase. Never put dynamic content in system prompt blocks that have cache_control. Build order in prompts/cache_manager.py: 1. Tool definitions (sorted by name —…
- Reads credentials
In this repository2 other files it gives its agents.
- openexec-apiSkill
- stewardSkill
The file
Show its rules and text
We split this CLAUDE.md into the rules it gives an agent
15 rules. About 0 are common, 3 are distinctive, and 12 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
12
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 83
Add YOUR_AGENT_PROMPT constant to prompts/domain_prompts.py
- Very specific: line 86
Add to SPECIALIST_REGISTRY dict
- Very specific: line 87
Add tool enum value to SPECIALIST_TOOLS[0]["input_schema"]["properties"]["specialist"]["enum"]
- Very specific: line 88
Add its area in plain words (e.g.
- Very specific: line 90
Add knowledge docs to knowledge/your_domain/
- Very specific: line 92
Add packages/core/openexecutive/evals/_scenarios/your_domain_001.yaml and your_domain_002.yaml
- Very specific: line 94
If the agent introduces a new pattern (new tool, new routing path, new memory contract), update …
- Very specific: line 110
All Anthropic API calls: use anthropic.AsyncAnthropic()
- Distinctive: line 111
No dynamic content in cached system prompt blocks
- Very specific: line 112
All agent analyze() calls are async
- Very specific: line 122
When your PR materially changes a documented topic, re-author the affected prebuilt/<section_id>.json in the same PR (the simplest path: …
- Distinctive: line 238
No stubs — working code only
- Distinctive: line 285
Before calling a code change done or opening a PR:
- Very specific: line 291
Every non-draft PR also gets an automated Claude review (.github/workflows/claude-code-review.yml) as inline comments marked 🔴 must-fix / …
- Very specific: line 292
When driving a PR to green, the steward skill (.claude/skills/steward/SKILL.md) covers CI failures and review findings.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Open Executive — Claude Code Context
Project Overview
Open Executive is a multi-agent AI system acting as a virtual corporate executive. Python backend (FastAPI) + Next.js 15 frontend. The "Executive" is a single coherent persona backed by 9 specialist sub-agents, all powered by the Anthropic Claude API.
Repository Layout
packages/core/ Python: all agent logic, API, CLI
packages/ui/ Next.js 15 web UI
knowledge/ Curated MBA knowledge base (git-tracked Markdown)
evals/ Eval scenarios + LLM-as-judge runner
docker/ Dockerfile + docker-compose.yml
.github/workflows/ CI + eval pipelineKey Commands
make dev # Start FastAPI (port 8000) + Next.js (port 3000)
make test # Run pytest
make lint # ruff check + mypy
make test-changed # fast loop: only the tests that cover what this branch changed
make check # everything CI checks: lint, unit tests, UI build (if touched), PR rules
make eval # Run eval suite against localhost
make docker # docker compose up --buildPython Setup
Uses uv for package management.
cd packages/core
uv sync
source .venv/bin/activateArchitecture — How the Agent System Works
- User message arrives at
Executive(orchestrator inorchestrator/executive.py) - Executive uses Anthropic tool use to call
consult_specialistfor relevant domains - For cross-domain questions, multiple specialists are called in parallel
- Each specialist:
- Gets its domain system prompt from
prompts/domain_prompts.py - Retrieves relevant chunks from ChromaDB (built-in knowledge + company docs)
- Returns analysis to the Executive
- Gets its domain system prompt from
- Executive synthesizes all specialist input into one coherent response
- The internal agent architecture is NEVER exposed to the user
Prompt Caching — Critical
The system is designed around Anthropic prompt caching. Breaking caching = 10x cost increase.
Never put dynamic content in system prompt blocks that have cache_control.
Build order in prompts/cache_manager.py:
- Tool definitions (sorted by name — MUST be sorted)
- Executive persona constant (from
prompts/executive_persona.py— NEVER f-stringed) - Company profile block (from
memory/company_profile.py) - Knowledge index summary
RAG context goes in the user turn, not the system prompt.
Adding a New Specialist Agent
- Create
packages/core/openexecutive/agents/your_agent.py:from openexecutive.agents.base import BaseAgent class YourAgent(BaseAgent): name = "your_agent" domain = "your_domain" visibility = "core" # listed in the Agent Council's simple view model = "claude-sonnet-5" def get_system_prompt(self) -> str: from openexecutive.prompts.domain_prompts import YOUR_AGENT_PROMPT return YOUR_AGENT_PROMPT - Add
YOUR_AGENT_PROMPTconstant toprompts/domain_prompts.py - Register in
orchestrator/router.py:- Add to
SPECIALIST_REGISTRYdict - Add tool enum value to
SPECIALIST_TOOLS[0]["input_schema"]["properties"]["specialist"]["enum"] - Add its area in plain words (e.g.
"sales": "sales") to_AREASinorchestrator/answer_sources.py. The web chat names that area when the specialist can't answer, and a test checks every registered specialist has one
- Add to
- Add knowledge docs to
knowledge/your_domain/ - Add
packages/core/openexecutive/evals/_scenarios/your_domain_001.yamlandyour_domain_002.yaml - If the agent introduces a new pattern (new tool, new routing path, new memory contract), update
packages/core/openexecutive/architecture/architecture-facts.yaml. Pure additions toSPECIALIST_REGISTRYare auto-reflected in theagentssection without YAML edits. - Submit PR — must include all of the above
Company Data
Company-specific data lives in packages/core/company/ — gitignored. Never commit company data. The .env file is also gitignored.
Structure:
company/profile.yaml— structured company profile (populated by onboarding wizard)company/docs/— uploaded documents (indexed into ChromaDB)
Code Style
- Python:
rufffor linting,mypyfor type checking,pytestfor tests - Pydantic v2 throughout — use
model_config = ConfigDict(...)notclass Config - All Anthropic API calls: use
anthropic.AsyncAnthropic() - No dynamic content in cached system prompt blocks
- All agent
analyze()calls are async
Architecture Docs
The /architecture page is served from static, hand-authored content under packages/core/openexecutive/architecture/prebuilt/<section_id>.json — one file per section in architecture/sections.py (SECTIONS). The backend (api/routes/architecture.py) only reads these files; nothing on this path calls an LLM. The files ship in the Docker image, so they redeploy automatically with any packages/core/** change.
architecture/architecture-facts.yaml is the curated, deep source-of-truth reference for the why behind the system (integrations, scheduler behavior, departments/people structure, caching layout, invariants, committee review, authority gates). It is no longer fed to a runtime generator — treat it as the authoritative notes you (or Claude Code) read when re-authoring a section.
The most common failure mode is new behavior added under an existing topic — e.g. adding Discord to integrations, or changing the response shape of an endpoint described under today. Nothing forces an update, so the page silently goes stale. Treat any change that alters what a section already describes as a required content update — same bar as adding a brand-new topic.
When your PR materially changes a documented topic, re-author the affected prebuilt/<section_id>.json in the same PR (the simplest path: ask Claude Code to re-author that section from the updated facts), and update the corresponding architecture-facts.yaml notes. Topics → section ids:
- New integration channel OR changed integration behavior →
integrations - New workflow primitive (e.g.,
wait_for_human) OR newWORKFLOW_REGISTRYentry with a new pattern →workflows - Cache layout change (block count, TTLs, what's cached) →
caching - New invariant or guardrail → the affected section (often
overview/agents) - New routing pattern (e.g., committee review) OR changed specialist routing →
routing-adjacent sections (agents,lifecycle) - Schema change to a documented table →
schemas - Endpoint added, removed, renamed, or response-shape changed →
api(and any section that names it) - New top-level module under
packages/core/openexecutive/→ add aSectionSpecinarchitecture/sections.py, a matching entry inpackages/ui/src/app/architecture/page.tsx(IDs must match), AND a newprebuilt/<id>.json
Each prebuilt/<id>.json has the keys section_id, title, markdown, mermaid (a Mermaid string or null), and generated_at. The Markdown must not include the section heading (the UI renders the title). Validate edits with python -m json.tool.
CI enforces this with scripts/pr_checks.py (also run by make check): a change under a documented module fails unless one of that module's prebuilt/<section>.json files changed too. The module → section map is SECTIONS_FOR in that script; update it when you add a module or section. When a change genuinely does not alter what a section describes, waive it with a line Arch-Docs: n/a - <reason> in a commit message or the PR description.
Local Hosts
make dev serves both:
- API — FastAPI backend on http://localhost:8000
- Sections list + availability:
GET /architecture/sections - Per-section content (static, pre-authored):
GET /architecture/sections/{id} - SQLite inspection:
sqlite3 ./episodic_memory.db(or$EPISODIC_DB_PATH)
- Sections list + availability:
- UI — Next.js frontend on http://localhost:3000
- Architecture page: http://localhost:3000/architecture
Deployment topology and operations live in docs/deployment.md. Any host URLs
and deploy configuration for a specific environment are kept outside this repo.
Environment Variables
See .env.example. Required: ANTHROPIC_API_KEY, EXEC_EMAIL_ADDRESS (no default). Signed callers (optional, recommended on a server, and required there for Act as me's Send): CALLER_ASSERTION_PRIVATE_KEY on the UI signs who is signed in, and CALLER_ASSERTION_PUBLIC_KEYS on the API checks it (api/caller.py, scripts/make-caller-keys.py, docs/auth.md). Routes read the caller only through api/caller.py, never the x-caller-email header. Optional integrations: SLACK_BOT_TOKEN, SLACK_APP_TOKEN (+ SLACK_CARD_BUTTONS for Approve and Skip on cards in a DM, with Interactivity on), TELEGRAM_BOT_TOKEN + TELEGRAM_WEBHOOK_SECRET (docs/telegram_setup.md), DISCORD_BOT_TOKEN + DISCORD_APP_ID, GOOGLE_CHAT_PROJECT_NUMBER + one of GOOGLE_CHAT_SERVICE_ACCOUNT_FILE / _EMAIL (docs/google_chat_setup.md). Email has no IMAP/SMTP settings: the poller (integrations/email_poller.py) reads and sends through the configured workspace backend (integrations/workspace/, chosen by EMAIL_PROVIDER / CALENDAR_PROVIDER, default google): the Gmail tools of the Google Workspace MCP (GOOGLE_OAUTH_CLIENT_ID / GOOGLE_OAUTH_CLIENT_SECRET) or the Outlook tools of the Microsoft 365 MCP (MS365_MCP_CLIENT_ID + a one-time ms365-mcp-launch.sh --login), signed in as EXEC_EMAIL_ADDRESS. Act as me (optional) reads the owner's own Gmail directly (delegation/gmail.py, never the MCP gateway) from a per-person credential in DELEGATION_GOOGLE_CREDENTIALS_DIR, minted by scripts/connect-own-gmail.py (or an Outlook one, delegation/outlook.py, minted by scripts/connect-own-outlook.py with the MS365_MCP_CLIENT_ID app); DELEGATION_COMPOSER_MODEL / DELEGATION_MAX_DRAFTS_PER_DAY tune the drafts, and DELEGATION_INBOX_POLL_MINUTES / DELEGATION_INBOX_MAX_DRAFTS_PER_DAY / DELEGATION_CLASSIFIER_MODEL the inbox watcher (delegation/inbox.py, off until the owner turns on Draft replies to my inbox); DELEGATION_WORKFLOW_MAX_SENDS_PER_DAY caps the emails a workflow sends as its creator (delegation/workflow_email.py). Drive folder sync (optional): DRIVE_SYNC_ENABLED + DRIVE_SYNC_SERVICE_ACCOUNT_FILE + DRIVE_SYNC_FOLDER_IDS (docs/drive_sync_setup.md) reads shared folders as a drive.readonly service account (knowledge/drive_client.py, never the MCP gateway) into the isolated drive_docs collection. Confluence sync (optional): CONFLUENCE_SYNC_ENABLED + CONFLUENCE_URL + CONFLUENCE_PERSONAL_TOKEN (Server/DC) or CONFLUENCE_USERNAME / CONFLUENCE_API_TOKEN (Cloud) + CONFLUENCE_SYNC_SPACE_KEYS (docs/confluence_sync_setup.md) reads the listed spaces over the Confluence REST API (knowledge/confluence_client.py, never the MCP gateway; names shared with mcp-atlassian) into the isolated confluence_wiki collection, skipping restricted pages unless CONFLUENCE_SYNC_SKIP_RESTRICTED=false. OneDrive folder sync (optional): ONEDRIVE_SYNC_ENABLED + ONEDRIVE_SYNC_FOLDERS (docs/onedrive_sync_setup.md) reads as the Executive's Microsoft 365 sign-in, with a token from ms365-mcp-launch.sh --access-token (knowledge/onedrive_account.py, never the MCP gateway), into the isolated onedrive_docs collection.
Testing
Local gotcha: if
BACKEND_SHARED_SECRETis set in your shell/container (e.g. for theopenexec-apiskill), full-appTestClienttests return401instead of their expected status. Run the suite with the var unset —env -u BACKEND_SHARED_SECRET uv run pytest tests/unit/— to match CI (CI does not set it).CALLER_ASSERTION_PUBLIC_KEYSdoes the same to any test that sendsx-caller-email;tests/conftest.pypops it.
OE_PUBLIC_DEPLOYMENTis the same trap, harder:api/main.pyrunsapp = create_app()at module level, and with that var set and noBACKEND_SHARED_SECRETthe guard raises at import time — so the failure is a collection error, not a test failure.tests/conftest.pypops it defensively; keep it unset in your shell anyway.
Ad-hoc scripts:
get_settings()requiresEXEC_EMAIL_ADDRESS(no default), so a one-offuv run pythonsnippet needs it exported alongsideANTHROPIC_API_KEY— the test suite sets both intests/conftest.py.
uv gotchas (learned on #87):
uv export/sync --frozenusesuv.lockas-is and does NOT detect a stale lock —--lockedis the flag that fails on staleness.uv syncin CI silently rewrites a stale lock before tests run, so lock freshness is gated by theuv lock --checkstep inci.yml.uv export -o FILEstill echoes the full export to stdout unless-q.
UI lint:
npm run lintiseslint .over the whole package (packages/ui/eslint.config.mjs) and CI runs it. ESLint stays on 9 (the Next config's plugins cap there), andtypescriptis aliased to the TS 6 API for typescript-eslint whiletscis still 7. CI's UI job also runsnpx tsc --noEmitandnpm test, and some of thosescripts/*.test.mjsparity tests parse Python source (api/main.py's_LOOPBACK_HOST_RE/_OWN_PAGE_FETCH_SITES,utils/deployment.py'sFALSEY_ENV) — moving or renaming one of those breaks the UI job, so runnpm testtoo when you touch them.
Audit-log test pollution:
audit.log_eventwrites to the default./episodic_memory.dbunless the test isolates it. A test module that exercises audited code (alerts review, sweeps) without monkeypatchingopenexecutive.audit.log_event(or the audit logger's DB) leaks rows that break other modules' assertions only in a full run (e.g. the brief's "handled overnight" block). Patch it in an autouse fixture, and delete a straypackages/core/episodic_memory.db(gitignored) if one appears.
Patching
episodic.DB_PATHisn't enough:memory/episodic.pyreads it at call time, but the audit logger keeps its own path, and modules that bindDB_PATHas a default argument at import (memory/session_store.py,knowledge/review_store.py, …) never see the patch, so they still use./episodic_memory.db. If a stray copy of that file lacks a table they read, the test fails with "no such table" depending on what ran before it. Point them at the test DB too, as thedbfixture intests/integration/test_scheduler_runner.pydoes for the audit logger.
ContextVar leaks between tests: a sync fixture or test that sets
current_session(or any module-level ContextVar) and doesn't reset it leaves that value bound for every later test in the process. pytest-asyncio isolates only async tests and fixtures. Usetoken = var.set(...),yield,var.reset(token). CI's-n auto --dist loadfileusually puts the leaking file and the one it breaks on different workers, so only a serial run (no-n) shows it.
Pre-existing ruff hits in
tests/unit/test_attachments.py(unsorted imports, unusedasyncio):make lintonly checksopenexecutive/, so CI is unaffected — lint the specific test files you touched rather thantests/as a whole.
Integration tests need no API key:
tests/integration/drives the FastAPI routes against a temp SQLite DB with the model calls stubbed, and CI runs it next totests/unit/. Stubutils.session_title.generate_session_titleandknowledge.retriever.retrievein any new route test, or it reaches the live API (seepatched_depsintest_chat_committee.py).
# Unit tests (no API calls)
pytest packages/core/tests/unit/ -v
# Integration tests (route-level, stubbed model calls, no API key)
pytest packages/core/tests/integration/ -v
# Eval suite
make eval # runs packages/core/openexecutive/evals/_scenarios/*.yaml, writes evals/results/PR Requirements
- No stubs — working code only
- Tests for new behavior
- Eval scenarios for new agents or prompt changes
ruff checkandmypymust pass —make checkruns these, the unit tests andscripts/pr_checks.py(no stubs, eval scenarios, arch-doc drift) the way CI does- Architecture docs updated per
## Architecture Docsabove (when integrations, scheduler, departments/people, caching, invariants, routing patterns, or top-level modules change) - PR title is
type(scope): what changed, in the imperative — e.g.fix(chat): bind the session for the whole SSE turn. Types:fix,feat,docs,chore,refactor,test,perf. Scope is the subsystem (chat,memory,alerts,briefing,orchestrator,integrations,ui,deps, …), not a file path; drop it only when the change genuinely spans the repo. Say what changed rather than what it is about, lowercase after the colon, no trailing period. See.github/PULL_REQUEST_TEMPLATE.md. The type sets the next version (release-please, before 1.0:featandfix→ patch, a breaking change (!/BREAKING CHANGE:) → minor;chore/docs/test/refactorrelease nothing on their own), andfeat/fixtitles become the changelog lines, so pick the type by what the change is, not by how big it is. PRs are squash-merged with the PR title as the commit subject, which is what release-please reads — branch commit messages do not reachmain. !/BREAKING CHANGE:only for a real break (a removed or reshaped endpoint, a new required env var, a migration an operator must run) — never to get a bigger bump. To release a larger version for any other reason, put aBEGIN_COMMIT_OVERRIDE/END_COMMIT_OVERRIDEblock at the end of one PR's description holding that PR's title, a blank line, thenRelease-As: X.Y.Z. release-please reads the block in place of the squash message. A bareRelease-As:line in the description does not work: release-please only reads footers in the commit's final paragraph, and GitHub or the Claude footer appends text after it (#210). It applies to the next release only. The same block with a corrected message un-marks a merged PR (e.g. drops a wrong!). Either takes effect on the next push tomain. Write the begin marker only once in a PR description, and never in prose: release-please takes the text after its first occurrence, so a backticked mention earlier in the body becomes the "message", fails to parse, and drops that commit from the release (#210, #211).- PR description is three sections and nothing else: Problem, Approach,
Checklist (see
.github/PULL_REQUEST_TEMPLATE.md). Rationale, review findings and alternatives go in the commit message; open questions go in the review thread. Keep the body short enough to read in one screen.
Definition of Done
While iterating, run make test-changed (the tests that cover what the branch
changed) plus ruff on the files you touched, not the full suite. Run
make check once, before the first push, then batch fixes: later pushes
rely on CI and rerun only what failed, unless the fix touched shared code
(tests/conftest.py, pyproject.toml, a base class).
Before calling a code change done or opening a PR:
make checkpasses. It unsetsBACKEND_SHARED_SECRET/OE_PUBLIC_DEPLOYMENTfor the tests, so the Testing gotchas above don't bite.- New behavior has tests; a new agent or prompt change has eval scenarios.
- A change to what a documented topic describes updates its
prebuilt/<section>.json(see Architecture Docs), or carries anArch-Docs: n/a - <reason>waiver. - A change touching
api/,integrations/,mcp_server/, auth,orchestrator/outbound_guard.py, the cached prompt blocks or.gitignoregets a pass from thesecurity-revieweragent (or/security-review) before the PR opens. - Every non-draft PR also gets an automated Claude review (
.github/workflows/claude-code-review.yml) as inline comments marked 🔴 must-fix / 🟡 optional / 🟣 pre-existing. Fix or answer each finding. A PR from a fork gets it only when a maintainer applies theclaude-reviewlabel (claude-code-review-fork.yml), once per label. - When driving a PR to green, the
stewardskill (.claude/skills/steward/SKILL.md) covers CI failures and review findings.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

