Harvis

Multi-agent orchestrator system for Eric Downs at Grain & Mortar.

Site Reference

Project Path ~/Projects/harvis/
GitHub github.com/ericdowns/harvis (private)
Server 134.122.11.98 (DigitalOcean, $6/mo)
Telegram @harvis_gm_bot
Theme Docs See CLAUDE.md at project root

Project Info

Integrations

Service Status Details
Harvest Not billable Internal product work
Todoist Active Harvis 🎩 β€” 6gW9QcxFHFPW6qCq
Telegram Active @harvis_gm_bot
Server Active DigitalOcean droplet at 134.122.11.98 ($6/mo)

Current state (as of 2026-05-01)

Ground-truth snapshot. If a status update below contradicts this table, the table wins (it gets refreshed on every change). Read this BEFORE skimming the chronological log.

Silos

Silo Console agent ID Model Memory store Skills attached MCP servers Sessions wired?
Sales agent_011Ca4TdxhgTododVoQrbvCb haiku-4-5 memstore_01BhvSLECT8wN8k3YmPoePya harvis-sales-conventions sales-proxy YES
Calendar agent_011Ca15PpfXXgTEpSo9JHPwq sonnet-4-6 memstore_01YZYVEePNWuJZoE3YYUTSZD none google-calendar YES
Roster agent_011CaUwxTSRbzmQH7BcmzF7g haiku-4-5 seeded but env unset harvis-roster-conventions roster-proxy partial β€” agent + skill live, memory NOT mounted
Email agent_011CZzhvmpvL7sEcLsfmoKMq (a.k.a. "Gmail Inbox Manager") sonnet-4-6 memstore_015Tg232K4kTYDJAPm4FPUCa none gmail YES
Intel agent_011Ca4TdtUS8KWGCnyXRgFmN haiku-4-5 none none intel-proxy NO β€” fully stateless, not in SILO_CONFIGS
Morning Brief agent_011CaQvQk2niUj23iVdrgH8R sonnet-4-6 memstore_01S4fcbkbPHiAMcJfujqt9Yj none gmail + slack cron-driven (separate path)
General agent_011CaWjQCft3SepBBtv9bCdn haiku-4-5 memstore_01Tj6x4jdXVDekmW3h2pQknZ harvis-todoist (NEW 2026-05-01, skill_017LCGb5fzyqDJn7pH8BSS4K) none β€” agent uses built-in bash to hit Todoist REST YES

There's also agent_011Ca13Fnrvk66C2qHgm4gug ("Harvis", model claude-haiku-4-5-20251001) that appears in beta.agents.list but isn't referenced in current code β€” investigate before any cleanup.

Memory store extras (general only)

The harvis-general store carries 4 extra files beyond the standard triplet: - projects.md β€” synced live from Eric's Mac via ~/.claude/skills/todoist/sync-projects-to-harvis.sh - routing-patterns.md β€” Eric-curated phrase β†’ project mappings (read-only to agent) - learnings.md β€” agent appends Todoist routing corrections here - secrets.md β€” bearer token mount (TODOIST_API_TOKEN); agent sources before curl

See ~/Projects/harvis/docs/MEMORY-STORES.md "harvis-general extended layout" for the full story.

Active follow-ups (open)

Project Status

May 1, 2026 (later) β€” search-proxy scoped + Todoist subtask rule codified

Mobile session, no code shipped. Two artifacts dropped:

May 1, 2026 β€” General silo on Todoist (replaces retired cards system)

Wired the general silo to Todoist as the single source of truth for Eric's to-dos, replacing the GitHub-Issues "cards" system retired 2026-04-30. Telegram/Slack messages can now create real Todoist tasks with correct project routing and the 5-section task template. End-to-end smoke tested.

What shipped (no PR — direct edit + redeploy on prod): - New Console Skill harvis-todoist (skill_017LCGb5fzyqDJn7pH8BSS4K) — codifies trigger phrases, project-routing rule (default to Inbox if not 100% certain), 5-section task template, label/section conventions, and curl shapes. Source: lib/agents/skills/todoist/SKILL.md. - lib/agents/general.ts rewritten — removed create_card/list_cards/cards_link (which were dead code anyway, never registered with the managed agent), GENERAL_TOOLS = [], system prompt rewritten for Todoist + workflow memory. - scripts/create-skills.ts — todoist entry added to SKILLS registry. - scripts/create-managed-agent.ts — general silo gets skillIds: [HARVIS_TODOIST_SKILL_ID]. Console agent now version 2. - scripts/create-memory-stores.ts — GENERAL_SEED rewritten. profile.md drops cards lingo and points at Todoist + the harvis-todoist skill. Adds 3 new files: routing-patterns.md (hand-curated phrase→project mappings, agent-read-only), learnings.md (agent-maintained, append-only Todoist corrections), projects.md (synced from local Mac). - .env — HARVIS_TODOIST_SKILL_ID added. - Memory store harvis-general (memstore_01Tj6x4jdXVDekmW3h2pQknZ) now has 7 files. The secrets.md holds TODOIST_API_TOKEN so the agent's bash can source it before curl.

