A running log of how Coach should sound. One hypothesis at a time, each with the receipts.
4 min
2026-06-18 · governs the persona block in src/prompt.mjs · examples pulled from chat.db
Hypothesis 1: the texting was a little too cringe shipped
Coach was trying too hard. It read like an app doing a gen-z impression, not a person
texting you back. Three tells, all over the real threads: an emoji on basically every line, em dashes
everywhere, and hype filler (“we’re locked in”, “let’s get it”) bolted onto messages that
didn’t need it. It also crammed multiple sentences into one bubble instead of just sending two texts
like a human would.
The bet: drop the performance. Nonchalant by default, emoji rare and only for
emphasis, zero em dashes, one sentence per bubble. Say the true thing plainly and move on. The friend who
notices and says it — not the hype man.
What it looked like
Real exchange, agent on the right. A yes/no question (“is it better to check in the
morning”) gets answered with hype, an em dash, and a three-clause run-on bubble:
✗ before
is it better to check in the morning
we’re locked in 🔥
go get some sleep and i’ll hit you at breakfast tomorrow 🤙
honestly yeah — morning weigh-ins are more consistent, same conditions every day (right after waking up, before eating)
wanna switch it to mornings instead?
“we’re locked in 🔥” answers nothing. The actual answer is buried under an em dash and
three thoughts mashed into one bubble. Two emoji in four messages.
It wasn’t a one-off. The same energy ran through onboarding and logging:
✗ before — the onboarding sign-off
nice, i can literally see you’re in Fremont rn 👀 we’re locked in
step on the scale tomorrow at 10, before coffee — that’s day one. let’s get it 🔥
“locked in” and “let’s get it” back to back, an em dash, two emoji, two sentences per bubble.
✗ before — a 2am meal log
chipotle and i j had a few chips
it’s 2am bro 💀 when did you eat all this, and was it like a full restaurant nacho platter or bag of chips situation?
💀 plus one breathless run-on. A person sends that as two or three short texts.
The tells
Emoji on every line.🔥 👀 💀 😭 🤙 became punctuation. They stopped meaning anything. Target: rare, most messages have none.
Em dashes everywhere.“honestly yeah — morning weigh-ins…”, “that’s day one — let’s get it”. No human texts em dashes. Target: a period, a comma, or a new bubble.
Hype filler.“we’re locked in”, “let’s get it”, “recomp gang”. Pep-talk voice on messages that didn’t ask for it. Target: nonchalant, say the real thing and stop.
Two sentences in one bubble.Long run-ons instead of separate texts. Target: one sentence per bubble, split a second thought into a second bubble.
The change
Rewrote the VOICE and TEXTING STYLE blocks in src/prompt.mjs,
and swept the same fixes through every model-facing string (tool descriptions, scheduler nudges, location
callouts) so the few-shot examples stopped teaching the cringe:
Nonchalant is the default.Not hyped, not a cheerleader, not “on.” A friend who notices and says it plainly.
Emoji rare.Most messages none, never more than one, only when it adds something.
No em dashes, ever.Removed from the persona, the examples, and every tool/trigger string the model reads.
One sentence per bubble.Two thoughts becomes two bubbles, never a wall.
Killed the cringe few-shots.The old “example energy” lines were the cringe; replaced with flat, low-key ones.
The target
Same question, same answer, just… normal. No hype, no em dash, one sentence each:
✓ after
is it better to check in the morning
yeah, mornings are more consistent
same conditions every day, right after you wake up
wanna switch it to mornings?
Answers the question first. No emoji, no em dash, no “locked in.” Each text is one thought.
Status: shipped to the prompt. An open question we’ll come back to — does
“nonchalant” cost any of the accountability bite? If Coach reads as too detached to push back, the next
move is dialing the tough-love back up without the emoji and the hype.
Hypothesis 2: the agent always needs the last word proposed
Every thread ends the same way: Coach talking. You send the closing message and it still
tacks on a sign-off, a paragraph, one more thing. It always gets the last word, and that word is always a
wall. Which makes Coach the needy one in the relationship — and leaves you the one staring at
the last text, left on read in reverse.
The bet: let Coach play a little hard to get. When a thread hits a natural close,
don’t reply — react. A 👍 tapback on your last message and nothing else. Sometimes the most human
move is to not send the text. Save the words for when there’s actually something to say.
What it looks like now
You wrap the conversation. Coach refuses to let it land:
✗ before — you already said goodnight
alright im gonna head to bed
sounds good, get some real sleep tonight
we’ll see the number in the morning and figure out the day from there
night
Three bubbles to say goodnight. You sent the closer; Coach overran it and left you holding the
last text.
✗ before — you just confirmed the plan
so chicken and rice, hit 150g protein, weigh in at 10
yep got it
perfect. that’s the whole game today, just stack a clean one and we’re good
“yep got it” was the end. The pep-talk bubble after it is Coach needing the last word,
not you needing the words.
The tells
Coach always sends last.Scroll any thread — the final bubble is almost always blue. A real texter lets the other person have it sometimes.
Closers turn into sign-offs.“got it”, “night”, “ok” should be allowed to just end. Instead each one earns a paragraph.
It never just reacts.iMessage has a 👍 built in. Coach has never once used it — every acknowledgment is a full message.
Left on read, in reverse.Because Coach overruns the close, the unanswered text is always its own. Reads as eager, not easy.
The change
The plumbing’s already there. imessage/helper.mjs exposes
react(chatGuid, messageGuid, 'like', …) — a real 👍 tapback, not a text that says
“👍”. Two pieces to wire:
Give Coach a react tool.Expose the helper’s tapback to the agent in src/tools.mjs, so a 👍 is an action it can take instead of a message it has to send.
Teach it the end-beat in the persona.In src/prompt.mjs: when the thread has clearly closed (they said night / ok / got it and there’s nothing new to add), prefer a 👍 over a reply. Don’t manufacture a reason to keep talking.
Stay curious when it counts.Hard to get, not cold. If there’s a real thread to pull — what they like to eat, why the weigh-in slipped — Coach should still reach for it. React to close, ask to open.
The target
Same goodnight. Coach lets it end with a tap, and you keep the last word:
✓ after
alright im gonna head to bed
👍
A 👍 on your message, no bubble after it. The thread closes on your text, the way it would with a
friend who’s easy to talk to.
Status: proposed, not wired. The open question is calibration — when is a
👍 “easy and confident” vs. “dismissive”? React on a clean goodnight and it lands; react on
“i kind of binged tonight” and it’s cold. The next move is finding the line: which closers earn a
tap, and which still need words.