<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Agent Orchestration on ICE-ICE-BEAR-BLOG</title><link>https://ice-ice-bear.github.io/tags/agent-orchestration/</link><description>Recent content in Agent Orchestration on ICE-ICE-BEAR-BLOG</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Thu, 28 May 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://ice-ice-bear.github.io/tags/agent-orchestration/index.xml" rel="self" type="application/rss+xml"/><item><title>Creative Agent Studio #5 — Polish Week: LLM Token Telemetry, Multi-Session Isolation, Inline Rewind, and the Revision-Request Affordance</title><link>https://ice-ice-bear.github.io/posts/2026-05-28-creative-agent-studio-dev5/</link><pubDate>Thu, 28 May 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/posts/2026-05-28-creative-agent-studio-dev5/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post Creative Agent Studio #5 — Polish Week: LLM Token Telemetry, Multi-Session Isolation, Inline Rewind, and the Revision-Request Affordance" /&gt;&lt;h2 id="overview"&gt;Overview
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://ice-ice-bear.github.io/posts/2026-05-22-creative-agent-studio-dev4/" &gt;Previous post: #4 — five gates, four canvases, and the revise-mode system&lt;/a&gt; ended the build-out push. Six days later, &lt;strong&gt;23 commits over four working days&lt;/strong&gt; closed out polish week — the kind of work that turns a feature-complete system into something that holds up in production under a real user&amp;rsquo;s hands.&lt;/p&gt;
&lt;p&gt;Four threads ran in parallel through the week. First, &lt;strong&gt;LLM-call observability&lt;/strong&gt; — every model invocation now emits structured logs with token usage and a propagated logCtx, plus the runtime got &lt;code&gt;LOG_LEVEL&lt;/code&gt; threshold control. Second, &lt;strong&gt;per-project state isolation under refresh&lt;/strong&gt; — eight commits on 2026-05-26 hunted down hooks-order bugs, stranded bootstraps, and crypto.randomUUID gaps that surfaced once the multi-session work landed. Third, &lt;strong&gt;inline past-gate rewind&lt;/strong&gt; — a single feat commit shipped the affordance that lets users say &amp;ldquo;change the third copy back at GATE 2&amp;rdquo; from inside an active storyboard session without leaving. Fourth, &lt;strong&gt;the revision-request affordance pass&lt;/strong&gt; — five commits unified the marked-card visual across every canvas tab, softened the rewind dialog copy, and fixed a state bug where the gate selector forgot which card the user just clicked.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 Prev["#4 — build-out (5cb4106)"] --&gt; Thread1["Thread 1 &amp;lt;br/&amp;gt; LLM observability &amp;lt;br/&amp;gt; token usage + logCtx"]
 Prev --&gt; Thread2["Thread 2 &amp;lt;br/&amp;gt; Per-project isolation &amp;lt;br/&amp;gt; hooks order + bootstrap fixes"]
 Prev --&gt; Thread3["Thread 3 &amp;lt;br/&amp;gt; Inline past-gate rewind &amp;lt;br/&amp;gt; chat-driven pipeline edits"]
 Prev --&gt; Thread4["Thread 4 &amp;lt;br/&amp;gt; Revision-request UX &amp;lt;br/&amp;gt; unified marked-card affordance"]
 Thread1 --&gt; End["#5 end (67b820e) &amp;lt;br/&amp;gt; 2026-05-28"]
 Thread2 --&gt; End
 Thread3 --&gt; End
 Thread4 --&gt; End&lt;/pre&gt;&lt;p&gt;One running theme — &lt;strong&gt;the features were done; what remained was making them honest under refresh, under load, and under a confused user.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="thread-1--llm-call-observability"&gt;Thread 1 — LLM-Call Observability
