Files
rm-circle-team-router/suite-email.js
T
martbost 3f8d23d66c Circle Suite: complete the ladder — every paid level now unlocks live software
Four new tools, so no tier is a placeholder any more:

L4 Voice Profile (/suite/voice) — five short answers that ride along with
every Copy Engine and Email Engine generation. It sits BEFORE the compliance
block in the prompt on purpose: a personal voice must never be able to talk
the model out of the honesty rules. suite-email builds its own prompt, so the
profile is threaded in there separately or email would keep sounding generic
while the Copy Engine sounded like the member.

L5 Split Tester (/suite/split) — 2-3 variants launched as one test with the
impressions split evenly, then click counters compared. Deliberately
conservative: it will not declare a winner until both arms have real volume
AND the leader is clearly ahead, because a 3-click gap on 400 impressions is
noise. A genuine tie is reported as a tie, which is useful information too.
A failed arm rolls back the whole test rather than leaving half of it running.
Apex is the right home for it — that is where impressions jump to 50,000.

L6 Funnel Factory (/suite/funnel) — extra named pages at /p/<id>/<slug>, so a
leader can run one page per audience and point different ads at each. Missing
slugs fall back to the member's main page, same no-dead-ends rule as /p/<id>.

L7 Leader Ops (/suite/leader) — the coaching radar already existed in chain.js
and only admin could see it, which was backwards: the leaders running those
legs need it more than anyone. Now scoped to the member's own organisation,
with a written plan built on contract-read facts only. Follows the
teach-forward rule — the digest gives the leader words to hand down, not just
numbers to act on.

Tile copy across the wall and the payout alerts now describes what actually
exists rather than what was sketched. suite-tools.js was stale in both
directions (it still had the Traffic Desk at L5 and L3 not live).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 10:49:36 -05:00

98 lines
4.5 KiB
JavaScript

// Circle Suite — Email Engine (L3). Same engine, same compliance layer as the
// Copy Engine; the difference is shape: it writes SEQUENCES, not single pieces.
//
// Output contract is the same trick that works elsewhere: labelled plain text
// parsed deterministically, with a forgiving parser (models drop colons and
// add markdown emphasis).
'use strict';
const suiteAI = require('./suite-ai');
const KINDS = {
welcome: {
label: 'Welcome sequence',
count: 4,
brief: 'a 4-email welcome sequence for someone who just joined the team under this member. Email 1 welcomes them and points at the first 48 hours. Email 2 covers getting their first two. Email 3 handles the doubt that shows up in week one. Email 4 is about teaching their two to do the same.'
},
followup: {
label: 'Follow-up sequence',
count: 4,
brief: 'a 4-email follow-up sequence for someone who looked at the opportunity but has not joined. Space the emails a few days apart in tone. No pressure, no fake deadlines, no guilt. Each email should be useful on its own and easy to ignore.'
},
reengage: {
label: 'Re-engagement sequence',
count: 3,
brief: 'a 3-email re-engagement sequence for a member who joined but went quiet. Warm, no shaming, assume life got busy. Make the next step small and specific.'
},
broadcast: {
label: 'Single broadcast',
count: 1,
brief: 'ONE standalone broadcast email to a list about the team build.'
}
};
function buildPrompt(kind, brief, member) {
const k = KINDS[kind] || KINDS.broadcast;
const link = (member && member.link) || '';
return (
'Write ' + k.brief + '\n\n' +
'CONTEXT FROM THE MEMBER (who the list is, what to emphasise):\n' +
(brief || '(nothing specific — write something honest and general)') + '\n\n' +
'Return EXACTLY this shape, repeated ' + k.count + ' time(s), and nothing else:\n' +
'EMAIL 1\n' +
'SUBJECT: one line, under 60 characters, no clickbait, no ALL CAPS\n' +
'BODY: 90-170 words, plain text, short paragraphs separated by blank lines, ending with a sign-off line\n' +
(k.count > 1 ? 'EMAIL 2\nSUBJECT: ...\nBODY: ...\n(and so on)\n' : '') + '\n' +
(link ? 'Where a link belongs, use exactly: ' + link + '\n' : 'Do not invent a link.\n') +
'No HTML, no markdown headers, no emoji in subject lines, no merge tags.' +
// The member's own voice profile (level 4+), when they have one. This module
// builds its own prompt rather than going through suite-ai's message builder,
// so the profile has to be threaded in here too or email would keep writing
// in the generic team voice while the Copy Engine sounded like them.
(member && member.voice ? '\n\n' + member.voice : '')
);
}
// Tolerant parse: EMAIL n / SUBJECT / BODY, colon and emphasis optional.
function parseEmails(raw) {
const text = String(raw || '').replace(/\r/g, '');
const marks = [];
const re = /(?:^|\n)[ \t]*[*#>\s]*EMAIL[ \t]*#?\s*(\d+)[ \t]*[*#]*[ \t]*:?[ \t]*/gi;
let m;
while ((m = re.exec(text)) !== null) marks.push({ n: Number(m[1]), at: m.index, from: m.index + m[0].length });
const chunks = [];
if (!marks.length) {
chunks.push(text);
} else {
marks.forEach(function (mk, i) {
chunks.push(text.slice(mk.from, i + 1 < marks.length ? marks[i + 1].at : text.length));
});
}
const out = [];
chunks.forEach(function (c) {
const sm = c.match(/(?:^|\n)[ \t]*[*#>\s]*SUBJECT[ \t]*[*#]*[ \t]*:?[ \t]*[*#]*[ \t]*(.*)/i);
const bm = c.match(/(?:^|\n)[ \t]*[*#>\s]*BODY[ \t]*[*#]*[ \t]*:?[ \t]*[*#]*[ \t]*([\s\S]*)/i);
const subject = sm ? sm[1].replace(/[*#]+/g, '').trim().slice(0, 120) : '';
let body = bm ? bm[1] : (sm ? c.slice(c.indexOf(sm[0]) + sm[0].length) : c);
body = String(body).replace(/^[*#\s]+/, '').trim();
if (subject || body) out.push({ subject: subject || '(no subject)', body: body });
});
return out.filter(function (e) { return e.body && e.body.length > 20; });
}
async function generate(kind, brief, member) {
const raw = await suiteAI.generateRaw(buildPrompt(kind, brief, member));
let emails = parseEmails(raw);
if (!emails.length) throw new Error('The writer came back incomplete — try again.');
const link = member && member.link;
if (link) {
emails = emails.map(function (e) {
let b = e.body.replace(/https?:\/\/(?:www\.)?rmcircle\.team\/\S*/gi, link);
b = b.replace(/\[LINK\]|\{link\}/gi, link);
return { subject: e.subject, body: b };
});
}
return emails;
}
module.exports = { KINDS, generate, parseEmails };