// Phase 04 · LIVE steel-man gate. // // Each party must restate the OTHER party's posted synthesis fairly before the room unlocks. // Per direction: (attempter → subject) // 1. attempter writes an attempt. judge (their coach, private) scores 1..5. // 2. if score ≥ 4, attempt is sent to subject. subject can ACCEPT ("yes, you got me") or // PUSH BACK with one line of what was missed. // 3. if subject accepts, that direction is 'accepted'. // 4. both directions must be 'accepted' to unlock phase 05. // // shared.steelman shape (live mode; new top-level field on shared state): // { // A_attempts: [{ text, score, got_right, missed, added_your_own, verdict, status, push_back }], // B_attempts: [{ text, score, got_right, missed, added_your_own, verdict, status, push_back }], // } // status ∈ 'needs_review' | 'accepted' | 'pushback' // Each party writes their OWN attempts array. Each party writes the status/push_back on the // array that describes attempts about THEM (because they are the subject). // // Session whitelist: we extend _applyGuestProposal via the 'openings.B' / cursor.B pattern // by piggybacking on a new opt-in key 'steelman' (see below) — but we don't actually need to // touch cg-session here because `setShared` on host applies directly, and guest writes go // through _applyGuestProposal which already merges whitelisted keys. We'll add 'steelman' to // the whitelist in cg-session. (function () { const { useState, useEffect } = React; window.LiveSteelmanPhase = function LiveSteelmanPhase({ 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 myPostedSynthesis = shared.openings?.[seat]?.synthesis || ''; const theirPostedSynthesis = shared.openings?.[otherSeat]?.synthesis || ''; // Guard: if openings aren't posted, bail. const myPosted = shared.openings?.[seat]?.step === 'posted'; const theirPosted = shared.openings?.[otherSeat]?.step === 'posted'; if (!myPosted || !theirPosted) { return (
phase 04 · steel-man gate

not ready.

both openings must be posted before the steel-man gate opens.{' '} onPhase && onPhase('opening')} style={{ color: 'var(--blue)', cursor: 'pointer' }}>← back to phase 03

); } // shared.steelman is the live record const steel = shared.steelman || {}; const myAttemptsKey = `${seat}_attempts`; // attempts I've made about OTHER const theirAttemptsKey = `${otherSeat}_attempts`; // attempts OTHER has made about ME const myAttempts = steel[myAttemptsKey] || []; const theirAttempts = steel[theirAttemptsKey] || []; const latestMine = myAttempts[myAttempts.length - 1]; const latestTheirs = theirAttempts[theirAttempts.length - 1]; // direction A: I restate them. Gate passes when some attempt in myAttempts has status 'accepted'. const meDoneSteelmanning = myAttempts.some(a => a.status === 'accepted'); // direction B: they restate me. Gate passes when some attempt in theirAttempts has status 'accepted'. const theyDoneSteelmanning = theirAttempts.some(a => a.status === 'accepted'); const bothDone = meDoneSteelmanning && theyDoneSteelmanning; // Presence: steelman activity label useEffect(() => { let activity; if (bothDone) activity = 'gate passed'; else if (meDoneSteelmanning && !theyDoneSteelmanning) activity = `waiting on ${otherName}`; else if (!meDoneSteelmanning && theyDoneSteelmanning) activity = `steel-manning ${otherName}`; else if (latestTheirs && latestTheirs.status === 'needs_review') activity = 'reviewing their restatement'; else activity = `steel-manning ${otherName}`; try { CG.session.setCursor && CG.session.setCursor({ activity }); } catch (e) {} }, [bothDone, meDoneSteelmanning, theyDoneSteelmanning, latestTheirs?.status, otherName]); const [attempt, setAttempt] = useState(''); const [scoring, setScoring] = useState(false); const [scoreResult, setScoreResult] = useState(null); // latest judge result for UI (before send) const [error, setError] = useState(''); // Hydrate attempt box with latest draft that hasn't been sent yet useEffect(() => { (async () => { const saved = await CG.store.get(`steelman_draft_${seat}`); if (saved && typeof saved === 'string') setAttempt(saved); })(); }, [seat]); useEffect(() => { CG.store.set(`steelman_draft_${seat}`, attempt); }, [attempt, seat]); const writeSteelman = (patchFn) => { const next = { ...(shared.steelman || {}) }; patchFn(next); CG.session.setShared({ steelman: next }); }; const runJudge = async () => { const t = attempt.trim(); if (!t || scoring) return; setScoring(true); setError(''); try { const r = await CG.agents.steelmanJudge.score(seat, theirPostedSynthesis, t); setScoreResult(r); } catch (e) { setError(String(e.message || e)); } finally { setScoring(false); } }; const sendAttempt = () => { if (!scoreResult) return; writeSteelman(s => { const arr = s[myAttemptsKey] || []; s[myAttemptsKey] = [...arr, { text: attempt.trim(), score: scoreResult.score, got_right: scoreResult.got_right, missed: scoreResult.missed, added_your_own: scoreResult.added_your_own, verdict: scoreResult.verdict, status: 'needs_review', ts: Date.now(), }]; }); setScoreResult(null); setAttempt(''); CG.store.set(`steelman_draft_${seat}`, ''); }; // Subject actions on THEIR attempts (the ones OTHER made about me). const acceptAttempt = (idx) => { writeSteelman(s => { const arr = [...(s[theirAttemptsKey] || [])]; arr[idx] = { ...arr[idx], status: 'accepted' }; s[theirAttemptsKey] = arr; }); }; const pushbackAttempt = (idx, pushText) => { writeSteelman(s => { const arr = [...(s[theirAttemptsKey] || [])]; arr[idx] = { ...arr[idx], status: 'pushback', push_back: pushText }; s[theirAttemptsKey] = arr; }); }; // Solo simulation · let solo users complete the flow by auto-accepting after a short delay. useEffect(() => { if (mp) return; if (!latestMine) return; if (latestMine.status !== 'needs_review') return; const t = setTimeout(() => { writeSteelman(s => { const arr = [...(s[myAttemptsKey] || [])]; arr[arr.length - 1] = { ...arr[arr.length - 1], status: 'accepted' }; s[myAttemptsKey] = arr; }); }, 1500); return () => clearTimeout(t); }, [mp, latestMine?.status]); useEffect(() => { if (mp) return; if (theyDoneSteelmanning) return; if (!meDoneSteelmanning) return; // Auto-insert a solo "partner also steelmanned you" accepted record. writeSteelman(s => { const arr = s[theirAttemptsKey] || []; if (arr.some(a => a.status === 'accepted')) return; s[theirAttemptsKey] = [...arr, { text: `(solo mode · ${otherName} did not participate. auto-accepted to let you walk the flow.)`, score: 5, got_right: [], missed: [], added_your_own: [], verdict: 'solo auto-accept', status: 'accepted', ts: Date.now(), }]; }); }, [mp, meDoneSteelmanning, theyDoneSteelmanning]); return (
phase 04 · steel-man gate · live

