Pearl Analytics¶
Overview¶
Pearl Analytics is Pearl's Slack-integrated system for collecting, validating, and auditing revenue growth metrics across multiple analytics dashboards. It provides slash commands under /pearl analytics that allow team members to trigger metric collection, validate live data against stored snapshots, audit dashboard health, and explore metric coverage across the organization's dashboard ecosystem. It also provides /pearl goals commands for managing Matomo conversion goals, /pearl experiment commands for managing A/B experiments, /pearl intent, /pearl effect, and /pearl council commands for interacting with the M6 Commander intent inbox, and /pearl invoice commands for originating and tracking invoices with founder-approval gating.
Architecture¶
Pearl Analytics is composed of fifteen main components:
-
Command Handler (
src/handlers/analytics_command_handler.py) — Routes/pearl analytics <subcommand>Slack commands to the appropriate service. Depends on the validation service, metric collector, vector memory (Weaviate), and audit service. -
Metric Collector Service (
src/services/metric_collector_service.py) — Polls TheSmithy's agent metrics endpoint (/api/metrics/agent/) on a schedule and records snapshots into Pearl's database viaRevenueAnalyticsService. Deduplicates same-day values automatically. Collects four core metrics:waitlist_size(fromwaitlist)beta_activations(frombeta_users)pre_orders(frompreorders)revenue_monthly(fromcash)
-
Matomo Collector Service (
src/services/matomo_collector_service.py) — Polls Matomo's Reporting API for web traffic metrics and records them viaRevenueAnalyticsService. Runs on a schedule (default: every 6 hours, with a 30-minute stagger after the TheSmithy collector). Collection is organized into two tiers:Tier 1 — Calls
VisitsSummary.getand records five metrics directly from the response: -mau(fromnb_uniq_visitors) -bounce_rate(frombounce_rate) -total_visits(fromnb_visits) -avg_session_duration(fromavg_time_on_site) -actions_per_visit(fromnb_actions_per_visit)Tier 3 — Calls additional Matomo API methods and derives scalar metrics. Errors in Tier 3 collectors are non-fatal and do not break Tier 1 collection: -
VisitFrequency.get→new_visitor_ratio(percentage of visits from new visitors) -Referrers.getReferrerType→referrer_search_pct,referrer_direct_pct,referrer_social_pct(visit percentages by referrer type) -DevicesDetection.getType→device_desktop_pct(percentage of visits from desktop devices)The service tracks Matomo authentication state internally (
auth_status:unchecked,valid, orinvalid). On scheduler startup,validate_auth()is called to verify the token before entering the collection loop. If auth is invalid, the collector automatically retries validation on the next collection cycle — a restart is no longer required. See Error Handling for details. -
Analytics Validation Service (
src/services/analytics_validation_service.py) — Cross-references live growth data from TheSmithy's API against Pearl's stored metric snapshots. Reports per-metric status as one of:ok,drift,stale,missing_live, ormissing_stored. Also surfaces which dashboards Pearl can and cannot reach.- Staleness threshold: 24 hours
- Drift tolerance: 1%
-
Dashboard Registry (
src/services/dashboard_registry.py) — Single source of truth for all known analytics dashboards, their auth requirements, integration status, and metric mappings. Currently tracks five dashboards:- Grit Growth — integrated (agent key auth)
- Matomo Web Analytics — integrated (Matomo token auth) — 4 API endpoints, 10 metrics
- Virginia Pipeline — blocked (staff session required)
- Campaign Performance — blocked (upstream TheSmithy fixes needed: #687, #688, #689)
- Conversion Funnel — blocked (upstream TheSmithy fix needed: #686)
-
Matomo Goals Service (
src/services/matomo_goals_service.py) — Manages Matomo conversion goals via the Goals Admin API and collects conversion data via the Goals Reporting API. Stores goal definitions and conversion history in PostgreSQL (matomo_goalsandmatomo_goal_conversionstables) for trend analysis and Slack reporting. Phase 2 of Matomo integration, building on the Matomo Collector Service (Phase 1). Capabilities include:- Creating, listing, deleting, and syncing goals with Matomo
- Collecting per-goal conversion data (conversions, conversion rate, revenue)
- Formatting goal summaries for Slack
-
Experiment Service (
src/services/experiment_service.py) — Lightweight A/B experiment framework that uses Matomo segments for measurement. Pearl owns the experiment definitions, variant segments, and statistical analysis — it does not depend on Matomo's A/B testing plugin. Phase 3 of Matomo integration. Stores data in PostgreSQL (experiments,experiment_variants,experiment_measurementstables). Capabilities include:- Experiment lifecycle management (
draft→running→completed|cancelled) - Variant management with Matomo segment mapping (each variant maps to a Matomo segment)
- Metric collection per variant via Matomo
VisitsSummary.getwith variant-specific segments - Statistical significance analysis using a two-proportion z-test (p < 0.05 threshold)
- Experiment lifecycle management (
-
Dashboard Audit Service (
src/services/dashboard_audit_service.py) — Runs cross-dashboard health checks and aggregates findings into anAuditReport. Depends on the validation service (for Grit Growth auditing) and the Matomo collector service (for Matomo auditing). Findings are categorized by status (passed,failed,warning,blocked) and severity (info,warning,critical). Currently audits:- Grit Growth — validates live metrics against stored snapshots via
AnalyticsValidationService, checking for drift, staleness, and missing data - Matomo Web Analytics — performs live auth validation via
validate_auth()and a data freshness check viacollect(). If auth fails, data collection checks are skipped. If the Matomo collector is not configured, returns ablockedfinding - Campaign Performance — blocked (upstream TheSmithy fixes #687, #688, #689)
- Conversion Funnel — blocked (upstream TheSmithy fix #686)
Actionable findings (failed or warning) are posted to the
#revenueSlack channel via the notification service. - Grit Growth — validates live metrics against stored snapshots via
-
Goals Command Handler (
src/handlers/goals_command_handler.py) — Routes/pearl goals <subcommand>Slack commands to the Matomo Goals Service. Provides a dedicated command namespace for managing conversion goals, separate from the/pearl analyticscommands. Depends on theMatomoGoalsService; if the service is unavailable, all subcommands return a "not available" message directing the user to checkMATOMO_URL,MATOMO_TOKEN_AUTH, andPEARL_MATOMO_GOALS_ENABLED. -
Experiment Command Handler (
src/handlers/experiment_command_handler.py) — Routes/pearl experiment <subcommand>Slack commands to the Experiment Service. Provides a dedicated command namespace for managing A/B experiments, separate from the/pearl analyticsand/pearl goalscommands. Depends on theExperimentService; if the service is unavailable, all subcommands return a "not available" message directing the user to checkMATOMO_URL,MATOMO_TOKEN_AUTH, andPEARL_EXPERIMENTS_ENABLED. -
Intent Command Handler (
src/handlers/intent_command_handler.py) — Routes/pearl intent <subcommand>Slack commands to theIntentReceivermixin methods onPearlSlackBot. Provides a Slack surface for the M6 Commander intent inbox (pearl#96), building on the mixin wired in pearl#95. Subcommands include viewing assigned intents, inspecting intent detail, submitting back-briefs, and logging deviations. Each subcommand parses--flag "value"style arguments viashlex. If theIntentReceivermixin is unavailable (e.g.,coworkers_shareddatabase unreachable), operations surface the error when invoked.NotAssignedErroris caught per-subcommand and returned as a user-friendly refusal message. -
Effect Command Handler (
src/handlers/intent_command_handler.py) — Routes/pearl effect <subcommand>Slack commands toIntentReceiver.report_effect_evidence(). Provides a dedicated command namespace for recording outcome evidence against intent effects. Accepts numeric or text values; numeric values are attempted first viafloat()and fall back to text. Shares the same module and error-handling patterns as the Intent Command Handler. -
Council Command Handler (
src/handlers/intent_command_handler.py) — Routes/pearl council <subcommand>Slack commands toIntentReceiver.propose_council_terms(). Provides a dedicated command namespace for submitting proposals in council conflict-resolution rounds. Validates proposal types against a fixed set (give_up,defer,share,compromise) before forwarding to the mixin. Shares the same module and error-handling patterns as the Intent Command Handler. -
Invoice Service (
src/services/invoice_service.py) — Pearl originates and tracks invoices on the sharedfinance_invoicestable in thecoworkers_shareddatabase (schema from agents-shared migration 006, applied on boot viaensure_schema_current). Pearl writes invoices; she does not writefinance_transactions— that ledger is Chisel's (SINGLE_WRITER='chisel'). A paid invoice is the durable revenue event; Chisel's reconciler books the revenue from it. Phase pearl#204 of the invoicing & receivables feature. The invoice lifecycle is a guarded state machine:draft ──approve──▶ approved ──send──▶ sent ──▶ paid │ │ │ └──▶ overdue ──▶ paid └──────────────────┴──────────────────┴──▶ voidEvery state change goes through
_transition()— the single choke point that (a) validates the transition against the allowed-transitions map and (b) enforces the founder-approval gate. No outbound status (approved,sent,paid,overdue) is reachable without an authorized human on record, matching thefinance_invoicesCHECK constraint (status IN ('draft','void') OR approved_by IS NOT NULL) for defense-in-depth. Money is integer cents through the shared strict parser — never floats.Approval authorization is delegated to the
ApproverRegistry(fromservices.content_drafts.approvers) keyed onContentType.INVOICE. Only Slack users listed inconfig/content_approvers.yamlwithinvoicein theircan_approvelist can approve invoices. Currently, only founders have this permission.Capabilities include: - Creating draft invoices with auto-generated invoice numbers (
INV-00001,INV-00002, …) - Founder-only approval (the sole path that setsapproved_by, unlocking every outbound status) - State transitions:send,mark_paid(with optionalpaid_transaction_id),mark_overdue,void- Listing invoices (optionally filtered by status) - Receivables aging report via thefinance_ar_agingdatabase view (buckets:current,1-30,31-60,61-90,90+) -
Invoice Command Handler (
src/handlers/invoice_command_handler.py) — Routes/pearl invoice <subcommand>Slack commands to the Invoice Service. Provides a dedicated command namespace for originating and tracking invoices. Theagents_shared.financemodule is imported lazily inside each method so the handler stays import-safe against a stale agents-shared wheel (the handler is imported eagerly at bot startup). Depends on theInvoiceService; if the service is unavailable (e.g.,CoworkersDBunreachable orPEARL_INVOICE_ENABLEDnot set), all subcommands return a "not available" message. Ships dark — the command is only registered with the command router whenPEARL_INVOICE_ENABLEDis set to"true"AND theInvoiceCommandHandlerwas successfully initialized.
PearlSlackBot and the IntentReceiver mixin¶
The PearlSlackBot class (src/slack/main.py) — which hosts all of the above components — now inherits from IntentReceiver (provided by agents_shared.coworkers). This wires Pearl into the M6 Commander loop (pearl#95), giving her a subordinate-side interface for the Bungay alignment-gap closure system shared across all Smithy agents. The mixin exposes six methods on PearlSlackBot:
list_inbox(status=...)— list intents assigned to Pearl, optionally filtered by statusview_intent(intent_id)— view full detail of an intent (row, effects, briefbacks)submit_backbrief(intent_id, understanding, plan, risks)— post a back-brief for an assigned intentlog_deviation(briefback_id, original_plan, actual_action, rationale)— log a deviation from a briefed planreport_effect_evidence(intent_id, criterion, actual_value, ...)— report measurement evidence for an intent's effectspropose_council_terms(council_id, round_number, proposal_type, proposal_terms)— post a proposal in a council conflict-resolution round
The agent identity is registered as "pearl" (lowercase) at init time. Write methods (submit_backbrief, log_deviation, report_effect_evidence) enforce that Pearl is listed in the intent's assignees before allowing writes, raising NotAssignedError otherwise.
If the coworkers_shared database is unreachable at startup, IntentReceiver.__init__ is caught and logged as a warning — the bot continues to start normally and all other components (analytics, goals, experiments, invoicing, etc.) remain fully functional. Intent-related operations will surface the error when invoked. See Error Handling for details.
After the IntentReceiver mixin is initialized, PearlSlackBot.__init__ calls ensure_schema_current() (also from agents_shared.coworkers) to apply any pending coworkers_shared database migrations on boot (pearl#126). This is the architecture-recurrence guard introduced by agents-shared#25: every agent that uses the coworkers_shared schema calls this function on startup, so new migrations land automatically on the next deploy of any agent. The function has never-raises semantics — migration failures are logged at ERROR level with a full traceback, but the bot continues to boot. An opt-out is available via the COWORKERS_SKIP_MIGRATIONS env var for test environments and ephemeral pods that must not mutate the shared schema. See Configuration and Error Handling for details.
The three M6 Commander command handlers (components 11–13) are instantiated in _init_handlers() with pearl_bot=self and registered with the command router as synchronous handlers (no requires_async). The Invoice Command Handler (component 15) is also instantiated in _init_handlers(), where it is constructed with an InvoiceService backed by CoworkersDB(). If the CoworkersDB or InvoiceService import fails, the handler is set to None and a warning is logged — the bot continues to start normally. The handler is registered with the command router only when PEARL_INVOICE_ENABLED is "true" and the handler was successfully initialized.
Data flow walkthroughs¶
The following walkthroughs describe how data moves through Pearl Analytics for each major operation. All flows originate from either a scheduled background task (in src/mvp_slack_bot.py) or a manual Slack command (routed by src/handlers/analytics_command_handler.py).
TheSmithy metric collection¶
- Scheduler start — On bot startup,
start_slack_bot()creates an asyncio task for_metric_collector_scheduler()(gated byPEARL_METRIC_COLLECTOR_ENABLED). The scheduler sleeps 30 seconds to let initialization complete, then enters an infinite loop polling at the configured interval. - Interval parsing — The scheduler reads
PEARL_METRIC_COLLECTOR_INTERVALvia_interval_seconds()(fromcore/scheduler_utils.py), which parses the env var as an integer number of minutes, falls back to 360 (6 hours) on a missing or malformed value, and floors the result at 60 seconds to prevent busy-looping. - Collection — Each cycle calls
pearl.metric_collector.collect()onMetricCollectorService. The service issues a GET request to the TheSmithy agent metrics endpoint (THESMITHY_METRICS_URL) with anX-Agent-Keyheader (THESMITHY_METRICS_API_KEY). - Metric mapping — The JSON response is split into cumulative fields (
waitlist→waitlist_size,beta_users→beta_activations,preorders→pre_orders,cash→revenue_monthly) and top-level fields (library_purchases,library_revenue_cents,paragon_purchases). Each field maps to a Pearl metric name. - Deduplication — For each metric,
_is_duplicate()checks whether the latest stored record has the same value and was recorded today. If so, the metric is skipped. - Storage — Non-duplicate values are persisted via
RevenueAnalyticsService.record_metric()(PostgreSQL), tagged withrecorded_by="pearl-collector"andsource="thesmithy_api". - Result — The collect method returns a dict with
recorded,skipped, anderrorslists. The scheduler logs the counts.
A manual /pearl analytics collect command follows the same path from step 3 onward, bypassing the scheduler.
Matomo web traffic collection¶
- Scheduler start —
_matomo_collector_scheduler()is created as an asyncio task (gated byPEARL_MATOMO_COLLECTOR_ENABLED). It sleeps 1830 seconds (30 minutes and 30 seconds) as a stagger after the TheSmithy collector to spread API load. - Auth validation — On the first cycle, the scheduler calls
pearl.matomo_collector.validate_auth(), which issues a lightweightVisitsSummary.getPOST to the Matomo Reporting API. The result sets the internalauth_statustovalid,invalid, or leaves it atuncheckedon non-auth errors. If auth is invalid, the collector logs a warning and retries on the next cycle — no restart required. - Tier 1 collection —
collect()calls_fetch_visits_summary()(POST toVisitsSummary.get) and maps five response fields to Pearl metrics:nb_uniq_visitors→mau,bounce_rate→bounce_rate,nb_visits→total_visits,avg_time_on_site→avg_session_duration,nb_actions_per_visit→actions_per_visit. Values are sanitized (e.g.,"65%"→65.0) via_to_float(). - Tier 3 collection — Three additional API methods are called independently, each wrapped in a try/except so a Tier 3 failure never breaks Tier 1:
VisitFrequency.get→_extract_visit_frequency()derivesnew_visitor_ratiofromnb_visits_new/ total visits.Referrers.getReferrerType→_extract_referrer_breakdown()derivesreferrer_search_pct,referrer_direct_pct,referrer_social_pctfrom per-referrer-type visit counts.DevicesDetection.getType→_extract_device_breakdown()derivesdevice_desktop_pctfrom device-type visit counts.
- Dedup and storage — Same dedup and
record_metric()pattern as TheSmithy collection, tagged withrecorded_by="pearl-matomo-collector"andsource="matomo_api". - Auth failure alerting — If
auth_statusisinvalidafter collection, the scheduler posts a warning toPEARL_MATOMO_ALERT_CHANNELvia the notification service, deduplicating alerts to at most once per 24 hours. When auth recovers, a resolution notice is posted.
Validation¶
- Trigger —
/pearl analytics validateroutes toAnalyticsCommandHandler._handle_validate(). - Live fetch —
AnalyticsValidationService.validate_growth_data()callscollector._fetch()to get live data from the TheSmithy agent metrics endpoint (the same endpoint used by collection). - Stored lookup — For each metric in the cumulative mapping,
revenue_service.get_latest()retrieves the most recent stored snapshot from PostgreSQL. - Comparison — Each metric is classified:
ok— live and stored values match within 1% drift tolerance.drift— values differ by more than 1%.stale— the stored snapshot is older than 24 hours.missing_stored— no stored snapshot exists (suggests running/pearl analytics collect).missing_live— the live API returned no value for this metric.
- Dashboard coverage — The report also lists dashboards from the
DashboardRegistryas eitherREACHABLE(integrated) orBLOCKED(not yet integrated). - Response — The handler formats the
ValidationReportas a Slack message with per-metric status lines and dashboard coverage.
Dashboard audit¶
- Trigger —
/pearl analytics auditroutes toAnalyticsCommandHandler._handle_audit(). - Full audit —
DashboardAuditService.run_full_audit()runs four audit steps sequentially:- Grit Growth — calls
validation_service.validate_growth_data()and converts eachValidationResultinto anAuditFinding(passed/failed/warning based on metric status). - Campaign Performance — checks the agent endpoint's
campaignssection for well-formed records withvisitsandconversion_ratefields. Currently returnsblockedif the dashboard registry marks it as not integrated. - Conversion Funnel — checks the agent endpoint's
funnelsection for monotonically non-increasing steps (the hallmark of correct email deduplication). Currently returnsblockedif the dashboard registry marks it as not integrated. - Matomo Web Analytics — validates Matomo auth via
validate_auth(), then runs a livecollect()as a data freshness check. If auth fails, data collection checks are skipped.
- Grit Growth — calls
- Reporting — Findings are aggregated into an
AuditReport. The handler formats it for Slack and, if actionable findings exist (failed or warning), posts to the#revenuechannel via the notification service.
Invoice lifecycle¶
- Trigger —
/pearl invoice <subcommand>routes toInvoiceCommandHandler.handle_command()via the command router (only whenPEARL_INVOICE_ENABLEDis"true"). - Create —
createparses the counterparty and amount (viaparse_amount_to_cents()fromagents_shared.finance), then callsInvoiceService.create(). The service validates theInvoicemodel (positive integer cents,due_on >= issued_on), auto-generates an invoice number (INV-NNNNN), and inserts adraftrow intofinance_invoices. Default payment terms are net-30. - Approve —
approvecallsInvoiceService.approve(invoice_id, approver_slack_id). The service checks theApproverRegistry(keyed onContentType.INVOICE) to verify the Slack user is authorized — only founders listed inconfig/content_approvers.yamlwithinvoicein theircan_approvelist may approve. On success,approved_byis set and the status moves toapproved. This is the only path that setsapproved_by, and thus the only way to unlock outbound statuses. - Send / Paid / Overdue / Void — Each calls
InvoiceService._transition(), which validates the transition against_ALLOWED_TRANSITIONSand updates the row.mark_paidoptionally records apaid_transaction_idlinking to Chisel's ledger. - List —
list [status]callsInvoiceService.list_invoices()with an optional status filter and returns a formatted Slack message. - AR aging —
arcallsInvoiceService.ar_aging(), which queries thefinance_ar_agingdatabase view (sent/overdue invoices bucketed by days overdue) and returns a formatted receivables aging report.
flowchart TD
subgraph Schedulers ["Background Schedulers (mvp_slack_bot.py)"]
S1["_metric_collector_scheduler\n30s delay → 6h loop"]
S2["_matomo_collector_scheduler\n30min stagger → 6h loop"]
end
subgraph Commands ["Slash Commands"]
C1["/pearl analytics collect"]
C2["/pearl analytics validate"]
C3["/pearl analytics audit"]
C4["/pearl invoice create/approve/send/paid/void/list/ar"]
end
subgraph Services
MCS["MetricCollectorService\n.collect()"]
MACS["MatomoCollectorService\n.collect()"]
AVS["AnalyticsValidationService\n.validate_growth_data()"]
DAS["DashboardAuditService\n.run_full_audit()"]
IS["InvoiceService\n.create() / .approve() / …"]
end
subgraph External ["External APIs"]
TS["TheSmithy\n/api/metrics/agent/"]
MA["Matomo\nReporting API"]
end
RAS["RevenueAnalyticsService\n.record_metric()\nPostgreSQL"]
CDB["CoworkersDB\nfinance_invoices\nPostgreSQL"]
S1 --> MCS
C1 --> MCS
MCS --> TS
MCS --> RAS
S2 --> MACS
MACS --> MA
MACS --> RAS
C2 --> AVS
AVS --> TS
AVS --> RAS
C3 --> DAS
DAS --> AVS
DAS --> MACS
DAS -->|"posts findings"| NS["NotificationService\n→ #revenue"]
C4 --> IS
IS --> CDB
Usage¶
All analytics commands are invoked via the /pearl analytics Slack slash command. Goals management commands use the /pearl goals command. Experiment commands use the /pearl experiment command. M6 Commander commands use /pearl intent, /pearl effect, and /pearl council. Invoice commands use /pearl invoice:
Analytics Commands¶
| Command | Description |
|---|---|
/pearl analytics audit |
Run a full dashboard audit (Grit Growth + Matomo) and post the report to #revenue |
/pearl analytics validate |
Validate live growth data against stored snapshots |
/pearl analytics collect |
Manually trigger metric snapshot collection |
/pearl analytics dashboards |
List all known dashboards with status, URL, auth type, and metrics |
/pearl analytics overlap |
Show which Pearl metrics are covered by which dashboards |
/pearl analytics index-dashboards |
Index dashboard documentation and data flow into Weaviate |
/pearl analytics help |
Show available analytics subcommands |
Goals Commands¶
| Command | Description |
|---|---|
/pearl goals list |
List all Matomo conversion goals |
/pearl goals create <name> <url_pattern> |
Create a new goal (last argument is the URL pattern, all preceding arguments form the name) |
/pearl goals delete <goal_id> |
Delete a goal by its Matomo ID |
/pearl goals sync |
Sync goals from Matomo into Pearl's local database |
/pearl goals conversions |
Collect current month conversion data for all goals |
/pearl goals help |
Show available goals subcommands |
Experiment Commands¶
| Command | Description |
|---|---|
/pearl experiment list |
List all experiments |
/pearl experiment create <metric> <name> |
Create a draft experiment (first argument is the Matomo metric name, remaining arguments form the experiment name) |
/pearl experiment variant <id> <name> <segment> [--control] |
Add a variant with a Matomo segment to an experiment (use --control to designate the control group) |
/pearl experiment start <id> |
Start a draft experiment (begins metric collection) |
/pearl experiment collect <id> |
Manually collect metrics for all variants in an experiment |
/pearl experiment results <id> |
Show results with statistical significance analysis |
/pearl experiment view <id> |
Show experiment details, variants, measurements, and significance |
/pearl experiment stop <id> |
Mark a running experiment as completed |
/pearl experiment help |
Show available experiment subcommands |
Intent Commands¶
| Command | Description |
|---|---|
/pearl intent inbox [--status <status>] |
List intents assigned to Pearl, optionally filtered by status (pending, confirmed, in-execution) |
/pearl intent view <id> |
Show full detail of an intent (title, tier, status, why, assignees, effects, back-briefs, parent) |
/pearl intent backbrief <id> --understanding "..." --plan "..." [--risks "..."] |
Submit a back-brief on an assigned intent |
/pearl intent deviate <briefback_id> --original-plan "..." --actual-action "..." --rationale "..." |
Log a deviation from a briefed plan |
/pearl intent help |
Show available intent subcommands |
Effect Commands¶
| Command | Description |
|---|---|
/pearl effect record <intent_id> <criterion> <value> [--notes "..."] |
Record measurement evidence for an intent's effect criterion (numeric or text value) |
/pearl effect help |
Show available effect subcommands |
Council Commands¶
| Command | Description |
|---|---|
/pearl council propose <council_id> <type> "<terms>" [--round N] |
Submit a proposal in a council conflict-resolution round (type: give_up, defer, share, or compromise; round defaults to 1) |
/pearl council help |
Show available council subcommands |
Invoice Commands¶
| Command | Description |
|---|---|
/pearl invoice create <counterparty> <amount> [notes] |
Draft an invoice (net-30 payment terms), e.g. create @northwind 1200.00 monthly retainer |
/pearl invoice approve <id> |
Founder-only approval (unlocks send) |
/pearl invoice send <id> |
Mark an approved invoice as sent |
/pearl invoice paid <id> [txn_id] |
Mark a sent or overdue invoice as paid (optional transaction ID links to Chisel's ledger) |
/pearl invoice overdue <id> |
Flag a sent invoice as overdue |
/pearl invoice void <id> |
Cancel an invoice (allowed from any non-terminal status) |
/pearl invoice list [status] |
List recent invoices, optionally filtered by status (draft, approved, sent, paid, overdue, void) |
/pearl invoice ar |
Show receivables aging report (buckets: current, 1-30, 31-60, 61-90, 90+ days) |
/pearl invoice help |
Show available invoice subcommands |
Example: Seeding initial baselines¶
When Pearl has no stored snapshots yet, validation will report all metrics as missing_stored and suggest:
After collecting, /pearl analytics validate will compare live values against the stored snapshots.
Example: Validation output¶
*Analytics Validation* — 3/4 OK
1 drift
[OK] *waitlist_size* — live: 45, stored: 45 (0.0% drift)
[DRIFT] *beta_activations* — live: 12, stored: 10 (20.0% drift)
[OK] *pre_orders* — live: 3, stored: 3 (0.0% drift)
[OK] *revenue_monthly* — live: 500, stored: 500 (0.0% drift)
*Dashboard Coverage*
REACHABLE Grit Growth
REACHABLE Matomo Web Analytics
BLOCKED Virginia Pipeline
BLOCKED Campaign Performance
BLOCKED Conversion Funnel
Example: Creating a conversion goal¶
Response:
Example: Running an A/B experiment¶
Create the experiment, add variants, then start it:
Response:
Created experiment #1: *TikTok vs LinkedIn Chapter 1*
Metric: `nb_visits` | Status: draft
Add variants with `/pearl experiment variant 1 <name> <matomo_segment>`
Then start with `/pearl experiment start 1`
Add a control variant and a test variant:
/pearl experiment variant 1 LinkedIn utm_source==linkedin --control
/pearl experiment variant 1 TikTok utm_source==tiktok
Start the experiment:
After collecting data, view results with significance analysis:
Response:
*Experiment #1 Results*
Control (*LinkedIn*): 320 (n=1500)
*TikTok*: lift +12.5%, p=0.0312 (significant)
Example: Viewing the intent inbox and submitting a back-brief¶
Check which intents are assigned to Pearl:
Response:
:mailbox_with_mail: *Intent inbox* (1 intent(s))
• #8 _strategic_ *Secure first paying household* — `issued`
Use `/pearl intent view <id>` for full detail, `/pearl intent backbrief <id>` to submit a back-brief.
View details of a specific intent:
Response:
:scroll: *Intent #8*
*Title:* Secure first paying household
*Tier:* strategic
*Status:* `issued`
*Why:* north star
*Assignees:* pearl, arturo
*Effects (1):*
• `paying_households` → 1
Submit a back-brief:
/pearl intent backbrief 8 --understanding "I will execute the first-household conversion strategy" --plan "Step 1: identify top prospects, Step 2: run targeted campaign" --risks "delay\nbudget"
Response:
:white_check_mark: *Back-brief submitted* on intent `#8` (briefback #42).
Taryn will review and either confirm or request clarification.
Example: Recording effect evidence¶
Response:
Example: Submitting a council proposal¶
Response:
Example: Creating and sending an invoice¶
Draft a net-30 invoice:
Response:
🧾 Drafted *INV-00001* (#1): $1200.00 to @northwind, due 2026-08-13.
_Founder approval required before sending: `/pearl invoice approve 1`._
A founder approves the invoice:
Response:
Send the invoice:
Response:
Mark it paid (with optional transaction ID linking to Chisel's ledger):
Response:
Example: Receivables aging report¶
Response:
*Receivables aging*
current: 2 invoice(s), $200.00
90+: 1 invoice(s), $50.00
*Total outstanding: $250.00*
Configuration¶
Pearl Analytics requires the following environment variables:
| Variable | Purpose | Required for |
|---|---|---|
THESMITHY_METRICS_URL |
URL of TheSmithy's agent metrics endpoint | Metric collection and validation |
THESMITHY_METRICS_API_KEY |
API key for the agent metrics endpoint | Metric collection and validation |
MATOMO_URL |
Matomo instance URL | Matomo metric collection, goals, experiments, audit |
MATOMO_TOKEN_AUTH |
Matomo API authentication token (must be non-empty) | Matomo metric collection, goals, experiments, audit |
MATOMO_SITE_ID |
Matomo site ID (default: 1) |
Matomo metric collection, goals, experiments |
PEARL_MATOMO_GOALS_ENABLED |
Enable Matomo goals service (true or 1) |
/pearl goals commands |
PEARL_EXPERIMENTS_ENABLED |
Enable experiment service (true or 1) |
/pearl experiment commands |
PEARL_INVOICE_ENABLED |
Enable invoice service (true or 1; ships dark, default false) |
/pearl invoice commands |
PEARL_REVENUE_CHANNEL_ID |
Slack channel ID for audit reports | Dashboard audit reporting |
COWORKERS_PG_HOST |
Hostname for the coworkers_shared database (default: POSTGRES_HOST or postgres) |
Intent inbox (M6 Commander loop), invoicing |
COWORKERS_PG_PORT |
Port for the coworkers_shared database (default: POSTGRES_PORT or 5432) |
Intent inbox (M6 Commander loop), invoicing |
COWORKERS_PG_USER |
Database user (default: coworkers) |
Intent inbox (M6 Commander loop), invoicing |
COWORKERS_PG_PASSWORD |
Database password (required for non-localhost) | Intent inbox (M6 Commander loop), invoicing |
COWORKERS_PG_DB |
Database name (default: coworkers_shared) |
Intent inbox (M6 Commander loop), invoicing |
COWORKERS_SKIP_MIGRATIONS |
Opt out of boot-time schema migrations (true, 1, or yes) |
Boot-time migration runner (pearl#126) |
If THESMITHY_METRICS_URL or THESMITHY_METRICS_API_KEY are not set, the metric collector, validation service, and audit service will all be unavailable and commands will return a "not configured" message.
If MATOMO_TOKEN_AUTH is set but empty or contains only whitespace, the Matomo collector is disabled at startup and a warning is logged. When a valid token is provided, Pearl validates it on the first scheduler run via validate_auth() — if the token is rejected by the Matomo API (HTTP 401/403 or a token_auth-related API error), the collector's auth_status is set to invalid and collection is skipped for that cycle. On the next collection cycle the collector automatically retries auth validation, so recovery does not require a restart.
The Matomo Goals Service requires MATOMO_URL, MATOMO_TOKEN_AUTH, and PEARL_MATOMO_GOALS_ENABLED to be set. If any of these are missing or PEARL_MATOMO_GOALS_ENABLED is not true/1, the goals service is not initialized and /pearl goals commands will return a "not available" message.
The Experiment Service requires MATOMO_URL, MATOMO_TOKEN_AUTH, and PEARL_EXPERIMENTS_ENABLED to be set. If any of these are missing or PEARL_EXPERIMENTS_ENABLED is not true/1, the experiment service is not initialized and /pearl experiment commands will return a "not available" message.
The Invoice Service requires PEARL_INVOICE_ENABLED to be set to true and a reachable coworkers_shared database (via the COWORKERS_PG_* variables). The service reads and writes the finance_invoices table (schema from agents-shared migration 006, applied on boot via ensure_schema_current). If PEARL_INVOICE_ENABLED is not true, the /pearl invoice command is not registered with the command router. If CoworkersDB is unreachable during handler initialization, the handler is set to None and a warning is logged — all other Pearl features remain functional. Approval authorization is read from config/content_approvers.yaml; only users with invoice in their can_approve list may approve invoices.
The COWORKERS_PG_* variables are read lazily by CoworkersDB (from agents_shared.coworkers) on first use. If these are not set or the database is unreachable, IntentReceiver initialization degrades gracefully — the bot starts normally and all analytics features remain available, but intent-inbox and invoice operations will be unavailable until the database becomes reachable.
The COWORKERS_SKIP_MIGRATIONS variable controls the boot-time migration runner (ensure_schema_current()). When set to true, 1, or yes, the function logs and returns without touching the database. This is intended for test environments and ephemeral pods that must not mutate the shared schema. When unset or set to any other value, migrations are applied normally on boot.
Deployment¶
Pearl runs as a single-replica Kubernetes Deployment in the ai-coworkers namespace with a Recreate strategy (ensures clean shutdown/startup and personality consistency). The pod has init containers that copy Claude OAuth credentials and wait for PostgreSQL and Redis readiness before the main container starts.
Container image — Built from Dockerfile, which layers app code on top of a pearl-base image (carrying Node.js, Claude Code CLI, glab, and Playwright Chromium). The entry point is python src/mvp_slack_bot.py. All non-secret config is injected via the pearl-config ConfigMap (kubernetes/configmap.yaml); secrets come from pearl-secrets.
CI/CD auto-deploy — Merges to main trigger the GitLab CI deploy stage, which posts to the CI deploy webhook receiver. The receiver runs kubectl rollout restart deployment/pearl -n ai-coworkers. CI pipelines only run for trigger events (agent self-maintenance), scheduled pipelines, or explicit opt-in (git push -o ci.variable=RUN_FULL_CI=true); routine pushes skip cloud CI and rely on the local pre-push hook.
Local development — docker-compose.yml provides Pearl alongside Redis, PostgreSQL, and Neo4j containers. Configuration is read from a .env file.
Maintenance commands:
# View logs
kubectl logs -f -n ai-coworkers deployment/pearl
# Restart after config change
kubectl apply -f kubernetes/configmap.yaml
kubectl rollout restart deployment/pearl -n ai-coworkers
# Emergency stop / resume
kubectl scale deployment/pearl --replicas=0 -n ai-coworkers
kubectl scale deployment/pearl --replicas=1 -n ai-coworkers
Scheduling configuration¶
All background schedulers live in src/mvp_slack_bot.py and are launched as asyncio tasks in start_slack_bot(). Each scheduler is independently gated by an _ENABLED env var and reads its poll interval via _interval_seconds() (from core/scheduler_utils.py), which parses the env var as an integer number of minutes, falls back to the coded default on missing or malformed values, and floors the result at 60 seconds to prevent busy-looping.
Schedulers use staggered initial delays after pod boot to spread API load and avoid thundering-herd effects on startup.
| Scheduler | Env var (enabled) | Interval env var | Default (min) | Boot stagger | Purpose |
|---|---|---|---|---|---|
| TheSmithy metric collector | PEARL_METRIC_COLLECTOR_ENABLED |
PEARL_METRIC_COLLECTOR_INTERVAL |
360 (6h) | 30s | Polls TheSmithy agent metrics |
| Matomo collector | PEARL_MATOMO_COLLECTOR_ENABLED |
PEARL_MATOMO_COLLECTOR_INTERVAL |
360 (6h) | 30 min 30s | Polls Matomo Reporting API |
| Matomo goals collector | PEARL_MATOMO_GOALS_ENABLED |
PEARL_MATOMO_COLLECTOR_INTERVAL |
360 (6h) | 40 min | Collects goal conversion data |
| Experiment collector | PEARL_EXPERIMENTS_ENABLED |
PEARL_MATOMO_COLLECTOR_INTERVAL |
360 (6h) | 45 min | Collects A/B experiment metrics |
| LinkedIn collector | PEARL_LINKEDIN_COLLECTOR_ENABLED |
PEARL_LINKEDIN_COLLECTOR_INTERVAL |
1440 (24h) | 60 min 30s | Polls LinkedIn engagement metrics |
| TikTok collector | PEARL_TIKTOK_COLLECTOR_ENABLED |
PEARL_TIKTOK_COLLECTOR_INTERVAL |
1440 (24h) | 90 min 30s | Polls TikTok creator analytics |
| Self-maintenance | PEARL_SELF_MAINTENANCE_ENABLED |
PEARL_SELF_MAINTENANCE_INTERVAL_MINUTES |
1440 (24h) | 2h | Autonomous fix MRs on Pearl's repo |
| Intent adapter | PEARL_INTENT_ADAPTER_ENABLED |
PEARL_INTENT_ADAPTER_INTERVAL |
30 | 15 min | M6 Commander intent decomposition |
| Auto-backbrief | PEARL_AUTO_BACKBRIEF_ENABLED |
PEARL_AUTO_BACKBRIEF_INTERVAL |
15 | 30 min | Auto-draft back-briefs for Taryn |
| Effect evidence reporter | PEARL_EFFECT_REPORTER_ENABLED |
PEARL_EFFECT_REPORTER_INTERVAL |
360 (6h) | 30 min | Reports outcome evidence to intents |
| OAuth health | PEARL_OAUTH_HEALTH_ENABLED |
PEARL_OAUTH_HEALTH_INTERVAL |
60 (1h) | 3 min | Checks Agent SDK token health |
| MR reminders | PEARL_MR_REMINDERS_ENABLED |
PEARL_MR_REMINDERS_INTERVAL |
240 (4h) | 45 min | Nudges stale MR reviewers |
| Morning briefing | PEARL_MORNING_BRIEFING_ENABLED |
PEARL_MORNING_BRIEFING_INTERVAL |
60 (1h) | 55 min | Daily 9 AM PST revenue digest |
| Auto-rollout | PEARL_EXPERIMENT_AUTO_ROLLOUT_ENABLED |
PEARL_EXPERIMENT_AUTO_ROLLOUT_INTERVAL |
60 (1h) | 75 min | Experiment winner promotion |
The Matomo goals collector, experiment collector, and auto-rollout scheduler share PEARL_MATOMO_COLLECTOR_INTERVAL for their loop interval (they are downstream of Matomo data and run at the same cadence). The staggered boot delays ensure they do not all hit the Matomo API simultaneously.
To override any interval, set the corresponding env var in kubernetes/configmap.yaml (value is in minutes) and restart the pod:
# Example: collect TheSmithy metrics every 2 hours instead of 6
kubectl edit configmap pearl-config -n ai-coworkers
# Set PEARL_METRIC_COLLECTOR_INTERVAL: "120"
kubectl rollout restart deployment/pearl -n ai-coworkers
Error Handling¶
- Service not configured: Each subcommand checks for its required service and returns a clear message if unavailable (e.g., "Metric collector is not configured — check THESMITHY_METRICS_URL and THESMITHY_METRICS_API_KEY.").
- Goals service not available: If the
MatomoGoalsServiceis not initialized (missingMATOMO_URL,MATOMO_TOKEN_AUTH, orPEARL_MATOMO_GOALS_ENABLED), all/pearl goalssubcommands return: "Goals service is not available — check MATOMO_URL, MATOMO_TOKEN_AUTH, and PEARL_MATOMO_GOALS_ENABLED." - Experiment service not available: If the
ExperimentServiceis not initialized (missingMATOMO_URL,MATOMO_TOKEN_AUTH, orPEARL_EXPERIMENTS_ENABLED), all/pearl experimentsubcommands return: "Experiment service is not available — check MATOMO_URL, MATOMO_TOKEN_AUTH, and PEARL_EXPERIMENTS_ENABLED." - Invoice service not available: If the
InvoiceServiceis not initialized (e.g.,PEARL_INVOICE_ENABLEDnot set totrue, orCoworkersDBunreachable), all/pearl invoicesubcommands return: "Invoice service is not available." When the feature is disabled, the/pearl invoicecommand is not registered with the command router at all. - Invoice handler init failure: If
CoworkersDBorInvoiceServiceraises an exception during_init_handlers(), the error is caught and logged as a warning. The bot continues to start — all other features remain fully functional. The invoice handler is set toNoneand the command is not registered. - Unauthorized invoice approval:
InvoiceService.approve()checks theApproverRegistry(keyed onContentType.INVOICE). If the Slack user is not inconfig/content_approvers.yamlwithinvoicein theircan_approvelist,UnauthorizedApproveris raised. The handler catches this and returns a user-friendly message (e.g., "🔒 U2 is not authorized to approve invoices."). - Illegal invoice transition:
InvoiceService._transition()validates every state change against_ALLOWED_TRANSITIONS. Invalid transitions (e.g., sending a draft without approval, voiding a paid invoice) raiseIllegalTransition, which is surfaced as a clean Slack error. - Invoice not found:
InvoiceService._transition()raisesInvoiceNotFoundif the given invoice ID does not exist in the database. - Invoice bad input: The handler's
_BadInputexception covers malformed arguments (bad invoice ID, invalid amount, unknown status filter, too few arguments). These are returned directly to Slack as user-facing messages. - Invoice unexpected errors: Any unhandled exception in a
/pearl invoicesubcommand is caught by the top-level handler, logged at ERROR level with a full traceback, and returned as "❌ Invoice error: …". These errors do not affect other Pearl services. - IntentReceiver init failure: If
CoworkersDBraises an exception duringPearlSlackBot.__init__(e.g.,coworkers_shareddatabase unreachable), the error is caught and logged as a warning. The bot continues to start — all analytics, goals, experiment, and invoice features remain fully functional. Intent-inbox operations (list_inbox,submit_backbrief, etc.) will surface the error when invoked. - Boot-time migration failure:
ensure_schema_current()(pearl#126) is called duringPearlSlackBot.__init__afterIntentReceiverinitialization. It has never-raises semantics: if migration application fails (e.g., database unreachable, SQL error), the error is logged at ERROR level with a full traceback, but the bot continues to boot normally. Intent-loop paths may fail at query time if the schema is not current. Migrations that succeeded before a mid-sequence failure are committed and retained — only the return value is lost. SetCOWORKERS_SKIP_MIGRATIONS=trueto opt out entirely (see Configuration). - Intent command errors: Each
/pearl intentsubcommand catches exceptions from theIntentReceivermixin and returns a descriptive Slack-formatted error (e.g., "Inbox unavailable: …", "
View failed: …", "
Back-brief failed: …"). These errors do not affect other Pearl services.
- Effect command errors: The
/pearl effect recordsubcommand catches exceptions fromreport_effect_evidence()and returns a descriptive error (e.g., "Evidence record failed: …"). These errors do not affect other Pearl services.
- Council command errors: The
/pearl council proposesubcommand catches exceptions frompropose_council_terms()and returns a descriptive error (e.g., "Council proposal failed: …"). Invalid proposal types (not one of
give_up,defer,share,compromise) are rejected before calling the mixin. - Intent authority errors: Write methods (
submit_backbrief,log_deviation,report_effect_evidence) raiseNotAssignedErrorif Pearl is not in the intent'sassigneeslist. This is aPermissionErrorsubclass. The intent, effect, and council command handlers catchNotAssignedErrorand return a user-friendly refusal message (e.g., "Pearl is not in the assignees of intent
#8— back-brief refused."). - Experiment API errors: Each experiment subcommand catches exceptions from the
ExperimentServiceand returns a descriptive error message (e.g., "Failed to create experiment: Connection refused", "Failed to collect metrics for experiment #1: ..."). These errors do not affect other Pearl services. - Goals API errors: Each goals subcommand catches exceptions from the
MatomoGoalsServiceand returns a descriptive error message (e.g., "Failed to list goals: Connection refused"). These errors do not affect other Pearl services. - Matomo authentication errors: The Matomo collector distinguishes auth failures from other errors using the
MatomoAuthErrorexception. HTTP 401 (invalid/expired token) and 403 (insufficient permissions) responses, as well as Matomo API-leveltoken_autherrors, raiseMatomoAuthError. When aMatomoAuthErroris caught, the collector'sauth_statusis set toinvalidand collection is skipped for that cycle. On the next collection cycle, the collector automatically retries auth validation — a restart is no longer required for recovery. Non-auth errors (network timeouts, other API errors) do not disable future collection attempts. TheMatomoGoalsServiceandExperimentServicealso raiseMatomoAuthErroron 401/403 responses from the Matomo API. - Tier 3 collector failures: Errors in Tier 3 collectors (
VisitFrequency.get,Referrers.getReferrerType,DevicesDetection.getType) are logged as warnings but do not prevent Tier 1 metrics from being collected. Each Tier 3 collector is isolated — a failure in one does not affect the others. - Live fetch failure: If the live API call fails during validation, all metrics are marked
missing_liveand the error is included in the report. - Collection errors: The
collectcommand reports per-metric results asrecorded,skipped(duplicate), orerrors. - Matomo audit failures: During a dashboard audit, if the Matomo collector is not configured, the audit returns a
blockedfinding. If auth validation fails (invalid token or network error), the audit returns afailedfinding withcriticalseverity and skips data freshness checks. Collection errors during the audit produce awarning-level finding with a recommendation to check the Matomo API. - Unknown subcommands: Returns an error message with a hint to run
/pearl analytics help,/pearl goals help,/pearl experiment help,/pearl intent help,/pearl effect help,/pearl council help, or/pearl invoice help.
Prerequisites¶
- Redis + PostgreSQL — Required by
MemoryManagerandRevenueAnalyticsServicefor metric storage. Also required byMatomoGoalsServiceandExperimentServicefor goal and experiment data. coworkers_sharedPostgreSQL database — Required by theIntentReceivermixin (viaagents_shared.coworkers.CoworkersDB) for the M6 Commander intent inbox, and by theInvoiceServicefor thefinance_invoicestable andfinance_ar_agingview. Connection is configured viaCOWORKERS_PG_*env vars. If unavailable at startup, the bot degrades gracefully — analytics features are unaffected. On boot,ensure_schema_current()applies any pending migrations fromagents-sharedto this database (pearl#126), including migration 006 which creates thefinance_invoicestable. Migration failures are non-fatal — see Error Handling.- Weaviate — Required for the
index-dashboardssubcommand (vector memory). - Network access — Pearl must be able to reach the TheSmithy metrics endpoint and (optionally) the Matomo instance. The Matomo collector now calls four API methods (
VisitsSummary.get,VisitFrequency.get,Referrers.getReferrerType,DevicesDetection.getType). The dashboard audit service also requires Matomo network access to perform live auth validation and data freshness checks. agents-sharedpackage — Must include thecoworkersmodule (added in agents-shared !25) and thefinancemodule (providingInvoice,InvoiceStatus,parse_amount_to_cents,format_cents). The CI pipeline installs agents-shared with--force-reinstall --no-depsto ensure new modules are picked up even when the package is cached in the CI venv. The package also providesensure_schema_current()and the migration files applied on boot.
Testing¶
Pearl Analytics tests use pytest with pytest-asyncio for async service methods. Services are isolated using unittest.mock.AsyncMock (for async methods like collect() and validate_auth()) and MagicMock (for sync dependencies like revenue_service). External APIs are never called in tests — all HTTP calls are patched at the service boundary.
Test files and coverage:
| Test file | Covers | Key scenarios |
|---|---|---|
tests/test_metric_collector_service.py |
MetricCollectorService |
Successful collection of all 7 metrics, dedup skipping, fetch failures, per-metric error handling, correct source/recorded_by tagging |
tests/test_matomo_collector_service.py |
MatomoCollectorService |
Tier 1 collection (5 metrics), Tier 3 isolation (failures don't break Tier 1), auth validation, MatomoAuthError handling, bounce_rate string parsing, dedup |
tests/test_analytics_validation_service.py |
AnalyticsValidationService |
All-ok validation, drift detection, staleness detection, missing_stored/missing_live states, dashboard coverage reporting |
tests/test_dashboard_audit_service.py |
DashboardAuditService |
Grit Growth audit (maps validation results to findings), Matomo audit (auth + data freshness), blocked dashboard findings, Slack report formatting, channel posting |
tests/test_analytics_command_handler.py |
AnalyticsCommandHandler |
Subcommand routing, not-configured messages, help output, unknown subcommand error, formatted report output |
tests/test_scheduler_functions.py |
Background schedulers | Initial delays, interval defaults, custom intervals from env vars, loop termination via CancelledError |
tests/test_invoice_service.py |
InvoiceService |
Draft creation, sequential invoice numbering, negative amount rejection, due-before-issued rejection, founder-approval gate (authorized/unauthorized/empty approver), state machine transitions (full lifecycle, overdue→paid, void from any non-terminal, terminal state enforcement), missing invoice handling, list/status-filter reads, AR aging bucket ordering, Postgres round-trip (integration, skipped in CI) |
tests/test_invoice_command_handler.py |
InvoiceCommandHandler |
Subcommand routing, create with notes/bad amount/negative amount/too few args, approve with user_id passthrough/unauthorized message, send/paid/void transitions, list/status-filter/bad-status, AR aging/empty aging, service-unavailable degradation |
Running tests locally:
# Run all analytics-related tests
pytest tests/test_metric_collector_service.py \
tests/test_matomo_collector_service.py \
tests/test_analytics_validation_service.py \
tests/test_dashboard_audit_service.py \
tests/test_analytics_command_handler.py \
tests/test_scheduler_functions.py -v
# Run invoice tests
pytest tests/test_invoice_service.py \
tests/test_invoice_command_handler.py -v
# Run with coverage
pytest tests/ -v --cov=src --cov-report=term
CI pipeline — The test stage runs pytest tests/ -v --cov=src with markers excluding integration tests (tests marked requires_redis, requires_postgres, requires_slack, etc. are skipped). The lint stage checks formatting with black --check src/ tests/.
Writing new tests — Follow the existing patterns:
- Create fixtures for
revenue_service(MagicMock withrecord_metricreturningTrueandget_latestreturningNone) and the service under test. - Patch external calls (e.g.,
_fetch,_fetch_visits_summary,_fetch_matomo_api) withAsyncMockat the service object level. - For scheduler tests, use the
make_sleep_breakerfixture pattern: a factory that returns a fakeasyncio.sleepwhich raisesCancelledErrorafter N calls, breaking out of the infinite scheduler loop predictably. - For invoice tests, use the in-memory
FakeDBandFakeApproversdoubles (seetest_invoice_service.py) to exercise the state machine and approval gate without Postgres. TheFakeDBsimulates cursor operations for INSERT, UPDATE, SELECT, and the AR aging view.
Troubleshooting¶
Metric collector reports all errors:
- Verify
THESMITHY_METRICS_URLpoints to the correct endpoint (https://glassumbrella.io/grit/api/metrics/agent/— note the/grit/path prefix, not thegrit.subdomain). - Check that
THESMITHY_METRICS_API_KEYis set inpearl-secretsand matches the server-side agent key. - Look for
Failed to fetch metrics from TheSmithyin pod logs (kubectl logs -n ai-coworkers deployment/pearl | grep "Failed to fetch").
Matomo auth failing:
- Check pod logs for
Matomo auth FAILEDor the:warning: Matomo auth failedSlack alert in the channel configured byPEARL_MATOMO_ALERT_CHANNEL. - Verify
MATOMO_TOKEN_AUTHis non-empty and valid — the token can be regenerated in the Matomo admin UI under Personal > Security > Auth tokens. - The collector retries auth validation on every collection cycle automatically. A restart is not required for recovery — once the token is fixed, the next cycle will succeed and post a
:white_check_mark: Matomo auth recoverednotice. - If
MATOMO_TOKEN_AUTHis set to an empty or whitespace-only string, the collector is disabled at startup (check logs forMATOMO_TOKEN_AUTH is set but empty/whitespace).
Validation shows all metrics as missing_stored:
- This means Pearl has no stored snapshots yet. Run
/pearl analytics collectto seed initial baselines, then re-run/pearl analytics validate.
Validation shows all metrics as missing_live:
- The live API call to TheSmithy failed. Check the
Fetch error:line in the validation output and verify TheSmithy endpoint reachability and API key.
Scheduler not running:
- Confirm the
_ENABLEDenv var is set totrueinkubernetes/configmap.yaml(e.g.,PEARL_METRIC_COLLECTOR_ENABLED: "true"). - Check pod logs for
scheduler startedmessages — each scheduler logs on launch. - If the interval env var has a non-integer value (e.g.,
=5m),_interval_seconds()logs a warning and falls back to the default. Search logs foris not an integer; falling back.
Audit posts nothing to #revenue:
- The audit only posts to the channel when actionable findings exist (at least one
failedorwarningstatus). If all checks pass, no message is sent. - Verify
PEARL_REVENUE_CHANNEL_IDis set to a valid Slack channel ID in the configmap.
Tier 3 Matomo metrics missing but Tier 1 works:
- Tier 3 failures are logged as warnings (
Tier 3 collector <method> failed (non-fatal)) and do not affect Tier 1 collection. Check logs for the specific Matomo API method that failed. - The Matomo instance may not have the required plugins enabled (VisitFrequency, Referrers, DevicesDetection).
Invoice commands not available:
- Verify
PEARL_INVOICE_ENABLEDis set to"true"inkubernetes/configmap.yaml. The feature ships dark (default"false"). After setting it, apply the configmap and restart the pod: - Check pod logs for
Invoice handler initialized(success) orInvoiceCommandHandler unavailable(failure, typically aCoworkersDBconnection issue). - If the handler initialized but the command is not registered, check for
Invoice command disabled (PEARL_INVOICE_ENABLED != true)in logs.
Invoice approval rejected:
- Only Slack users listed in
config/content_approvers.yamlwithinvoicein theircan_approvelist may approve invoices. Currently only founders have this permission. - The handler returns "🔒 \<user> is not authorized to approve invoices." when the approver check fails.
Invoice state transition errors:
- The service enforces a strict state machine. Common illegal transitions: sending a draft (must approve first), voiding a paid invoice (terminal state). The error message includes the current status and target status.
Feature Repositories¶
pearl→ project_id:the-smithy1/agents/pearl
Code Paths to Explore¶
tests/test_analytics_command_handler.pyinpearlsrc/slack/main.pyinpearlsrc/services/analytics_validation_service.pyinpearlsrc/handlers/analytics_command_handler.pyinpearlsrc/services/invoice_service.pyinpearlsrc/handlers/invoice_command_handler.pyinpearlconfig/content_approvers.yamlinpearl