Local Mac changes: - New ~/.claude/skills/todoist/sync-projects-to-harvis.sh β€” fetches live Todoist projects, formats as projects.md, SSH-pipes to droplet which writes to memory store. Uses droplet's working ANTHROPIC_API_KEY since the local one returns 401 (separate cleanup card). - scaffold-project.py patched to call sync-projects-to-harvis.sh after each successful project creation. So the Telegram agent learns about new projects automatically.

Smoke test (from droplet, via sendToSilo('general', ...)): - "card this: smoke test from todoist wiring" β†’ agent inferred Harvis project, created task with proper 5-section description, returned URL βœ“ - "card this on Harvis for tonight: smoke test labeled night-run" β†’ Harvis project, night-run label, 5-section description βœ“ - Both deleted after verification.

Doc updates same day: - docs/MEMORY-STORES.md β€” store table corrected (was missing 4 stores), new "harvis-general extended layout" section, "Why secrets.md instead of an env var" rationale. - TECHNOLOGY-STACK.md (NEW) β€” full stack canonical doc. - This README β€” "Current state" snapshot table near the top.

Open after this: - Eric to dogfood from Telegram. Watch for "card this for tonight: ..." replies β€” should land in correct project (or Inbox) with the right labels. - Project routing strictness: in smoke test, agent inferred Harvis project from "smoke test of harvis wiring" content. Skill says "default to Inbox if not 100% certain" β€” that's defensible (content was about Harvis) but Eric may want to tighten the wording later. - Local ANTHROPIC_API_KEY rotation card dropped in Inbox: https://app.todoist.com/app/task/6gWHChP9895P7wcf.

Why this took a while to get right (worth recording): The earlier exploration agent reported the general silo as "stateless, no memory store" β€” when in fact lib/sessions.ts SILO_CONFIGS already had general wired with harvis-general memory since 2026-04-28. The wiring info was buried in a chronological status update; the structured reference docs (MEMORY-STORES.md "Where stores live" table) only listed sales + calendar. An explorer skim-reading the docs finds the table first, concludes general is stateless, misses the truth in the timeline. The fix going forward: the "Current state" snapshot at the top of THIS README is now the single ground-truth table β€” refresh it on every change. Don't make the next agent dig through six months of status updates to reconstruct the present.

Apr 28, 2026 (evening) β€” Format hardening + Roster scope discipline + email-training UX fixes (6 PRs)

Live dogfooding session β€” Eric flagged a string of formatting bugs and broken UX in Slack/Telegram, each got a focused PR. Net result: agent output now reads cleanly across both surfaces, training loop has working buttons, and roster stops leaking general workflow notes into client summaries.

What shipped (6 PRs): - PR #116 β€” fix(formatting): sanitize agent output at Telegram/Slack boundary. Telegram bot was raw passthrough β€” **bold** and run-on sentences (creating it.You're free) were rendering literally. Added fixSentenceSpacing() helper to lib/formatting/render.ts, wired into both toTelegramHtml + toSlackMrkdwn. Telegram bot now runs every response through toTelegramHtml at the send boundary. Tests cover the verbatim broken text from the SPSL screenshot. - PR #118 β€” re-land of stacked PRs #105 + #106 after the chain collapsed when #104 merged (PR #105's base was card/101-roster-slash, which got deleted; #106's base was card/102-link-unfurl, same problem). Brings the link unfurl + Edit-in-Roster button to main. Lesson: avoid stacked PRs whose bases are sibling feature branches. - PR #119 β€” fix(slack): visual rhythm β€” divider-separated sections + tighter bullet density. Roster responses were rendering as wall-of-text inside a single section block. New splitOnBlankLines() helper splits on blank-line groups so each becomes its own section block with a divider between. Also tightened CRITICAL_RULES in shared-format.ts with hard ❌/βœ… examples β€” bullets max 2 sentences, sub-bullets for longer content. - PR #120 β€” fix(roster): client-only scope discipline in notes + memory. "Tell me about Within Reach" was leaking workflow preferences (Todoist Complete section ID, claude/claude-royal account symlinks) because those facts got misfiled in clients.notes. Cleaned the DB row, moved the workflow facts to harvis-general/profile.md (new "Workflow tooling" section), and added "Scope discipline (client-only)" block to roster system prompt. Console agent re-synced to v6. - PR #121 β€” fix(email-training): render actual β–Ά Start / β–Ά Train next buttons (closes #117). The train_start action handler existed but no code anywhere created a button with that action_id β€” the train_stop receipt rendered italic text saying "Tap β–Ά Start training" with no actual button. Added a real button to the stop receipt + a "β–Ά Train next" button alongside "πŸšͺ Stop training" on save receipts. - PR #122 β€” fix(email-training): always set category in sender_domain_reputation upsert. Tap-save was failing with null value in column "category" because the existing-row branch only defaulted to 'unknown' when the row didn't exist. If a row had category=null from earlier partial state, the UPDATE never set category and the NOT NULL constraint fired. Always populate row.category before upsert now.

Cards closed: #117 (training arrow button) shipped via #121. Stale awaiting-review cards #101/#102/#103 closed since their work shipped via #118.