prove you heard them. they grade you. nobody moves until they say yes.

restate {otherName}'s posted synthesis in your own words — fairly, charitably, without slipping into your own frame.{' '} {otherName} confirms you got them. you do the same. then the room opens.

{/* LEFT · ME steelmanning THEM */}
your steel-man of {otherName}
their posted synthesis (your target to restate):
{theirPostedSynthesis}
{!meDoneSteelmanning && ( setScoreResult(null)} error={error} history={myAttempts} otherName={otherName} /> )} {meDoneSteelmanning && (
accepted by {otherName}
{myAttempts.find(a => a.status === 'accepted')?.text}
)} {meDoneSteelmanning && !theyDoneSteelmanning && mp && (
waiting on {otherName} to restate your opening — you'll grade theirs when it arrives.
)}
{/* RIGHT · THEM steelmanning ME (subject view) */}
{otherName}'s steel-man of you · you grade
{otherName} is trying to restate YOUR posted synthesis. you accept or push back.
{theirAttempts.length === 0 && (
{mp ? `waiting for ${otherName}…` : `in solo mode, this auto-completes after you finish yours.`}
)} {theirAttempts.map((a, i) => ( acceptAttempt(i)} onPushback={(text) => pushbackAttempt(i, text)} /> ))}
{bothDone && (
gate unlocked
each of you confirmed the other got them. the room opens.
)}
); }; // ---- subcomponents ---- function DirectionBanner({ meDone, theyDone, myName, otherName }) { const pill = (label, done) => (
{done ? '✓' : '○'} {label}
); return (
gate {pill(`${myName} restates ${otherName}`, meDone)} {pill(`${otherName} restates ${myName}`, theyDone)}
); } function MyAttemptWriter({ attempt, setAttempt, onJudge, scoring, scoreResult, onSend, onDiscardScore, error, history, otherName }) { return (
{history.length > 0 && (
previous attempts ({history.length})
{history.map((a, i) => (
#{i + 1} · judge {a.score}/5 · {a.status}
{a.text}
{a.push_back && (
{otherName} pushed back: {a.push_back}
)}
))}
)}