// Phase 03 · LIVE blind opening + mediator synthesis. // // State machine (drawn on shared.openings[seat]): // step 'draft' → party is writing raw draft. visible only to them. // step 'review' → coach has returned neutralized_draft + red_flags. party reviews/edits. // step 'submitted'→ party has committed a submission. waiting for partner. // step 'approve' → both submitted; mediator has returned syntheses. party must approve theirs. // step 'posted' → party approved; their synthesis is now in shared.openings[seat].synthesis. // // shared.openings[seat] shape (live mode): // { _intakeSubmitted, good_outcome, // carried from phase 02 // step, submitted, synthesis, approved, framing } // // Host drives the mediator synth call once both sides are 'submitted'. Results go into // shared.openings[A|B].synthesis + shared.openings.framing. Guest sees the same via session // state sync. // // The raw draft is NEVER written to shared state. It lives only in local React state + // (optionally) the party's private thread. The submitted text is party-approved. (function () { const { useState, useEffect, useRef } = React; window.LiveOpeningPhase = function LiveOpeningPhase({ seat, onPhase }) { const [, tick] = useState(0); useEffect(() => CG.session.onState(() => tick(t => t + 1)), []); const shared = CG.session.shared || {}; const names = (CG.session.getNames && CG.session.getNames()) || {}; const mp = CG.session.isMultiplayer && CG.session.isMultiplayer(); const isHost = !mp || seat === 'A'; const otherSeat = seat === 'A' ? 'B' : 'A'; const myName = names[seat] || (seat === 'A' ? 'host' : 'guest'); const otherName = names[otherSeat] || (otherSeat === 'A' ? 'host' : 'guest'); const goodOutcome = shared.openings?.[seat]?.good_outcome || null; const mine = shared.openings?.[seat] || {}; const theirs = shared.openings?.[otherSeat] || {}; const myStep = mine.step || 'draft'; const theirStep = theirs.step || 'draft'; // Presence: map opening step → activity label useEffect(() => { const map = { draft: 'writing raw draft', review: 'reviewing with coach', submitted: `waiting on ${otherName}`, approve: 'approving synthesis', posted: theirStep === 'posted' ? 'both posted' : `waiting on ${otherName}`, }; const activity = map[myStep] || 'drafting'; try { CG.session.setCursor && CG.session.setCursor({ activity }); } catch (e) {} }, [myStep, theirStep, otherName]); // local-only draft text const [raw, setRaw] = useState(''); const [neutralized, setNeutralized] = useState(''); const [editable, setEditable] = useState(''); // the actively-editable version shown in review const [redFlags, setRedFlags] = useState([]); const [coachMsg, setCoachMsg] = useState(''); const [reviewing, setReviewing] = useState(false); const [error, setError] = useState(''); const [synthError, setSynthError] = useState(''); const [synthesizing, setSynthesizing] = useState(false); const synthRunningRef = useRef(false); // Persist raw across reloads locally (never shared). useEffect(() => { (async () => { const saved = await CG.store.get(`opening_raw_${seat}`); if (saved && typeof saved === 'string') setRaw(saved); })(); }, [seat]); useEffect(() => { CG.store.set(`opening_raw_${seat}`, raw); }, [raw, seat]); // --- actions --- const patchMine = (patch) => { // Read from the live session object, not the React render snapshot — avoids overwriting // the partner's state when both sides write openings at nearly the same time. const currentOpenings = CG.session.shared?.openings || {}; const updatedMine = { ...(currentOpenings[seat] || {}), ...patch }; CG.session.setShared({ openings: { ...currentOpenings, [seat]: updatedMine } }); }; const requestReview = async () => { const text = raw.trim(); if (!text || reviewing) return; setReviewing(true); setError(''); try { const r = await CG.agents.openingCoach.review(seat, text); setNeutralized(r.neutralized_draft); setEditable(r.neutralized_draft || text); setRedFlags(r.red_flags || []); setCoachMsg(r.message || ''); patchMine({ step: 'review' }); } catch (e) { setError(String(e.message || e)); } finally { setReviewing(false); } }; const submitOpening = () => { const text = (editable || '').trim(); if (!text) return; patchMine({ step: 'submitted', submitted: text }); }; // Reset back to draft (e.g. change my mind) const resetToDraft = () => { patchMine({ step: 'draft', submitted: null }); }; // Approve my own synthesis → post it. const approveMine = () => { patchMine({ step: 'posted', approved: true }); }; // --- host-only: run mediator synth when both submitted --- const bothSubmitted = mine.step === 'submitted' || mine.step === 'approve' || mine.step === 'posted'; const partnerSubmitted = theirs.step === 'submitted' || theirs.step === 'approve' || theirs.step === 'posted'; const needsSynth = isHost && (mine.step === 'submitted') && (theirs.step === 'submitted') && !shared.openings?.framing; useEffect(() => { if (!needsSynth || synthRunningRef.current) return; let cancelled = false; synthRunningRef.current = true; const openingsSnapshot = shared.openings; (async () => { setSynthesizing(true); setSynthError(''); try { const submittedA = openingsSnapshot?.A?.submitted || ''; const submittedB = openingsSnapshot?.B?.submitted || ''; if (!submittedA || !submittedB) return; const r = await CG.agents.mediatorSynth.synthesize(submittedA, submittedB); if (cancelled) return; // Re-read live state after the async call so we don't overwrite any changes // that arrived while the mediator was running. const liveOpenings = CG.session.shared?.openings || {}; const openings = { ...liveOpenings, A: { ...(liveOpenings.A || {}), synthesis: r.synthesis_A, step: 'approve' }, B: { ...(liveOpenings.B || {}), synthesis: r.synthesis_B, step: 'approve' }, framing: r.framing, }; CG.session.setShared({ openings }); } catch (e) { if (!cancelled) setSynthError(String(e.message || e)); } finally { synthRunningRef.current = false; if (!cancelled) setSynthesizing(false); } })(); return () => { cancelled = true; synthRunningRef.current = false; }; }, [needsSynth]); // --- solo fallback: simulate the partner in solo mode --- // In solo, the partner doesn't exist. We auto-fill their submission with a placeholder // so the user can walk through the full flow end-to-end. This is clearly labeled. useEffect(() => { if (mp) return; if (mine.step !== 'submitted') return; if (theirs.step === 'submitted' || theirs.step === 'approve' || theirs.step === 'posted') return; const openings = { ...(shared.openings || {}) }; openings[otherSeat] = { ...(openings[otherSeat] || {}), step: 'submitted', submitted: `(solo mode · ${otherName} did not participate. the mediator will proceed with a placeholder opening so you can see the full flow.)`, }; CG.session.setShared({ openings }); }, [mp, mine.step, theirs.step]); // --- render --- return (
phase 03 · blind opening + synthesis · live

say it once, in your own words. the mediator speaks you back in neutral.

write what you want {otherName} to hear first. your coach will soften the edges without changing what you're saying. neither of you sees the other's draft until both submit.{' '} then the mediator rewrites each opening for the other side, and you sign off on yours before it posts.

{goodOutcome && (
your anchor · from intake
"{goodOutcome}"
)}
{/* LEFT — the writing surface, which changes by step */}
your opening · {myName}
{myStep === 'draft' && ( )} {myStep === 'review' && ( patchMine({ step: 'draft' })} onSubmit={submitOpening} /> )} {myStep === 'submitted' && ( )} {(myStep === 'approve' || myStep === 'posted') && ( )}
{/* RIGHT — partner status + continue gate */}
continue
{mine.step === 'posted' && theirs.step === 'posted' ? :
{mine.step !== 'posted' ? 'your synthesis isn\'t approved yet.' : `waiting on ${otherName}.`}
}
); }; // --- sub-steps --- function DraftStep({ raw, setRaw, onReview, reviewing, error, otherName }) { return (
step 1 · your raw draft · nobody else sees this yet
say what you actually want {otherName} to hear. no self-editing yet. the coach will soften the edges in a second.