// Late phases: Pattern callout detail, Apology on-ramp, Artifact
// Depends on: SCENARIO, Icons, ScribbleCheck, AgentBadge
function CalloutPhase({ seat, godView, onPhase }) {
const S = window.SCENARIO;
const calloutEvent = S.chat.find(c => c.type === 'callout');
// Firewall-aware sim stages:
// 1 trigger — maya's problematic message is live in the room
// 2 detect — mediator detects pattern (internal; no public leak yet)
// 3 room — neutral callout posts PUBLICLY (no quote, no pattern names)
// 4 opaque — jordan sees status chip: "claude is speaking privately with maya"
// 5 private — maya's coach opens private intervention with quote + evidence + taxonomy
// 6 response — maya picks an action (withdraw / park / open new topic)
// 7 resume — room resumes; jordan sees only that the pause ended
const [stage, setStage] = React.useState(1);
const [playing, setPlaying] = React.useState(true);
const [resetKey, setResetKey] = React.useState(0);
const [done, setDone] = React.useState(false);
const [mayaAction, setMayaAction] = React.useState(null);
const reset = () => {
setStage(1);
setDone(false);
setMayaAction(null);
setResetKey(k => k + 1);
setPlaying(true);
};
React.useEffect(() => {
if (!playing) return;
const timers = [];
const at = (ms, fn) => timers.push(setTimeout(fn, ms));
at(1100, () => setStage(2));
at(2300, () => setStage(3));
at(3500, () => setStage(4));
at(4700, () => setStage(5));
at(7600, () => setMayaAction('withdraw'));
at(8400, () => setStage(6));
at(9800, () => { setStage(7); setDone(true); setPlaying(false); });
return () => timers.forEach(clearTimeout);
}, [playing, resetKey]);
const stageLabel = {
1: "maya's message just posted · mediator reading for patterns…",
2: "2 patterns detected · deciding what's public vs. private…",
3: "neutral callout posting to the room · no quote, no pattern names…",
4: "jordan sees only an opaque status chip · content stays walled",
5: "maya's coach opens a private intervention · quote + evidence + taxonomy",
6: "maya chose an action · coach relays only the action, not the conversation",
7: "room resumes · accountability was public, content wasn't",
}[stage];
const taxonomy = [
{ name: 'kitchen-sinking', desc: 'piling unrelated grievances on the current topic', severity: 'yellow', triggered: true },
{ name: 'absolutizing', desc: '"always" / "never" / "everything"', severity: 'yellow', triggered: true },
{ name: 'mind-reading', desc: "claiming knowledge of the other's motive", severity: 'yellow' },
{ name: 'criticism', desc: 'attacking character vs. the behavior', severity: 'yellow' },
{ name: 'defensiveness', desc: 'redirecting instead of engaging', severity: 'yellow' },
{ name: 'stonewalling', desc: 'refusing to respond after prompts', severity: 'red' },
{ name: 'contempt', desc: 'mockery, sneering — most corrosive', severity: 'red' },
{ name: 'DARVO', desc: 'deny, attack, reverse victim and offender', severity: 'red' },
{ name: 'gaslighting', desc: 'denying shared reality', severity: 'red' },
{ name: 'whataboutism', desc: "redirecting to the other's faults to dodge", severity: 'yellow' },
];
// What each seat renders
const isMaya = seat === 'A';
const isJordan = seat === 'B';
const isGod = seat === 'god';
return (
phase 06 · pattern callout · firewall in action
same moment. three different views. that's the point.
when the mediator flags a pattern, accountability is public, content is private. maya sees her coach pulling her aside with evidence. jordan sees that maya was pulled aside. the room sees a neutral callout — no quote, no pattern names, no diagnosis.
setPlaying(true)} onPause={() => setPlaying(false)} onReset={reset}
done={done} label={stageLabel}
/>
{/* Ledger symmetry panel — the coach's ledger is ALWAYS building for both parties */}
this yellow card isn't fresh. maya's coach has been tracking patterns across every phase — intake, openings, each room turn — and kitchen-sinking + absolutizing were already in her ledger before c5. jordan's coach is also building a ledger. {isJordan ? "you have your own — it's in the drawer bottom-right." : isMaya ? "you can read yours below, and jordan has his own that you can't see." : "both are visible to you in god view."} the callout is evidence-backed, not vibes-based.
{/* Seat lens strip — makes the asymmetry literal */}
viewing as
{/* Stage 1/2: always show the trigger in maya's/god view */}
{(isMaya || isGod) && (
what triggered it · maya's last message · still in the room
maya
"honestly also — you ALWAYS do this. your family always comes first, your friends, and i'm left reheating pasta. and your sister never asks how i'm doing either, by the way—"
firewall reasoning · the public callout names the shape (temperature rose, new topics piled on) but never the diagnosis or the quote. the diagnosis is clinical; airing it publicly would humiliate maya and be counterproductive. jordan's opaque status preserves accountability (he knows something happened) without inviting him to weaponize it. if maya refuses the coaching, that refusal is what becomes visible — not the content.
)}
{stage === 7 && (
the room exhales. nothing about maya's private conversation leaked — except the action she chose.
)}
{/* Full ledger section — shown after stage 5 when coaching is happening */}
{stage >= 5 && (
the ledger behind this moment.
{isGod ? "god view · both visible · firewall still enforced in reality" :
isMaya ? "your ledger · jordan's is firewalled" :
"your ledger · maya's is firewalled"}
each coach has been noting things throughout — not to weaponize them, but so patterns can be named with evidence and strengths get their due. you only ever read your own. dismiss anything you disagree with; your coach will push back once, then drop it.
{isGod ? (
) : (
coach's ledger · {isMaya ? 'jordan' : 'maya'}
firewalled
{isMaya ? "jordan" : "maya"}'s coach is keeping their own notes. you will never see them.
);
}
function RoomLens({ stage, calloutEvent }) {
// What EVERYONE sees in the shared thread. Strictly neutral.
return (
↑ earlier messages in the room ↑
{stage < 3 && (
(nothing posted yet — mediator still reading)
)}
{stage >= 3 && (
public callout · to both
i'm pausing us. the temperature rose and we drifted — new topics got added to the current one. let's stick to the one we locked. i'm going to speak with each of you briefly.
behavior named · no quote · no names of patterns · no blame assigned
)}
{stage >= 4 && stage < 7 && (
claude is speaking privately with maya. room paused.
)}
{stage === 7 && (
private coaching complete · maya withdrew "always" and parked new topics · resuming
only the action taken is relayed. the conversation itself stays in maya's coach room.
{stage < 3 ? "(your message is being read…)"
: stage === 3 ? "a neutral callout just appeared in the room — same for both of you."
: "heads up — coach is asking to talk privately…"}
)}
{stage >= 5 && (
coach · private with you
"i want to talk with you about what just posted. two things i noticed — and i'm only saying them to you, not jordan."
evidence · cited from your message
"ALWAYS" — absolutizing
"your family, your friends, your sister" — 3 new topics piled on mid-thread (kitchen-sinking)
"is 'always' literally true, or is the recent pattern closer to three? and your sister — that's real stuff, but it's not this topic. do you want to park it, or open a second topic after we finish this one?"
{!mayaAction ? (
) : (
chosen · {mayaAction === 'withdraw' ? 'withdraw "always"' : mayaAction === 'park' ? 'park the side topics' : 'open a new topic after this one'}
coach will relay only the action — never the conversation.
)}
)}
);
}
function JordanLens({ stage, mayaAction }) {
return (
{stage < 3 && (
(room is live · nothing new yet)
)}
{stage >= 3 && (
from the room
mediator paused the room. (same message maya sees.)
)}
{stage >= 4 && stage < 7 && (
status chip · no content
firewalled
"claude is speaking privately with maya about that last exchange."
you don't see what was said. you don't see which pattern was named. you only know this is happening — and that if maya declined the coaching, that would also be visible.
what you can not see
the specific words that triggered it
the pattern names (kitchen-sinking, absolutizing)
the evidence cited
what maya is saying back to her coach
)}
{stage === 7 && (
resumed
the room is live again. you see that maya withdrew "always" and parked the side topics.
that's the action, not the conversation. the coaching itself stays in her room — forever.
)}
);
}
// ApologyPhase moved to components/phases-apology.jsx
function ArtifactPhase({ seat, godView }) {
const S = window.SCENARIO;
const [asymPolicy, setAsymPolicy] = React.useState('hybrid');
return (
phase 08 · artifact · both parties must approve before generation
the document you both sign. the plan you'll actually follow.
{/* the document */}
clarguments · room {S.room.id}
what we agreed, what we didn't, and what happens next.
apr 15 · 10:42pm · 58 minutes in-room
{/* the topic */}
01 · the topic we locked
{S.topic.refined}
co-signed · maya · jordan — with amendment: "…and what counts as 'last-minute'?"
04 · what each of us acknowledged doing wrong · voluntary · both approved this wording
maya
{S.acknowledgments.A.map((a, i) => (
"{a.text}"
))}
jordan
{S.acknowledgments.B.map((a, i) => (
"{a.text}"
))}
{/* asymmetry policy */}
mediator's asymmetry note · both parties approved inclusion
On the Apr 9 incident specifically, the record shows Jordan changed plans with under an hour's notice and the reason initially given was incomplete; he has acknowledged this. Maya's patterns (absolutizing, extended silences) are real and acknowledged, but operate at a different level of specificity. We're noting this not to assign blame but so the action items are proportionate.
policy · hybrid (symmetric default, asymmetric where evidence supports it, both must sign)
maya approved · 10:38pmjordan approved · 10:40pm
{/* action items */}
05 · action items · specific · operational · with check-ins