A local-first AI chief of staff for inbox triage, drafting, and approval-based execution.
Gmail · Google Calendar · Google Drive · Slack · iMessage → one review queue.
I'm the EVP of UCLA ACM — around 300 officers and 3,500 members — so a lot of my day turns into routing sponsor emails, approvals, intros, calendar invites, and follow-ups across several inboxes.
A lot of that work is repetitive. The tricky part is that I still need to see what the assistant is about to do before it does anything as “UCLA ACM EVP.” Drafting is fine. Sending without approval is not.
This started from OpenClaw, a local-first, Markdown-backed agent framework with chat, skills, ingestion, and RAG built in. The core plumbing was already there. What I wanted was a different product shape on top of it:
- A queue instead of a thread. OpenClaw is mainly chat-first. For 100+ incoming messages across Gmail, Slack, Calendar, and Drive, I don’t want to ask a bot what to do one message at a time. I want a task queue with explicit states: skip, answer, or task.
- A real approval gate. OpenClaw can send emails and run tools, but a prompt-level rule is not enough for this use case. Chief of Staff shows the exact tool call, the full payload, and approve/deny buttons before anything side-effectful runs.
- An undoable action log. Every external mutation gets written to a structured side-effect log with enough context to reverse it later.
So this is intentionally narrower than OpenClaw. OpenClaw is the general framework. Chief of Staff is the workflow app built around the loop I actually wanted: triage → review → approve.
The first use case is running a large student organization, but the system is not ACM-specific. Point it at a different set of accounts, describe your workflows, and the same pattern works for a consultancy, a sales pipeline, or personal admin.
Every new email, calendar invite, Slack DM, Drive share, and iMessage is ingested and passed through a small triage model. It chooses one of three outcomes:
- Skip it — newsletters, noise, ambient chatter.
- Answer it — draft a reply using local context and RAG.
- Task it — create a review card with the original source, a short explanation, and the tools the agent wants to use.
Tasks do not run until you approve them.
Reusable workflows live as Markdown skills. You can describe a workflow in chat, have the agent turn it into a SKILL.md, and hot-load it without restarting the app. Every outside-world mutation is recorded in an undoable side-effect log.
It runs on your laptop. The only outbound model call is to the Claude API.
Everything the agent sees lands here with a triage state attached. The right pane shows the original message verbatim, so you can always inspect the source before trusting the summary.
The planner chooses a skill, or creates a one-off plan. The executor proposes tool calls. The task card sits in the review queue until you approve or deny it.
Side-effectful tools go through a canUseTool gate. The modal shows the tool name, full payload, and approve/deny controls before execution.
Chat is for one-off questions, manual task creation, and workflow authoring. Describe a recurring process, and the agent can search your local knowledge base, draft a Markdown skill, and show it inline as a skill created card.
Confirming the skill writes it to skills/<slug>/SKILL.md. The loader picks it up automatically, and scheduled skills register with APScheduler.
Skills are just Markdown files with YAML frontmatter. The frontmatter defines things like schedule, allowed_tools, inputs_schema, and uses_skills; the body is the natural-language procedure the agent follows.
The app watches the filesystem and hot-reloads skills as they change.
Memory has two layers: context injected into every role prompt, and explicit memory.* tools the agent can call. The UI separates plain notes, people, and recurring workflows.
Every external mutation is logged with the information needed to undo it.
┌─────────────┐ ┌────────────┐ ┌─────────────┐ ┌──────────────┐ ┌──────────┐
│ INGEST │──▶│ TRIAGE │──▶│ PLANNER / │──▶│ APPROVAL │──▶│ TOOLS │
│ gmail · gcal│ │ claude │ │ EXECUTOR │ │ human │ │ mcp │
│ gdrive·slack│ │ skip/answer│ │ picks skill │ │ │ │ emails, │
│ imessage │ │ /task │ │ or oneshot │ │ │ │ events… │
└─────────────┘ └─────┬──────┘ └──────┬──────┘ └──────┬───────┘ └────┬─────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ SQLite task queue · ChromaDB RAG · memory · log │
└─────────────────────────────────────────────────────────────┘
- Reasoning core — Claude Agent SDK. Triage, planning, and execution run as separate resumable sessions.
- Skills — Markdown files with YAML frontmatter. The loader parses them on boot, on a timer, and through a filesystem watcher.
- Tools — local MCP servers for Gmail, Calendar, Drive, Slack, iMessage, RAG, memory, and task control.
- Approval — side-effectful tools are blocked by
canUseToolunless the task is explicitly marked for auto-execution. - RAG — ChromaDB with
sentence-transformers/all-MiniLM-L6-v2. Embeddings are computed locally. - Memory — proactive prompt injection plus reactive
memory.*tools. - Side-effect log — every mutation is recorded with enough context to undo it.
Backend: Python · FastAPI · APScheduler · SQLite · ChromaDB · Claude Agent SDK · MCP
Frontend: Vite · React · TypeScript · Tailwind · shadcn/ui
Integrations: Gmail API · Google Calendar API · Google Drive API · Slack via slackdump · iMessage local SQLite
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # ANTHROPIC_API_KEY required
python scripts/bootstrap_db.py # creates data/app.db + seeds builtin skills
./run_dev.sh # api :8000 · worker · frontend :5173./run_dev.sh --debug prints per-call agent traces to the terminal.
./run_dev.sh --no-auto runs the UI against the database without firing triage or scheduled skills. This is useful when you want to inspect the app without spending model tokens.
You can set APP_TRIAGE_ALLOWED_SENDERS in .env to limit which senders enter the triage queue:
APP_TRIAGE_ALLOWED_SENDERS=@uclaacm.com,@bloomberg.net,kevin@drymail.comLeaving it empty allows everyone.
Without Google, Slack, or iMessage credentials, the integration MCPs return stubs. The full loop still runs end-to-end: ingest → triage → task → approval → tool → side-effect → done.
app/ Python package: config, db, agent, rag, memory, ingest, worker, api
mcp_servers/ MCP tool servers: rag, gmail, gcal, gdrive, slack, imessage, memory, taskctl
skills/ SKILL.md files the agent can run
frontend/ Vite + React + TypeScript
scripts/ bootstrap_db, Google/Slack auth helpers, RAG reindexing, smoke tests
docs/ architecture notes, skill authoring guide, tool cheatsheet
data/ runtime state: SQLite, ChromaDB, cached tokens, ingested Markdown
See docs/architecture.md for the full architecture walkthrough and docs/skills_how_to_add.md for skill authoring.