Memory + DB cleanups: - clients.notes for Within Reach trimmed from 1100+ chars (mixed client + workflow garbage) to 518 chars of pure client facts. - harvis-general/profile.md got a "Workflow tooling" section with Todoist Complete-section preference and claude/claude-royal account symlinks. Re-seeded via scripts/create-memory-stores.ts --silo=general. - New audit/repair scripts in scripts/ β€” inspect-roster-mem.ts, inspect-within-reach.ts, clean-within-reach.ts. Useful references for future similar misfile cleanups.

Stacked-PR gotcha worth recording: when sibling cards share a chain (#101 β†’ #102 β†’ #103), each PR's base branch points at the previous card's branch, not main. When #104 merged into main and --delete-branch removed card/101-roster-slash, GitHub auto-closed #105 (whose base no longer existed). Lost both #105 and #106 to the closure. Recovery: rebase the head branch onto main, open a new PR. Going forward: stacked cards should each target main directly, even if the work is sequential.

Follow-ups still open: #112 intel silo migration (sessions+memory), #113 implicit-signal observer for email training (night-run), #114 extract harvis-general-conventions Console Skill.

Apr 28, 2026 (PM, late) β€” General silo on sessions+memory (card #109, PR #111 merged)

Picked up #109 right after the mail-training PR merged. General was the last hand-coded silo on the stateless messages.create() path β€” no memory store, no SILO_CONFIGS entry, no Console managed agent. So "remember X" requests outside the calendar personal-fact short-circuit got the canned LLM "I have no memory" disclaimer and were silently dropped. PR #108 fixed calendar + email memory awareness; this card extends the pattern to general so the principle "every silo has memory" is actually true.

What shipped (PR #111): - lib/agents/general.ts (new) β€” extracted from index.ts. Slim domain prompt composed from shared-format.ts (CRITICAL_RULES + RESPONSE_SHAPES + MEMORY_AWARENESS). Same card tools (create_card, list_cards, cards_link). Added redirect rules so the agent knows when to point Eric at calendar/email/sales/roster instead of saving cross-domain. - Memory store harvis-general (memstore_01Tj6x4jdXVDekmW3h2pQknZ) β€” seeded with profile.md (cards conventions, voice, redirect rules), index.md, patterns.md. - Console managed agent Harvis General (agent_011CaWjQCft3SepBBtv9bCdn, Haiku 4.5). - lib/sessions.ts SILO_CONFIGS adds general so isSessionsSilo('general') returns true and bot dispatchers route through the sessions pipeline. - HARVIS_GENERAL_MEMORY_STORE_ID set on prod. - ops_workflow_registry rows: harvis-general-agent + harvis-general-memory-store. - CLAUDE.md Platform Agents table + Agents overview row updated.

Verification pending Eric (DM tests): "remember when I say 'card it' default to harvis + night-run" β†’ expect agent writes to /mnt/memory/harvis-general/profile.md and confirms what it saved (no LLM disclaimer). Brand new conversation: "what should you do when I say 'card it'?" β†’ reads memory, answers correctly. Both Slack and Telegram.

Gotcha: First Management API insert into ops_workflow_registry failed with "column 'component_type' does not exist" β€” schema uses name/slug/platform/client/owner not component_type/component_name. Worth checking the actual schema before composing inserts in the future. Pattern from existing roster rows: Harvis <Silo> Agent / harvis-<silo>-agent / platform=claude-agent and Harvis <Silo> Memory Store / harvis-<silo>-memory-store / platform=anthropic-memory-store.

Follow-ups dropped as cards: intel silo migration to sessions+memory (mirrors #109), implicit-signal observer for email training (Sanebox-style "I noticed you always trash X β€” auto-trash future?"), extract harvis-general-conventions Console Skill once patterns accumulate.

Apr 28, 2026 (PM) β€” Mail-training redesigned: action-axis + auto-feed (PR #110, deployed)

Activated PR #99's training loop and immediately learned the v1 design was wrong. Eric pushed back on three layers: typing replies was lazy (β†’ buttons), 7 categories didn't capture his actual inbox (β†’ research), and "category" wasn't his real decision axis at all (β†’ action-axis). Reframed the whole feature, deployed live in #harvis-mail-training, and shipped freeform notes via thread reply before parking.

Activated v1 of the loop β€” created #harvis-mail-training (C0B1956HRME), set HARVIS_MAIL_TRAINING_CHANNEL_ID in prod, restarted harvis-slack. Smoke-tested both legs: synthetic Block Kit post + a direct invocation of sendToSilo('email', ...) with buildTrainingFollowupPrompt confirmed the agent classifies replies and calls set_sender_reputation end-to-end. ~24s round-trip on the agent path. Cleanup row + cleanup card after.

v2 redesign β€” action-axis instead of category-axis. Eric's complaints triggered ~30 min of research (Hey.com Imbox/Feed/Paper Trail, SaneBox SaneLater/SaneBlackHole, Superhuman 2-min rule, MS CHIIR'19 paper) + a 1264-message inbox pull grouped by sender domain. The clusters that emerged in his real data were action-shaped, not identity-shaped (clients sit alongside vendor receipts in "things I file"; recruiting + Shopify marketing cluster into "things I delete on sight"). Eric picked the action-axis option from a 3-way AskUserQuestion preview.

What shipped (PR #110): - 4 action buttons: βœ‹ Show me / πŸ“° Feed / πŸ“ Auto-file / πŸ—‘ Trash. Plain-English explanation block sits above the buttons (added after Eric flagged "what the fuck does feed mean"). - Schema: sender_domain_reputation.action_policy text CHECK (...) + is_vip boolean. category stays for agent-derived identity. - Side effects on click: Auto-file/Trash create real Gmail filters via lib/gmail.ts:applySenderActionPolicy (skipInbox + Auto-filed label, or skipInbox + TRASH). Optional checkbox archives or trashes past mail via applyActionToPastMail (capped 500/click). VIP modifier on Show-me sets is_vip=true. - Continuous feed: lib/email-training-queue.ts:pickNextCandidate builds a candidate pool (last 14d, max 500 msgs) from Gmail, filters by action_policy IS NOT NULL, sorts by volume. Cached 30 min in-memory. After every save, the bot auto-posts the next candidate. β–Ά Start training button parked in the channel kicks off; πŸšͺ Stop training on receipts breaks the chain. - Direct DB writes via new lib/sender-reputation.ts β€” bypasses the agent on click for sub-second response (was 24s through sendToSilo). - Freeform notes via thread reply: any reply in a card's thread saves to sender_domain_reputation.notes AND distills a one-line rule into harvis-email/patterns.md via extractEmailPatternRule. The bot stops deleting pendingTrainingQuestions entries on action click so notes work both before and after the action.

Deployed live, awaiting Eric's first real click validation. Smoke cards verified Slack accepts the Block Kit shape; the only step that needs Eric's hands is the click + thread-reply test.

Outstanding: PR #110 awaiting review/merge. Training-side TODO items in v2: (a) update agent system prompt to emit proposed_action directly (currently mapped via heuristic in buildTrainingBlocks), (b) implicit-signal observer (Sanebox-style "I noticed you always trash mail from X β€” auto-trash future?"). Both noted in docs/EMAIL-TRAINING.md.

Gotcha worth remembering: Local working tree got reset to main mid-session (probably a hook); thought the work was lost until I found it on feat/email-training-queue already pushed. Future session β€” if local files vanish, check git branch first; the work may already be branched.

Apr 27/28, 2026 β€” Card-clearing session (4 PRs, 3 cards closed)

Worked the harvis night-run queue start-to-finish. Three harvis cards merged + deployed; one helper script PR'd to claude_skills. Two new feature flags landed default-OFF β€” Eric flips when ready.

Block Kit blocks for Morning Brief (#27 β†’ PR #97). Renderer detects the morning-brief shape (β‰₯2 --- dividers + headline) and emits header + context + section + divider + section. mcp/scheduler posts via chat.postMessage with blocks when present. Phase 2.0 of the channel-formatting layer (#41 / PR #42 was Phase 1). Card/DataTable/Alert/Carousel components from the original card scope deferred β€” those need consumers (sales-side Card, calendar Data Table) that don't exist yet.

Slack streaming for sessions silos (#22 β†’ PR #98). New lib/formatting/slack-stream.ts lazy ChatStreamer wrapper. lib/sessions.ts runTurn/sendToSilo accept optional onTextChunk(chunk) callback that fires per agent.message text block. bot.ts main DM handler computes useStreaming = isStreamingEnabled() && isSessionsSilo(route.agent), threads onChunk through, finalizes with attribution + email-action blocks. Gated HARVIS_SLACK_STREAMING=1. QC layer SKIPPED on streaming path β€” user already sees the live response, retry would be confusing UX. Stateless silos (email, intel) still use the legacy thinking-message path; follow-up to migrate them via messages.stream().

Email mail-training reactive loop (#66 β†’ PR #99). Agent emits <<TRAINING_QUESTION>>{json}<<END_TRAINING_QUESTION>> sentinel when ALL three confidence signals come back empty (no Roster, no thread history, no domain reputation). New lib/email-training.ts extracts/strips/builds Block Kit. Bot posts to #harvis-mail-training (gated HARVIS_MAIL_TRAINING_CHANNEL_ID), records pending state per (channel, ts), listens for thread replies, forwards to email agent via stable training:<channelId> conversation key so memory accumulates cumulatively. Agent calls set_sender_reputation + appends to harvis-email/patterns.md. Trigger frequency: aggressive per Eric's call (any signal empty fires). 7-day TTL on pending questions.

Local Sites GitHub onboarding helper (claude_skills PR #11 β€” refs harvis#6). cards/onboard-local-site.sh resolves the GM-custom theme under ~/Local Sites/<slug>/, refuses stock themes (Divi/base/storefront/twenty*), runs gh repo create grainandmortar/<slug> --private --source --push + install-labels.sh. Naming convention locked: grainandmortar/<site-slug-with-hyphens>. Auto-picks the only non-stock theme; refuses + lists candidates if multiple. Dry-run smoke-tested against within-reach. Operational sweep (~10 GM-custom themes without repos) runs at the keyboard post-merge.

Merge sequence had a self-conflict. PR #99 hit a merge conflict on bot.ts post-#98 (predictable β€” both touched the same file). Rebased #99 onto main, kept both features (streaming onTextChunk parameter + training-question extraction in handleSessionMessage), force-pushed, re-merged. Future learning: when two PRs touch bot.ts, sequence them through merge-rebase rather than parallel-branch.

Cards still blocked. Harvis #6 (Local Sites onboard, refs claude_skills#11) and #4 (Morning Brief auto-creates cards from inbound, depends on #6) β€” both correctly blocked, will cascade once #11 merges + sweep runs. No other action needed on harvis tonight.

Deployed. rsync + npm install + restart of BOTH harvis.service (Telegram) + harvis-slack.service (Slack). Both active, Slack reported "online (Socket Mode)". Tomorrow's 8:01 AM Morning Brief will be the first real test of the Block Kit blocks.

Apr 28, 2026 β€” Test harness shipped (PR #95) + 9-PR session summary

Closing out a long night that shipped 9 PRs and 10 cards (#61, #62, #63, #64, #65, #67, #68, #69, #70 + the test harness as #95).

Email silo transformation. Stateless monolith β†’ sessions+memory specialist with full MCP tool surface. Classification path is now a 4-step fast path: get_sender_reputation (Supabase cache) β†’ search_roster (G&M directory) β†’ get_thread_history (cold-vs-known via Gmail) β†’ match_project (free-text project resolution). Self-correcting via πŸ‘Ž-reaction-then-reply, which Sonnet extracts into a dated rule appended to harvis-email/patterns.md. Memory store grows on its own as the agent learns.

Roster surface. 18 tools across two domains (clients external + team_members internal G&M) with Phase 2 writes (update_client/contact, archive_client/contact, link_project) and Slack enrichment UX (/roster-add-note slash command + πŸ“ reaction β†’ DM-driven note capture). N+1 in listClients fixed during harness work.

New Worker. scheduler-proxy MCP exposing schedule_task / list_scheduled_tasks / disable_scheduled_task / delete_scheduled_task. Self-contained nextCronRun parser. Wired into the email Console agent. Vault credential vcrd_0122gM82pAG8eNtpGKprHc1v added.

Test harness. scripts/tests/harness.ts (29 cases, 9 categories) + tsx runner + project-local /harvis-test Claude Code skill. CLONE-PLAYBOOK now points at this as the per-client copy unit. Old scripts/smoke-test-managed-agent.ts deleted.

Memory seeded for harvis-email: classification taxonomy, hard rules (client domains never go to spam, never classify a roster name as solicitation, @grainandmortar.com is internal-team), VIP exemplars, three-Kaneko gotcha, voice cues. set_sender_reputation write target so corrections persist across sessions.

Bugs caught and fixed in-flight. SDK regression in skills.versions.create (path-stripping + wrong form-field name) β€” workaround in scripts/create-skills.ts. Missing vault credential for scheduler-proxy. listClients N+1 hitting Cloudflare 50-subrequest cap.

Email chain remaining: only #66 left (training channel β€” Eric creates the private Slack channel manually before the agent can post low-confidence questions there). Plus older queue (#22, #27, #4, #6).

Apr 28, 2026 β€” Roster Phase 2 writes (card #62)

Five new gated write tools on roster-proxy: - update_client(slug, ...patch) and update_contact(contact_id, ...patch) β€” PATCH semantics, only provided fields are applied. Other fields untouched. - archive_client(slug) β€” sets status='archived', never deletes. Reversible with update_client(status='active'). - archive_contact(contact_id) β€” soft-archive. Appends [archived YYYY-MM-DD] to notes and clears is_primary. Row preserved for history. - link_project({client, project_slug?, name?, ...}) β€” idempotent upsert keyed on (client_id, project_slug). Auto-derives slug from name on create.

All five reuse the existing silo='roster-write' permission. list_contacts and get_client now expose contact id so the agent can chain a read β†’ an update without having to dig the uuid out of search_roster. New helpers in mcp/roster-proxy/src/index.ts: sbPatch() for PostgREST PATCH, pruneUndefined() for clean partial updates. Skill bumped, Console agent re-synced. Roster-proxy surface now 17 tools (8 read + 9 write).

Apr 28, 2026 β€” Roster team_members table + tools (card #61)

Added the internal-G&M-staff dimension to the Roster silo. New team_members table on the main Harvis Supabase (hqszpyjklinxniyrcjzo) with id/full_name/initials/slack_user_id/email/role/status/notes/created_at, unique-on-initials + unique-on-email, RLS enabled. Seeded with the four current staff (ED Eric Downs, BR Brooke Ratliff, KD Kristin DeKay, MD Mike DeKay) from ~/.claude/people/team.md.

Three new MCP tools on roster-proxy: list_team(status?), get_team_member(query) resolving by initials/full-name fragment/email/slack_user_id, and gated add_team_member(...) reusing the existing silo='roster-write' permission. Total roster-proxy surface now 8 read + 4 write tools across two domains (clients, team).

System prompt in lib/agents/roster.ts updated to call out the two-domain split and the @grainandmortar.com heuristic ("teammate, never a client"). roster-conventions SKILL.md gained a "Two domains: clients vs team" section + new tool-pick rows. Console agent re-synced to v3.

Same-day fix to scripts/create-skills.ts. Initial PR #85 noted that skills.versions.create was rejecting with "SKILL.md file must be exactly in the top-level folder". Root-caused as TWO bugs in @anthropic-ai/sdk@0.91.1 (not an API regression): (1) versions.create calls multipartFormRequestOptions(opts, client) and inherits the default stripFilenames: true, dropping the upload's folder prefix; skills.create explicitly passes false and works fine. (2) The SDK appends the file as form field files but the API requires files[]. Fix: rawCreateSkillVersion() helper in scripts/create-skills.ts does a direct fetch POST with both fixes. Re-ran successfully on both roster-conventions (now version 1777332886097690) and sales-conventions (1777332924334671). Memory entry: ~/.claude-royal/projects/.../memory/anthropic-sdk-skill-versions-bug.md.

Apr 27, 2026 β€” Roster silo shipped (Phase 1)

New silo: Roster β€” G&M client + people directory. End-to-end functional via Console managed agent (Harvis Roster, agent_011CaUwxTSRbzmQH7BcmzF7g) and orchestrator routing. 9 MCP tools (6 read + 3 gated write) backed by 4 new Supabase tables on hqszpyjklinxniyrcjzo. Identity envelope flow: bot prepends [caller: <platform>/<user_id>] β†’ agent forwards to write-tool args β†’ Worker validates against permissions for silo='roster-write'. Eric + Kristin granted write; everyone else read-open via channel membership.

11 cards processed and shipped (#43–#53 + #50 closed via PR #72). Slack channel #harvis-roster (C0B058J0P62) live with Eric + Kristin (write) + Brooke + Mike (read). Deployed to DO; both harvis.service + harvis-slack.service restarted and active.

Two refinements landed same-day: PR #71 split the roster system prompt into a Console Skill (harvis-roster-conventions, skill_01YQa9Q8WsXz68aLSos18W3g) β€” same anti-pattern Sales fixed in 2026-04-24, caught and corrected here. PR #71 also wired bot identity envelope injection (needsIdentityEnvelope / buildIdentityEnvelope helpers in lib/permissions.ts) so write tools work end-to-end from Slack/Telegram DMs and the channel, not just the Console agent.

Card #55 dropped + closed: ~/.claude/skills/agent-builder/SKILL.md now codifies the 11-step checklist + slim-prompt rule + verification gates so future-me can't repeat the prompt-fat anti-pattern silently. CLAUDE.md "Adding a New Silo" section points at it.

Worker: roster-proxy.eric-downs.workers.dev. Memory: memstore_01NaAJDd7wgPBSRgvo5qRUeK. Vault credential: vcrd_01QbMAuvPRQXL5ScH81bWZ8Y.

Phase 2 cards (expanded write tools, slash command, optional project-notes LLM parser) will drop after Phase 1 bakes for a week.


Apr 26, 2026 β€” Slack app polish, second pass

Worked the night-run queue end-to-end (cards skill, Mode 4). Six cards processed; five PRs merged into main:

Card What landed PR
#3 nextCronRun() validates field ranges β€” no more silent acceptance of DOM 0/32, hour 24, etc. New scripts/test-cron-parser.ts (18 cases) harvis #17
#5 Auto-discovery of cards repo allowlist via ~/.claude/skills/cards/discover-repos.sh (walks ~/Projects + ~/Local Sites/.../themes, parses GitHub origins). New install-labels-all.sh for bulk install. SKILL.md now references the discovery script in three places. (skill files only β€” no PR; verified via comment on issue #5)
#13 slack-app-manifest.json at harvis root + docs/SLACK-APP.md apply runbook. Declarative source-of-truth for scopes, slash commands, events, identity. Mirrors brand-guide-mcp pattern. harvis #18
#14 App Home with new-user empathy spec β€” hero, About, channel routing, slash commands, "Try asking…" prompts, action buttons. lib/slack/home.ts + app_home_opened handler in bot.ts with per-user publish cache. harvis #19
#7 🎴 reaction β†’ card + /card-this slash command. Restricted to SLACK_ALLOWED_USER_ID. Body captures sender + message + permalink. harvis #20
#2 (bgm) tenants.json.logoUrl for per-tenant App Home hero image. Tenant.logoUrl?: string \| null in lib/tenants.ts; CF wordmark stays as back-compat fallback for california-forever only. brand-guide-mcp #7

Skipped/blocked (in queue): - #4 Morning Brief auto-cards β€” needs architecture decision (cards-proxy MCP Worker vs inline GitHub call from the morning-brief Worker). Marked blocked,needs-decision. - #6 Onboard WP sites to GitHub β€” creates new private repos under grainandmortar/ org. Skipped autonomously since it modifies shared org state. Eric's call. - #16 Apply Slack manifest live β€” still blocked,needs-browser. Once Eric pastes the manifest into api.slack.com β†’ App Manifest and reinstalls, App Home + reaction event become visible.

Process notes / gotchas captured today: - brand-guide-mcp branch divergence: When git checkout -b <branch> from a stale main, then committing while inadvertently on main, then git push -u origin <branch> pushes local-main HEAD to a new remote branch, leaving main + remote out of sync. Force-push was correctly blocked by sandbox; pushed to card/2-tenant-logo-app-home-v2 instead. Lesson: git branch --show-current before every commit when juggling repos. - Closing GitHub issues without a PR is gated: gh issue close got blocked when card #5's work product (skill files in ~/.claude/) couldn't be expressed as a harvis PR. Workaround: add awaiting-review label + comment with proof; Eric closes after review. The cards skill's "PR per card" assumption breaks when work product is global, not in any repo. - Tasks with shared bot.ts that are queued sequentially can mutate each other's mergeable state: cards #14 and #7 both modify integrations/slack/bot.ts in different sections. After #14 merged, GitHub recomputed #7's mergeable status to UNKNOWN; resolved to MERGEABLE/CLEAN after a short wait. No conflicts in this case but worth watching.

Live deployment NOT YET DONE. Main is current at the merge commits; DO server (134.122.11.98) still runs pre-merge code. Standard deploy:

rsync -avz --exclude node_modules --exclude .git --exclude .env ~/Projects/harvis/ root@134.122.11.98:/opt/harvis/ && ssh root@134.122.11.98 "cd /opt/harvis && npm install && systemctl restart harvis && systemctl restart harvis-slack"

Per gotcha: BOTH harvis.service AND harvis-slack.service need restarting.


Apr 23, 2026 β€” Sales agent OAuth outage + keepalive cron

Symptom. Sales agent in Slack started replying with "temporary authentication issue, try again in a moment." Token-refresh failure at the sales-proxy Worker layer.

Root cause. The shared Google OAuth row in Supabase (oauth_tokens, eric@grainandmortar.com) hadn't been refreshed since Apr 15 β€” 8 days. Google's refresh-token TTL for OAuth apps in Testing publishing status is 7 days; the token was invalidated server-side. Worker's refreshAccessToken got back a response with no access_token and threw "Token refresh failed."

Fix today. Re-ran npx tsx scripts/google-auth.ts, fresh refresh token landed in Supabase, sales verified working immediately (no Worker redeploy needed β€” Worker reads from Supabase on every call).

Fix forever (stopgap). Added a Cron Trigger to sales-proxy: 0 12 */3 * * UTC. New scheduled handler in mcp/sales-proxy/src/index.ts calls getAccessToken() every 3 days, forcing a refresh. Refreshing in any one Worker keeps gmail-proxy + calendar-proxy alive too β€” they all read the same row. Commit 06b34ca.

Fix forever (real). Publish the OAuth app to Google's "In production" publishing status to remove the 7-day TTL entirely. Requires verification submission (privacy policy URL, scope justifications; Gmail's restricted scope is the painful one). Filed in docs/ROADMAP.md under cross-cutting follow-ups.

Captured in gotcha catalog at ~/.claude/projects/-Users-edowns-Projects-harvis/memory/gotcha-catalog.md so future sessions diagnose this in minutes, not hours.

Open follow-up. Bump last_verified on the harvis-sales-mcp-proxy row in ops_workflow_registry and update ~/Projects/workflow-docs/systems/harvis.md to mention the cron. Supabase Management API was timing out during the session β€” pending a retry.


Apr 14, 2026 β€” Codified "one Console agent per silo" + backfilled intel & sales

Operating vision articulated and written into CLAUDE.md + docs/AGENT-ARCHITECTURE.md so future sessions don't miss it: Harvis is a master-orchestrator + specialists system (OpenClaw-shaped, Anthropic-runtime'd). Every specialist silo gets TWO surfaces β€” the in-process specialist in the Harvis bot AND a matching Anthropic Console managed agent. This makes each silo reachable from Claude.ai chat, scheduled runs, and external API callers, not just Slack/Telegram.

Backfilled the two missing Console agents. Intel (shipped yesterday) and sales (shipped earlier today) were both missing their Console entries β€” I'd treated that as follow-up, which was wrong. Now:

Silo Agent ID
Harvis Intel agent_011Ca4TdtUS8KWGCnyXRgFmN
Harvis Sales agent_011Ca4TdxhgTododVoQrbvCb

Created via new scripts/create-managed-agent.ts β€” idempotent, imports system prompt + MCP config directly from lib/agents/<silo>.ts, adapts <b> tags for Console markdown rendering. Re-run the script any time a specialist prompt changes to sync.

Rule now codified. CLAUDE.md has a "New Silo Checklist" β€” Console agent creation is Step 4, not optional. docs/AGENT-ARCHITECTURE.md has the decision tree for new-agent-vs-extension-of-existing. docs/ROADMAP.md is a living doc so the vision persists outside of chat transcripts.

Also surfaced the missing Calendar Agent ID (agent_011Ca15PpfXXgTEpSo9JHPwq) via the script's --list flag, fixed in CLAUDE.md.

Vault step β€” bearer tokens for heyroyal-intel and sales-proxy must exist in the Eric Downs vault (vlt_011CZzrQ2XtPGTDHS12cMVGF) before session create works against either agent. V1 does this manually via Console UI. Documented in docs/MANAGED-AGENTS.md.

Status: Backfill complete. New rule documented. Roadmap published.


Apr 14, 2026 β€” Sales agent (MCP-native, Google Sheets)

Fourth Harvis specialist shipped: sales agent, read-only access to allow-listed G&M sales spreadsheets via a new CF Worker MCP (sales-proxy.eric-downs.workers.dev). Scoping is enforced at the Worker β€” only spreadsheet IDs in the hardcoded SALES_SPREADSHEETS array are accessible; arbitrary sheet access is refused. Same Worker is consumed by Harvis (DO) AND Claude Code via ~/.claude.json β€” one MCP, multiple clients. Future Claude.ai custom connector can use the same URL.

Tools: list_sales_spreadsheets, list_tabs, get_sheet_data, search_quotes. Agent reasons over returned rows for totals/win-loss/pipeline questions; no server-side SQL layer.

Auth: reused Harvis's existing Google OAuth flow β€” added spreadsheets.readonly to scripts/google-auth.ts SCOPES. Re-auth run once to grant the new scope. Same oauth_tokens Supabase table serves Gmail, Calendar, and now Sheets.

Slack: #harvis-sales private channel (C0ASR89LBM3) created via conversations.create, Eric invited, added to CHANNEL_AGENT_MAP.

1Password: "Harvis sales-proxy MCP bearer token" in Claude Bot vault.

Status: Deployed. Worker live, tools/list verified. Bot agent registered. Sheet allow-list will be populated as Eric provides sheet IDs (placeholder at launch).


Apr 13, 2026 β€” Intel agent (MCP-native) + Slack bot + ops-table rename

Shipped the third Harvis specialist: intel agent, a read-only interface to the HeyRoyal Intel Supabase via a new CF Worker MCP server (heyroyal-intel-mcp.eric-downs.workers.dev). First agent to use the MCP-native pattern β€” the bot passes mcp_servers to claude.messages.create() and the SDK handles tool discovery and invocation. Added the anthropic-beta: mcp-client-2025-04-04 header to both Telegram and Slack clients to unlock this.

Also built out the Slack bot (Socket Mode) with channel-based routing. Added channels:join, channels:manage, groups:read/write, reactions:read scopes. Three private channels force specific agents via CHANNEL_AGENT_MAP: - #harvis-cal (C0ASRF9LZB6) β†’ calendar - #harvis-mail (C0ASKRGP1ML) β†’ email - #harvis-intel (C0ATGKXTYJU) β†’ intel

DMs still route through the orchestrator classifier. Channel membership doubles as per-user access control.

Side quests: enabled RLS on all 7 public tables in sxbogpopuggodyzpezyj (resolves Supabase security alert; service-role bypasses RLS so intel MCP keeps working). Renamed workflow_registry / workflow_run_log to ops_workflow_registry / ops_workflow_run_log so ops tables group visually apart from HeyRoyal intel domain tables. Updated Skill Hub server.js, ai-clients n8n workflow JSON, AND the live n8n workflow via its session-auth /rest/workflows/ PATCH endpoint.

Saved Supabase PAT to 1Password (Claude Bot vault, item ywyyctiqkzakszhtxnl62iui5u) with notes explaining the Management API path β€” why the service-role JWT can't do DDL, how to use it next time.

Status: Deployed to DO. Local Slack bot working in DMs and all 3 channels.


Apr 13, 2026 β€” Feedback system documentation

Reviewed the feedback loop built on Apr 12 (thumbs buttons, Supabase logging, review agent). Confirmed it's wired up and working. Created docs/FEEDBACK-SYSTEM.md with full guide: how logging works, three review methods (Telegram /review, review agent script, direct SQL), ready-to-run queries for common investigations, and what to look for when diagnosing issues.

Also set up docs/ folder with README index.

Status: Feedback system documented. Ready to start collecting data and running reviews.


Apr 12, 2026 β€” First build session

Built from zero to working system in one session. Email agent, Calendar agent, Telegram bot deployed to DO, orchestrator with quality check, tiered model routing, screenshot-to-event vision feature.

Key decisions: - Started with managed agents + Pipedream MCP, abandoned Pipedream due to 15-60s latency - Rebuilt with direct Gmail/Calendar API calls via Supabase token storage - Kept managed agents on the platform for Console/scheduled use, direct API for Telegram (speed) - Orchestrator v2: route β†’ specialist (owns output) β†’ quality check. No "refinement" step. - Haiku for reads, Sonnet for writes. Orchestrator and QC always Haiku.

Status: Live and running. Email + Calendar agents working. Telegram bot deployed 24/7.


Ideas / Future Work

Known Issues

Docs

File Description
README.md This file β€” status timeline and ideas
~/Projects/harvis/docs/INTEL-AGENT.md Intel agent reference (MCP-native architecture, tools, how to extend)
~/Projects/harvis/docs/CHANNEL-ACCESS-AUDIT.md Slack channel-based access control audit ideas