diff --git a/skills/deploy/SKILL.md b/skills/deploy/SKILL.md index 62326db..f81e225 100644 --- a/skills/deploy/SKILL.md +++ b/skills/deploy/SKILL.md @@ -1,28 +1,603 @@ -# deploy +--- +name: deploy +description: > + Activates approved rules from the Tracker in Gmail. Use when the user says "deploy the + rules", "turn on the rules", "make the rules live", "activate my Gmail rules", "set up the + filters", or "I'm ready to go live". Implements gmail_filter_safe rules via Google Workspace + MCP directly, walks the user through installing apps_script_needed rules via Google Apps + Script with a step-by-step walkthrough, and parks studio_candidate rules for future use. + Also creates the "! Rule Needed" label on first deploy. Runs after import approves rules. + Part of the Gmail Inbox Architect plugin. +--- -## Trigger -Use this skill when approved rules are ready to be implemented in Gmail, the user says -"deploy the rules", "create the filters", "let's push this batch", or a specific rule -set has been approved through the `import` review process. +# deploy — Gmail Inbox Architect -## Purpose -Implements approved rules from the Tracker into Gmail. This means: -- Gmail filters via Google Workspace MCP (Claude executes directly) -- Apps Script deployment via step-by-step user walkthrough (Claude guides, user executes) +## What This Skill Does -## Non-Technical User Standard -This skill must never assume the user knows what a Gmail filter is, what Apps Script is, -or how to navigate Google's admin interfaces. Every instruction must be written in plain -English with numbered steps, screenshots described in words, and explicit "click X, then Y" -guidance. +Takes approved rules from the Tracker and makes them live. There are three deployment +paths depending on the rule type: -## Bulk Operation Rule (North Star) -For operations involving hundreds or thousands of existing emails (mass label changes, -backfills, restructuring), Claude must: -1. Recommend the lowest-cost path first (Gmail UI, a pre-built script, or a task order - to a lower-cost agent) -2. Guide the user through that path OR produce a task order that handles it -3. Never burn Claude tokens on mechanical, row-by-row email operations +- **Gmail sorting rules** (`gmail_filter_safe`) — Claude creates these directly via its + Gmail connection. Fast, no user action required beyond confirming. +- **Automation scripts** (`apps_script_needed`) — Claude writes a script, walks the user + through installing it in Google Apps Script, guides the test run, and reviews the logs. +- **Studio candidates** (`studio_candidate`) — acknowledged and parked. No deployment + until Studio support is added to this plugin. -## Skill Stub — Implementation Pending -Full SKILL.md to be written in build session. Load CW-020_Concept.md for scope and design intent. +Nothing goes live without explicit user confirmation. All deployed rules are marked active +in the Tracker after successful deployment. + +This skill is entirely self-contained. All templates, script patterns, and walkthrough +steps are embedded below. + +--- + +## When This Skill Is Triggered + +- User says: "deploy the rules", "turn on the rules", "set up the filters", + "let's push this batch", "go live", "activate the rules" +- Import skill directs the user here after the review queue is complete +- User says: "the script is ready to deploy", "install the automation" + +--- + +## Prerequisites + +Before deploying, verify: +- `_status.md` exists with Phase = "Rule Build" or "Deploy-ready" +- Tracker exists with at least one rule where `enabled = FALSE` and `deployability` is set +- Bible.md exists (needed to confirm label taxonomy before creating filters) + +Read the `apps_script_log.md` file from the project folder if it exists. This file tracks +previous script deployments and known issues for this project. Reference it before writing +any new Apps Script code to avoid repeating past mistakes. + +--- + +## Dependencies + +- **Google Workspace MCP** — for Gmail filter creation, label ID lookup, and Gmail search + during post-deploy verification +- **No other external dependencies** — Apps Script code is generated inline + +--- + +## Execution Flow + +``` +Phase 1 → Sanity check and queue load (what's ready to deploy) +Phase 2 → Deploy gmail_filter_safe rules (Claude executes via Gmail connection) +Phase 3 → Generate and deploy apps_script_needed rules (walkthrough) +Phase 4 → Acknowledge studio_candidate rules (park, no action) +Phase 5 → Update Tracker (mark deployed rules as enabled=TRUE) +Phase 6 → Update _status.md and close out +``` + +Phases 2, 3, and 4 are independent — run whichever apply based on what's in the queue. +It is valid to do Phase 2 only (no Apps Script rules in this batch), or Phase 3 only. + +--- + +## Phase 1 — Sanity Check and Queue Load + +Read `_status.md` and the Tracker. + +From the Tracker, load all rules where `enabled = FALSE`. Group by deployability: + +- `gmail_filter_safe` → Phase 2 queue +- `apps_script_needed` → Phase 3 queue +- `studio_candidate` → Phase 4 queue + +Also load any rules where `enabled = FALSE` and `deployability` is blank — treat these +as needing a deployability decision. Ask Claude which path they belong on before proceeding. + +Present the deployment plan: +> "Here's what's ready to go live: +> +> [N] sorting rules — I can create these directly in Gmail right now. +> [N] automation script rules — these need a small script installed in Google Apps Script. +> [N] parked rules — saved for a future step. +> +> Want to start with the sorting rules? That's the fastest part." + +--- + +## Phase 2 — Deploy Gmail Filter Rules + +### Step 2.1 — Present for final confirmation + +Before creating any filters, show a clean summary. Group rules by target label. + +> "Here are the [N] sorting rules I'm about to create. Take a look before I turn them on: +> +> **Finance/AMEX** — email from americanexpress.com with 'statement' in the subject +> **Vendors/Shipping** — email from ups.com, fedex.com — automatically filed, marked as read +> **Clients/Transit** — any email where [client domain] is sender or recipient +> [... etc] +> +> Once I create these, new email will start sorting automatically. Ready?" + +Wait for explicit confirmation. Do not proceed without it. + +### Step 2.2 — Look up label IDs + +Call `list_gmail_labels` to get the Gmail label ID for each target label. +Gmail filters require label IDs, not label names. + +Map each `base_label` value in the queue to its corresponding label ID. + +If a label is missing (not found in Gmail): +> "I can't find the label '[label name]' in Gmail — it may not have been created yet. +> Do you want me to create it now, or skip this rule for now?" + +Wait for answer before proceeding. + +### Step 2.3 — Create filters + +For each gmail_filter_safe rule, call `manage_gmail_filter` with: + +**Criteria** (build from Tracker fields): +- `from_domain`: build as `from:domain.com` — multiple domains use OR logic: + `{from:domain1.com from:domain2.com}` +- `to_or_cc_domain` (participant pattern): combine as: + `{from:domain.com to:domain.com}` +- `subject_contains`: `subject:keyword` — multiple keywords joined with OR or AND + based on the rule's intent (default OR unless notes specify otherwise) +- `has_attachment`: `has:attachment` +- Combined signals: join with spaces (AND logic in Gmail search) + +**Actions** (from Tracker fields): +- `addLabelIds`: the label ID from Step 2.2 +- `markAsRead`: TRUE if `mark_read = TRUE` +- Skip inbox (archive): set `removeLabelIds: ["INBOX"]` if `archive = TRUE` + +Create one filter per rule. Do not batch multiple rules into one filter — they are +harder to manage and delete individually. + +After each successful creation, note it. If a creation fails, log the error and continue +with the remaining rules — do not abort the entire batch. + +### Step 2.4 — Post-deploy verification + +After all filters are created, run a quick spot-check. For 2-3 of the higher-volume rules, +call `search_gmail_messages` using the same query and confirm Gmail returns the expected +threads. This validates the filter syntax fired correctly. + +Report back: +> "Done — [N] sorting rules are now active in Gmail. New email matching these patterns +> will be sorted automatically from this point forward. +> +> [If any failed:] [N] rules couldn't be created — [plain English reason]. Want to +> troubleshoot those now or skip them?" + +--- + +## Phase 3 — Apps Script Deployment + +Apps Script handles rules that Gmail's native filters can't: attachment filename patterns, +body text signals, multi-condition logic. + +### Step 3.1 — Read the issue log + +Before writing any code, check for `apps_script_log.md` in the project Drive folder. +If it exists, read it. Note any previous errors or patterns to avoid. + +### Step 3.2 — Generate the script + +Write a complete, runnable Google Apps Script file from the apps_script_needed rules +in the Tracker. The script must be syntactically correct, well-commented, and ready +to paste without modification. + +**Script architecture:** + +```javascript +/** + * Gmail Classification Script + * Project: Gmail Inbox Architect — [account] + * Generated: [date] + * Rules covered: [comma-separated list of rule_ids] + * + * BEFORE RUNNING: + * 1. Set DRY_RUN = true for your first test run + * 2. Review the execution log after the test + * 3. Set DRY_RUN = false only after confirming the test looks correct + * + * Issue log: apps_script_log.md in your project Drive folder + */ + +// ─── CONFIGURATION ──────────────────────────────────────────────────────────── +const DRY_RUN = true; // true = log only, no changes. Set false after testing. +const MAX_THREADS = 50; // Threads to process per rule per run (raise if needed) +const LOOKBACK_HOURS = 2; // Only check email newer than this many hours +// ────────────────────────────────────────────────────────────────────────────── + +function runClassification() { + const startTime = new Date(); + Logger.log('=== Gmail Classification Run ==='); + Logger.log('Mode: ' + (DRY_RUN ? 'DRY RUN (no changes)' : 'LIVE')); + Logger.log('Started: ' + startTime.toISOString()); + + const rules = buildRules(); + let totalLabeled = 0; + const errors = []; + + for (const rule of rules) { + try { + const count = applyRule(rule); + totalLabeled += count; + if (count > 0) { + Logger.log('[' + rule.id + '] Applied to ' + count + ' thread(s)'); + } + } catch (e) { + const msg = '[' + rule.id + '] ERROR: ' + e.message; + Logger.log(msg); + errors.push(msg); + } + } + + Logger.log('---'); + Logger.log('Total threads processed: ' + totalLabeled); + Logger.log('Errors: ' + errors.length); + Logger.log('Finished: ' + new Date().toISOString()); + + if (errors.length > 0) { + Logger.log('ERROR DETAILS:'); + errors.forEach(e => Logger.log(e)); + } +} + +function applyRule(rule) { + // Build time-bounded query so we only process recent email + const cutoff = new Date(Date.now() - LOOKBACK_HOURS * 60 * 60 * 1000); + const after = Utilities.formatDate(cutoff, 'UTC', 'yyyy/MM/dd'); + const query = rule.query + ' after:' + after; + + const threads = GmailApp.search(query, 0, MAX_THREADS); + if (threads.length === 0) return 0; + + const label = rule.labelName + ? GmailApp.getUserLabelByName(rule.labelName) + : null; + + if (rule.labelName && !label) { + throw new Error('Label not found: ' + rule.labelName + + ' — create this label in Gmail first'); + } + + let count = 0; + for (const thread of threads) { + const subject = thread.getFirstMessageSubject(); + + if (DRY_RUN) { + Logger.log(' [DRY RUN] Would label "' + subject + '" → ' + rule.labelName); + } else { + if (label) label.addToThread(thread); + if (rule.archive) thread.moveToArchive(); + if (rule.markRead) thread.markRead(); + Logger.log(' Labeled: "' + subject + '" → ' + rule.labelName); + } + count++; + } + return count; +} + +function buildRules() { + // Each entry below is one rule from the Tracker. + // query: Gmail search query that identifies matching threads + // labelName: exact Gmail label path (must exist in Gmail already) + // archive: move out of inbox after labeling + // markRead: mark as read after labeling (use sparingly) + return [ + [RULES_ARRAY — generated per rule below] + ]; +} +``` + +**Query building per rule type:** + +For each `apps_script_needed` rule in the Tracker, build a `query` string: + +- `from_domain` only: `"from:domain.com"` +- `from_domain` + `subject_contains`: `"from:domain.com subject:keyword"` +- `has_attachment` = TRUE: append `"has:attachment"` +- `body_signal` (body text): use bare keyword (Gmail search searches body by default): + `"from:domain.com invoice"` — note this in the rule's comment +- Attachment filename pattern: Gmail search does not support filename regex natively; + use `"has:attachment from:domain.com"` as the filter and note in comments that + the Apps Script `getAttachments()` check refines this. Add attachment name check + inline using `thread.getMessages()[0].getAttachments()` pattern match if the rule + requires filename specificity. + +**Attachment filename check pattern (embed when needed):** + +```javascript +// Check attachment filename pattern +const messages = thread.getMessages(); +let hasMatchingAttachment = false; +for (const msg of messages) { + for (const att of msg.getAttachments()) { + if (/\.(stp|step)$/i.test(att.getName())) { + hasMatchingAttachment = true; + break; + } + } + if (hasMatchingAttachment) break; +} +if (!hasMatchingAttachment) continue; // skip thread +``` + +**After writing the script:** + +Show it to Claude for review before presenting to the user: +> "Here's the script I've written. It covers [N] rules. Take a look — I'll walk you +> through installing it once you're ready." + +Then present the script to the user in a code block they can copy. + +### Step 3.3 — Installation walkthrough + +Walk the user through step by step. Use numbered steps, plain English. + +> "Here's how to install this. It sounds more technical than it is — just follow +> the steps and let me know if anything looks different from what I describe. +> +> **Step 1 — Open Google Apps Script** +> Go to script.google.com. You may need to sign in with your Google account. +> +> **Step 2 — Create a new project** +> Click the blue '+ New project' button in the top left. +> You'll see a page with some starter code already in it. +> +> **Step 3 — Replace the existing code** +> Click anywhere in the code editor. Press Ctrl+A (Windows) or Cmd+A (Mac) to +> select everything. Then delete it. Paste the script I gave you above. +> +> **Step 4 — Name the project** +> Click 'Untitled project' at the top left and rename it to: +> 'Gmail Classifier — [account]' +> Then press Enter. +> +> **Step 5 — Save** +> Press Ctrl+S (Windows) or Cmd+S (Mac). You should see the save icon stop spinning. +> +> Let me know when you've done these steps and I'll walk you through the test run." + +Wait for confirmation before continuing. + +### Step 3.4 — Test run walkthrough + +> "Now let's test it. The script is set to 'dry run' mode right now — it will tell +> you what it *would* do without actually changing anything. +> +> **Step 6 — Run the test** +> At the top of the script editor, you'll see a dropdown that says 'runClassification'. +> Make sure that's selected, then click the triangle (▶) Run button. +> +> **Step 7 — Authorize the script** +> A popup will appear asking for permission. Click 'Review permissions', choose your +> Google account, then click 'Allow'. This lets the script read your Gmail. +> (This only happens the first time.) +> +> **Step 8 — View the logs** +> After the run finishes, click 'Execution log' at the bottom of the screen. +> You'll see a list of what the script found. +> +> Copy everything from the execution log and paste it back to me." + +Wait for the user to paste the logs. + +### Step 3.5 — Review the test logs + +Read the pasted execution log. Check for: + +- **Errors**: any `[ERROR]` lines → diagnose and fix the script +- **Unexpected matches**: threads labeled that shouldn't be → tighten the query +- **Missing matches**: expected threads not found → check the query or LOOKBACK_HOURS +- **Label not found errors**: the Gmail label doesn't exist yet → create it first + +If the log looks clean: +> "The test looks good — the script found [N] threads that match the rules, and nothing +> looks out of place. Ready to go live? +> +> Here's what you need to change: open the script, find the line that says +> `const DRY_RUN = true;` near the top, and change `true` to `false`. Save it. +> That's the only change." + +If there are issues, fix the script, show the corrected version, and repeat from Step 3.4. + +### Step 3.6 — Set up the trigger + +After the user confirms DRY_RUN = false: + +> "Last step — let's set it to run automatically. +> +> **Step 9 — Open triggers** +> In the script editor, click the clock icon on the left sidebar (it looks like a +> stopwatch). Or go to Extensions → Apps Script (if you're in Google Sheets) → Triggers. +> +> Actually, since you're already in the editor: look for 'Triggers' in the left sidebar. +> It's the icon that looks like an alarm clock. Click it. +> +> **Step 10 — Add a trigger** +> Click '+ Add Trigger' in the bottom right. +> +> Set these options: +> - Function to run: runClassification +> - Deployment: Head +> - Event source: Time-driven +> - Type of time-based trigger: Hour timer +> - Every: [1 hour is a good starting point — suggest 15 minutes if the user +> wants near-real-time; suggest 6 hours if they prefer low noise] +> +> Click Save. +> +> The script will now run automatically on that schedule." + +### Step 3.7 — Escalation path + +At any point in the walkthrough where the user is uncertain or stuck: + +> "If any of this feels unclear or you want someone to help install it, this is a +> great time to loop in one of your team's AI experts or technical contacts. +> The script and all these instructions are saved in your project Drive folder — +> they'll have everything they need to get it set up." + +Log this suggestion in `apps_script_log.md` as a note if the user escalates. + +--- + +## Phase 4 — Studio Candidates + +After handling the two active deployment paths, acknowledge parked rules: + +> "[N] rules are saved for a future step — these are cases where the pattern is +> too fuzzy for the current tools to handle reliably. They're in your Tracker and +> won't be lost. When we add Studio support to this system, we'll come back to them." + +No action required. These remain in the Tracker with `enabled = FALSE` and +`deployability = studio_candidate`. + +--- + +## Phase 5 — Update Tracker + +After successful deployment, update each deployed rule in the Tracker: +- Set `enabled = TRUE` +- Append to `notes`: "Deployed [date] via [gmail_filter / apps_script v[N]]" + +For rules that failed deployment: +- Leave `enabled = FALSE` +- Append to `notes`: "Deploy attempted [date] — failed: [reason]" + +For Apps Script rules deployed via script: update all rules covered by the script +to `enabled = TRUE` together, since they go live as a bundle when DRY_RUN is set false. + +--- + +## Phase 6 — Update apps_script_log.md + +After any Apps Script deployment (successful or not), update or create +`apps_script_log.md` in the project Drive folder. + +**File format:** + +```markdown +# Apps Script Log +## Project: [Gmail account] +## Purpose: Read this file before writing any new Apps Script for this project. +## Reference it to avoid repeating past mistakes. + +--- + +## Script v[N] — [date] + +**Rules covered:** [comma-separated rule_ids] +**Status:** [Testing / Active / Retired] +**DRY_RUN disabled:** [date] +**Trigger:** Every [N] hours + +### Known issues +[List any errors encountered during testing or after going live] + +### Resolutions +[What was changed to fix each issue] + +### Notes +[Anything the next session should know before touching this script] +``` + +If the file already exists, append a new version block. Do not overwrite old entries — +the history is the point. + +--- + +## Phase 7 — Update _status.md and Close Out + +Update `_status.md`: + +``` +Last Updated: [timestamp] +Last Agent: Claude (CoWork) — deploy skill +Phase: [Deploy / Maintenance — depending on whether all rules are now live] +Last Completed Step: [N] gmail_filter_safe rules created. [N] apps_script_needed rules + deployed via Apps Script v[N]. [N] studio_candidate rules parked. +Pending Work: [NONE / or: [N] rules failed deployment — see notes] +Handoff Target: NONE +Notes: [Apps Script log updated. Tracker updated. Any escalations noted.] +``` + +Close out with the user: + +**If everything deployed successfully:** +> "You're live. Here's what's now running: +> +> ✅ [N] sorting rules active in Gmail — new email will be sorted automatically +> ✅ [N] automation script rules running every [N] hours — checking for matching patterns +> 🅿️ [N] rules parked for future Studio support +> +> If you notice anything sorting incorrectly, say 'review a rule' and I'll help +> adjust it. If email stops sorting the way you expect, say 'check the script' and +> I'll look at the logs." + +**If some rules failed:** +> "Most rules are live. [N] couldn't be deployed right now — [plain English summary]. +> Want to troubleshoot those now or come back to them later?" + +--- + +## Handling Bulk Backfill Requests + +If the user asks to apply labels to thousands of existing emails (not just new ones +going forward), this is a separate operation from filter deployment. Apply the North +Star rule: + +1. **Lowest-cost path first.** If the volume is manageable (under 500 threads per label), + Claude can run the Apps Script with a larger `MAX_THREADS` value and a wider lookback + window. Explain this option. + +2. **For large volumes** (thousands of threads), produce a task order to a lower-cost + agent. Do not burn Claude tokens on mechanical row-by-row operations. + +3. **Never auto-backfill.** Always confirm with the user before bulk-applying labels + to existing email. + +--- + +## Non-Technical User Language Rules + +Never use: filter, API, MCP, schema, regex, Apps Script (except when naming the product +explicitly), boolean, deploy, trigger (use "schedule" instead) + +Substitute: +- "Gmail filter" → "sorting rule" +- "Apps Script" → "automation script" (or "Google's automation tool" on first mention) +- "trigger" → "schedule" or "automatic schedule" +- "deploy" → "turn on" or "go live" +- "boolean" → "yes/no" +- "query" → "search pattern" + +Number every step in the walkthrough. Confirm before proceeding to the next major step. + +--- + +## Safety Rules (Hardcoded — Cannot Be Overridden) + +1. **Explicit confirmation before any Gmail mutation.** Present the full list of rules + to be deployed and wait for a clear "yes" before creating any filter or running + any live script. + +2. **DRY_RUN = true in every generated script.** Never generate a script with + DRY_RUN = false. The user sets this manually after reviewing test logs. + +3. **No filter creation without a valid label ID.** If a label doesn't exist in Gmail, + stop and ask. Do not create filters pointing to non-existent labels. + +4. **No bulk backfill without explicit request and confirmation.** Filters and scripts + apply to new email only by default. Backfilling existing email requires a separate + explicit decision. + +5. **Tracker updated after deployment.** Every deployed rule must be marked enabled=TRUE. + Every failed rule must have its failure noted. No silent state drift. + +6. **apps_script_log.md always updated.** Never complete an Apps Script deployment + without updating the log file. The log is how future sessions avoid repeating mistakes.