// 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 (
);
}
// 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.