From efe021d886f0d6973c662c953afdc96ad948f43c Mon Sep 17 00:00:00 2001 From: yuanzonghao Date: Thu, 4 Jun 2026 15:48:14 +0800 Subject: [PATCH] fix(engine): pin entry-beat roster to the plan in Phase B MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Painter composites exactly plan.entryActiveCharacters into the entry frame (the same roster the Cinematographer framed). Phase B is told to reuse that roster, but only the entry beat's id was code-enforced — so an LLM slip could leave a character in the painted frame that the runtime entry beat says isn't there. Pin activeCharacters onto the plan's entry beat as a last line of defense, mirroring the existing id pin. Speaker is intentionally left to the prompt: it's coupled to line/TTS, so overwriting it could mis-attribute or orphan Phase B's dialogue. Addresses Copilot review feedback on PR #27. Co-Authored-By: Claude Opus 4.7 --- lib/engine/agents/writer.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/engine/agents/writer.ts b/lib/engine/agents/writer.ts index e2df125..b560d56 100644 --- a/lib/engine/agents/writer.ts +++ b/lib/engine/agents/writer.ts @@ -497,6 +497,15 @@ export async function runWriterBeats( beats = renameBeatId(beats, beats[0]!.id, plan.entryBeatId); } + // 把入场 beat 的 roster 钉成 plan 的:画师合成进帧的正是 + // plan.entryActiveCharacters,运行时入场 beat 必须显示同一批人(与上面钉 + // id 同理)。speaker 故意不钉——它和 line/TTS 耦合,强行覆盖会错配台词。 + const entryRoster = + plan.entryActiveCharacters.length > 0 ? plan.entryActiveCharacters : undefined; + beats = beats.map((b) => + b.id === plan.entryBeatId ? { ...b, activeCharacters: entryRoster } : b, + ); + return { beats, storyStatePatch: coerceStoryStatePatch(parsed.storyStatePatch),