&lt;/h2&gt;&lt;p&gt;The structured logger that landed in #2 was the foundation; this week wired it through every LLM call site so production telemetry actually means something.&lt;/p&gt;
&lt;p&gt;Two commits did the heavy lifting:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(logger): add LOG_LEVEL threshold, silence node:sqlite warning&lt;/code&gt; — a &lt;code&gt;LOG_LEVEL&lt;/code&gt; env var so production can drop the &lt;code&gt;info&lt;/code&gt; chatter and keep only &lt;code&gt;warn&lt;/code&gt;/&lt;code&gt;error&lt;/code&gt;. The companion change suppressed the &lt;code&gt;node:sqlite&lt;/code&gt; experimental-warning that printed on every worker fork.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(observability): emit structured logs across worker + lifecycle paths&lt;/code&gt; — &lt;code&gt;runtime/workers/worker-loop.js&lt;/code&gt; and &lt;code&gt;runtime/orchestration/run-lifecycle.js&lt;/code&gt; now log structured events at each meaningful transition (job claimed, run started, gate emitted, stage advanced, run completed/errored).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(observability): instrument LLM calls with token usage + logCtx&lt;/code&gt; — this is the load-bearing one. Every model call now logs:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// runtime/agents/_llm-instrument.js (paraphrased)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;callModelWithLogging&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logCtx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;llm_call&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;logCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;prompt_tokens&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;output_tokens&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;duration_ms&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;cached_tokens&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cached_input_tokens&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;llm_call_failed&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;logCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;duration_ms&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;logCtx&lt;/code&gt; is propagated from &lt;code&gt;worker-loop.js&lt;/code&gt; and carries &lt;code&gt;{ runId, projectId, stage, role }&lt;/code&gt;. So a single LLM call&amp;rsquo;s log line tells you which project, which session, which gate, which agent, which model — and how many tokens it cost. The Grafana dashboard set up back in #2 now has actual data flowing into it.&lt;/p&gt;
&lt;p&gt;The infra-side companion was &lt;code&gt;feat(terraform): widen EC2 start cron to every day&lt;/code&gt; + &lt;code&gt;fix(terraform): update daily stop schedule to 03:00 KST&lt;/code&gt;. The EC2 instance had been running on a weekday-only schedule from the prototype days; the cron now starts the box every day and stops it at 03:00 KST. Operational, not feature work.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="thread-2--per-project-state-isolation-under-refresh"&gt;Thread 2 — Per-Project State Isolation Under Refresh
&lt;/h2&gt;&lt;p&gt;The multi-session work in #4 introduced an entire class of refresh-time bugs. When the user hard-refreshes inside a workspace, the React tree mounts before any data is loaded — and the multi-session logic was making assumptions that only held if the user had navigated &lt;em&gt;into&lt;/em&gt; the workspace from the launcher (where the bootstrap had already happened).&lt;/p&gt;
&lt;p&gt;Eight commits on 2026-05-26 hunted down the edge cases:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fix(web): bootstrap projects on workspace refresh, fix hooks order&lt;/code&gt;&lt;/strong&gt; — the workspace page assumed &lt;code&gt;projects&lt;/code&gt; was already loaded. It wasn&amp;rsquo;t on a refresh. The fix added a bootstrap call inside the workspace effect — but that triggered a React hooks-order violation because the bootstrap call was conditionally inside a &lt;code&gt;useEffect&lt;/code&gt;. Both got fixed in one commit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fix(web): isolate per-project state on switch, show loader during session hydration&lt;/code&gt;&lt;/strong&gt; — switching from project A to project B was leaving A&amp;rsquo;s session list visible while B was loading. The fix: when &lt;code&gt;projectId&lt;/code&gt; changes, immediately clear the per-project slices and show a loader, then hydrate B&amp;rsquo;s data. The user sees a brief loader instead of mistakenly attributing A&amp;rsquo;s sessions to B.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fix(web): don't strand /sessions bootstrap behind a stale ref guard&lt;/code&gt;&lt;/strong&gt; — there was a &amp;ldquo;don&amp;rsquo;t re-bootstrap if already bootstrapping&amp;rdquo; guard implemented with a ref. The ref was being set to &lt;code&gt;true&lt;/code&gt; and then never cleared on certain error paths, so subsequent navigations got stranded. Fix: track the bootstrap state in the slice instead of in a ref, and clear it on success &lt;em&gt;and&lt;/em&gt; failure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fix(web): make Composer first-brief-only, flip ApproveBar toggle label&lt;/code&gt;&lt;/strong&gt; — the Composer (chat input) had been the entry point for &lt;em&gt;every&lt;/em&gt; user turn. But once a session was past the first brief, the gate-based flow took over — the Composer&amp;rsquo;s role should be hidden. The fix made the Composer visible only before the first brief is submitted; after that, the ApproveBar is the user&amp;rsquo;s surface.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fix(web): make bootstrap dep stable so /sessions can't be cancelled forever&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;useChatStream&lt;/code&gt; was using an AbortController whose dependency array was unstable, causing it to abort on every re-render. The fix stabilized the dep so the controller only aborted on actual user cancel or unmount.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fix(web): polyfill crypto.randomUUID so HTTP prod can create projects/sessions&lt;/code&gt;&lt;/strong&gt; — the production EC2 was serving over plain HTTP for some clients (an intermediate proxy stripped HTTPS), and &lt;code&gt;crypto.randomUUID&lt;/code&gt; is only available in secure contexts. Projects and sessions couldn&amp;rsquo;t be created. The polyfill restores it.&lt;/p&gt;
&lt;p&gt;The last one is the most &amp;ldquo;production reveals what your dev environment hid&amp;rdquo; of the week. Localhost is a secure context. The deploy target isn&amp;rsquo;t always. A one-line polyfill kept the EC2 build usable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fix: preserve 분석 보고서 across key-concept revisions&lt;/code&gt;&lt;/strong&gt; — the runtime fix companion. When the user revised the key-concept selection at GATE 1, the analysis report (도서 단계의 산출물) was being recomputed alongside, which was unnecessary work and produced a slightly different report each time. The fix kept the original analysis frozen across key-concept revisions.&lt;/p&gt;
&lt;p&gt;And &lt;code&gt;docs(claude): register Diffs Runtime harness pointer in CLAUDE.md&lt;/code&gt; added a pointer so Claude Code sessions could find the runtime&amp;rsquo;s harness conventions automatically.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="thread-3--inline-past-gate-rewind-from-chat"&gt;Thread 3 — Inline Past-Gate Rewind From Chat
&lt;/h2&gt;&lt;p&gt;A single commit shipped a major UX move: &lt;code&gt;feat(web): add inline past-gate rewind for chat-driven pipeline edits&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The setup: by polish week, the user had ApproveBar with revise-mode for the current gate, and the workflow advanced one stage at a time. But what if the user wanted to change a &lt;em&gt;past&lt;/em&gt; decision? &amp;ldquo;Actually, go back to the second key concept&amp;rdquo; while sitting in the storyboard stage?&lt;/p&gt;
&lt;p&gt;Previously this required leaving the canvas, navigating back to GATE 1 manually, reselecting, and walking forward through every subsequent gate again. Painful, and a violation of the chat-first principle.&lt;/p&gt;
&lt;p&gt;The inline rewind detects past-gate intent in chat:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// (paraphrased — combined chat-stream + dispatch-sse path)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Backend classifies the chat message:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// &amp;#34;두 번째 키 컨셉으로 다시 가자&amp;#34; → rewind_intent: { gate: &amp;#34;GATE_1&amp;#34;, selection: 2 }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Frontend receives a rewind_proposal SSE event:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;rewind_proposal&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;fromGate&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;GATE_5&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;toGate&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;GATE_1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;affectedDownstreamGates&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;GATE_2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;GATE_3&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;GATE_4&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;GATE_5&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// UI shows the inline ConfirmRewindDialog (the one polished in Thread 4)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The dialog explicitly lists what will be discarded — copy approval at GATE 2, scenario approval at GATE 4, etc. — so the user knows the cost before confirming. On confirm, the runtime walks the project back to the target gate, regenerates everything downstream, and the user lands at the target gate&amp;rsquo;s approval surface ready to make the new choice.&lt;/p&gt;
&lt;p&gt;This is the natural extension of the gate-based-auto-run principle (decision 3 in &lt;code&gt;interaction-model.md&lt;/code&gt;) — but in &lt;em&gt;reverse&lt;/em&gt;. The principle says: each stage advances to the next on user approval. The rewind says: each stage can also walk &lt;em&gt;back&lt;/em&gt;, and the runtime knows what to invalidate.&lt;/p&gt;
&lt;p&gt;The companion &lt;code&gt;docs(claude): add triage-prod-bug skill trigger to enforce browser-first debugging&lt;/code&gt; is a harness rule — when a bug is reported, look at the browser first (devtools, network, console) before reading any code. The chat-first product principle has a debugging analogue: see-first, code-second.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="thread-4--the-revision-request-affordance-pass"&gt;Thread 4 — The Revision-Request Affordance Pass
&lt;/h2&gt;&lt;p&gt;The final five commits unified the revision-request UI across every canvas tab. Before this work, every tab had implemented its own marked-card style — yellow background on Copy, red dashed border on KeyConcept, blue left-edge bar on Storyboard (only this one), no visual treatment at all on Scene — and users kept asking &lt;em&gt;&amp;ldquo;why does only 콘티 (Storyboard) show this blue mark?&amp;rdquo;&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="soften-rewind-dialog-copy--hide-final-gate-from-discard-list"&gt;Soften rewind dialog copy + hide final gate from discard list
&lt;/h3&gt;&lt;p&gt;Commit &lt;code&gt;e27316a&lt;/code&gt;. The first attack was the most jarring piece of copy. The rewind dialog read:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&amp;ldquo;이 결정을 되돌릴까요?&amp;rdquo;
&amp;ldquo;카피 검토 단계부터 다시 진행합니다.&amp;rdquo;
&amp;ldquo;아래 후속 결정이 새 버전으로 대체됩니다:&amp;rdquo;
– 컨셉 확정 결정
– 시나리오 검토 결정
– &lt;strong&gt;최종 승인 결정&lt;/strong&gt; ← shouldn&amp;rsquo;t be in this list&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;Two problems: the discard list included the &lt;em&gt;final&lt;/em&gt; approval gate (but that gate is what you would re-confirm at the end, not something that vanishes), and &amp;ldquo;결정&amp;rdquo; felt corporate when the actual artifact is a creative judgment about a draft.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// web/src/components/approve/ConfirmRewindDialog.tsx
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt; &lt;span class="na"&gt;data-testid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;confirm-rewind-discard-list&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;gates&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;final-approval&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&amp;gt;{&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;softTitle&lt;/span&gt;&lt;span class="p"&gt;}&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Filter &lt;code&gt;final-approval&lt;/code&gt; gates out of the discard preview, and use &lt;code&gt;softTitle&lt;/code&gt; (e.g., &amp;ldquo;콘티 검토&amp;rdquo; instead of &amp;ldquo;콘티 검토 결정&amp;rdquo;). Both come from the same &lt;code&gt;Gate&lt;/code&gt; interface but rendered differently in destructive vs. informational contexts.&lt;/p&gt;
&lt;h3 id="gate-titles-in-the-rewind-dialog"&gt;Gate titles in the rewind dialog
&lt;/h3&gt;&lt;p&gt;Commit &lt;code&gt;4ddff68&lt;/code&gt;. The dialog had been using machine-generated gate ids (&lt;code&gt;g-2&lt;/code&gt;, &lt;code&gt;g-3&lt;/code&gt;) as titles. A small label map fixed comprehension:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GATE_LABELS&lt;/span&gt;: &lt;span class="kt"&gt;Record&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;GateKind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;concept-confirm&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;컨셉 검토&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;scenario-review&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;시나리오 검토&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;storyboard-approve&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;콘티 검토&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;cut-finalize&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;컷 검토&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;final-approval&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;최종 승인&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now the dialog reads &amp;ldquo;콘티 검토 단계부터 다시 진행합니다&amp;rdquo; — a phrase that matches the canvas tab labels exactly, so users see in advance which tab they&amp;rsquo;ll land on after rewinding.&lt;/p&gt;
&lt;h3 id="approvebar-gate-preselection-on-수정요청-enter"&gt;ApproveBar gate preselection on 수정요청 enter
&lt;/h3&gt;&lt;p&gt;Commit &lt;code&gt;c55891b&lt;/code&gt;. When the user clicked 수정요청 on a card, the ApproveBar slid up with a gate selector — but the selector started empty, so the user had to re-click the same gate they just marked. A two-line &lt;code&gt;useEffect&lt;/code&gt; synced the implicit context into the explicit selection:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// web/src/components/approve/ApproveBar.tsx
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;수정요청&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;activeCard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;setGateSelection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeCard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gateId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;idle&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;setGateSelection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// clear on 닫기
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;activeCard&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A security-review pass on this change surfaced one note: the gate id flows from a user-controlled DOM event into a state field that gets passed to a server-side mutation. Since &lt;code&gt;gateId&lt;/code&gt; is validated server-side against the current workflow&amp;rsquo;s gate set anyway, no additional client-side validation was needed — but the review made that invariant explicit.&lt;/p&gt;
&lt;h3 id="unifying-the-marked-card-visual-across-five-tabs"&gt;Unifying the marked-card visual across five tabs
&lt;/h3&gt;&lt;p&gt;Commit &lt;code&gt;2804420&lt;/code&gt;. The visual unification pass. The unified treatment lives in a single design token:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// design-system / marked-card.css
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;marked&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;position&lt;/span&gt;: &lt;span class="kt"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;outline&lt;/span&gt;: &lt;span class="kt"&gt;2px&lt;/span&gt; &lt;span class="nx"&gt;solid&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;outline&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;marked&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nx"&gt;before&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;position&lt;/span&gt;: &lt;span class="kt"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;top&lt;/span&gt;: &lt;span class="kt"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;: &lt;span class="kt"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;: &lt;span class="kt"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;width&lt;/span&gt;: &lt;span class="kt"&gt;3px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;background&lt;/span&gt;: &lt;span class="kt"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And every tab component now wraps the card in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;card&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;marked&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;marked-card&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)}&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;marked&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;RevisionLabel&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* tab-specific content */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;RevisionLabel&lt;/code&gt; was also extracted — previously each tab built its own label inline with different copy (&amp;ldquo;수정 요청됨&amp;rdquo;, &amp;ldquo;리비전&amp;rdquo;, &amp;ldquo;Edit Pending&amp;rdquo;). Now there is one component, one string.&lt;/p&gt;
&lt;h3 id="show-every-gate-marker-once-and-reflect-live-gate-state"&gt;Show every gate marker once and reflect live gate state
&lt;/h3&gt;&lt;p&gt;The very last commit of the day (&lt;code&gt;67b820e&lt;/code&gt;). The StageStepper at the top of the workspace was showing duplicate gate markers in some states (a re-emitted gate event would add a second dot) and was lagging behind the actual gate_state transitions. Two bugs in one fix:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deduplicate by gate id so each gate renders exactly once&lt;/li&gt;
&lt;li&gt;Wire the live &lt;code&gt;gate_state&lt;/code&gt; from the pipeline slice (the 12-state field from #4) so the stepper reflects whatever transition just happened&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The 12-state gate_state field had been in place for a week, but the stepper had still been using the older &amp;ldquo;last completed gate&amp;rdquo; inference. This commit closed that gap.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="commit-log-23-total"&gt;Commit Log (23 total)
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Date&lt;/th&gt;
 &lt;th&gt;Message&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-25&lt;/td&gt;
 &lt;td&gt;fix stale approve gates during storyboard runs&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-25&lt;/td&gt;
 &lt;td&gt;feat(terraform): widen EC2 start cron to every day&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-25&lt;/td&gt;
 &lt;td&gt;refactor(canvas): drop submit prop drilling, polish selected-copy card&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-25&lt;/td&gt;
 &lt;td&gt;feat(logger): add LOG_LEVEL threshold, silence node:sqlite warning&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-25&lt;/td&gt;
 &lt;td&gt;feat(observability): emit structured logs across worker + lifecycle paths&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-25&lt;/td&gt;
 &lt;td&gt;feat(observability): instrument LLM calls with token usage + logCtx&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-25&lt;/td&gt;
 &lt;td&gt;chore: refresh lockfile peer-dep flags&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-26&lt;/td&gt;
 &lt;td&gt;fix(web): bootstrap projects on workspace refresh, fix hooks order&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-26&lt;/td&gt;
 &lt;td&gt;docs(claude): register Diffs Runtime harness pointer in CLAUDE.md&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-26&lt;/td&gt;
 &lt;td&gt;fix: preserve 분석 보고서 across key-concept revisions&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-26&lt;/td&gt;
 &lt;td&gt;fix(web): isolate per-project state on switch, show loader during session hydration&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-26&lt;/td&gt;
 &lt;td&gt;fix(web): don&amp;rsquo;t strand /sessions bootstrap behind a stale ref guard&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-26&lt;/td&gt;
 &lt;td&gt;fix(web): make Composer first-brief-only, flip ApproveBar toggle label&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-26&lt;/td&gt;
 &lt;td&gt;fix(web): make bootstrap dep stable so /sessions can&amp;rsquo;t be cancelled forever&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-26&lt;/td&gt;
 &lt;td&gt;fix(web): polyfill crypto.randomUUID so HTTP prod can create projects/sessions&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-27&lt;/td&gt;
 &lt;td&gt;docs(claude): add triage-prod-bug skill trigger to enforce browser-first debugging&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-27&lt;/td&gt;
 &lt;td&gt;feat(web): add inline past-gate rewind for chat-driven pipeline edits&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-27&lt;/td&gt;
 &lt;td&gt;fix(terraform): update daily stop schedule to 03:00 KST in variables&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-27&lt;/td&gt;
 &lt;td&gt;fix(gitignore): add harnesskit session-logs to .gitignore&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-28&lt;/td&gt;
 &lt;td&gt;fix(web): soften rewind dialog copy and hide final gate from discard list&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-28&lt;/td&gt;
 &lt;td&gt;fix(web): update gate titles in ConfirmRewindDialog for clarity&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-28&lt;/td&gt;
 &lt;td&gt;fix(web): preselect gateSelection on 수정요청 enter, clear it on 닫기&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-28&lt;/td&gt;
 &lt;td&gt;fix(web): unify 수정요청 marked-card visual and label across all tabs&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-28&lt;/td&gt;
 &lt;td&gt;fix(web): show every gate marker once and reflect live gate state&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="insights"&gt;Insights
&lt;/h2&gt;&lt;p&gt;Polish week&amp;rsquo;s pattern: &lt;strong&gt;observability and isolation were both about exposing state that had been quietly assumed.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The LLM token telemetry exposed &lt;em&gt;what was actually costing money&lt;/em&gt; — the prior dashboards showed queue depth and job duration, both important, but a slow agent might be cheap and a fast agent might be expensive depending on token usage. Wiring &lt;code&gt;logCtx&lt;/code&gt; through every call surfaced the per-project, per-stage, per-agent breakdown the cost analysis needs.&lt;/p&gt;
&lt;p&gt;The multi-session refresh fixes exposed &lt;em&gt;what the workspace was assuming about its bootstrap&lt;/em&gt;. The features worked when the user navigated in from the launcher because the launcher&amp;rsquo;s bootstrap was a precondition that happened to be satisfied. When refresh broke that precondition, the bugs surfaced — but they had been there all along, latent.&lt;/p&gt;
&lt;p&gt;The inline past-gate rewind is the same idea applied to the user&amp;rsquo;s mental model — the user &lt;em&gt;thinks&lt;/em&gt; &amp;ldquo;I want to change something back at GATE 1.&amp;rdquo; The rewind feature exposed that intent to the system instead of forcing the user to translate it into navigation steps. The chat-first principle isn&amp;rsquo;t just an input mechanism; it&amp;rsquo;s a commitment that the system will understand intent.&lt;/p&gt;
&lt;p&gt;The revision-request affordance pass is the small visible tip of the same iceberg. Three layers of clarity — visual unity, honest copy, state preservation — applied to a destructive workflow action, so the dialog asking for confirmation tells the truth about what will be lost.&lt;/p&gt;
&lt;p&gt;Next: from here forward, the system is feature-complete enough that future dev logs will tilt toward production lessons rather than feature additions — agent prompt tuning, cost trending, the next round of UX patterns that emerge from real user sessions. The five-post backfill closes a clean arc: mockup, production-readying, megapush, gate workflow, polish. The product is real now.&lt;/p&gt;</description></item><item><title>Creative Agent Studio #4 — Five Gates, Four Canvases, and the Revise-Mode System</title><link>https://ice-ice-bear.github.io/posts/2026-05-22-creative-agent-studio-dev4/</link><pubDate>Fri, 22 May 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/posts/2026-05-22-creative-agent-studio-dev4/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post Creative Agent Studio #4 — Five Gates, Four Canvases, and the Revise-Mode System" /&gt;&lt;h2 id="overview"&gt;Overview
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://ice-ice-bear.github.io/posts/2026-05-19-creative-agent-studio-dev3/" &gt;Previous post: #3 — the 134-commit megapush&lt;/a&gt; ended with PR4 (ApproveSheet + three gate variants) merged and project state surviving reload. Three days later, &lt;strong&gt;153 more commits&lt;/strong&gt; had completed every remaining piece of the four-stage agent workflow: a four-variant canvas panel that materialized the stage outputs (PR5), a workflow consolidation from three gates to &lt;strong&gt;five&lt;/strong&gt; with the key-concept planner at the new GATE 1, a UI re-architecture that replaced the bottom-drawer ApproveSheet with a slim ApproveBar plus output tabs, and the &lt;strong&gt;revise-mode multi-select system&lt;/strong&gt; that lets users surgically re-run a single key concept, copy draft, cut, or storyboard panel instead of redoing an entire stage.&lt;/p&gt;
&lt;p&gt;Underneath, the runtime gained the Planner-Generator-Evaluator loop, continuity anchors authored from user feedback, evolution notes injected into prompts, an HTML template set for the planning report and storyboard, and session isolation so a single project can carry multiple parallel attempts.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 Prev["#3 — megapush (9a2f851)"] --&gt; Theme1["Theme 1 &amp;lt;br/&amp;gt; PR5 — 4 stage canvases &amp;lt;br/&amp;gt; + resize + collapse"]
 Prev --&gt; Theme2["Theme 2 &amp;lt;br/&amp;gt; 5-gate workflow &amp;lt;br/&amp;gt; key-concept planner at GATE 1"]
 Prev --&gt; Theme3["Theme 3 &amp;lt;br/&amp;gt; ApproveBar + output tabs &amp;lt;br/&amp;gt; drawer replaced"]
 Prev --&gt; Theme4["Theme 4 &amp;lt;br/&amp;gt; Revise-mode system &amp;lt;br/&amp;gt; partial vs bulk revision"]
 Theme1 --&gt; End["#4 end (5cb4106) &amp;lt;br/&amp;gt; 2026-05-22"]
 Theme2 --&gt; End
 Theme3 --&gt; End
 Theme4 --&gt; End&lt;/pre&gt;&lt;p&gt;Four themes, one recurring shape — &lt;strong&gt;the workflow stopped being &amp;ldquo;all or nothing&amp;rdquo; at every layer it touched.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="theme-1--pr5-the-canvas-panel-with-four-stage-variants"&gt;Theme 1 — PR5: The Canvas Panel With Four Stage Variants
&lt;/h2&gt;&lt;p&gt;PR2 reserved a &lt;code&gt;CanvasPanel&lt;/code&gt; placeholder. PR5 filled it. The canvas is the right-hand column of the workspace; it shows the &lt;em&gt;current artifact&lt;/em&gt; of the current stage, rendered with a stage-specific component.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// web/src/components/canvas/stage-canvas.tsx (paraphrased)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;StageCanvas&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;stage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;stage&lt;/span&gt;: &lt;span class="kt"&gt;PresentationStage&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;research&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ResearchCanvas&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;copy&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;CopyCanvas&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;scenario&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ScenarioCanvas&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;storyboard&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;StoryboardCanvas&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each variant reads its data from the &lt;code&gt;pipeline&lt;/code&gt; slice and renders a stage-appropriate layout:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Stage&lt;/th&gt;
 &lt;th&gt;Variant&lt;/th&gt;
 &lt;th&gt;What it shows&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;research&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;ResearchCanvas&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;AdBriefRecap&lt;/code&gt; + &lt;code&gt;ResearchSummaryCards&lt;/code&gt; from &lt;code&gt;pipelineContext&lt;/code&gt; + active task list&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;copy&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;CopyCanvas&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;ConceptGrid&lt;/code&gt; of &lt;code&gt;CopyOptions&lt;/code&gt; from &lt;code&gt;copyHistory&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;scenario&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;ScenarioCanvas&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;SceneStrip&lt;/code&gt; per Act + &lt;code&gt;CutChip&lt;/code&gt; strip from &lt;code&gt;scenarioHistory&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;storyboard&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;StoryboardCanvas&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;1 &lt;code&gt;StoryboardPage&lt;/code&gt; per scene from &lt;code&gt;storyboardImageUrls&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Storyboard image handling needed a small architectural choice. Base64 PNG payloads arrive via &lt;code&gt;storyboard_image&lt;/code&gt; SSE events. The pipeline slice converts each to a blob URL (&lt;code&gt;URL.createObjectURL&lt;/code&gt;) with a revoke-on-replace step so a re-generation doesn&amp;rsquo;t leak the previous image. The &lt;code&gt;addStoryboardImage&lt;/code&gt; reducer holds the revoke logic:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;addStoryboardImage&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storyboardImageUrls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revokeObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base64ToBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;image/png&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;storyboardImageUrls&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storyboardImageUrls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="resize--collapse"&gt;Resize + collapse
&lt;/h3&gt;&lt;p&gt;Two commits added the resize affordance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): add useCanvasResizer hook (pointer capture + clamped delta -&amp;gt; ui.canvasWidth)&lt;/code&gt; — &lt;code&gt;ui.canvasWidth&lt;/code&gt; is clamped to 360..800px in the ui slice&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add CanvasResizer component (vertical drag handle + warm hover tint)&lt;/code&gt; — the visual handle, hover tint follows Creative Warmth (a subtle warm gradient, no pure highlight)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): WorkspacePage grid column reads ui.canvasWidth + respects canvasCollapsed&lt;/code&gt; — the grid actually responds to the width&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;CanvasHeader&lt;/code&gt; got a collapse toggle so power users can hide the canvas entirely when they want to focus on chat.&lt;/p&gt;
&lt;h3 id="pr6--polish--backend-swap--mockup-deletion"&gt;PR6 — Polish + backend swap + mockup deletion
&lt;/h3&gt;&lt;p&gt;PR6 was the bridge between &amp;ldquo;everything in React works&amp;rdquo; and &amp;ldquo;we&amp;rsquo;re actually serving React to users&amp;rdquo;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): ui slice gains fatalError + setFatalError&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add ErrorScreen (vercel_unsupported + unexpected kinds, ko/en)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add AppErrorBoundary class component (catches descendants -&amp;gt; ErrorScreen)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): useChatStream detects Vercel 503 -&amp;gt; ui.fatalError = vercel_unsupported&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(server): serve web/dist instead of mockup/ (MOCKUP_DIR -&amp;gt; WEB_DIST_DIR)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(deploy): vercel buildCommand + outputDirectory point to web/dist&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chore: remove legacy mockup/ vanilla-JS SPA&lt;/code&gt; — the mockup, six weeks after birth, was deleted in one commit&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;vercel_unsupported&lt;/code&gt; error is a specific kind: Vercel&amp;rsquo;s serverless functions can&amp;rsquo;t host long-lived SSE connections, so deploying the backend there produces a 503 on the chat route. The frontend detects it explicitly and shows a screen explaining the EC2-only constraint instead of a generic network error.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="theme-2--the-workflow-consolidates-to-five-gates"&gt;Theme 2 — The Workflow Consolidates to Five Gates
&lt;/h2&gt;&lt;p&gt;Through PR4, the workflow had three approval points — one after copy, one after scenario, one at the end of storyboard. May 21 introduced &lt;strong&gt;GATE 1 (key concept selection)&lt;/strong&gt; and made the gate count five.&lt;/p&gt;
&lt;h3 id="why-gate-1-needed-to-exist"&gt;Why GATE 1 needed to exist
&lt;/h3&gt;&lt;p&gt;The pre-existing flow ran &lt;code&gt;research → copy directly&lt;/code&gt;. The user submitted a brief, research happened, and then the copy stage emitted four parallel drafts (one each for 감성/직설/유머/하이브리드 angles). The problem: the copy drafts inherited the research agent&amp;rsquo;s interpretation of the brief without ever asking the user &lt;em&gt;which direction&lt;/em&gt; they wanted. Two days of revisions per project were going into pushing the copy stage back to a different angle the user hadn&amp;rsquo;t asked for.&lt;/p&gt;
&lt;p&gt;The fix was a new specialist between research and copy:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;research → [key_concept_planner produces 10 candidate concepts: 3-3-2-2 distribution]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → GATE 1 (user picks one)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → copy stage runs anchored to selected concept
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The 3-3-2-2 distribution (3 commercial, 3 emotional, 2 narrative, 2 conceptual) came from &lt;code&gt;CATEGORY_PLAN&lt;/code&gt;. A later refactor (&lt;code&gt;refactor(agents): derive key_concept distribution from CATEGORY_PLAN&lt;/code&gt;) made the distribution computed from the plan rather than hardcoded, so adjusting the mix in one place propagates.&lt;/p&gt;
&lt;h3 id="frontend--approvegatekeyconcept"&gt;Frontend — ApproveGateKeyConcept
&lt;/h3&gt;&lt;p&gt;The 10-card selection UI shipped as &lt;code&gt;ApproveGateKeyConcept&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): add KeyConcept gate types&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): ApproveGateKeyConcept 10-card selection drawer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): wire GATE 1 key-concept gate into ApproveGate&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Backend pieces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(runtime): emit GATE 1 key-concept gate after research stage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fix(runtime): only block key_concept_planner re-enqueue while job is in flight&lt;/code&gt; — prevented a race where a slow re-render dispatch was being filtered out&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(runtime): route selectedKeyConcept to copy and feedback re-entry&lt;/code&gt; — copy stage receives the selected concept&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(agents): copywriter_agent anchors copy to the selected key concept&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(runtime): persist key_concept_set artifacts&lt;/code&gt; + &lt;code&gt;record GATE 1 key-concept selections in diff_history&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="gate-3-concept-approval-and-gate-5-final-approval"&gt;GATE 3 (concept approval) and GATE 5 (final approval)
&lt;/h3&gt;&lt;p&gt;Two more gates filled the workflow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(runtime): add GATE 3 컨셉 승인 — two-phase scenario stage&lt;/code&gt; — scenario got split into concept-approval-then-full-scenario&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add GATE 5 (최종 승인) to the storyboard stage&lt;/code&gt; — the final approval before the project is considered done&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Combined with GATE 2 (copy) and GATE 4 (full scenario), this gave the canonical 5-gate flow that all the UI labels would refer to from this point on.&lt;/p&gt;
&lt;h3 id="gate_state--12-state-live-transitions"&gt;gate_state — 12-state live transitions
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;feat(runtime): wire gate_state 12-state live transitions&lt;/code&gt; split the gate lifecycle from job lifecycle. The 12 states encode every meaningful transition (pending → emitted → awaiting_user → user_approved → user_revising → rerunning → reapproved → &amp;hellip;), each surfaced to the frontend so the UI can render a meaningful &amp;ldquo;what&amp;rsquo;s happening with this gate&amp;rdquo; without polling for inferred state.&lt;/p&gt;
&lt;h3 id="the-planning-report-at-gate-1"&gt;The planning report at GATE 1
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;feat: add report_writer agent + 리서치 분석 보고서 at GATE 1&lt;/code&gt; added a second specialist that runs alongside key_concept_planner — it produces a structured 리서치 분석 보고서 (research analysis report) that gives the user the &lt;em&gt;context&lt;/em&gt; behind the 10 key concepts. Without this, the user is picking from concepts cold; with it, they see the underlying reasoning.&lt;/p&gt;
&lt;p&gt;Then a follow-up — &lt;code&gt;feat: add report_writer planning mode for 최종 기획 보고서&lt;/code&gt; — taught the same agent to produce a different &lt;em&gt;kind&lt;/em&gt; of report at the end of the workflow (a planning report summarizing the entire project).&lt;/p&gt;
&lt;h3 id="html-template-set"&gt;HTML template set
&lt;/h3&gt;&lt;p&gt;The reports needed to &lt;em&gt;look&lt;/em&gt; like documents, not chat bubbles. A whole HTML template set arrived in successive commits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat: add base.css slide canvas and primitives&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add storyboard cover and continuity-grid templates&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add deck.css and four simple deck templates&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add photo-caption and fullbleed-caption deck templates&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add annotated-photo, product-lineup, info-card, creative-board templates&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add report_writer HTML conversion design spec&lt;/code&gt; + &lt;code&gt;implementation plan&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add template gallery index&lt;/code&gt; + &lt;code&gt;LLM-facing template catalog&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The template catalog is the LLM-facing piece — the agent gets a structured list of templates to choose from when assembling a multi-page document. That&amp;rsquo;s how it can produce a storyboard cover, continuity grid, treatment grid, and conti sequence all from one prompt without hand-coding a layout per page.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="theme-3--approvebar-replaces-the-drawer-output-tabs-replace-the-inline-render"&gt;Theme 3 — ApproveBar Replaces the Drawer; Output Tabs Replace the Inline Render
&lt;/h2&gt;&lt;p&gt;By 2026-05-21 the bottom drawer pattern (&lt;code&gt;ApproveSheet&lt;/code&gt;) had become a UX problem: opening it covered the canvas, and the canvas was where the &lt;em&gt;result&lt;/em&gt; lived. The user had to keep collapsing the drawer to look at the storyboard they were approving.&lt;/p&gt;
&lt;p&gt;Three commits killed the drawer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): add slim ApproveBar, replace ApproveGate in ChatPanel&lt;/code&gt; — a horizontal bar at the bottom of the chat panel&lt;/li&gt;
&lt;li&gt;&lt;code&gt;refactor(web): remove ApproveSheet/StageCanvas drawer stack&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): show output panel on active gate + scenario gate advances&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The ApproveBar is just two buttons (Approve / 수정요청) plus the gate context — minimal, no drawer to drag. The actual artifact to inspect lives in the canvas (right column), where it always did.&lt;/p&gt;
&lt;h3 id="output-tabs--canvas-pivots-from-show-current-stage-to-tabbed-history-of-all-stages"&gt;Output tabs — canvas pivots from &amp;ldquo;show current stage&amp;rdquo; to &amp;ldquo;tabbed history of all stages&amp;rdquo;
&lt;/h3&gt;&lt;p&gt;Right after the ApproveBar landed, the canvas itself was re-architected:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): add output-tabs definitions and visibility logic&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add activeOutputTab to ui slice&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add output fields to pipeline slice&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): hydrate output slots from artifacts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): route gate/planning outputs into pipeline slice&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add analysis/keyConcept/planning/concept tab bodies&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add selectable copy tab body&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add OutputTabs container&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): swap CanvasPanel body to OutputTabs&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Previously the canvas only ever showed the &lt;em&gt;current&lt;/em&gt; stage&amp;rsquo;s artifact. Now it has tabs across the top — Analysis, Key Concept, Concept, Copy, Scenario, Storyboard — and the user can flip back to look at any prior artifact while still working on the current stage. That changes the UX entirely: the canvas became a project workspace, not just a current-state display.&lt;/p&gt;
&lt;h3 id="the-planner-generator-evaluator-loop"&gt;The Planner-Generator-Evaluator loop
&lt;/h3&gt;&lt;p&gt;The runtime equivalent of this UX move is the &lt;strong&gt;Planner-Generator-Evaluator loop&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat: Planner-Generator-Evaluator loop + BDI structure&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): register copy_evaluator in agent-copy map&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(runtime): extend the evaluator loop to the scenario stage (§3.4)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(runtime): extend the evaluator loop to the storyboard stage (§3.4)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fix(runtime): re-assess regenerations in the evaluator loop&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The pattern: every generation stage now runs Planner → Generator → Evaluator. The Evaluator decides whether the output meets quality bars; if not, the loop re-runs Generator with feedback. The user sees the &lt;em&gt;final&lt;/em&gt; output after the loop converges (or hits a max iteration cap). This is also what made the output tabs valuable — the planning artifact and evaluation notes for each stage are visible if the user wants to dig in.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="theme-4--the-revise-mode-system"&gt;Theme 4 — The Revise-Mode System
&lt;/h2&gt;&lt;p&gt;This is the biggest UX commitment in this window. Before this work, &amp;ldquo;I want to redo just one card&amp;rdquo; meant &amp;ldquo;redo the whole stage&amp;rdquo; — the user had no way to surgically pin everything except the piece they wanted regenerated.&lt;/p&gt;
&lt;h3 id="intent-classification"&gt;Intent classification
&lt;/h3&gt;&lt;p&gt;The first piece (&lt;code&gt;feat: add revision-intent — classify chat revision as bulk/partial&lt;/code&gt;) is a small classifier on chat input. When the user types in a gate context, the system asks: is this a &lt;em&gt;bulk&lt;/em&gt; request (&amp;ldquo;make all the copy more emotional&amp;rdquo;) or a &lt;em&gt;partial&lt;/em&gt; request (&amp;ldquo;change just the third one&amp;rdquo;)? The output drives different runtime paths:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bulk&lt;/strong&gt; → re-run the entire stage with the feedback as context&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Partial&lt;/strong&gt; → enter &amp;ldquo;revise mode&amp;rdquo; in the UI, let the user pick which items to regenerate&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// runtime/orchestration/revision-intent.js (paraphrased)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;classifyRevisionIntent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gateContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasPartialMarkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;partial&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;high&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasBulkMarkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;bulk&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;high&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ambiguous&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// UI nudges the user to clarify
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="revise-mode-ui"&gt;Revise-mode UI
&lt;/h3&gt;&lt;p&gt;The frontend gained a revise-mode toggle on the ApproveBar and multi-select checkboxes on every selectable tab:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat: pipeline store — reviseMode + reviseSelection state&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add RevisionContext type + ChatContext.revision field&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: ApproveBar revise-mode toggle with selection count&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: KeyConceptTab multi-select checkboxes in revise mode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: CopyTab multi-select checkboxes in revise mode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: ScenarioCanvas cut-level revise checkboxes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: StoryboardCanvas panel-level revise checkboxes&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;reviseSelection&lt;/code&gt; is a &lt;code&gt;Set&amp;lt;id&amp;gt;&lt;/code&gt; per artifact kind. The user toggles individual cards; the count appears live in the ApproveBar so they always know how many items will be regenerated.&lt;/p&gt;
&lt;h3 id="revision-merge--the-index-splice"&gt;Revision-merge — the index splice
&lt;/h3&gt;&lt;p&gt;The runtime side needed a way to actually &lt;em&gt;merge&lt;/em&gt; the regenerated subset back into the existing set. That&amp;rsquo;s the &lt;code&gt;revision-merge&lt;/code&gt; module:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// runtime/orchestration/revision-merge.js (paraphrased)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;mergeRevision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;regeneratedSubset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedIndices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;originalSet&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;regeneratedSubset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;newItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;selectedIndices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newItem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Pure function — splice at the indices that were selected, leave the rest frozen. Tested end-to-end: &lt;code&gt;test: end-to-end partial revision keeps frozen items + 3-3-2-2&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="each-agent-learned-partial-regeneration"&gt;Each agent learned partial regeneration
&lt;/h3&gt;&lt;p&gt;Five specialist agents learned to regenerate only the selected slots:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat: key_concept_planner regenerates selected slots (category-preserving)&lt;/code&gt; — preserves the 3-3-2-2 distribution&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: copywriter_agent regenerates selected copy drafts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: concept_anchor applies revision feedback (GATE 3, whole-doc)&lt;/code&gt; — GATE 3 is whole-doc only because concept anchor is itself a single artifact&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: scene_designer cut-level revision (structure + cut count preserved)&lt;/code&gt; — preserves total cut count&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: storyboard_generator regenerates selected panels + images&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each agent had to know what its frozen-item constraints were. Storyboard could regenerate just panel 3 without recomputing the cut count, but scene_designer had to preserve total cut count because the scenario&amp;rsquo;s structural contract is the cut count.&lt;/p&gt;
&lt;h3 id="routing-layer"&gt;Routing layer
&lt;/h3&gt;&lt;p&gt;Two routing commits put it together:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat: root-planner routes context.revision to single target agent&lt;/code&gt; — backend only enqueues the one specialist needed&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: route chat revision intent — bulk runs, partial nudges revise mode&lt;/code&gt; — frontend dispatches to the right path&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: dispatch revise_mode_hint — open revise mode on partial chat intent&lt;/code&gt; — backend can &lt;em&gt;suggest&lt;/em&gt; revise mode to the frontend via a hint event, so a partial intent auto-opens the UI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The hint event was a careful boundary — backend never forces the frontend; it suggests. The frontend can override if its local state has reason to.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="theme-5--session-isolation-multi-attempt-per-project"&gt;Theme 5 — Session Isolation (multi-attempt per project)
&lt;/h2&gt;&lt;p&gt;On 2026-05-22 the workspace went from one-session-per-project to multi-session-per-project. A project can now hold parallel attempts — a single creative brief might be tried three different ways, each as its own session, with their own pipeline state.&lt;/p&gt;
&lt;p&gt;Key commits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Isolate workspace sessions and restore state per session&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server: project sessions list endpoint&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;web: session restore and run recovery&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Start a fresh pipeline for a new session's first brief&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;launcher: show per-session states on project cards&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;launcher/workspace: deletable sessions + per-session gate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;web: scope workspace canvas to the active session&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;web: honor session routes and preserve revision scopes&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The launcher card now shows per-session gates — a project that had three sessions, two at storyboard and one at copy, renders all three states. Sessions can be deleted individually. The workspace canvas, the revise mode, and the gate state all scope to the active session.&lt;/p&gt;
&lt;p&gt;The biggest invisible commit: &lt;code&gt;Lock the session after final storyboard approval&lt;/code&gt;. Once GATE 5 is approved, the session is read-only. This prevents accidental edits to a sealed deliverable, and forces the user to consciously start a new session if they want to iterate further.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="insights"&gt;Insights
&lt;/h2&gt;&lt;p&gt;Four themes in three days, but the recurring shape is the same: &lt;strong&gt;collapse of &amp;ldquo;all-or-nothing&amp;rdquo; into something granular.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The drawer-or-canvas binary became the output-tabs continuum (you can see every artifact at once)&lt;/li&gt;
&lt;li&gt;The three gates became five (with the new gate guarding the most expensive downstream cost: copy + scenario + storyboard run on a chosen direction)&lt;/li&gt;
&lt;li&gt;The whole-stage-re-run became partial revision (you regenerate exactly what you want)&lt;/li&gt;
&lt;li&gt;The one-session-per-project became multi-session-per-project (a project can hold parallel ideas)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each of these moves cost more code than the binary version would have — partial revision alone required intent classification, frozen-item preservation logic in five agents, a Set-of-IDs in the store, and an index-splice merge function — but each one removed a class of frustration the user had been quietly bearing. The 153-commit count looks heroic; the through-line is more modest: every time the system said &amp;ldquo;all or nothing,&amp;rdquo; replace it with &amp;ldquo;exactly what you meant.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Next: polish week — observability instrumentation, per-project state isolation under refresh, inline past-gate rewind from chat, and the revision-request affordance pass that finally unified the visual treatment across every tab.&lt;/p&gt;</description></item></channel></rss>