<?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/ko/tags/agent-orchestration/</link><description>Recent content in Agent Orchestration on ICE-ICE-BEAR-BLOG</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Thu, 28 May 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://ice-ice-bear.github.io/ko/tags/agent-orchestration/index.xml" rel="self" type="application/rss+xml"/><item><title>Creative Agent Studio 개발일지 #5 — 폴리시 주간: LLM 토큰 텔레메트리, 멀티세션 격리, 인라인 되감기, 그리고 수정요청 어포던스</title><link>https://ice-ice-bear.github.io/ko/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/ko/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 — 폴리시 주간: LLM 토큰 텔레메트리, 멀티세션 격리, 인라인 되감기, 그리고 수정요청 어포던스" /&gt;&lt;h2 id="개요"&gt;개요
&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;이전 글: #4 — 5개 게이트, 4개 캔버스, 수정 모드 시스템&lt;/a&gt;이 빌드아웃 푸시를 끝냈다. 6일 뒤, &lt;strong&gt;4 작업일에 23커밋&lt;/strong&gt;이 폴리시 주간을 마무리했다 — 기능 완성된 시스템을 실제 사용자의 손 아래 production에서 버티는 무언가로 바꾸는 종류의 작업.&lt;/p&gt;
&lt;p&gt;주중에 네 갈래가 병렬로 흘렀다. 첫째, &lt;strong&gt;LLM 호출 관측성&lt;/strong&gt; — 모든 모델 호출이 이제 토큰 사용량과 전파된 logCtx를 갖춘 구조화 로그를 발신하고, 런타임은 &lt;code&gt;LOG_LEVEL&lt;/code&gt; 임계 제어를 받았다. 둘째, &lt;strong&gt;새로고침 아래 프로젝트별 상태 격리&lt;/strong&gt; — 2026-05-26의 여덟 커밋이 멀티세션 작업이 떨어진 뒤 표면화된 hooks-order 버그, 끊긴 부트스트랩, crypto.randomUUID 격차를 사냥했다. 셋째, &lt;strong&gt;인라인 과거-게이트 되감기&lt;/strong&gt; — 단일 feat 커밋이 사용자가 활성 콘티 세션 안에서 &amp;ldquo;GATE 2의 세 번째 카피로 바꿔줘&amp;quot;라고 말하고 세션을 떠나지 않을 수 있는 어포던스를 출시했다. 넷째, &lt;strong&gt;수정요청 어포던스 패스&lt;/strong&gt; — 다섯 커밋이 모든 캔버스 탭의 마킹 카드 비주얼을 통일하고, 되돌리기 다이얼로그 카피를 다듬고, 게이트 셀렉터가 사용자가 방금 클릭한 카드를 잊는 상태 버그를 수정했다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 Prev["#4 — 빌드아웃 (5cb4106)"] --&gt; Thread1["갈래 1 &amp;lt;br/&amp;gt; LLM 관측성 &amp;lt;br/&amp;gt; 토큰 사용량 + logCtx"]
 Prev --&gt; Thread2["갈래 2 &amp;lt;br/&amp;gt; 프로젝트별 격리 &amp;lt;br/&amp;gt; hooks order + 부트스트랩 수정"]
 Prev --&gt; Thread3["갈래 3 &amp;lt;br/&amp;gt; 인라인 과거-게이트 되감기 &amp;lt;br/&amp;gt; 채팅 주도 파이프라인 편집"]
 Prev --&gt; Thread4["갈래 4 &amp;lt;br/&amp;gt; 수정요청 UX &amp;lt;br/&amp;gt; 통일된 마킹 카드 어포던스"]
 Thread1 --&gt; End["#5 종료 (67b820e) &amp;lt;br/&amp;gt; 2026-05-28"]
 Thread2 --&gt; End
 Thread3 --&gt; End
 Thread4 --&gt; End&lt;/pre&gt;&lt;p&gt;관통하는 한 주제 — &lt;strong&gt;기능은 끝났다. 남은 건 새로고침, 부하, 그리고 혼란스러운 사용자 아래에서 정직하게 만드는 일이었다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="갈래-1--llm-호출-관측성"&gt;갈래 1 — LLM 호출 관측성
