STEVE
A meeting intelligence and team operations platform for the Fullstory Strategic Solutions team. STEVE runs autonomously on macOS, connecting Google Calendar, Google Drive, Slack, Jira, Confluence, BigQuery, and Salesforce into a single coherent workflow.
Google Workspace
Slack
Jira + Confluence
Salesforce
BigQuery
Gemini (Vertex AI)
Claude (Anthropic)
Gong (planned)
1 Capabilities & Features
Core Automation — Always Running in the Background
| Feature | Script | Schedule | What It Does |
| Transcript Pipeline | run.py | Every 30 min | Scans Google Drive for new Gemini meeting notes, routes to AI summarization profile, creates structured Jira call-log tickets with action items |
| Morning Digest | morning-digest.py | 8 AM weekdays | Builds a full daily briefing from today's calendar: past context per attendee, open Jira items, Slack canvas content, and Claude-synthesized prep notes — delivered as a Slack DM |
| 1:1 Canvas Prep | prep_one_on_one_canvases.py | 5 PM weekdays | Creates Slack canvases for each next-day 1:1, pre-loaded with recent DM history, Slack channel signals, and Steve Notes |
| Team Context Canvas | canvas_manager.py | 8 AM + 6 PM weekdays | Maintains a long-lived Slack canvas with structured context (Current Focus, Active Deals, Open Items, Flags, Notes) for every team member; flags stale sections |
| Opportunity Audit | opportunity_assist/audit_runner.py | Daily | Scans #ask-solutions + customer channels for team activity, cross-references Salesforce opportunity team membership, produces compliance findings |
| Daily Pipeline Data | daily_data_service.py | 6 AM weekdays | Queries BigQuery for open pipeline KPIs + closed deals; persists to SQLite for real-time UI rendering without live BQ calls |
| Slack Tunnel | slack_tunnel.py | Every 30 sec | Lane↔STEVE private Slack relay: reads Lane's messages and replies via Claude — no approval gate |
| Slack Poll | poll_slack.py | Every 15 min | Two-phase inbound DM handler: generates proposed replies, sends to Lane for approval before posting |
| Triage Agent | triage_agent.py | On file change | Log synthesis → remediation canvas in Slack; WatchPath-triggered by new files in the polish directory |
STEVE UI — On-Demand (make steve)
React + Node.js PWA at http://localhost:5173. Eight tabs:
Profiles
Browse, create, edit, and delete AI summarization profiles. Generate new profiles from a description using Claude. Split-panel refinement chat.
Opportunity Assist
Live compliance audit: per-opportunity status, team attachment, COMPLIANT / CLAIMED_NOT_LOGGED / UNCLAIMED state. Dry-run + live SF attachment.
Projects
PM project tracker: charter new projects, track milestones, decisions, artifacts, actions. Syncs to Jira + Confluence.
Talk to Steve
Claude-powered conversational interface with 20+ tools: calendar lookup, Slack context, Jira search, Salesforce queries, canvas creation.
Agenda
Generate pre-meeting Jira agendas for any calendar event on demand.
Weekly Report
View and generate EOW synthesis reports aggregating the week's call-log tickets.
Team Config
Edit team roster (name, email, Slack ID, role, SF role) without leaving the UI.
Logs
Tail and search LaunchAgent log output across all scheduled scripts.
2 System Architecture & Tech Stack
Tech Stack
| Layer | Technology |
| Scheduling | macOS LaunchAgents (12 plist templates in plists/) |
| Backend — Python | Python 3.13+, stdlib only (no Django/Flask) |
| Backend — Node.js | Node.js 18+, better-sqlite3, native http/https modules |
| Frontend | React 18, Vite, CSS custom properties |
| Local Databases | SQLite — steve.db (pipeline/opportunity), projects.db (PM) |
| AI — Transcripts | Google Gemini 2.0 Flash via Vertex AI Agent Engine (customer data boundary) |
| AI — Synthesis | Anthropic Claude API (claude-sonnet-4-6) for prep notes, profiles, chat |
| Slack | Slack Web API (bot token + user token); Canvas API |
| Google | Calendar v3, Drive v3, Vertex AI Agent Engine, BigQuery (bq CLI subprocess) |
| Jira / Confluence | Atlassian REST API v3 (Basic auth with API token) |
| Salesforce | REST API v59.0 via OAuth 2.0 client credentials flow |
| Gong | REST API (credentials provisioned, integration planned) |
Module Map
meetings-automation/
├── run.py Core transcript pipeline (30-min LaunchAgent)
├── morning-digest.py Daily briefing (8am LaunchAgent)
├── pre_meeting_agenda.py Pre-meeting Slack reminder (15 min before)
├── prep_one_on_one_canvases.py 1:1 canvas prep (5pm LaunchAgent)
├── canvas_manager.py Team context canvas management
├── slack_context.py Shared Slack library (tokens, roster, DM history)
├── slack_tunnel.py Lane↔STEVE relay (30s LaunchAgent)
├── poll_slack.py Inbound DM handler + approval workflow (15m LaunchAgent)
├── triage_agent.py Log triage → canvas (WatchPath trigger)
├── sfdc_client.py Salesforce REST client (OAuth client credentials)
├── sfdc_attachment.py SF Opportunity Team auto-attachment
├── pm_sync.py PM project → Jira/Confluence sync
│
├── opportunity_assist/ Opportunity compliance + pipeline analytics
│ ├── audit_runner.py Compliance audit orchestrator
│ ├── bq_client.py BigQuery access via bq CLI subprocess
│ ├── daily_data_service.py Daily BQ → SQLite ingestion
│ ├── db.py SQLite schema + read/write API
│ ├── slack_scanner.py Slack channel scanning for team activity
│ ├── team_matcher.py Member resolution: email/emoji/slack_id
│ └── ...
│
├── profiles/ AI summarization profile .md files
├── adk/ Vertex AI Agent Engine (Gemini ADK agent)
├── plists/ LaunchAgent plist templates (12 agents)
├── mcp/ MCP server for Claude Code integration
├── ui/ React + Node.js STEVE UI
│ ├── serve.js Node.js API server, Jira proxy, SQLite access
│ └── src/ React components (20+ files)
└── scripts/ install-launchagents.sh
Runtime Data Flows
Google Drive (Meet Recordings) → run.py → Vertex AI (Gemini) → Jira ticket + Slack DM
Google Calendar → morning-digest.py → Claude → Slack digest DM
Slack channels → opportunity_assist/ → BigQuery / Salesforce → SQLite + Canvas
STEVE UI (localhost:5173) → serve.js → Claude / Jira / Slack / Salesforce
Lane Slack DM → STEVE bot → poll_slack.py (approval) or slack_tunnel.py (direct)
3 STEVE Dataflow Architecture
3.1 — Core Transcript Pipeline (run.py)
flowchart TD
A([LaunchAgent fires\nevery 30 minutes]) --> B[Load config.json\n+ ~/.jira-token\n+ state.json]
B --> C{Network ready?\nwait_for_network}
C -- No --> Z1([Exit — launchd will retry])
C -- Yes --> D[Scan Google Drive\nMeet Recordings via Drive API]
D --> E{New .gdoc files\nsince lastRun?}
E -- No --> Z2([Exit — nothing to process])
E -- Yes --> F[For each new transcript]
F --> G[Export doc as plain text\nDrive API /export]
G --> H{Profile selection\nkeyword match on title}
H --> I[agent_discover_meetings\nVertex AI Agent Engine]
I -- Success --> J[Agent Engine: Gemini\nsummarizes transcript\nwith profile instructions]
I -- Failure --> K[direct_discover_meetings\nCalendar API fallback]
K --> J
J --> L[Parse json_meetings block\nextract attendees + actions]
L --> M{Dedup check:\nticket exists today?}
M -- Duplicate --> N[Update existing ticket\nor skip]
M -- New --> O[Create Jira call-log ticket\nADF description + labels]
O --> P[Resolve attendees\n→ Jira account IDs]
P --> Q[Set Relevant Party field\n+ Security Level]
Q --> R{1:1 detected?}
R -- Yes --> S[Set Jira security to\nRelevant Party Only]
R -- No --> T[Create subtasks\nfor each action item]
S --> T
T --> U[Post Slack notification\nwith ticket link]
U --> V[Update state.json\nmark file processed]
V --> F
3.2 — Morning Digest Pipeline (morning-digest.py)
flowchart TD
A([LaunchAgent fires\n8:00 AM weekdays]) --> B[Load config.json\nteam.json\nSlack tokens]
B --> C[Fetch today's calendar\nGoogle Calendar API]
C --> D[Filter: recurring + has attendees\nskip solo/cancelled/all-day]
D --> E[For each meeting]
E --> F{1:1 meeting?\n2 attendees}
F -- Yes --> G[Search for 1:1 canvas\nSlack search.files]
G --> H{Canvas found?}
H -- Yes --> I[Use canvas content\nskip channel scan]
H -- No --> J[Fetch DM history\nwith team member]
J --> K[Scan team channels\nfor person context]
K --> I
F -- No --> L[Scan team channels\nfor all attendees]
L --> I
I --> M[Fetch Steve Notes\nDM-to-self scan]
M --> N[Fetch open Jira items\nassigned to user]
N --> O[Fetch past meeting summaries]
O --> P{Pipeline data?\nSQLite steve.db}
P -- Yes --> Q[Include pipeline KPIs]
P -- No --> R[Skip pipeline section]
Q --> S[Claude API synthesis\ngenerate prep note]
R --> S
S --> T[Collect all meetings]
T --> U[Render full digest]
U --> V{Slack configured?}
V -- Yes --> W[Post digest to\nuser self-DM]
V -- No --> X[Write to log file]
3.3 — Opportunity Assist / Compliance Audit
flowchart TD
A([Triggered: UI button\nor LaunchAgent]) --> B[Load team.json\nfilter: direct_reports + self]
B --> C[Phase A: Formal requests\nScan #ask-solutions channel]
C --> D[For each message:\nparse SF opportunity URL]
D --> E{SF URL found?}
E -- Yes --> F[Resolve opportunity ID]
F --> G[Query BigQuery\nsfdc_opportunityteammember]
G --> H{Team member\non SF Opportunity Team?}
H -- Yes --> I[Mark COMPLIANT]
H -- No --> J[Mark CLAIMED_NOT_LOGGED]
E -- No --> K[Check emoji reactions\nmatch_emoji_to_member]
K --> L{Team member emoji?}
L -- Yes --> M[Mark INFORMAL_NOT_LOGGED]
L -- No --> N[Mark UNCLAIMED]
C --> O[Phase B: Channel activity scan]
O --> P[search_member_channels\nSlack search.messages API]
P --> Q[Find win-, int-, ext- channels]
Q --> R[Audit each customer channel]
R --> S[Cross-reference SF opportunity team]
I --> T[Consolidate → AuditLog]
J --> T
M --> T
N --> T
S --> T
T --> U[Persist to SQLite\naudit_runs table]
U --> V[Stream results to UI\nSSE progress events]
V --> W{Auto-attach enabled?}
W -- Yes --> X[sfdc_attachment.py:\nadd member to SF Opportunity Team]
W -- No --> Y[Show in UI for\nmanual review]
3.4 — 1:1 Canvas Preparation (prep_one_on_one_canvases.py)
flowchart TD
A([LaunchAgent fires\n5:00 PM weekdays]) --> B[Compute next business day]
B --> C[Fetch calendar\nGoogle Calendar API]
C --> D[Detect 1:1 meetings:\nrecurring + 2 attendees\n+ team member match]
D --> E[For each 1:1]
E --> F[Read Team Context Canvas\nvia canvas_manager]
F --> G[canvas_manager.read_person\nget person section]
G --> H[Fetch last 60 DMs\nbetween user and person]
H --> I[Scan team channels\nfor person context]
I --> J[Fetch Steve Notes\n@person forwards]
J --> K{Canvas already exists\nfor this date?}
K -- Yes --> L[Skip — no duplicate]
K -- No --> M[Claude API synthesis\nState of Mind, What's Live\nOpen Items, Agenda Hooks]
M --> N[canvases.create API\npost canvas to Slack]
N --> O[DM link to user\nself-DM channel]
O --> E
3.5 — UI Architecture (serve.js)
flowchart LR
Browser["Browser\nReact/Vite\nlocalhost:5173"] <-->|HTTP + SSE| Server["serve.js\nNode.js HTTP server\nlocalhost:5173"]
Server <-->|Basic Auth| Jira["Jira REST API v3\nfullstory.atlassian.net"]
Server <-->|Bearer token| Slack["Slack Web API\nslack.com/api/*"]
Server <-->|Bearer token| Anthropic["Anthropic Claude API\napi.anthropic.com"]
Server <-->|execFile python3| Python["Python scripts\nrun.py, morning-digest.py\nopportunity_assist/*"]
Server <-->|SQL| DB1["projects.db\nSQLite PM data"]
Server <-->|SQL| DB2["steve.db\nSQLite pipeline data"]
Python <-->|REST| GCP["Google APIs\nCalendar, Drive\nVertex AI Agent Engine"]
Python <-->|bq CLI| BQ["BigQuery\nfs-biz-intel\nSalesforce mirror tables"]
Python <-->|OAuth| SF["Salesforce REST\nfullstory.my.salesforce.com"]
3.6 — MCP Server — Claude Code Integration
mcp/steve-integrations/index.js is a standalone Node.js MCP server loaded by Claude Code CLI and desktop as a subprocess. It provides 20+ STEVE tools directly inside Claude Code conversations — distinct from the serve.js UI server.
| Category | Tools |
| Jira | create_jira_issue, get_jira_issue, search_jira, transition_jira_issue |
| Confluence | create_confluence_page, get_confluence_page, update_confluence_page |
| Slack | post_slack_message, read_channel, search_public, get_person_slack_context |
| PM Projects | charter_project, list_pm_projects, get_pm_project_detail, find_projects_for_attendees |
| Directory | lookup_employee, get_directory_changes |
| STEVE | get_whats_new, get_slack_thread |
⚠ Migration Note
The MCP server uses stdio transport and does
not read
config.json. All identity values (
JIRA_EMAIL,
JIRA_HOST, Confluence page IDs, Slack channel IDs) are hardcoded. Any team member running Claude Code with this MCP server is currently making API calls authenticated as
lane@fullstory.com.
3.7 — Inbound Slack Workflows
poll_slack.py — Two-Phase Approval (every 15 min)
flowchart TD
A([LaunchAgent fires\nevery 15 minutes]) --> B[Load bot + user tokens\nscan pending proposals from SQLite]
B --> C[Phase 1: Detect new\ninbound DMs to STEVE bot]
C --> D{New message?\nnot from Lane}
D -- Yes --> E[Claude API: generate\nproposed reply]
E --> F[Post proposal to Lane\nbot-DM with approve/reject prompt]
F --> G[Store proposal in SQLite]
D -- No --> H[Phase 2: Check\npending proposals]
G --> H
H --> I{Lane replied\nto proposal thread?}
I -- ok --> J[Send proposed reply\nto original sender]
I -- custom text --> K[Send Lane's text\nto original sender]
I -- no/dismiss --> L[Drop proposal\ndelete from SQLite]
I -- No reply yet --> M([Wait for next run])
slack_tunnel.py — Direct Relay (every 30 sec)
flowchart TD
A([LaunchAgent fires\nevery 30 seconds]) --> B[Load tunnel_state.json\nthread_ts + last_lane_ts]
B --> C{Active tunnel session?\nthread_ts set?}
C -- No --> Z([Exit])
C -- Yes --> D[Fetch thread replies\nuser token im:history]
D --> E{New Lane message\nsince last_lane_ts?}
E -- No --> Z2([Exit])
E -- Yes --> F[Build multi-turn context\nfrom thread history]
F --> G[Claude API call\nno approval gate]
G --> H[Post reply to thread\nbot token chat.postMessage]
H --> I[Update last_lane_ts\nin tunnel_state.json]
4 Fullstory IT Migration Guide
This section documents every change required to migrate STEVE from a single-user macOS localhost application to a centralized, company-wide hosted service.
4.1 — Architecture Gap Summary
| Concern | Current State | Required for Multi-Tenant |
| Scheduling | macOS LaunchAgents | Linux cron / Kubernetes CronJobs / Cloud Scheduler |
| Identity | Single user; hardcoded lane@fullstory.com | OAuth2/OIDC; per-user identity claims |
| Secrets | Local files (~/.jira-token, etc.) | HashiCorp Vault / GCP Secret Manager / AWS Secrets Manager |
| Databases | Per-machine SQLite | PostgreSQL / Cloud SQL (shared, multi-tenant) |
| File storage | Local ~/.meetings-automation/ | Cloud Storage bucket or shared NFS |
| Google Drive | Local Drive for Desktop mount | Direct Drive API calls using service account |
| Slack identity | Single bot + user token | Per-user Slack OAuth; shared bot token for workspace actions |
| BigQuery | Per-user bq CLI + gcloud ADC | Service account with BigQuery Data Viewer |
| Network | Zscaler SSL intercept workarounds | Direct egress — remove all SSL bypass code |
4.2 — Hardcoded Variables — Complete Inventory
Audit Note
An initial pass of this inventory (reading ~15 files) missed 10 values and undercounted file coverage for every value listed. A full harness sweep across 23+ files was required to produce the complete picture below.
4.2.1 — User Identity (Per-User Dynamic)
| Value | Occurrences | Override? | Fix |
lane@fullstory.com | 22 across 8+ files incl. mcp/index.js, pm_sync.py, confluence_updater.py, adk/agent.py (deployed) | Partial | Add loadConfig() to non-compliant files |
5cc358c66fbf5a10040d3b81 (Jira account ID) | run.py:145, morning-digest.py:80, config.template.json | Partial | Remove fallback constant; fail loudly if config absent |
U8M7CQ2GM (Slack user ID) | 12 across serve.js, poll_slack.py, slack_tunnel.py, triage_agent.py | None | Load from config.json user.slack_user_id |
D8M4563M2 (self-DM channel) | slack_context.py, serve.js ×3, triage_agent.py, mcp/index.js | Partial | Derive via conversations.open everywhere else |
D8LHB6W1X (Slackbot DM) | slack_context.py:39 | None | Derive dynamically or remove |
D0AN7PQQR1T (bot-app DM) | slack_tunnel.py:37 | None | Resolve via conversations.open at startup |
calendarId=lane@fullstory.com | adk/agent.py system prompt (deployed GCP artifact) | None | Pass per-request in agent payload; requires adk/deploy.py update |
| Team roster (8 members) | adk/agent.py system prompt (deployed GCP artifact) | None | Serialize from team.json per-request; requires redeployment |
GoogleDrive-lane@fullstory.com | run.py:64 (path fragment) | None | Derive from user.email or switch to API-only |
4.2.2 — Workspace / Org Identity (Per-Org Dynamic)
| Value | Occurrences | Override? | Fix |
T02FE3LFK (Slack workspace) | 17 across serve.js, morning-digest.py, poll_slack.py, slack_tunnel.py, triage_agent.py | None | Add to config.json as slack.workspace_id |
fullstory.atlassian.net | 71 occurrences across 6+ files | Partial | Add loadConfig() to serve.js and mcp/index.js |
C09K2QUJD60 (#ask-solutions) | audit_runner.py:35, mcp/index.js | None | Add to config.json as slack.ask_solutions_channel_id |
fullstory.my.salesforce.com | sfdc_client.py:28 | None | Add to config.json as salesforce.login_url |
4.2.3 — Confluence Page IDs (Entirely Absent from Prior Documentation)
| Value | Meaning | Files | Override? |
30769334 | SPECOPS Confluence homepage | serve.js, mcp/index.js, confluence_updater.py | None |
190251010 | PM master index page | serve.js ×3, mcp/index.js | None |
4.2.4 — GCP / Infrastructure (Environment-Driven)
| Value | Location | Override? | Fix |
fs-playpen | run.py, morning-digest.py | Yes | Already in config.json gemini.project |
fs-biz-intel | bq_client.py:12 | None | Add to config.json as gcp.bq_project |
3167755122029625344 (Agent Engine ID) | run.py:93 | None | Add to config.json as gcp.agent_engine_resource_id |
877462458422 (GCP project number) | run.py | Partial | Wire _load_config() to replace the constant |
4.3 — Infrastructure Changes Required
Scheduling: LaunchAgents → Cloud Scheduler
apiVersion: batch/v1
kind: CronJob
metadata:
name: steve-morning-digest
spec:
schedule: "0 8 * * 1-5" # 8 AM weekdays (per-user timezone required)
jobTemplate:
spec:
template:
spec:
containers:
- name: morning-digest
image: gcr.io/fs-specops/steve-python:latest
command: ["python3", "morning-digest.py"]
envFrom:
- secretRef:
name: steve-user-credentials
ADK Agent: System Prompt Contains Deployed Identity
Highest Severity Identity Binding
adk/agent.py is the source definition of a
deployed Vertex AI Agent Engine instance on GCP. The
calendarId=lane@fullstory.com and the full 8-person team roster are baked into the live system instructions. Changing either requires editing the file
and running
python adk/deploy.py update. Until that executes, the live agent calls Lane's calendar regardless of which user triggers summarization.
MCP Server: stdio vs. Hosted
Option A (fix for current per-machine model): Add loadConfig() at MCP server startup. Each employee's machine reads their own config. Fixes the active auth bug immediately.
Option B (multi-tenant): Replace stdio transport with a hosted HTTP MCP server with OAuth2. Users authenticate via Fullstory SSO; the server calls downstream APIs on their behalf. Correct long-term architecture, separate infrastructure project.
Secrets: Local Files → Vault
~/.jira-token → Vault: secret/steve/{user_id}/jira_token
~/.slack-bot-token → Vault: secret/steve/shared/slack_bot_token
~/.slack-user-token → Vault: secret/steve/{user_id}/slack_user_token
~/.anthropic-api-key → Vault: secret/steve/shared/anthropic_api_key
~/.sfdc-credentials.json → Vault: secret/steve/shared/salesforce_credentials
~/.gong-token → Vault: secret/steve/shared/gong_credentials
SSL Bypass: Conditionalize for Cloud
All Google API calls use ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE to bypass Zscaler TLS inspection on Fullstory laptops. A cloud-hosted instance must use standard TLS. Gate on STEVE_ENV=local environment variable.
grep -rn "CERT_NONE\|check_hostname = False" .
5 IT Permissions Matrix & Enterprise Hardening
5.1 — Per-User Permissions
Run make it-request to generate a pre-filled request with your name and email.
| Service | Permission | Scope / Role | Why |
| GCP | roles/serviceusage.serviceUsageConsumer | Project: fs-playpen | Enables Calendar API, Drive API, and direct Vertex AI Gemini calls |
| Jira | Existing org access | Project: STRAT | Must be a member of STRAT project |
| Jira | Security scheme access | Scheme: "Relevant Party Only" | Required for 1:1 tickets to be visible to creator |
| Slack | User OAuth token | channels:history, groups:history, im:history, im:write, files:read, search:read, canvases:write, users:read | Canvas creation, DM history, channel scanning |
| Gong | API Access Key + Secret | Gong admin generates | Transcript pull; currently blocked |
5.2 — Team-Wide Permissions (One-Time)
| Item | Action | Why |
| Jira STRAT | Enable "1:1 Participants Only" security scheme | Restricts 1:1 summaries to participants only |
| Jira STRAT | Enable "Relevant Party" field (customfield_11518) | Auto-populates ticket visibility |
| Vertex AI Agent Engine | Grant Vertex AI User role on project 877462458422 | Access to shared STEVE summarization agent |
| Salesforce | Connected App with client_credentials OAuth flow | Required for sfdc_client.py; see REVOPS-25107 |
| BigQuery | roles/bigquery.dataViewer on fs-biz-intel | Pipeline/opportunity analytics |
5.3 — Slack App Scopes
| Token | Scope | Used By | Why |
Bot Token
xoxb-* | chat:write | All scripts | Posting DMs and channel messages |
channels:history | slack_context.py | Reading context channels |
files:read | slack_context.py | Reading canvases |
users:read | slack_context.py | Resolving display names |
User Token
xoxp-* | channels:history, groups:history | slack_context.py | Channels where bot is not a member |
im:history | slack_context.py | DM history with team members |
im:write | serve.js | Opening DM channels programmatically |
search:read | slack_context.py, serve.js | Searching messages and files |
canvases:write | canvas_manager.py, serve.js | Creating and updating Slack canvases |
files:read | serve.js | Reading canvas content |
5.4 — Enterprise Hardening Requirements
Secrets Management
Replace Path.home() / ".token" patterns with a secrets.get(key) abstraction calling GCP Secret Manager or Vault. Rotate annually; rotate Slack + Jira tokens on offboarding.
RBAC
Users read/write own data only. Team leads read direct reports. Admins manage config. 1:1 tickets enforce Relevant Party Only at API level, not just Jira UI.
Data Compliance Boundary
Raw transcripts → Gemini only (enforced in code). Claude receives only Gemini-produced summaries. Must be enforced as a code review gate, not just documentation.
Network Boundaries
VPC egress-only to: slack.com, fullstory.atlassian.net, googleapis.com, aiplatform.googleapis.com, api.anthropic.com, fullstory.my.salesforce.com, bigquery.googleapis.com
Rate Limiting
Current: 30 Slack calls/60s per process. Multi-user multiplies volume — implement Redis-based distributed token bucket shared across all users.
Input Validation
BigQuery queries in bq_client.py use f-string interpolation with user-controlled data. Convert to parameterized queries before multi-tenant deployment.
6 Fullstory Employee Quickstart
Prerequisites
uname # → Darwin (macOS required)
brew --version # → Homebrew 4.x
python3 --version # → 3.13+ (brew install python@3.14)
node --version # → 18+ (brew install node)
gcloud --version # → google-cloud-sdk (brew install --cask google-cloud-sdk)
claude --version # → Claude Code CLI
Step 1 — Clone and Open with Claude Code
git clone git@github.com:fullstorydev/mn.git ~/Documents/mn
cd ~/Documents/mn/users/lane2day/meetings-automation
claude # reads CLAUDE.md automatically — fully context-aware
Step 2 — Let Claude Code Run Setup
Claude Code Quickstart
Once Claude Code is open in the directory, say:
"Run make setup for me and guide me through each step". Claude Code knows the full STEVE codebase and will walk you through every credential, run
make test, and generate your IT permissions request.
Step 3 — Manual Setup (if preferred)
make setup # interactive wizard: profile → credentials → install → UI
make test # validate all API connections
make it-request # print IT permissions request (copy into a ticket)
make steve # open STEVE UI at http://localhost:5173
Step 4 — Configure Your Team Roster
{
"team": [
{
"name": "Full Name",
"email": "name@fullstory.com",
"slack_id": "UXXXXXXXXXX",
"slack_handle": "firstname",
"role": "direct_report",
"relationship": "direct_report",
"aliases": ["firstname@fullstory.com"]
}
],
"context_channels": [
{ "id": "CXXXXXXXXXX", "name": "your-team-channel" }
],
"steve_dm_channel_id": "DXXXXXXXXXX"
}
Claude Code — Day-to-Day Prompts
What did the morning digest pick up today?
The canvas for my 1:1 didn't get created — what went wrong?
Add Jordan Smith (jordan@fullstory.com, slack ID U123456789) as a direct report
Generate a pre-meeting agenda for my call with the Acme Corp team tomorrow
Run the opportunity audit for the last 30 days and show me what's unclaimed
7 Configuration Reference
{
"user": {
"name": "Your Full Name",
"email": "you@fullstory.com",
"jira_account_id": "...", // from /rest/api/3/myself
"slack_user_id": "UXXXXXXXXXX",
"calendar_id": "primary",
"role": "manager" // manager|se|mobile-se|web-se|data-specialist
},
"gemini": {
"project": "fs-playpen",
"region": "us-central1",
"model": "gemini-2.0-flash"
},
"jira": {
"base_url": "https://fullstory.atlassian.net",
"project": "STRAT",
"team_lead_account_id": "your-jira-account-id"
},
"gcp": {
"quota_project": "fs-playpen",
"agent_engine_project": "project-364429034474444891",
"agent_engine_number": "877462458422"
}
}
8 Credentials Reference
| File | Purpose | How to Obtain |
~/.jira-token | Jira API token | id.atlassian.com → Security → API tokens |
~/.slack-bot-token | Slack bot token (line 1) + Slack user ID (line 2) | Slack app → OAuth & Permissions → install |
~/.slack-user-token | Slack user OAuth token | Slack app → user token scopes → install |
~/.slack-webhook-url | Slack incoming webhook (simpler alternative) | Slack app → Incoming Webhooks |
~/.anthropic-api-key | Anthropic Claude API key | console.anthropic.com/account/keys |
~/.sfdc-credentials.json | Salesforce Connected App OAuth | IT / RevOps: see REVOPS-25107 |
~/.gong-token | Gong API Key (line 1) + Secret (line 2) | Gong admin request |
| gcloud ADC | Google Calendar, Drive, Vertex AI | gcloud auth login --enable-gdrive-access --update-adc |
9 Commands Reference
make setup # First-time interactive setup wizard
make configure # Update profile (name, email, IDs)
make credentials # Update API tokens only
make install # Re-install scripts + LaunchAgents after code updates
make venv # Create/update Python venv and install dependencies
make test # Test all API connections + show LaunchAgent status
make it-request # Print IT permissions request
make status # Installation status + last run info + recent logs
make steve # Open STEVE UI at http://localhost:5173
make ui-dev # Start UI in hot-reload dev mode
make slack-setup # Configure Slack (guided walkthrough)
make slack-test # Send a test Slack notification
make push # Ship STEVE changes via auto-merge PR
make pull # Sync worktree with latest master
make uninstall # Remove LaunchAgents + scripts (keeps credentials)
10 Profile System
Summarization profiles are markdown files in profiles/ with YAML frontmatter. They define AI instructions for specific meeting types. Profile selection uses keyword matching on the meeting title.
Profile Dimensions
| Dimension | Options | Effect |
delivery | internal / external | External = customer-safe language, no internal strategy leaks |
audience | 1:1 / group | 1:1 = coaching/career lens; group = decisions/alignment lens |
org_focus | specops / services-solutions / product / sales | Shapes which stakeholders and outcomes are emphasized |
Included Profiles
| Profile | Keywords | Type | Delivery | Audience |
1-to-1-weekly.md | 1:1, one-on-one | transcript | internal | 1:1 |
se-round-table.md | round table, se round | transcript | internal | group |
strategic-solutions-weekly-sync.md | specops weekly, team sync | transcript | internal | group |
services-solutions-wbr.md | wbr, weekly business review | transcript | internal | group |
external-sales-discovery.md | discovery, first touch | transcript | external | group |
external-customer-deal-work.md | (default) | transcript | external | group |
lees-eow.md | eow, end of week | synthesis | internal | group |
11 Troubleshooting
| Symptom | Cause | Fix |
make test shows ❌ on Google Calendar or Vertex AI | IT hasn't granted serviceUsageConsumer on fs-playpen | Run make it-request, submit IT ticket. STEVE still works via Agent Engine fallback. |
| LaunchAgents not running | Scripts not registered | make install → make test. Check ~/.meetings-automation/logs/ |
| "No transcript available" on every ticket | Drive API returning 403 | IT blocker. STEVE creates placeholder ticket; fills when transcript available. Check logs. |
| Duplicate Jira tickets | Wrong jira_account_id in config | Verify config.json user.jira_account_id matches your Jira account ID from /rest/api/3/myself |
| Canvas URLs broken | Wrong Slack workspace ID | T02FE3LFK is hardcoded — currently requires a code change (see §4.2.2) |
| Slack "not_in_channel" errors | Bot not in channel | Invite STEVE bot: /invite @STEVE, or use user token (default for most calls) |
| BigQuery data not loading | gcloud ADC lacks BQ access | bq query --project_id=fs-biz-intel --use_legacy_sql=false 'SELECT 1' |
| Salesforce auth failing | Credentials file missing or Connected App not configured | Check ~/.sfdc-credentials.json. Connected App needs client_credentials flow — see REVOPS-25107. |
| STEVE UI won't open | Server stuck or npm missing | make ui-stop && make steve. If npm error: cd ui && npm install |
✦ Data Compliance Summary
| Data Type | LLM Allowed | Stored Where |
| Raw meeting transcripts | Gemini only (Vertex AI Agent Engine) | Google Drive (Google's custody) |
| Gemini-produced summaries | Claude OK | Jira ticket descriptions |
| Pre-meeting prep notes | Claude OK | Slack canvas / DM |
| Team context (DM excerpts) | Claude OK | Slack canvas (ephemeral) |
| Pipeline / deal data | Claude OK | SQLite steve.db (local) |
| Customer names / Salesforce data | Claude OK (no raw PII) | SQLite + Jira labels |