betterfiles-autolabel
Skill: betterfiles-autolabel Version: 0.1.0 (Phase 1B — taxonomy locked, implementation pending) Inbox: teamsteph@betterfiles.com Tokens: /home/opsadmin/.openclaw/workspace-betterfiles/gmail-tokens.json Audit table: Supabase CCP (svueekfvfrvhylxygktb) → gmail_label_audit Systemd timer: betterfiles-autolabel-sweep.{service,timer} (Phase 4) Plan: /home/opsadmin/.claude/plans/betterfiles-autolabel-skill-2026-04-27.md
Trigger keywords
- “autolabel emails”
- “label teamsteph”
- “gmail label sweep”
- “betterfiles inbox”
- “teamsteph label”
CLI usage (Phase 2+)
# Preview what labels would be applied to one thread
node bin/bf-autolabel preview <gmail-thread-id>
# Classify all INBOX messages from last N minutes (shadow = no writes)
node bin/bf-autolabel sweep-once --since=15m --shadow
node bin/bf-autolabel sweep-once --since=15m --apply
# One-shot backfill of 201+ unread INBOX messages
node bin/bf-autolabel backfill --scope=inbox --dry-run
node bin/bf-autolabel backfill --scope=inbox --apply
# 90-day scope (Phase 3, after INBOX clean)
node bin/bf-autolabel backfill --scope=90d --dry-run
# Re-derive taxonomy from live HubSpot pipeline 816046
node bin/bf-autolabel taxonomy-rebuild
# Tail the audit log (last 50 ops)
node bin/bf-autolabel audit-tail
# Roll back all operations since timestamp
node bin/bf-autolabel rollback --since=2026-04-27T00:00:00ZShared libs (adopt, do not duplicate)
// Gmail API client — atomic token refresh, rate limiting, batchModify
const { GmailClient } = require('/home/opsadmin/.openclaw/workspace/scripts/lib/gmail-client');
// 4-tier address matcher — use prefix:'BetterFiles/' for new taxonomy
const { GmailLabelMatcher } = require('/home/opsadmin/.openclaw/workspace/scripts/lib/gmail-label-matcher');GmailClient constructor for this skill:
new GmailClient({
tokensPath: '/home/opsadmin/.openclaw/workspace-betterfiles/gmail-tokens.json'
})GmailLabelMatcher for new taxonomy (sweep + Push):
new GmailLabelMatcher(labels, { prefix: 'BetterFiles/' })GmailLabelMatcher for backfill (matching old labels to new):
new GmailLabelMatcher(labels, { prefix: '' }) // all user labelsClassifier flow
Thread received
│
▼
classifier-rules.js (deterministic, free)
│ confidence >= 0.75?
├─ YES → apply label + log action='rule'
│
└─ NO (ambiguous)
│
▼
classifier-llm.js (Haiku 4.5 via Portkey 127.0.0.1:18900)
│ confidence >= 0.6?
├─ YES → apply label + log action='llm'
│
└─ NO → apply BetterFiles/Review + log action='review'
Portkey auth: Authorization: Bearer $PORTKEY_API_KEY (from master.env).
Proxy injects x-portkey-config — do NOT add x-portkey-virtual-key directly.
Invariants (enforced in code, not just policy)
- NEVER modify or delete any label matching
/^Hiver-/i - NEVER remove a label that was not created by this skill (check audit_log for
source='betterfiles-autolabel') - NEVER write to gmail-tokens.json outside the GmailClient.refreshToken() path
- NEVER modify messages in SENT, TRASH, or SPAM
- ALWAYS write gmail_label_audit row BEFORE applying the Gmail label (fail-safe: if audit write fails, skip Gmail op)
- Cost ceiling: if LLM daily cost > $5, switch to rules-only mode and alert Discord ops
Label taxonomy (locked 2026-04-27, derived from live HubSpot pipeline 816046)
Stage labels — BetterFiles/Stage/
| Gmail label path | HubSpot stage ID | Display order |
|---|---|---|
| BetterFiles/Stage/00-New-Property | 1006885379 | 0 |
| BetterFiles/Stage/01-Due-Diligence | 1170866446 | 1 |
| BetterFiles/Stage/02-New-Property-InvestorLift | 1307960614 | 2 |
| BetterFiles/Stage/03-Send-Out-Prop | 1006885380 | 3 |
| BetterFiles/Stage/04-Property-Sent-Out | 1117598129 | 4 |
| BetterFiles/Stage/05-Previewing | 816047 | 5 |
| BetterFiles/Stage/06-EX-Make-Offer | 816048 | 6 |
| BetterFiles/Stage/07-EX-Follow-Up-on-Offer | 1028553805 | 7 |
| BetterFiles/Stage/08-Negotiating-Price-Reduction | 1333137720 | 8 |
| BetterFiles/Stage/09-Follow-Up-w-CH-Skip-Trace-Seller | 1191618358 | 9 |
| BetterFiles/Stage/10-Need-to-Send-Follow-Up | 1328863392 | 10 |
| BetterFiles/Stage/11-Finding-a-Buyer | 1014571368 | 11 |
| BetterFiles/Stage/12-Buyer-Showing | 1018636841 | 12 |
| BetterFiles/Stage/13-Review-Offers | 1029060732 | 13 |
| BetterFiles/Stage/14-Sold-FU-to-see-if-Buyer-Performing | 1032113198 | 14 |
| BetterFiles/Stage/15-Waiting-On-Wholesaler-Contract | 816049 | 15 |
| BetterFiles/Stage/16-Send-Contract-To-Buyer | 1006965582 | 16 |
| BetterFiles/Stage/17-Contract-Sent-to-Buyer | 816050 | 17 |
| BetterFiles/Stage/18-Pending-EMD-Needed | 1000464560 | 18 |
| BetterFiles/Stage/19-Pending-Delayed | 1000087330 | 19 |
| BetterFiles/Stage/20-Pending-EMD-Received | 816051 | 20 |
| BetterFiles/Stage/21-Final-Steps-For-Close | 1318346580 | 21 |
| BetterFiles/Stage/22-Funding-Needed | 973582851 | 22 |
| BetterFiles/Stage/23-Funded | 1324832324 | 23 |
| BetterFiles/Stage/24-Set-For-Recording | 1318494337 | 24 |
| BetterFiles/Stage/25-Closed-Payment-Pending | 973594290 | 25 |
| BetterFiles/Stage/26-Closed-Disbursement-Pending | 1318494338 | 26 |
| BetterFiles/Stage/27-Closed-Seller-Vacating-Property | 984457861 | 27 |
| BetterFiles/Stage/28-Closed | 816052 | 28 |
| BetterFiles/Stage/29-Cancelled-Need-to-Refund-Money | 1000087331 | 29 |
| BetterFiles/Stage/30-Lost-Watch | 1073748864 | 30 |
| BetterFiles/Stage/31-Lost-Sold-skip-trace | 1073747859 | 31 |
| BetterFiles/Stage/32-Lost | 830401 | 32 |
Stage label slugs are emoji-stripped, special-char-normalized versions of HubSpot stage names.
Taxonomy rebuild (bf-autolabel taxonomy-rebuild) re-derives from live pipeline 816046 and updates this file.
Action labels — BetterFiles/Action/
| Gmail label path | Trigger |
|---|---|
| BetterFiles/Action/Wire-Pending | subject regex: wire |
| BetterFiles/Action/EMD-Needed | subject regex: earnest money |
| BetterFiles/Action/Sig-Needed | DocuSign sender + awaiting signature |
| BetterFiles/Action/Refund-Due | stage=29-Cancelled + refund keyword |
| BetterFiles/Action/Funding-Needed | stage=22-Funding-Needed keyword |
| BetterFiles/Action/Recording-Pending | subject regex: recording |
Source labels — BetterFiles/Source/
| Gmail label path | Trigger |
|---|---|
| BetterFiles/Source/DocuSign | sender domain: docusign.net |
| BetterFiles/Source/Escrow | sender domain in escrow company list OR subject: escrow |
| BetterFiles/Source/Title | sender domain in title company list OR subject: title |
| BetterFiles/Source/Buyer | thread participant matches buyer email on deal |
| BetterFiles/Source/Seller | thread participant matches seller email on deal |
| BetterFiles/Source/Lender | subject: lender |
| BetterFiles/Source/Attorney | subject: attorney |
Property labels — BetterFiles/Property/
Dynamically created per deal. Slug derived from HubSpot property_address field:
- Lowercase, spaces to dashes, strip punctuation
- Example:
1354 Orange Ave, Signal Hill→1354-orange-ave-signal-hill - Only created for active deals (stages 00–28)
- Archive closed-deal property labels after 90 days (Phase 7)
Review label — BetterFiles/Review
Applied when classifier confidence < 0.6. Stephania’s manual triage queue. Flat label (no sub-hierarchy).
Audit log schema (Supabase CCP)
Table: gmail_label_audit (created Phase 2B)
| Column | Type | Notes |
|---|---|---|
| id | BIGSERIAL | PK |
| occurred_at | TIMESTAMPTZ | default now() |
| inbox | TEXT | ’teamsteph@betterfiles.com’ |
| message_id | TEXT | Gmail msg ID |
| thread_id | TEXT | Gmail thread ID |
| action | TEXT | ’add’ / ‘remove’ / ‘create-label’ / ‘shadow’ |
| label_name | TEXT | full Gmail label path |
| label_id | TEXT | Gmail label ID (nullable until label created) |
| classifier | TEXT | ’rule’ / ‘llm’ / ‘manual’ / ‘push’ |
| confidence | NUMERIC(4,3) | classifier confidence score |
| reason | TEXT | human-readable explanation |
| rule_id | TEXT | which rule matched (if classifier=rule) |
| llm_model | TEXT | model used (if classifier=llm) |
| portkey_request_id | TEXT | Portkey trace ID |
| cost_cents | NUMERIC(8,4) | LLM cost in cents |
| sweep_run_id | UUID | groups all ops from one sweep run |
| UNIQUE | (message_id, label_name, action) | idempotency |
Stage cache
Path: /home/opsadmin/.openclaw/workspace-betterfiles/data/stage-cache.json
TTL: 1 hour
Format: { fetchedAt: ISO, stages: [{id, label, displayOrder, labelPath}] }
Manual invalidation: bf-autolabel taxonomy-rebuild
File layout
~/.claude/skills/betterfiles-autolabel/
SKILL.md ← this file
bin/
bf-autolabel ← CLI entry (Phase 2A)
lib/
classifier-rules.js ← deterministic rules (Phase 2C)
classifier-llm.js ← Haiku 4.5 via Portkey (Phase 2D)
audit-log.js ← Supabase gmail_label_audit writer (Phase 2B)
hubspot-stage-fetcher.js ← live pipeline 816046 + 1h cache (Phase 2A)
tests/
test-classifier-rules.js
test-classifier-llm.js
test-audit-log.js
test-hubspot-stage-fetcher.js
Shared libs required (DO NOT DUPLICATE):
/home/opsadmin/.openclaw/workspace/scripts/lib/gmail-client.js/home/opsadmin/.openclaw/workspace/scripts/lib/gmail-label-matcher.js/home/opsadmin/.openclaw/workspace/scripts/lib/logger.js/home/opsadmin/.openclaw/workspace/scripts/lib/state-manager.js/home/opsadmin/.openclaw/workspace/scripts/lib/retry.js
Invokes / Invoked by
Invokes: _summary, dispo-lifecycle Invoked by: SKILL, SKILL