&lt;/h2&gt;&lt;p&gt;#2에서 떨어진 구조화 로거가 토대였다 — 이번 주는 그것을 모든 LLM 호출 사이트에 꿰어서 production 텔레메트리가 실제로 의미를 갖게 했다.&lt;/p&gt;
&lt;p&gt;두 커밋이 무거운 짐을 졌다.&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; — &lt;code&gt;LOG_LEVEL&lt;/code&gt; env var로 production이 &lt;code&gt;info&lt;/code&gt; 잡담을 떨구고 &lt;code&gt;warn&lt;/code&gt;/&lt;code&gt;error&lt;/code&gt;만 유지할 수 있다. 동반 변경은 모든 워커 fork에서 출력되던 &lt;code&gt;node:sqlite&lt;/code&gt; 실험적 경고를 억제했다.&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;와 &lt;code&gt;runtime/orchestration/run-lifecycle.js&lt;/code&gt;가 이제 각 의미 있는 전이(잡 클레임, 실행 시작, 게이트 발신, 단계 전진, 실행 완료/오류)에서 구조화 이벤트를 로깅.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(observability): instrument LLM calls with token usage + logCtx&lt;/code&gt; — 이게 하중 지지 조각이다. 모든 모델 호출이 이제 다음을 로깅.&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 (의역)
&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;는 &lt;code&gt;worker-loop.js&lt;/code&gt;에서 전파되며 &lt;code&gt;{ runId, projectId, stage, role }&lt;/code&gt;를 들고 다닌다. 그래서 단일 LLM 호출의 로그 줄이 어느 프로젝트, 어느 세션, 어느 게이트, 어느 에이전트, 어느 모델, 토큰이 얼마나 들었는지를 말해준다. #2에서 셋업했던 Grafana 대시보드에 이제 실제 데이터가 흐른다.&lt;/p&gt;
&lt;p&gt;인프라 측 동반은 &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;. EC2 인스턴스는 프로토타입 시절부터 평일 전용 스케줄로 돌고 있었다 — cron이 이제 매일 박스를 시작하고 03:00 KST에 멈춘다. 운영, 기능 작업 아님.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="갈래-2--새로고침-아래-프로젝트별-상태-격리"&gt;갈래 2 — 새로고침 아래 프로젝트별 상태 격리
&lt;/h2&gt;&lt;p&gt;#4의 멀티세션 작업이 새로고침 시점 버그의 전체 부류를 도입했다. 사용자가 workspace 안에서 하드 새로고침하면, React 트리는 어떤 데이터도 로드되기 전에 마운트된다 — 그리고 멀티세션 로직은 사용자가 launcher에서 workspace로 &lt;em&gt;들어왔을&lt;/em&gt; 때만 성립하는 가정(부트스트랩이 이미 일어났다)을 만들고 있었다.&lt;/p&gt;
&lt;p&gt;2026-05-26의 여덟 커밋이 엣지 케이스를 사냥했다.&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; — workspace 페이지가 &lt;code&gt;projects&lt;/code&gt;가 이미 로드됐다고 가정했다. 새로고침 시에는 아니었다. 수정은 workspace effect 안에 부트스트랩 호출을 추가했다 — 하지만 그게 React hooks-order 위반을 트리거했다, 부트스트랩 호출이 &lt;code&gt;useEffect&lt;/code&gt; 안에서 조건부였기 때문. 둘 다 한 커밋으로 수정됐다.&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; — 프로젝트 A에서 B로 전환할 때 A의 세션 목록이 B가 로딩되는 동안 보이고 있었다. 수정 — &lt;code&gt;projectId&lt;/code&gt;가 바뀌면 즉시 프로젝트별 슬라이스를 비우고 로더를 보여준 다음 B의 데이터를 hydrate. 사용자는 잠깐의 로더를 보지, A의 세션을 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; — ref로 구현된 &amp;ldquo;이미 부트스트랩 중이면 재부트스트랩하지 마라&amp;rdquo; 가드가 있었다. ref가 &lt;code&gt;true&lt;/code&gt;로 세팅된 뒤 특정 에러 경로에서 클리어되지 않아, 후속 네비게이션이 stranded됐다. 수정 — 부트스트랩 상태를 ref가 아니라 슬라이스에서 추적하고, 성공 &lt;em&gt;과&lt;/em&gt; 실패 모두에서 클리어.&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; — Composer(채팅 입력)가 &lt;em&gt;모든&lt;/em&gt; 사용자 턴의 진입점이었다. 하지만 세션이 첫 브리프 너머로 가면 게이트 기반 흐름이 인계한다 — Composer의 역할은 숨겨져야 했다. 수정은 첫 브리프가 제출되기 전에만 Composer를 보이게 만들었다 — 그 뒤로는 ApproveBar가 사용자의 표면.&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;이 의존성 배열이 불안정한 AbortController를 사용하고 있어서 매 재렌더마다 abort됐다. 수정은 dep를 안정화해서 컨트롤러가 실제 사용자 취소 또는 언마운트에서만 abort되게 했다.&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; — production EC2가 일부 클라이언트에 평문 HTTP로 서빙하고 있었고(중간 프록시가 HTTPS를 떼어냈다), &lt;code&gt;crypto.randomUUID&lt;/code&gt;는 secure context에서만 사용 가능하다. 프로젝트와 세션이 생성될 수 없었다. 폴리필이 복원.&lt;/p&gt;
&lt;p&gt;마지막이 이 주에서 가장 &amp;ldquo;production이 dev 환경이 숨겼던 걸 드러낸다&amp;rdquo; 케이스다. 로컬호스트는 secure context. 배포 타깃은 항상 그렇지는 않다. 한 줄 폴리필이 EC2 빌드를 사용 가능하게 유지했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fix: preserve 분석 보고서 across key-concept revisions&lt;/code&gt;&lt;/strong&gt; — 런타임 수정 동반. 사용자가 GATE 1에서 키 컨셉 선택을 수정할 때, 분석 보고서가 함께 재계산되고 있었다 — 불필요한 작업이고 매번 약간 다른 보고서를 만들었다. 수정은 키 컨셉 수정에 걸쳐 원래 분석을 고정.&lt;/p&gt;
&lt;p&gt;그리고 &lt;code&gt;docs(claude): register Diffs Runtime harness pointer in CLAUDE.md&lt;/code&gt;가 Claude Code 세션이 런타임의 하네스 규약을 자동으로 찾을 수 있도록 포인터를 추가했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="갈래-3--채팅에서-인라인-과거-게이트-되감기"&gt;갈래 3 — 채팅에서 인라인 과거-게이트 되감기
&lt;/h2&gt;&lt;p&gt;단일 커밋이 큰 UX 이동을 출시했다 — &lt;code&gt;feat(web): add inline past-gate rewind for chat-driven pipeline edits&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;설정 — 폴리시 주간이 되자 사용자는 현재 게이트에 대한 수정 모드를 가진 ApproveBar를 갖췄고, 워크플로는 한 번에 한 단계씩 전진했다. 그런데 사용자가 &lt;em&gt;과거&lt;/em&gt; 결정을 바꾸고 싶으면? &amp;ldquo;사실 두 번째 키 컨셉으로 가자&amp;rdquo; — 콘티 단계에 앉아서?&lt;/p&gt;
&lt;p&gt;이전엔 캔버스를 떠나, 수동으로 GATE 1으로 돌아가고, 재선택하고, 후속 모든 게이트를 다시 걸어야 했다. 고통스럽고, 채팅 우선 원칙 위반.&lt;/p&gt;
&lt;p&gt;인라인 되감기는 채팅에서 과거-게이트 의도를 감지한다.&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;// (의역 — 결합된 chat-stream + dispatch-sse 경로)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&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;// &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;// 프론트엔드가 rewind_proposal SSE 이벤트 수신:
&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가 인라인 ConfirmRewindDialog 표시 (갈래 4에서 다듬은 것)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;다이얼로그는 폐기될 것을 명시적으로 나열한다 — GATE 2의 카피 승인, GATE 4의 시나리오 승인 등 — 그래서 사용자가 확인 전 비용을 안다. 확인 시 런타임이 프로젝트를 타깃 게이트로 되돌리고, 하류 모든 것을 재생성하고, 사용자는 새 선택을 할 준비가 된 타깃 게이트의 승인 표면에 도착한다.&lt;/p&gt;
&lt;p&gt;이건 게이트 기반 자동 실행 원칙(&lt;code&gt;interaction-model.md&lt;/code&gt;의 결정 3)의 자연스러운 확장이다 — 단 &lt;em&gt;역방향&lt;/em&gt;으로. 원칙은 — 각 단계는 사용자 승인에서 다음으로 전진. 되감기는 — 각 단계는 &lt;em&gt;되돌아갈&lt;/em&gt; 수도 있고, 런타임은 무엇을 무효화할지 안다.&lt;/p&gt;
&lt;p&gt;동반 &lt;code&gt;docs(claude): add triage-prod-bug skill trigger to enforce browser-first debugging&lt;/code&gt;는 하네스 규칙이다 — 버그가 보고되면, 코드를 읽기 전에 브라우저를 먼저 본다(devtools, network, console). 채팅 우선 제품 원칙에는 디버깅 유사물이 있다 — 먼저 보고, 그 다음 코드.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="갈래-4--수정요청-어포던스-패스"&gt;갈래 4 — 수정요청 어포던스 패스
&lt;/h2&gt;&lt;p&gt;마지막 다섯 커밋이 모든 캔버스 탭의 수정요청 UI를 통일했다. 이 작업 전까지 모든 탭은 자기 마킹 카드 스타일을 구현하고 있었다 — Copy에는 노란 배경, KeyConcept에는 빨간 점선 테두리, Storyboard에는 파란 왼쪽 엣지 바(이거 하나만), Scene에는 비주얼 처리 전혀 없음 — 그리고 사용자가 계속 물었다 — &lt;em&gt;&amp;ldquo;왜 콘티에만 파란 마크가 뜨나요?&amp;rdquo;&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="되돌리기-다이얼로그-카피-완화--폐기-리스트에서-최종-게이트-숨기기"&gt;되돌리기 다이얼로그 카피 완화 + 폐기 리스트에서 최종 게이트 숨기기
&lt;/h3&gt;&lt;p&gt;커밋 &lt;code&gt;e27316a&lt;/code&gt;. 첫 공격은 가장 거슬리던 카피였다. 되돌리기 다이얼로그가 이렇게 읽혔다.&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; ← 이 리스트에 있으면 안 됨&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;두 문제 — 폐기 리스트에 &lt;em&gt;최종&lt;/em&gt; 승인 게이트가 들어가 있었지만(그건 끝에 다시 확정하는 단계지 사라지는 단계가 아님), &amp;ldquo;결정&amp;quot;이라는 단어가 너무 기업스러웠다. 실제로는 초안에 대한 크리에이티브 판단이지 이사회 안건이 아니다.&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;폐기 미리보기에서 &lt;code&gt;final-approval&lt;/code&gt; 게이트를 빼고, &lt;code&gt;softTitle&lt;/code&gt;(예: &amp;ldquo;콘티 검토 결정&amp;rdquo; 대신 &amp;ldquo;콘티 검토&amp;rdquo;)을 쓴다. 같은 &lt;code&gt;Gate&lt;/code&gt; 인터페이스에서 가져오지만 파괴적 컨텍스트와 정보성 컨텍스트에서 다르게 렌더링한다.&lt;/p&gt;
&lt;h3 id="되돌리기-다이얼로그의-게이트-타이틀"&gt;되돌리기 다이얼로그의 게이트 타이틀
&lt;/h3&gt;&lt;p&gt;커밋 &lt;code&gt;4ddff68&lt;/code&gt;. 다이얼로그가 기계 생성 게이트 id(&lt;code&gt;g-2&lt;/code&gt;, &lt;code&gt;g-3&lt;/code&gt;)를 타이틀로 쓰고 있었다. 작은 라벨 맵이 이해도를 고쳤다.&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;이제 다이얼로그가 &amp;ldquo;콘티 검토 단계부터 다시 진행합니다&amp;quot;라고 읽힌다 — 캔버스 탭 라벨과 정확히 일치하는 표현이라, 사용자가 되감기 후 어느 탭에 도착할지 미리 알 수 있다.&lt;/p&gt;
&lt;h3 id="수정요청-진입-시-approvebar-게이트-미리-선택"&gt;수정요청 진입 시 ApproveBar 게이트 미리 선택
&lt;/h3&gt;&lt;p&gt;커밋 &lt;code&gt;c55891b&lt;/code&gt;. 카드의 수정요청을 누르면 ApproveBar가 게이트 셀렉터와 함께 슬라이드 업했는데, 셀렉터가 비어 있어서 사용자가 방금 마킹한 게이트를 다시 클릭해야 했다. 두 줄짜리 &lt;code&gt;useEffect&lt;/code&gt;로 암묵적 컨텍스트를 명시적 선택으로 동기화했다.&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;// 닫기 시 초기화
&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;이 변경의 보안 리뷰 패스가 한 가지 노트를 표면화했다 — 게이트 id가 사용자 제어 DOM 이벤트에서 서버 사이드 뮤테이션으로 흘러가는데, 서버에서 어차피 현재 워크플로의 게이트 집합과 대조 검증하니 추가 클라이언트 검증은 불필요. 다만 그 불변식이 리뷰를 통해 명시적으로 기록됐다.&lt;/p&gt;
&lt;h3 id="다섯-탭에-걸친-마킹-카드-비주얼-통일"&gt;다섯 탭에 걸친 마킹 카드 비주얼 통일
&lt;/h3&gt;&lt;p&gt;커밋 &lt;code&gt;2804420&lt;/code&gt;. 비주얼 통일 패스. 통일된 처리는 디자인 토큰 하나에 모았다.&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;그리고 모든 탭 컴포넌트는 이제 카드를 이렇게 감싼다.&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;/* 탭별 콘텐츠 */&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;도 추출했다 — 예전엔 각 탭이 자기 라벨을 인라인으로 만들면서 카피가 제각각이었다 (&amp;ldquo;수정 요청됨&amp;rdquo;, &amp;ldquo;리비전&amp;rdquo;, &amp;ldquo;Edit Pending&amp;rdquo;). 이제 컴포넌트 하나, 문자열 하나다.&lt;/p&gt;
&lt;h3 id="모든-게이트-마커를-한-번씩만-표시하고-라이브-게이트-상태-반영"&gt;모든 게이트 마커를 한 번씩만 표시하고 라이브 게이트 상태 반영
&lt;/h3&gt;&lt;p&gt;이 날의 마지막 커밋(&lt;code&gt;67b820e&lt;/code&gt;). workspace 상단의 StageStepper가 어떤 상태에서 중복 게이트 마커를 표시하고 있었다(재발신된 게이트 이벤트가 두 번째 점을 추가) 그리고 실제 gate_state 전이보다 뒤처져 있었다. 한 수정에 두 버그.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;게이트 id로 중복 제거해서 각 게이트가 정확히 한 번 렌더&lt;/li&gt;
&lt;li&gt;파이프라인 슬라이스의 라이브 &lt;code&gt;gate_state&lt;/code&gt;(#4의 12상태 필드)를 와이어드해서 stepper가 방금 일어난 어떤 전이든 반영&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;12상태 gate_state 필드는 일주일 전부터 있었지만, stepper는 여전히 오래된 &amp;ldquo;마지막 완료 게이트&amp;rdquo; 추론을 쓰고 있었다. 이 커밋이 그 격차를 닫았다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="커밋-로그-총-23개"&gt;커밋 로그 (총 23개)
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;날짜&lt;/th&gt;
 &lt;th&gt;메시지&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="인사이트"&gt;인사이트
&lt;/h2&gt;&lt;p&gt;폴리시 주간의 패턴 — &lt;strong&gt;관측성과 격리 모두 조용히 가정되던 상태를 드러내는 일이었다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;LLM 토큰 텔레메트리가 &lt;em&gt;실제로 돈을 쓰던 것&lt;/em&gt;을 드러냈다 — 이전 대시보드는 큐 깊이와 잡 지속시간을 보여줬는데 둘 다 중요하지만, 느린 에이전트가 쌀 수도 있고 빠른 에이전트가 비쌀 수도 있다, 토큰 사용량에 따라. &lt;code&gt;logCtx&lt;/code&gt;를 모든 호출에 꿰어 넣자 비용 분석이 필요로 하는 프로젝트별, 단계별, 에이전트별 분해가 표면화됐다.&lt;/p&gt;
&lt;p&gt;멀티세션 새로고침 수정들이 &lt;em&gt;workspace가 자기 부트스트랩에 대해 가정하던 것&lt;/em&gt;을 드러냈다. 사용자가 launcher에서 들어왔을 때 기능이 작동했던 이유는 launcher의 부트스트랩이 우연히 만족된 전제조건이었기 때문. 새로고침이 그 전제조건을 깨자 버그가 표면화됐다 — 하지만 내내 거기 있었다, 잠복해.&lt;/p&gt;
&lt;p&gt;인라인 과거-게이트 되감기는 같은 아이디어를 사용자의 멘탈 모델에 적용한 것이다 — 사용자는 *&amp;ldquo;GATE 1로 돌아가서 뭔가 바꾸고 싶다&amp;rdquo;*고 생각한다. 되감기 기능은 그 의도를 시스템에 노출시켜 사용자가 네비게이션 단계로 번역하도록 강제하지 않는다. 채팅 우선 원칙은 단지 입력 메커니즘이 아니다 — 시스템이 의도를 이해할 것이라는 약속이다.&lt;/p&gt;
&lt;p&gt;수정요청 어포던스 패스는 같은 빙산의 작은 보이는 끝이다. 세 겹의 명료함 — 비주얼 일관성, 정직한 카피, 상태 보존 — 이 파괴적 워크플로 동작에 적용되어, 확인을 요청하는 다이얼로그가 잃게 될 것에 대해 진실을 말한다.&lt;/p&gt;
&lt;p&gt;다음 — 여기서부터 시스템은 기능적으로 완성됐으니, 미래 개발일지는 기능 추가보다 production 교훈으로 기울 것이다 — 에이전트 프롬프트 튜닝, 비용 추세, 실제 사용자 세션에서 떠오르는 다음 라운드의 UX 패턴. 5포스트 백필이 깔끔한 호를 닫는다 — 목업, production-readying, 메가푸시, 게이트 워크플로, 폴리시. 제품이 이제 진짜다.&lt;/p&gt;</description></item><item><title>Creative Agent Studio 개발일지 #4 — 5개의 게이트, 4개의 캔버스, 그리고 수정 모드 시스템</title><link>https://ice-ice-bear.github.io/ko/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/ko/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 — 5개의 게이트, 4개의 캔버스, 그리고 수정 모드 시스템" /&gt;&lt;h2 id="개요"&gt;개요
&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;이전 글: #3 — 134커밋 메가푸시&lt;/a&gt;가 PR4(ApproveSheet + 세 게이트 변형)가 머지되고 프로젝트 상태가 리로드를 살아남는 상태로 끝났다. 3일 뒤 &lt;strong&gt;153 커밋&lt;/strong&gt;이 더 떨어져 4단계 에이전트 워크플로의 남은 모든 조각을 완성했다 — 단계 출력을 실체화한 4변형 캔버스 패널(PR5), 새 GATE 1의 키 컨셉 플래너로 3게이트에서 &lt;strong&gt;5&lt;/strong&gt;게이트로의 워크플로 통합, 하단 드로어 ApproveSheet를 슬림 ApproveBar + 출력 탭으로 대체한 UI 재설계, 그리고 사용자가 전체 단계를 다시 하는 대신 단일 키 컨셉, 카피 드래프트, 컷, 또는 콘티 패널을 외과적으로 재실행할 수 있게 하는 &lt;strong&gt;수정 모드 다중 선택 시스템&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;아래에서는 런타임이 Planner-Generator-Evaluator 루프, 사용자 피드백으로부터 저작되는 연속성 앵커, 프롬프트에 주입되는 진화 노트, 기획 보고서와 콘티를 위한 HTML 템플릿 세트, 그리고 한 프로젝트가 여러 병렬 시도를 들고 갈 수 있도록 하는 세션 격리를 받았다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 Prev["#3 — 메가푸시 (9a2f851)"] --&gt; Theme1["흐름 1 &amp;lt;br/&amp;gt; PR5 — 4단계 캔버스 &amp;lt;br/&amp;gt; + 리사이즈 + 접기"]
 Prev --&gt; Theme2["흐름 2 &amp;lt;br/&amp;gt; 5게이트 워크플로 &amp;lt;br/&amp;gt; GATE 1에 키 컨셉 플래너"]
 Prev --&gt; Theme3["흐름 3 &amp;lt;br/&amp;gt; ApproveBar + 출력 탭 &amp;lt;br/&amp;gt; 드로어 교체"]
 Prev --&gt; Theme4["흐름 4 &amp;lt;br/&amp;gt; 수정 모드 시스템 &amp;lt;br/&amp;gt; 부분 vs 일괄 수정"]
 Theme1 --&gt; End["#4 종료 (5cb4106) &amp;lt;br/&amp;gt; 2026-05-22"]
 Theme2 --&gt; End
 Theme3 --&gt; End
 Theme4 --&gt; End&lt;/pre&gt;&lt;p&gt;네 흐름, 반복되는 한 모양 — &lt;strong&gt;워크플로가 만지는 모든 레이어에서 &amp;ldquo;전부 아니면 전무&amp;quot;가 되기를 멈췄다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="흐름-1--pr5-4단계-변형의-캔버스-패널"&gt;흐름 1 — PR5: 4단계 변형의 캔버스 패널
&lt;/h2&gt;&lt;p&gt;PR2가 &lt;code&gt;CanvasPanel&lt;/code&gt; 플레이스홀더를 예약했다. PR5가 그것을 채웠다. 캔버스는 workspace의 오른쪽 컬럼 — 현재 단계의 &lt;em&gt;현재 artifact&lt;/em&gt;를 단계별 컴포넌트로 렌더링한다.&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 (의역)
&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;각 변형은 &lt;code&gt;pipeline&lt;/code&gt; 슬라이스에서 데이터를 읽고 단계에 맞는 레이아웃을 렌더한다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;단계&lt;/th&gt;
 &lt;th&gt;변형&lt;/th&gt;
 &lt;th&gt;보여주는 것&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;pipelineContext&lt;/code&gt;의 &lt;code&gt;AdBriefRecap&lt;/code&gt; + &lt;code&gt;ResearchSummaryCards&lt;/code&gt; + 활성 태스크 목록&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;copyHistory&lt;/code&gt;의 &lt;code&gt;CopyOptions&lt;/code&gt;로 만든 &lt;code&gt;ConceptGrid&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;Act별 &lt;code&gt;SceneStrip&lt;/code&gt; + &lt;code&gt;scenarioHistory&lt;/code&gt;의 &lt;code&gt;CutChip&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;&lt;code&gt;storyboardImageUrls&lt;/code&gt;에서 scene당 &lt;code&gt;StoryboardPage&lt;/code&gt; 1개&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;콘티 이미지 처리는 작은 아키텍처 선택이 필요했다. base64 PNG 페이로드가 &lt;code&gt;storyboard_image&lt;/code&gt; SSE 이벤트로 도착한다. 파이프라인 슬라이스가 각각을 blob URL(&lt;code&gt;URL.createObjectURL&lt;/code&gt;)로 변환하고, 재생성이 이전 이미지를 누수하지 않도록 replace-on-revoke 단계를 둔다. &lt;code&gt;addStoryboardImage&lt;/code&gt; 리듀서가 revoke 로직을 갖는다.&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="리사이즈--접기"&gt;리사이즈 + 접기
&lt;/h3&gt;&lt;p&gt;두 커밋이 리사이즈 어포던스를 추가했다.&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;는 ui 슬라이스에서 360..800px로 클램프&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add CanvasResizer component (vertical drag handle + warm hover tint)&lt;/code&gt; — 비주얼 핸들, 호버 틴트는 Creative Warmth를 따른다(미묘한 따뜻한 그라데이션, 순백 하이라이트 없음)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): WorkspacePage grid column reads ui.canvasWidth + respects canvasCollapsed&lt;/code&gt; — 그리드가 실제로 너비에 반응&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;CanvasHeader&lt;/code&gt;는 접기 토글을 얻어서 파워 유저가 채팅에 집중하고 싶을 때 캔버스를 완전히 숨길 수 있다.&lt;/p&gt;
&lt;h3 id="pr6--폴리시--백엔드-스왑--목업-삭제"&gt;PR6 — 폴리시 + 백엔드 스왑 + 목업 삭제
&lt;/h3&gt;&lt;p&gt;PR6은 &amp;ldquo;React로 모든 게 동작한다&amp;quot;와 &amp;ldquo;실제로 사용자에게 React를 서빙한다&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; — 목업이, 탄생 6주 만에, 한 커밋으로 삭제됐다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;vercel_unsupported&lt;/code&gt; 에러는 특정 종류다 — Vercel의 서버리스 함수는 장기 SSE 연결을 호스트할 수 없으니, 거기 백엔드를 배포하면 채팅 라우트에서 503이 나온다. 프론트엔드가 이걸 명시적으로 감지하고 일반 네트워크 에러 대신 EC2 전용 제약을 설명하는 화면을 보여준다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="흐름-2--워크플로가-5게이트로-통합됨"&gt;흐름 2 — 워크플로가 5게이트로 통합됨
&lt;/h2&gt;&lt;p&gt;PR4까지 워크플로는 승인 지점이 세 개였다 — 카피 뒤 하나, 시나리오 뒤 하나, 콘티 끝에 하나. 5월 21일이 **GATE 1(키 컨셉 선택)**을 도입하고 게이트 수를 다섯으로 만들었다.&lt;/p&gt;
&lt;h3 id="gate-1이-존재해야-했던-이유"&gt;GATE 1이 존재해야 했던 이유
&lt;/h3&gt;&lt;p&gt;기존 흐름은 &lt;code&gt;research → copy로 직접&lt;/code&gt; 실행했다. 사용자가 브리프를 제출하면 리서치가 일어나고, 그 다음 카피 단계가 네 가지 평행 드래프트(각각 감성/직설/유머/하이브리드 각도)를 발신했다. 문제 — 카피 드래프트는 사용자에게 &lt;em&gt;어느 방향&lt;/em&gt;을 원하는지 묻지 않고 리서치 에이전트의 브리프 해석을 상속받았다. 프로젝트당 이틀의 수정이 사용자가 요청하지 않은 다른 각도로 카피 단계를 밀어내는 데 들어가고 있었다.&lt;/p&gt;
&lt;p&gt;수정은 리서치와 카피 사이에 새 스페셜리스트.&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가 10개 후보 컨셉 생성: 3-3-2-2 분배]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → GATE 1 (사용자가 하나 선택)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 카피 단계가 선택된 컨셉에 앵커링되어 실행
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;3-3-2-2 분배(상업적 3, 감성적 3, 내러티브 2, 컨셉추얼 2)는 &lt;code&gt;CATEGORY_PLAN&lt;/code&gt;에서 왔다. 뒤이은 리팩토링(&lt;code&gt;refactor(agents): derive key_concept distribution from CATEGORY_PLAN&lt;/code&gt;)이 분배를 하드코딩에서 계획으로부터 계산되게 바꿔서, 한 곳의 믹스 조정이 전파된다.&lt;/p&gt;
&lt;h3 id="프론트엔드--approvegatekeyconcept"&gt;프론트엔드 — ApproveGateKeyConcept
&lt;/h3&gt;&lt;p&gt;10카드 선택 UI가 &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;백엔드 조각.&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; — 느린 재렌더 dispatch가 필터아웃되던 레이스 방지&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(runtime): route selectedKeyConcept to copy and feedback re-entry&lt;/code&gt; — 카피 단계가 선택된 컨셉을 받는다&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컨셉-승인와-gate-5최종-승인"&gt;GATE 3(컨셉 승인)와 GATE 5(최종 승인)
&lt;/h3&gt;&lt;p&gt;두 게이트가 더 워크플로를 채웠다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(runtime): add GATE 3 컨셉 승인 — two-phase scenario stage&lt;/code&gt; — 시나리오가 컨셉 승인 → 전체 시나리오로 쪼개졌다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add GATE 5 (최종 승인) to the storyboard stage&lt;/code&gt; — 프로젝트가 완료된 것으로 간주되기 전 최종 승인&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GATE 2(카피)와 GATE 4(전체 시나리오)와 결합되면, 이게 이 시점부터 모든 UI 라벨이 참조할 정전 5게이트 흐름을 만들었다.&lt;/p&gt;
&lt;h3 id="gate_state--12상태-라이브-전이"&gt;gate_state — 12상태 라이브 전이
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;feat(runtime): wire gate_state 12-state live transitions&lt;/code&gt;가 게이트 라이프사이클을 잡 라이프사이클에서 분리했다. 12상태가 모든 의미 있는 전이를 인코딩(pending → emitted → awaiting_user → user_approved → user_revising → rerunning → reapproved → &amp;hellip;), 각각 프론트엔드로 표면화되어 UI가 추론된 상태를 폴링하지 않고 의미 있는 &amp;ldquo;이 게이트에 무슨 일이 일어나고 있는가&amp;quot;를 렌더할 수 있다.&lt;/p&gt;
&lt;h3 id="gate-1의-기획-보고서"&gt;GATE 1의 기획 보고서
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;feat: add report_writer agent + 리서치 분석 보고서 at GATE 1&lt;/code&gt;이 key_concept_planner와 나란히 도는 두 번째 스페셜리스트를 추가했다 — 10개 키 컨셉 뒤의 &lt;em&gt;컨텍스트&lt;/em&gt;를 사용자에게 주는 구조화된 리서치 분석 보고서를 생성한다. 이게 없으면 사용자는 컨셉을 차갑게 선택한다 — 있으면 기저 추론을 본다.&lt;/p&gt;
&lt;p&gt;그 다음 후속 — &lt;code&gt;feat: add report_writer planning mode for 최종 기획 보고서&lt;/code&gt; — 가 같은 에이전트에게 워크플로 끝에 다른 &lt;em&gt;종류&lt;/em&gt;의 보고서(전체 프로젝트를 요약하는 기획 보고서)를 생성하도록 가르쳤다.&lt;/p&gt;
&lt;h3 id="html-템플릿-세트"&gt;HTML 템플릿 세트
&lt;/h3&gt;&lt;p&gt;보고서는 채팅 버블이 아니라 &lt;em&gt;문서처럼&lt;/em&gt; 보여야 했다. 연속된 커밋들에서 HTML 템플릿 세트 전체가 도착했다.&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;템플릿 카탈로그가 LLM 면 조각이다 — 다중 페이지 문서를 조립할 때 선택할 템플릿의 구조화된 리스트를 에이전트가 받는다. 그래야 페이지별 레이아웃을 핸드코딩하지 않고 콘티 커버, 연속성 그리드, 트리트먼트 그리드, 콘티 시퀀스를 한 프롬프트로 만들 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="흐름-3--approvebar가-드로어를-교체하고-출력-탭이-인라인-렌더를-교체"&gt;흐름 3 — ApproveBar가 드로어를 교체하고, 출력 탭이 인라인 렌더를 교체
&lt;/h2&gt;&lt;p&gt;2026-05-21까지 하단 드로어 패턴(&lt;code&gt;ApproveSheet&lt;/code&gt;)은 UX 문제가 됐다 — 열면 캔버스를 덮었고, 캔버스가 &lt;em&gt;결과&lt;/em&gt;가 사는 곳이었다. 사용자는 승인 중인 콘티를 보기 위해 계속 드로어를 접어야 했다.&lt;/p&gt;
&lt;p&gt;세 커밋이 드로어를 죽였다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): add slim ApproveBar, replace ApproveGate in ChatPanel&lt;/code&gt; — 채팅 패널 하단의 가로 바&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;ApproveBar는 두 버튼(승인 / 수정요청) + 게이트 컨텍스트뿐이다 — 미니멀, 드래그할 드로어 없음. 검사할 실제 artifact는 캔버스(오른쪽 컬럼)에 산다, 늘 그랬듯이.&lt;/p&gt;
&lt;h3 id="출력-탭--캔버스가-현재-단계-표시에서-모든-단계의-탭-히스토리로-피벗"&gt;출력 탭 — 캔버스가 &amp;ldquo;현재 단계 표시&amp;quot;에서 &amp;ldquo;모든 단계의 탭 히스토리&amp;quot;로 피벗
&lt;/h3&gt;&lt;p&gt;ApproveBar가 떨어진 직후, 캔버스 자체가 재설계됐다.&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;이전엔 캔버스가 &lt;em&gt;현재&lt;/em&gt; 단계의 artifact만 보여줬다. 이제 위쪽에 탭이 있다 — 분석, 키 컨셉, 컨셉, 카피, 시나리오, 콘티 — 그리고 사용자는 현재 단계에서 작업하면서도 이전 artifact를 되돌아볼 수 있다. 그게 UX를 완전히 바꾼다 — 캔버스가 단지 현재 상태 디스플레이가 아니라 프로젝트 작업면이 됐다.&lt;/p&gt;
&lt;h3 id="planner-generator-evaluator-루프"&gt;Planner-Generator-Evaluator 루프
&lt;/h3&gt;&lt;p&gt;이 UX 이동의 런타임 등가물이 &lt;strong&gt;Planner-Generator-Evaluator 루프&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;패턴 — 모든 생성 단계가 이제 Planner → Generator → Evaluator를 돈다. Evaluator는 출력이 품질 바를 만족하는지 결정한다 — 아니면 루프가 피드백과 함께 Generator를 재실행한다. 사용자는 루프가 수렴한(또는 최대 반복 상한을 친) 뒤 &lt;em&gt;최종&lt;/em&gt; 출력을 본다. 이게 출력 탭이 가치 있어진 이유이기도 하다 — 각 단계의 기획 artifact와 평가 노트는 사용자가 파보고 싶을 때 보인다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="흐름-4--수정-모드-시스템"&gt;흐름 4 — 수정 모드 시스템
&lt;/h2&gt;&lt;p&gt;이게 이 윈도우에서 가장 큰 UX 약속이다. 이 작업 전에는 &amp;ldquo;카드 하나만 다시 하고 싶어요&amp;quot;는 &amp;ldquo;전체 단계를 다시 해주세요&amp;quot;를 의미했다 — 사용자가 재생성하고 싶은 조각 외 모든 걸 외과적으로 고정할 방법이 없었다.&lt;/p&gt;
&lt;h3 id="의도-분류"&gt;의도 분류
&lt;/h3&gt;&lt;p&gt;첫 조각(&lt;code&gt;feat: add revision-intent — classify chat revision as bulk/partial&lt;/code&gt;)이 채팅 입력의 작은 분류기다. 사용자가 게이트 컨텍스트에서 타이핑하면, 시스템이 묻는다 — 이게 &lt;em&gt;일괄&lt;/em&gt; 요청(&amp;ldquo;카피 전체를 더 감성적으로&amp;rdquo;)인가 &lt;em&gt;부분&lt;/em&gt; 요청(&amp;ldquo;세 번째만 바꿔줘&amp;rdquo;)인가? 출력이 다른 런타임 경로를 결정한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;일괄&lt;/strong&gt; → 피드백을 컨텍스트로 두고 전체 단계 재실행&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;부분&lt;/strong&gt; → UI에서 &amp;ldquo;수정 모드&amp;quot;에 진입하고 사용자가 재생성할 항목을 고름&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 (의역)
&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가 사용자에게 명확화를 nudge
&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="수정-모드-ui"&gt;수정 모드 UI
&lt;/h3&gt;&lt;p&gt;프론트엔드가 ApproveBar에 수정 모드 토글과 모든 선택 가능한 탭에 다중 선택 체크박스를 얻었다.&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;은 artifact 종류별 &lt;code&gt;Set&amp;lt;id&amp;gt;&lt;/code&gt;다. 사용자는 개별 카드를 토글한다 — 카운트가 ApproveBar에 라이브로 나타나 몇 개 항목이 재생성될지 항상 안다.&lt;/p&gt;
&lt;h3 id="revision-merge--인덱스-splice"&gt;Revision-merge — 인덱스 splice
&lt;/h3&gt;&lt;p&gt;런타임 측은 재생성된 부분집합을 기존 집합으로 &lt;em&gt;실제로 머지&lt;/em&gt;할 방법이 필요했다. 그게 &lt;code&gt;revision-merge&lt;/code&gt; 모듈이다.&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 (의역)
&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;순수 함수 — 선택된 인덱스에서 splice, 나머지는 고정. 끝까지 테스트됨 — &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="각-에이전트가-부분-재생성을-배웠다"&gt;각 에이전트가 부분 재생성을 배웠다
&lt;/h3&gt;&lt;p&gt;다섯 스페셜리스트 에이전트가 선택된 슬롯만 재생성하는 법을 배웠다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat: key_concept_planner regenerates selected slots (category-preserving)&lt;/code&gt; — 3-3-2-2 분배를 보존&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은 컨셉 앵커 자체가 단일 artifact이기 때문에 whole-doc만&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: scene_designer cut-level revision (structure + cut count preserved)&lt;/code&gt; — 총 컷 수 보존&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;각 에이전트는 자기의 고정 항목 제약이 무엇인지 알아야 했다. 콘티는 컷 수를 재계산하지 않고 패널 3만 재생성할 수 있었지만, scene_designer는 시나리오의 구조적 계약이 컷 수이기 때문에 총 컷 수를 보존해야 했다.&lt;/p&gt;
&lt;h3 id="라우팅-레이어"&gt;라우팅 레이어
&lt;/h3&gt;&lt;p&gt;두 라우팅 커밋이 함께 묶었다.&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; — 백엔드가 필요한 단일 스페셜리스트만 enqueue&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: route chat revision intent — bulk runs, partial nudges revise mode&lt;/code&gt; — 프론트엔드가 옳은 경로로 dispatch&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: dispatch revise_mode_hint — open revise mode on partial chat intent&lt;/code&gt; — 백엔드가 힌트 이벤트로 프론트엔드에 수정 모드를 &lt;em&gt;제안&lt;/em&gt;할 수 있어서 부분 의도가 UI를 자동으로 연다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;힌트 이벤트는 신중한 경계였다 — 백엔드는 결코 프론트엔드를 강제하지 않는다, 제안만 한다. 프론트엔드는 자기 로컬 상태가 이유가 있으면 오버라이드할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="흐름-5--세션-격리-프로젝트당-멀티-시도"&gt;흐름 5 — 세션 격리 (프로젝트당 멀티 시도)
&lt;/h2&gt;&lt;p&gt;2026-05-22에 workspace가 프로젝트당 단일 세션에서 프로젝트당 멀티 세션으로 갔다. 프로젝트가 이제 병렬 시도를 들고 갈 수 있다 — 단일 크리에이티브 브리프를 세 가지 다른 방식으로 시도할 수 있고, 각각 자기 세션이고 자기 파이프라인 상태를 갖는다.&lt;/p&gt;
&lt;p&gt;핵심 커밋.&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;Launcher 카드가 이제 세션별 게이트를 보여준다 — 세 세션을 가진 프로젝트, 둘은 콘티에, 하나는 카피에 있으면, 세 상태 모두 렌더한다. 세션은 개별로 삭제 가능. workspace 캔버스, 수정 모드, 게이트 상태가 모두 활성 세션으로 스코프된다.&lt;/p&gt;
&lt;p&gt;가장 큰 보이지 않는 커밋 — &lt;code&gt;Lock the session after final storyboard approval&lt;/code&gt;. GATE 5가 승인되면 세션은 읽기 전용이 된다. 봉인된 산출물에 우발적 편집을 막고, 사용자가 더 반복하고 싶으면 의식적으로 새 세션을 시작하도록 강제한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="인사이트"&gt;인사이트
&lt;/h2&gt;&lt;p&gt;3일에 네 흐름 — 하지만 반복되는 모양은 같다 — &lt;strong&gt;&amp;ldquo;전부 아니면 전무&amp;quot;의 무언가 granular한 것으로의 붕괴.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;드로어-또는-캔버스 이진이 출력 탭 연속체가 됐다 (모든 artifact를 한 번에 볼 수 있다)&lt;/li&gt;
&lt;li&gt;세 게이트가 다섯이 됐다 (새 게이트가 가장 비싼 하류 비용을 가드한다 — 카피 + 시나리오 + 콘티가 선택된 방향으로 실행)&lt;/li&gt;
&lt;li&gt;전체 단계 재실행이 부분 수정이 됐다 (정확히 원하는 것만 재생성)&lt;/li&gt;
&lt;li&gt;프로젝트당 단일 세션이 프로젝트당 멀티 세션이 됐다 (프로젝트가 병렬 아이디어를 들고 갈 수 있다)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 이동들 각각은 이진 버전보다 더 많은 코드 비용이 들었다 — 부분 수정 하나만 해도 의도 분류, 다섯 에이전트의 고정 항목 보존 로직, 스토어의 Set-of-IDs, 인덱스 splice 머지 함수가 필요했다 — 하지만 각각이 사용자가 조용히 견디고 있던 한 부류의 좌절을 제거했다. 153커밋 카운트는 영웅적으로 보인다 — 관통하는 줄은 더 겸손하다 — 시스템이 &amp;ldquo;전부 아니면 전무&amp;quot;라고 말할 때마다, &amp;ldquo;당신이 의미한 것만 정확히&amp;quot;로 교체.&lt;/p&gt;
&lt;p&gt;다음 — 폴리시 주간 — 관측성 계측, 새로고침 아래 프로젝트별 상태 격리, 채팅에서 인라인 과거 게이트 되감기, 그리고 모든 탭에서 비주얼 처리를 마침내 통일한 수정요청 어포던스 패스.&lt;/p&gt;</description></item></channel></rss>