mango-lollipop 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/schema.ts ADDED
@@ -0,0 +1,394 @@
1
+ // =============================================================================
2
+ // Mango Lollipop — TypeScript Schema & Validation
3
+ // =============================================================================
4
+
5
+ // -----------------------------------------------------------------------------
6
+ // Type Aliases
7
+ // -----------------------------------------------------------------------------
8
+
9
+ export type Channel = "email" | "sms" | "in-app" | "push";
10
+
11
+ export type AARRRStage = "AQ" | "AC" | "RV" | "RT" | "RF";
12
+
13
+ // -----------------------------------------------------------------------------
14
+ // Core Message Interfaces
15
+ // -----------------------------------------------------------------------------
16
+
17
+ export interface Trigger {
18
+ event: string;
19
+ type: "event" | "scheduled" | "behavioral";
20
+ schedule?: string;
21
+ }
22
+
23
+ export interface Guard {
24
+ condition: string;
25
+ expression: string;
26
+ }
27
+
28
+ export interface Suppression {
29
+ condition: string;
30
+ expression: string;
31
+ }
32
+
33
+ export interface CTA {
34
+ text: string;
35
+ url?: string;
36
+ }
37
+
38
+ export interface Message {
39
+ id: string;
40
+ stage: AARRRStage | "TX";
41
+ name: string;
42
+ classification: "transactional" | "lifecycle";
43
+ trigger: Trigger;
44
+ wait: string;
45
+ guards: Guard[];
46
+ suppressions: Suppression[];
47
+ subject: string;
48
+ preheader?: string;
49
+ body: string;
50
+ cta: CTA;
51
+ channel: Channel;
52
+ format: "plain" | "rich";
53
+ from: string;
54
+ segment: string;
55
+ tags: string[];
56
+ goal: string;
57
+ comments: string;
58
+ }
59
+
60
+ // -----------------------------------------------------------------------------
61
+ // Voice & Persona Interfaces
62
+ // -----------------------------------------------------------------------------
63
+
64
+ export interface SenderPersona {
65
+ name: string;
66
+ role: string;
67
+ use_for: string[];
68
+ }
69
+
70
+ export interface VoiceProfile {
71
+ tone: string;
72
+ formality: number; // 1-5
73
+ emoji_usage: "none" | "light" | "heavy";
74
+ signature_style: string;
75
+ sample_phrases: string[];
76
+ sender_personas: SenderPersona[];
77
+ }
78
+
79
+ // -----------------------------------------------------------------------------
80
+ // Event Taxonomy
81
+ // -----------------------------------------------------------------------------
82
+
83
+ export interface EventTaxonomy {
84
+ identity: string[];
85
+ activation: string[];
86
+ engagement: string[];
87
+ conversion: string[];
88
+ retention: string[];
89
+ }
90
+
91
+ // -----------------------------------------------------------------------------
92
+ // Analysis (analysis.json output from the analyze skill)
93
+ // -----------------------------------------------------------------------------
94
+
95
+ export interface AnalysisCompany {
96
+ name: string;
97
+ product_type: string;
98
+ target_audience: string;
99
+ key_value_prop: string;
100
+ aha_moment: string;
101
+ key_features: string[];
102
+ pricing_model: string;
103
+ }
104
+
105
+ export interface AnalysisTags {
106
+ sources: string[];
107
+ plans: string[];
108
+ segments: string[];
109
+ features: string[];
110
+ }
111
+
112
+ export interface ExistingPerformance {
113
+ open_rate_avg: string;
114
+ click_rate_avg: string;
115
+ problem_areas: string[];
116
+ }
117
+
118
+ export interface ExistingMessaging {
119
+ messages_count: number;
120
+ stages_covered: (AARRRStage | "TX")[];
121
+ stages_missing: (AARRRStage | "TX")[];
122
+ channels_used: Channel[];
123
+ performance: ExistingPerformance;
124
+ primary_goal: string;
125
+ messages: unknown[];
126
+ }
127
+
128
+ export interface Analysis {
129
+ path: "fresh" | "existing";
130
+ company: AnalysisCompany;
131
+ channels: Channel[];
132
+ voice: VoiceProfile;
133
+ events: EventTaxonomy;
134
+ tags: AnalysisTags;
135
+ existing?: ExistingMessaging;
136
+ recommendations: string[];
137
+ }
138
+
139
+ // -----------------------------------------------------------------------------
140
+ // Project Config (mango-lollipop.json)
141
+ // -----------------------------------------------------------------------------
142
+
143
+ export interface ProjectConfig {
144
+ name: string;
145
+ version: string;
146
+ created: string;
147
+ stage: string;
148
+ path: "fresh" | "existing" | null;
149
+ channels: Channel[];
150
+ analysis: Analysis | null;
151
+ matrix: { messages: Message[] } | null;
152
+ }
153
+
154
+ // -----------------------------------------------------------------------------
155
+ // Constants
156
+ // -----------------------------------------------------------------------------
157
+
158
+ const VALID_CHANNELS: Channel[] = ["email", "sms", "in-app", "push"];
159
+ const VALID_STAGES: (AARRRStage | "TX")[] = ["AQ", "AC", "RV", "RT", "RF", "TX"];
160
+ const VALID_AARRR_STAGES: AARRRStage[] = ["AQ", "AC", "RV", "RT", "RF"];
161
+
162
+ // ISO 8601 duration pattern: P[nY][nM][nD][T[nH][nM][nS]] or PnW
163
+ const ISO_8601_DURATION_RE =
164
+ /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$|^P(\d+)W$/;
165
+
166
+ // -----------------------------------------------------------------------------
167
+ // Type Guards
168
+ // -----------------------------------------------------------------------------
169
+
170
+ export function isValidChannel(ch: unknown): ch is Channel {
171
+ return typeof ch === "string" && VALID_CHANNELS.includes(ch as Channel);
172
+ }
173
+
174
+ export function isValidStage(stage: unknown): stage is AARRRStage {
175
+ return typeof stage === "string" && VALID_AARRR_STAGES.includes(stage as AARRRStage);
176
+ }
177
+
178
+ // -----------------------------------------------------------------------------
179
+ // Validation Helpers
180
+ // -----------------------------------------------------------------------------
181
+
182
+ export function isValidWaitDuration(wait: unknown): boolean {
183
+ if (typeof wait !== "string") return false;
184
+ return ISO_8601_DURATION_RE.test(wait);
185
+ }
186
+
187
+ export interface ValidationResult {
188
+ valid: boolean;
189
+ errors: string[];
190
+ }
191
+
192
+ export function validateMessage(msg: unknown): ValidationResult {
193
+ const errors: string[] = [];
194
+
195
+ if (typeof msg !== "object" || msg === null) {
196
+ return { valid: false, errors: ["Message must be a non-null object"] };
197
+ }
198
+
199
+ const m = msg as Record<string, unknown>;
200
+
201
+ // Required string fields
202
+ const requiredStrings: [string, string][] = [
203
+ ["id", "id"],
204
+ ["name", "name"],
205
+ ["subject", "subject"],
206
+ ["body", "body"],
207
+ ["from", "from"],
208
+ ["segment", "segment"],
209
+ ["goal", "goal"],
210
+ ["wait", "wait"],
211
+ ];
212
+
213
+ for (const [field, label] of requiredStrings) {
214
+ if (typeof m[field] !== "string" || (m[field] as string).length === 0) {
215
+ errors.push(`Missing or empty required field: ${label}`);
216
+ }
217
+ }
218
+
219
+ // Stage
220
+ const validStagesAll = VALID_STAGES;
221
+ if (typeof m.stage !== "string" || !validStagesAll.includes(m.stage as AARRRStage | "TX")) {
222
+ errors.push(`Invalid stage: "${String(m.stage)}". Must be one of: ${validStagesAll.join(", ")}`);
223
+ }
224
+
225
+ // Classification
226
+ if (m.classification !== "transactional" && m.classification !== "lifecycle") {
227
+ errors.push(`Invalid classification: "${String(m.classification)}". Must be "transactional" or "lifecycle"`);
228
+ }
229
+
230
+ // Wait duration
231
+ if (typeof m.wait === "string" && !isValidWaitDuration(m.wait)) {
232
+ errors.push(`Invalid wait duration: "${m.wait}". Must be ISO 8601 duration (e.g. "P0D", "PT5M", "P2D")`);
233
+ }
234
+
235
+ // Format
236
+ if (m.format !== "plain" && m.format !== "rich") {
237
+ errors.push(`Invalid format: "${String(m.format)}". Must be "plain" or "rich"`);
238
+ }
239
+
240
+ // Channel (singular)
241
+ if (!isValidChannel(m.channel)) {
242
+ errors.push(`Invalid channel: "${String(m.channel)}". Must be one of: ${VALID_CHANNELS.join(", ")}`);
243
+ }
244
+
245
+ // Tags
246
+ if (!Array.isArray(m.tags)) {
247
+ errors.push("tags must be an array");
248
+ }
249
+
250
+ // Guards
251
+ if (!Array.isArray(m.guards)) {
252
+ errors.push("guards must be an array");
253
+ }
254
+
255
+ // Suppressions
256
+ if (!Array.isArray(m.suppressions)) {
257
+ errors.push("suppressions must be an array");
258
+ }
259
+
260
+ // Trigger
261
+ if (typeof m.trigger !== "object" || m.trigger === null) {
262
+ errors.push("trigger must be a non-null object");
263
+ } else {
264
+ const t = m.trigger as Record<string, unknown>;
265
+ if (typeof t.event !== "string" || t.event.length === 0) {
266
+ errors.push("trigger.event is required");
267
+ }
268
+ if (t.type !== "event" && t.type !== "scheduled" && t.type !== "behavioral") {
269
+ errors.push(`Invalid trigger.type: "${String(t.type)}". Must be "event", "scheduled", or "behavioral"`);
270
+ }
271
+ }
272
+
273
+ // CTA
274
+ if (typeof m.cta !== "object" || m.cta === null) {
275
+ errors.push("cta must be a non-null object");
276
+ } else {
277
+ const c = m.cta as Record<string, unknown>;
278
+ if (typeof c.text !== "string" || c.text.length === 0) {
279
+ errors.push("cta.text is required");
280
+ }
281
+ }
282
+
283
+ return { valid: errors.length === 0, errors };
284
+ }
285
+
286
+ export function validateAnalysis(analysis: unknown): ValidationResult {
287
+ const errors: string[] = [];
288
+
289
+ if (typeof analysis !== "object" || analysis === null) {
290
+ return { valid: false, errors: ["Analysis must be a non-null object"] };
291
+ }
292
+
293
+ const a = analysis as Record<string, unknown>;
294
+
295
+ // Path
296
+ if (a.path !== "fresh" && a.path !== "existing") {
297
+ errors.push(`Invalid path: "${String(a.path)}". Must be "fresh" or "existing"`);
298
+ }
299
+
300
+ // Company
301
+ if (typeof a.company !== "object" || a.company === null) {
302
+ errors.push("company is required");
303
+ } else {
304
+ const c = a.company as Record<string, unknown>;
305
+ const companyFields = ["name", "product_type", "target_audience", "key_value_prop", "aha_moment", "pricing_model"];
306
+ for (const field of companyFields) {
307
+ if (typeof c[field] !== "string" || (c[field] as string).length === 0) {
308
+ errors.push(`Missing or empty company.${field}`);
309
+ }
310
+ }
311
+ if (!Array.isArray(c.key_features) || c.key_features.length === 0) {
312
+ errors.push("company.key_features must be a non-empty array");
313
+ }
314
+ }
315
+
316
+ // Channels
317
+ if (!Array.isArray(a.channels) || a.channels.length === 0) {
318
+ errors.push("Must have at least one channel");
319
+ } else {
320
+ for (const ch of a.channels) {
321
+ if (!isValidChannel(ch)) {
322
+ errors.push(`Invalid channel: "${String(ch)}". Must be one of: ${VALID_CHANNELS.join(", ")}`);
323
+ }
324
+ }
325
+ }
326
+
327
+ // Voice
328
+ if (typeof a.voice !== "object" || a.voice === null) {
329
+ errors.push("voice profile is required");
330
+ } else {
331
+ const v = a.voice as Record<string, unknown>;
332
+ if (typeof v.tone !== "string" || (v.tone as string).length === 0) {
333
+ errors.push("voice.tone is required");
334
+ }
335
+ if (typeof v.formality !== "number" || v.formality < 1 || v.formality > 5) {
336
+ errors.push("voice.formality must be a number between 1 and 5");
337
+ }
338
+ if (v.emoji_usage !== "none" && v.emoji_usage !== "light" && v.emoji_usage !== "heavy") {
339
+ errors.push(`Invalid voice.emoji_usage: "${String(v.emoji_usage)}". Must be "none", "light", or "heavy"`);
340
+ }
341
+ if (!Array.isArray(v.sample_phrases)) {
342
+ errors.push("voice.sample_phrases must be an array");
343
+ }
344
+ if (!Array.isArray(v.sender_personas)) {
345
+ errors.push("voice.sender_personas must be an array");
346
+ }
347
+ }
348
+
349
+ // Events
350
+ if (typeof a.events !== "object" || a.events === null) {
351
+ errors.push("events taxonomy is required");
352
+ } else {
353
+ const e = a.events as Record<string, unknown>;
354
+ const eventCategories = ["identity", "activation", "engagement", "conversion", "retention"];
355
+ for (const cat of eventCategories) {
356
+ if (!Array.isArray(e[cat])) {
357
+ errors.push(`events.${cat} must be an array`);
358
+ }
359
+ }
360
+ }
361
+
362
+ // Tags
363
+ if (typeof a.tags !== "object" || a.tags === null) {
364
+ errors.push("tags is required");
365
+ }
366
+
367
+ // Recommendations
368
+ if (!Array.isArray(a.recommendations)) {
369
+ errors.push("recommendations must be an array");
370
+ }
371
+
372
+ // PATH B: existing (only validated if path is "existing")
373
+ if (a.path === "existing") {
374
+ if (typeof a.existing !== "object" || a.existing === null) {
375
+ errors.push('existing messaging data is required when path is "existing"');
376
+ } else {
377
+ const ex = a.existing as Record<string, unknown>;
378
+ if (typeof ex.messages_count !== "number") {
379
+ errors.push("existing.messages_count must be a number");
380
+ }
381
+ if (!Array.isArray(ex.stages_covered)) {
382
+ errors.push("existing.stages_covered must be an array");
383
+ }
384
+ if (!Array.isArray(ex.stages_missing)) {
385
+ errors.push("existing.stages_missing must be an array");
386
+ }
387
+ if (typeof ex.primary_goal !== "string") {
388
+ errors.push("existing.primary_goal is required");
389
+ }
390
+ }
391
+ }
392
+
393
+ return { valid: errors.length === 0, errors };
394
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "mango-lollipop",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "AI-powered lifecycle messaging generator for SaaS companies",
6
+ "license": "MIT",
7
+ "author": "Sasha Kai",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/sr-kai/mango-lollipop.git"
11
+ },
12
+ "keywords": [
13
+ "lifecycle-messaging",
14
+ "saas",
15
+ "aarrr",
16
+ "email-marketing",
17
+ "claude-code",
18
+ "ai"
19
+ ],
20
+ "bin": {
21
+ "mango-lollipop": "bin/mango-lollipop.js"
22
+ },
23
+ "files": [
24
+ "bin/",
25
+ "dist/",
26
+ "skills/",
27
+ "templates/",
28
+ "lib/",
29
+ "CLAUDE.md"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsc",
33
+ "prepublishOnly": "npm run build"
34
+ },
35
+ "dependencies": {
36
+ "commander": "^13.1.0",
37
+ "open": "^10.1.0",
38
+ "xlsx-js-style": "^1.2.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^22.13.1",
42
+ "typescript": "^5.7.3"
43
+ }
44
+ }
@@ -0,0 +1,248 @@
1
+ # Audit Existing Lifecycle Messaging
2
+
3
+ You are a lifecycle messaging auditor. Your job is to perform a deep analysis of a company's existing messaging system, score its maturity, identify gaps, and provide actionable recommendations for improvement.
4
+
5
+ This skill can be used:
6
+ - As part of PATH B onboarding (called from the `start` skill)
7
+ - Standalone, to audit messaging at any time after initial setup
8
+ - To re-audit after changes have been made via the `iterate` skill
9
+
10
+ ---
11
+
12
+ ## Step 1: Accept Input
13
+
14
+ The user can provide their existing messages in any format. Accept all of the following:
15
+
16
+ ### Paste
17
+ The user pastes message content directly into the conversation. This could be:
18
+ - Full email bodies
19
+ - Subject lines only
20
+ - A list of message descriptions
21
+ - Screenshots (describe what you see and ask for confirmation)
22
+
23
+ ### Upload
24
+ The user provides a file:
25
+ - Spreadsheet (CSV, XLSX) -- parse rows as messages
26
+ - Document (PDF, DOCX, TXT) -- extract message descriptions
27
+ - HTML files -- parse as email templates
28
+ - JSON/YAML -- parse structured data
29
+
30
+ ### Natural Language Description
31
+ The user describes their setup conversationally:
32
+ - "We have a 5-email welcome series, a trial expiring email, and a weekly digest"
33
+ - "We use Intercom for in-app messages and Mailchimp for email"
34
+ - "We send about 12 emails total across the customer lifecycle"
35
+
36
+ ### Combination
37
+ The user may provide a mix of the above. Aggregate all information.
38
+
39
+ For each input, ask clarifying questions if needed:
40
+ - "What channel is this message sent on?" (if not clear)
41
+ - "When does this message get sent?" (if timing is unclear)
42
+ - "Who receives this?" (if segment is unclear)
43
+
44
+ ---
45
+
46
+ ## Step 2: Normalize into Message Schema
47
+
48
+ Convert whatever the user provided into the standard Message schema. For each message, extract or infer:
49
+
50
+ | Field | Extract from input | Infer if missing |
51
+ |-------|-------------------|------------------|
52
+ | `id` | Assign sequentially (EX-01, EX-02, ...) | Always assign |
53
+ | `stage` | Map to AARRR based on content/timing | Based on trigger and content |
54
+ | `name` | Subject line or title | Generate from content |
55
+ | `classification` | TX if transactional, lifecycle otherwise | Based on content type |
56
+ | `trigger` | What event or timing initiates it | Infer from description |
57
+ | `wait` | Delay after trigger | Default to P0D if unclear |
58
+ | `guards` | Any conditions mentioned | Leave empty if unknown |
59
+ | `suppressions` | Any skip conditions | Leave empty if unknown |
60
+ | `channels` | Email, SMS, in-app, push | Ask if unclear |
61
+ | `cta` | Button/link text | Extract from body |
62
+ | `tags` | Any categorization | Suggest based on content |
63
+
64
+ Present the normalized list back to the user: "Here's what I understood from your existing messages. Is this accurate?"
65
+
66
+ ---
67
+
68
+ ## Step 3: Analyze
69
+
70
+ Perform a comprehensive analysis across these dimensions:
71
+
72
+ ### 3a. AARRR Stage Mapping
73
+ Map each existing message to an AARRR stage. Identify:
74
+ - Which stages have coverage
75
+ - Which stages are completely missing
76
+ - Which stages have partial coverage (some messages but gaps remain)
77
+
78
+ ### 3b. TX vs. Lifecycle Classification
79
+ Classify each message:
80
+ - **Transactional:** Email verification, password reset, receipts, alerts triggered by user action
81
+ - **Lifecycle:** Welcome series, feature education, re-engagement, trial prompts, referral asks
82
+
83
+ Flag any messages that are misclassified in the user's current system (e.g., marketing content sent as transactional).
84
+
85
+ ### 3c. Coverage Scoring (0-5 per stage)
86
+ Score each AARRR stage on a 0-5 scale:
87
+
88
+ | Score | Meaning |
89
+ |-------|---------|
90
+ | 0 | No messages for this stage |
91
+ | 1 | 1 basic message, minimal effort |
92
+ | 2 | 2-3 messages, covers basics but gaps remain |
93
+ | 3 | Good coverage, most key scenarios handled |
94
+ | 4 | Strong coverage with good timing and guards |
95
+ | 5 | Excellent -- comprehensive, well-timed, multi-channel, with suppressions |
96
+
97
+ ### 3d. Channel Diversity
98
+ - Are they using only email? Missing in-app, push, SMS opportunities?
99
+ - For each message, is it on the right channel(s)?
100
+ - Identify messages that would benefit from additional channels
101
+
102
+ ### 3e. Timing Analysis
103
+ - Are messages well-spaced or clustered?
104
+ - Are there long silent periods where no messages are sent?
105
+ - Is the cadence too aggressive (e.g., daily emails for a week)?
106
+ - Are wait durations appropriate for each stage?
107
+
108
+ ### 3f. Guard & Suppression Logic
109
+ - Are they suppressing messages for users who already completed the desired action?
110
+ - Are there guard conditions to prevent irrelevant sends?
111
+ - Identify messages that should have guards/suppressions but don't
112
+
113
+ ### 3g. Voice Consistency
114
+ - Is the tone consistent across all messages?
115
+ - Are sender personas used appropriately?
116
+ - Are there jarring tone shifts between messages?
117
+ - Does the formality level match across stages?
118
+
119
+ ### 3h. CTA Clarity
120
+ - Does every lifecycle message have a clear, specific CTA?
121
+ - Are CTAs action-oriented (verb-first)?
122
+ - Is there only one primary CTA per message?
123
+ - Are CTAs relevant to the message's goal?
124
+
125
+ ### 3i. Personalization
126
+ - Are personalization tokens used effectively?
127
+ - Is there over-personalization (feels creepy) or under-personalization (feels generic)?
128
+ - Are there opportunities for dynamic content based on user behavior?
129
+
130
+ ### 3j. Tag Opportunities
131
+ - Suggest tags that could improve organization and filtering
132
+ - Identify patterns that could benefit from tagging (by feature, plan, segment, etc.)
133
+
134
+ ---
135
+
136
+ ## Step 4: Score Overall Maturity (1-5)
137
+
138
+ Calculate an overall maturity score:
139
+
140
+ | Level | Name | Description |
141
+ |-------|------|-------------|
142
+ | 1 | **Basic** | Only transactional messages. No lifecycle strategy. |
143
+ | 2 | **Developing** | Some lifecycle messages (welcome, maybe trial expiring). Big gaps in activation and retention. |
144
+ | 3 | **Established** | Covers most AARRR stages. Reasonable timing. Some personalization. Missing advanced features (multi-channel, suppressions, tags). |
145
+ | 4 | **Advanced** | Multi-channel, good suppression logic, personalized, well-timed. Minor gaps only. |
146
+ | 5 | **Best-in-class** | Comprehensive coverage, multi-channel, excellent timing, smart suppressions, fully personalized, tagged and organized. |
147
+
148
+ ---
149
+
150
+ ## Step 5: Benchmark Performance (if data provided)
151
+
152
+ If the user shared performance metrics (open rates, click rates, conversion rates, unsubscribe rates):
153
+
154
+ ### Industry Benchmarks (SaaS)
155
+ | Metric | Poor | Average | Good | Excellent |
156
+ |--------|------|---------|------|-----------|
157
+ | Open Rate | < 15% | 15-25% | 25-35% | > 35% |
158
+ | Click Rate | < 1.5% | 1.5-3% | 3-5% | > 5% |
159
+ | Click-to-Open | < 8% | 8-12% | 12-18% | > 18% |
160
+ | Unsubscribe | > 1% | 0.5-1% | 0.2-0.5% | < 0.2% |
161
+ | Trial Conversion | < 5% | 5-10% | 10-20% | > 20% |
162
+
163
+ Compare each message's metrics against these benchmarks. Flag underperformers.
164
+
165
+ Correlate timing and content with performance:
166
+ - Do messages with shorter wait times perform better or worse?
167
+ - Do personalized subject lines outperform generic ones?
168
+ - Which CTAs drive the highest click rates?
169
+
170
+ ---
171
+
172
+ ## Output
173
+
174
+ Present the audit results in a structured report format:
175
+
176
+ ### 1. Maturity Scorecard
177
+
178
+ ```
179
+ Overall Maturity: 2.5/5 -- Developing
180
+
181
+ Acquisition: [===------] 3/5 -- Welcome + verification present
182
+ Activation: [=--------] 1/5 -- Only 1 feature email, big gap
183
+ Revenue: [==-------] 2/5 -- Trial expiring exists, no in-app
184
+ Retention: [---------] 0/5 -- No re-engagement messaging
185
+ Referral: [---------] 0/5 -- No referral or invite prompts
186
+ ```
187
+
188
+ ### 2. Gap Analysis
189
+
190
+ For each gap found, provide:
191
+ - **Gap:** What's missing
192
+ - **Impact:** Why it matters (e.g., "Users who don't discover Feature X in the first week are 3x more likely to churn")
193
+ - **Priority:** Critical / High / Medium / Low
194
+ - **Recommendation:** Specific action to take
195
+
196
+ ### 3. Priority Recommendations
197
+
198
+ Ranked list of improvements, ordered by impact:
199
+ 1. Most impactful improvement first
200
+ 2. Quick wins early (low effort, high impact)
201
+ 3. Strategic investments later (high effort, high impact)
202
+
203
+ For each recommendation:
204
+ - What to do
205
+ - Why it matters
206
+ - Expected impact
207
+ - Effort level (low/medium/high)
208
+
209
+ ### 4. Suggested New Messages
210
+
211
+ For each gap, suggest specific messages to fill it:
212
+ - Message name and ID
213
+ - AARRR stage
214
+ - Trigger and timing
215
+ - Channel(s)
216
+ - Brief description of content
217
+ - Why this message is needed
218
+
219
+ ---
220
+
221
+ ## File Output
222
+
223
+ If an `analysis.json` exists in the project directory, update it with audit results in the `existing` field.
224
+
225
+ If running standalone (no existing project), write the audit results to `{project-directory}/audit-results.json` with this structure:
226
+
227
+ ```json
228
+ {
229
+ "audit_date": "2025-01-15T10:30:00Z",
230
+ "maturity_score": 2.5,
231
+ "stage_scores": {
232
+ "AQ": 3,
233
+ "AC": 1,
234
+ "RV": 2,
235
+ "RT": 0,
236
+ "RF": 0
237
+ },
238
+ "existing_messages": [...],
239
+ "gaps": [...],
240
+ "recommendations": [...],
241
+ "suggested_messages": [...],
242
+ "performance_benchmarks": {...}
243
+ }
244
+ ```
245
+
246
+ After presenting the audit, ask: "Would you like me to generate a complete matrix that incorporates your existing messages and fills these gaps? I'll preserve what's working and improve what isn't."
247
+
248
+ If yes, proceed to run the `generate-matrix` skill with the audit data.