A running log of how Coach should sound. One hypothesis at a time, each with the receipts.
6 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.
Hypothesis 3: it prescribes before it knows you proposed
When Coach has no memory of a place, it still acts like it does. You mention a spot and
before it has any idea what you actually order there, it’s already handing you the “right” version and
trimming the rest — protein style, no fries, skip the soda. But you already know what you’re getting. You
weren’t asking. The result is Coach negotiating your order like a stranger, on a spot it’s never seen you
visit, when the honest move is to just ask what your usual is and bank it.
The bet: when memory’s empty, default to asking, not suggesting. New restaurant,
no usual on file? The first move is “what do you usually get there?” — learn it, remember it, and only
then earn the swap. Be analytical before you’re prescriptive. The cutting comes later, once it actually
knows what it’s cutting.
What it looks like now
Real thread. You say where you’re headed; Coach has never logged a single In-N-Out order from
you, and by the second bubble it’s already writing your order for you:
✗ before — it’s never seen your In-N-Out order
maybe in n out lol
honestly not the worst call if you order right
protein style burger, no fries, add a double if you want the protein
what are you actually gonna get
No usualOrder on file for this spot, so it’s guessing at a stranger. It prescribes
the order, then asks what you’re getting — backwards. You already knew.
And once it starts, the whole thread is subtraction. Every reply trims one more thing:
✗ before — the running “cut it out”
the breads like the best part tho lol
fair, get the bun then
just skip the fries and you’re fine
how many cals r the fries
~395, and they’re not even that filling
if you’re gonna do it, do it, but that’s a real chunk of your day for basically no protein
No bun, no fries, no soda — cutting from message one, on a spot it has zero history with.
Feels like getting talked out of your own order by someone who just met you.
The tells
Prescribes before it knows.Hands you the “right” order before it’s ever learned what you actually get there. No remember_order on file means it’s coaching a stranger.
Leads with subtraction.“no fries”, “skip the soda”, “just get water.” The first instinct is to take things away, when you already know what you want.
Answers the unasked question.You said “maybe in n out,” it heard “optimize my order.” You were telling it where you are, not asking what to eat.
Treats every spot like the first time, forever.It never banks the usual, so the next In-N-Out trip starts the same negotiation from zero. The memory graph stays empty.
The change
This is a src/prompt.mjs change to the “WHAT SHOULD I EAT” / restaurants behavior,
read against the known spots line already in LIVE STATE:
Empty memory → ask, don’t suggest.Named spot with no usualOrder in LIVE STATE? First move is “what do you usually get there?” Then remember_order to bank it. Don’t prescribe into a void.
Analytical before prescriptive.Early on the job is to learn the pattern — what, how much, how often — not trim it. Observe and record. Hold the swaps.
Memory earns the swap.Once the usual is on file, then offer the lighter version or the danger-zone callout. That’s the geofence hero in DESIGN.md — it only works because it learned the order first.
Subtraction is a late move.Save “skip the fries” for when it knows the fries are the usual and the weight trend says they matter. Cutting needs something to cut from.
The target
Same opener, empty memory. Coach asks instead of prescribing, and banks the answer:
✓ after — no usual on file, so it learns one
maybe in n out lol
what do you usually get there?
One question. It stores the answer with remember_order and now it actually knows you.
No prescription, no subtraction, no negotiating with a stranger.
And the trip after that, with the usual on file, it’s allowed to coach — because now it
has earned it:
✓ after — usual is known, so the swap is a real offer
at in n out
double double animal style, the usual?
yeah
want it protein style today or you good with the bun?
Same advice as the “before,” but earned. It names your order from memory, confirms, then offers
the swap as a choice instead of a cold prescription.
Status: proposed. The line to find is how long to stay in learn-mode before it’s
allowed to push. Ask forever and it’s a passive notepad; switch to coaching on visit one and it’s the stranger
again. The bet: the swap waits until the usual is on file. Memory is what earns the right to suggest — same
principle as DESIGN.md’s confirm-to-log, where knowing the usual means it stops asking you to
describe anything.