diff --git a/apps/web/src/components/OneLoopStudio.tsx b/apps/web/src/components/OneLoopStudio.tsx
index c418a40d1..eebb07129 100644
--- a/apps/web/src/components/OneLoopStudio.tsx
+++ b/apps/web/src/components/OneLoopStudio.tsx
@@ -11,6 +11,7 @@ import {
pickOfficialTemplate,
stackCheckStatus,
stackCheckStatusLabel,
+ studioStackCheckView,
} from '@/lib/official-templates';
import { clsx } from 'clsx';
import Nav from '@/components/Nav';
@@ -76,6 +77,7 @@ import {
studioPlayerPhase,
studioPromotePackWorkbench,
studioRunQuality,
+ studioShipCta,
studioStatusLabel,
studioStatusMessage,
studioTranscriptBody,
@@ -406,6 +408,14 @@ export default function OneLoopStudio({
const holdReason = deployHoldReason(linkedSop, completedChecks, 'anonymous');
const officialTemplate = pickOfficialTemplate(linkedSop);
+ const stackCheckViews = stackChecks.map((item) => {
+ const status = stackCheckStatus(item, completedChecks, 'anonymous');
+ return {
+ ...studioStackCheckView(item, completedChecks, 'anonymous'),
+ status,
+ statusLabel: stackCheckStatusLabel(status),
+ };
+ });
useEffect(() => {
useDashboardStore.persist.rehydrate();
@@ -569,6 +579,7 @@ export default function OneLoopStudio({
hasLinkedSopSteps: Boolean(linkedSop?.steps.length),
hasProjectScaffold: Boolean(selected?.insights?.project_scaffold),
});
+ const shipCta = studioShipCta({ canExport: hasPayload, holdReason });
const runState = busy ? 'working' : selected ? 'ready' : 'idle';
const quality = studioRunQuality(
selected?.jobId ? { ok: true, status: 200, jobId: selected.jobId } : null,
@@ -1024,12 +1035,14 @@ export default function OneLoopStudio({
+ {showTranscriptRetry ? (
+
+ ) : null}
{statusText}
@@ -1122,6 +1145,15 @@ export default function OneLoopStudio({
outputPane={
{
+ const card = studioActionCard(action);
+ return {
+ tool: action.tool,
+ status: action.status,
+ result: action.result ?? card.detail,
+ };
+ })}
specReview={
selected?.videoPack?.pack ? (
{
expect(screen.getByTestId('studio-ide-splitter-right')).toBeTruthy();
// Toolbar carries the run + pack actions.
expect(screen.getByTestId('studio-ide-run')).toBeTruthy();
- expect(screen.getByTestId('studio-ide-export')).toBeTruthy();
+ expect(screen.getByTestId('studio-ide-ship')).toBeTruthy();
+ expect(screen.getByTestId('studio-ide-ship').textContent).toMatch(/Ship this/);
expect(screen.getByTestId('studio-build-live-button')).toBeTruthy();
});
@@ -92,6 +93,28 @@ describe('OneLoopStudio IDE shell', () => {
expect(screen.getByTestId('studio-ide-output-tab-tools')).toBeTruthy();
expect(screen.getByTestId('studio-ide-output-tab-intent')).toBeTruthy();
expect(screen.getByTestId('studio-ide-output-tab-signals')).toBeTruthy();
+ expect(screen.getByTestId('studio-ide-ship')).toBeTruthy();
+ expect((screen.getByTestId('studio-ide-ship') as HTMLButtonElement).disabled).toBe(false);
+ });
+
+ it('shows transcript retry when analysis failed and is retryable', () => {
+ useDashboardStore.setState({
+ videos: [
+ {
+ ...baseVideo,
+ status: 'failed',
+ failure: {
+ stage: 'analysis',
+ message: 'Gemini 3.8 Flash returned no extracted spec content.',
+ retryable: true,
+ failedAt: '2026-10-17T00:00:00.000Z',
+ },
+ },
+ ],
+ selectedVideoId: baseVideo.id,
+ });
+ render();
+ expect(screen.getByTestId('studio-transcript-retry')).toBeTruthy();
});
it('shows a cancel button while a run is in flight', async () => {
diff --git a/apps/web/src/components/studio/StudioIdeOutput.tsx b/apps/web/src/components/studio/StudioIdeOutput.tsx
index 6c5ad6ddd..ae4cc5b42 100644
--- a/apps/web/src/components/studio/StudioIdeOutput.tsx
+++ b/apps/web/src/components/studio/StudioIdeOutput.tsx
@@ -3,6 +3,14 @@
import { useState } from 'react';
import type { Video } from '@/store/dashboard-types';
import type { ExtractedEvent } from '@/lib/types';
+import { studioActionCard } from '@/lib/studio-pipeline-status';
+import type { StudioStackCheckView } from '@/lib/official-templates';
+
+export type StudioReviewActionInput = {
+ tool: string;
+ status: string;
+ result?: string;
+};
type OutputSection = 'events' | 'lingo' | 'tools' | 'intent' | 'signals' | 'spec' | 'transcript';
@@ -34,13 +42,23 @@ const SECTIONS: Array<{ id: OutputSection; label: string }> = [
export default function StudioIdeOutput({
video,
specReview,
+ stackChecks = [],
+ reviewActions = [],
}: {
video: Video | undefined;
specReview?: React.ReactNode;
+ stackChecks?: StudioStackCheckView[];
+ reviewActions?: StudioReviewActionInput[];
}) {
const [active, setActive] = useState('events');
- const hasContent = Boolean(video?.videoPack || video?.events?.length || video?.insights);
+ const hasContent = Boolean(
+ video?.videoPack ||
+ video?.events?.length ||
+ video?.insights ||
+ stackChecks.length > 0 ||
+ reviewActions.length > 0,
+ );
const pack = video?.videoPack?.pack;
const events: ExtractedEvent[] = video?.events ?? [];
@@ -141,7 +159,7 @@ export default function StudioIdeOutput({
{active === 'tools' && (
- {toolNames.length === 0 && !linkedSop?.steps.length ? (
+ {toolNames.length === 0 && !linkedSop?.steps.length && stackChecks.length === 0 ? (
) : (
<>
@@ -160,6 +178,28 @@ export default function StudioIdeOutput({
) : null}
+ {stackChecks.length > 0 ? (
+
+
Stack checks
+
+ {stackChecks.map((check) => (
+ -
+
+ {check.status === 'done' ? '✓' : check.status === 'pending' ? '•' : '?'}
+
+
+
{check.title}
+
{check.statusLabel}
+
+
+ ))}
+
+
+ ) : null}
{linkedSop?.steps?.length ? (
SOP steps
@@ -183,24 +223,49 @@ export default function StudioIdeOutput({
{active === 'intent' && (
- {actionItems.length === 0 ? (
+ {reviewActions.length === 0 && actionItems.length === 0 ? (
) : (
- actionItems.map((a, i) => (
- -
-
-
-
-
-
-
{a.title}
- {a.description ? (
-
{a.description}
- ) : null}
+ <>
+ {reviewActions.map((action, i) => {
+ const card = studioActionCard(action);
+ return (
+
-
+
+
+
+
+
+
+ {card.statusLabel}
+
+
{card.title}
+
{card.detail}
+
+
+
+ );
+ })}
+ {actionItems.map((a, i) => (
+
-
+
+
+
+
+
+
{a.title}
+ {a.description ? (
+
{a.description}
+ ) : null}
+
-
-
- ))
+
+ ))}
+ >
)}
)}
diff --git a/apps/web/src/components/studio/__tests__/StudioIdeOutput.playflow.test.tsx b/apps/web/src/components/studio/__tests__/StudioIdeOutput.playflow.test.tsx
new file mode 100644
index 000000000..fa67b7c34
--- /dev/null
+++ b/apps/web/src/components/studio/__tests__/StudioIdeOutput.playflow.test.tsx
@@ -0,0 +1,59 @@
+// @vitest-environment jsdom
+import React from 'react';
+import { cleanup, fireEvent, render, screen } from '@testing-library/react';
+import { afterEach, describe, expect, it } from 'vitest';
+import StudioIdeOutput from '@/components/studio/StudioIdeOutput';
+import type { Video } from '@/store/dashboard-types';
+
+const baseVideo: Video = {
+ id: 'row-output',
+ title: 'Playflow output',
+ url: 'https://www.youtube.com/watch?v=auJzb1D-fag',
+ status: 'complete',
+ progress: 100,
+ transcript: 'Enough transcript text for analysis ready state in Studio tests.',
+};
+
+afterEach(() => {
+ cleanup();
+});
+
+describe('StudioIdeOutput playflow', () => {
+ it('renders pack-named stack checks as skip/unknown rows that do not look like a hold', () => {
+ render(
+
,
+ );
+ fireEvent.click(screen.getByTestId('studio-ide-output-tab-tools'));
+ const row = screen.getByTestId('studio-stack-check-chk_tool_seadance');
+ expect(row.textContent).toMatch(/SeaDance 2\.5/);
+ expect(row.textContent).toMatch(/cannot run this check/i);
+ expect(row.textContent).not.toMatch(/production is held/i);
+ });
+
+ it('renders review_action results as cards with status, title, and detail', () => {
+ render(
+
,
+ );
+ fireEvent.click(screen.getByTestId('studio-ide-output-tab-intent'));
+ const card = screen.getByTestId('studio-review-action-card');
+ expect(card.textContent).toMatch(/review this result/i);
+ expect(card.textContent).toMatch(/needs review/i);
+ expect(card.textContent).toMatch(/Review evidence/);
+ });
+});
diff --git a/apps/web/src/lib/__tests__/official-templates.test.ts b/apps/web/src/lib/__tests__/official-templates.test.ts
index b58600bf1..8df57bcc2 100644
--- a/apps/web/src/lib/__tests__/official-templates.test.ts
+++ b/apps/web/src/lib/__tests__/official-templates.test.ts
@@ -7,6 +7,7 @@ import {
stackCheckItems,
stackCheckStatus,
stackCheckStatusLabel,
+ studioStackCheckView,
} from '@/lib/official-templates';
/** Live pBsT6v-ciO8 pack.stack.tools — none are runnable in UVAI. */
@@ -83,6 +84,9 @@ describe('deployHoldReason', () => {
expect(first).toBeDefined();
expect(stackCheckStatus(first, [], 'anonymous')).toBe('skip');
expect(stackCheckStatusLabel('skip')).toMatch(/not runnable while signed out/i);
+ expect(studioStackCheckView(first, [], 'anonymous').holdsShip).toBe(false);
+ expect(studioStackCheckView(first, [], 'signed-in').holdsShip).toBe(true);
+ expect(studioStackCheckView(first, [first.id], 'signed-in').holdsShip).toBe(false);
});
it('does not hold anonymous Deploy on pBsT6v-ciO8 seadance/cluely checks', () => {
@@ -107,6 +111,10 @@ describe('deployHoldReason', () => {
expect(stackCheckStatus(item, [], 'signed-in')).toBe('unknown');
}
expect(stackCheckStatusLabel('unknown')).toMatch(/cannot run this check/i);
+ const rows = stack.map((item) => studioStackCheckView(item, [], 'anonymous'));
+ expect(rows.every((row) => row.status === 'unknown')).toBe(true);
+ expect(rows.every((row) => row.holdsShip === false)).toBe(true);
+ expect(rows[0]?.statusLabel).toMatch(/cannot run this check/i);
});
it('does not let unknown tool checks hold a signed-in Vercel pack', () => {
diff --git a/apps/web/src/lib/__tests__/studio-pipeline-status.test.ts b/apps/web/src/lib/__tests__/studio-pipeline-status.test.ts
index 119c49e9a..05167ecba 100644
--- a/apps/web/src/lib/__tests__/studio-pipeline-status.test.ts
+++ b/apps/web/src/lib/__tests__/studio-pipeline-status.test.ts
@@ -6,6 +6,8 @@ import {
studioActionCard,
studioCanExport,
studioCanRetryTranscript,
+ studioShipCta,
+ studioShipEnabled,
studioDeployButtonLabel,
studioDeployEnabledHint,
studioDeployOutcomeMessage,
@@ -367,10 +369,34 @@ describe('studio-pipeline-status', () => {
expect(studio).toContain('studioTranscriptEtaLabel');
// IDE: transcript stage + ETA surface in the toolbar status line.
expect(studio).toContain('data-testid="studio-ide-status"');
+ expect(studio).toContain('data-testid="studio-transcript-retry"');
expect(output).toContain("data-testid={`studio-ide-output-tab-${s.id}`}");
expect(output).toContain("{ id: 'transcript', label: 'Transcript' }");
});
+ it('enables Ship this from pack exportability and ignores stack-check hold copy', () => {
+ expect(studioShipEnabled({ canExport: true, holdReason: null })).toBe(true);
+ expect(
+ studioShipEnabled({
+ canExport: true,
+ holdReason: 'Production is held until 4 stack check(s) pass. Next: Use SeaDance 2.5 as named in the video',
+ }),
+ ).toBe(true);
+ expect(studioShipEnabled({ canExport: false, holdReason: null })).toBe(false);
+ const cta = studioShipCta({ canExport: true, holdReason: 'Production is held until 4 stack check(s) pass.' });
+ expect(cta.label).toBe('Ship this');
+ expect(cta.enabled).toBe(true);
+ expect(cta.hint.toLowerCase()).toMatch(/skip|unknown/);
+ expect(studioShipCta({ canExport: false }).enabled).toBe(false);
+
+ const studio = readFileSync(join(process.cwd(), 'src/components/OneLoopStudio.tsx'), 'utf8');
+ expect(studio).toContain('studioShipCta');
+ expect(studio).toContain('data-testid="studio-ide-ship"');
+ expect(studio).toContain('Ship this');
+ expect(studio).not.toContain('studio-deploy-button');
+ expect(studio).not.toMatch(/disabled=\{Boolean\(holdReason\)\}/);
+ });
+
it('does not claim Deploy completed without a verified live receipt', () => {
expect(
studioDeployOutcomeMessage({
@@ -543,6 +569,8 @@ describe('studio-pipeline-status', () => {
const studio = readFileSync(join(process.cwd(), 'src/components/OneLoopStudio.tsx'), 'utf8');
const output = readFileSync(join(process.cwd(), 'src/components/studio/StudioIdeOutput.tsx'), 'utf8');
expect(studio).toContain('studioActionCard');
+ expect(output).toContain('studioActionCard');
+ expect(output).toContain('data-testid="studio-review-action-card"');
// IDE: actions surface in the output pane Intent tab.
expect(output).toContain("{ id: 'intent', label: 'Intent' }");
});
@@ -571,6 +599,7 @@ describe('studio-pipeline-status', () => {
expect(studio).toContain('data-testid="studio-export-toast"');
expect(studio).not.toMatch(/setExportToast\(toast\);\s*setMessage\(/);
expect(studio).toMatch(/role=\{exportToast\.tone === 'error' \? 'alert' : 'status'\}/);
+ expect(studio).not.toContain('bottom-20');
});
it('covers the player until load and names a load error instead of silent 0:00', () => {
diff --git a/apps/web/src/lib/__tests__/uvai-surface-unification.test.ts b/apps/web/src/lib/__tests__/uvai-surface-unification.test.ts
index c8146e97f..29cccc1a8 100644
--- a/apps/web/src/lib/__tests__/uvai-surface-unification.test.ts
+++ b/apps/web/src/lib/__tests__/uvai-surface-unification.test.ts
@@ -81,6 +81,9 @@ describe('UVAI is one product surface', () => {
expect(studio).toContain("deployHoldReason(linkedSop, completedChecks, 'anonymous')");
expect(studio).toContain('stackCheckStatus');
expect(studio).toContain('stackCheckStatusLabel');
+ expect(studio).toContain('studioShipCta');
+ expect(studio).toContain('data-testid="studio-ide-ship"');
expect(studio).not.toContain('href="/ship"');
+ expect(studio).not.toContain('studio-deploy-button');
});
});
diff --git a/apps/web/src/lib/official-templates.ts b/apps/web/src/lib/official-templates.ts
index d3be9b765..63c79df27 100644
--- a/apps/web/src/lib/official-templates.ts
+++ b/apps/web/src/lib/official-templates.ts
@@ -88,6 +88,29 @@ export function stackCheckStatusLabel(status: StackCheckStatus): string {
}
}
+export interface StudioStackCheckView {
+ id: string;
+ title: string;
+ status: StackCheckStatus;
+ statusLabel: string;
+ holdsShip: boolean;
+}
+
+export function studioStackCheckView(
+ item: ChecklistItem,
+ completedIds: Iterable
,
+ viewer: StudioViewer = 'anonymous',
+): StudioStackCheckView {
+ const status = stackCheckStatus(item, completedIds, viewer);
+ return {
+ id: item.id,
+ title: item.title,
+ status,
+ statusLabel: stackCheckStatusLabel(status),
+ holdsShip: status === 'pending',
+ };
+}
+
export function pickOfficialTemplate(sop: LinkedSop | null | undefined): OfficialTemplate | null {
const stacks = detectedStacks(sop);
if (stacks.has('vercel')) return NEXT;
diff --git a/apps/web/src/lib/studio-pipeline-status.ts b/apps/web/src/lib/studio-pipeline-status.ts
index c43fd886c..2bd869c7e 100644
--- a/apps/web/src/lib/studio-pipeline-status.ts
+++ b/apps/web/src/lib/studio-pipeline-status.ts
@@ -156,7 +156,7 @@ export function studioWorkbenchEmptyView(input: {
return {
title: 'Analysis in progress',
description:
- 'Waiting on transcript and pack storage. Export and Build live unlock when the run finishes.',
+ 'Waiting on transcript and pack storage. Ship this and Build live unlock when the run finishes.',
primaryAction: null,
};
}
@@ -240,6 +240,32 @@ export function studioCanExport(input: {
);
}
+/**
+ * Anonymous Ship this is pack-exportability only. Stack-check hold copy
+ * (SeaDance/Cluely-class unknown, signed-out vendor skip) never disables it.
+ */
+export function studioShipEnabled(input: {
+ canExport: boolean;
+ holdReason?: string | null;
+}): boolean {
+ void input.holdReason;
+ return input.canExport;
+}
+
+export function studioShipCta(input: {
+ canExport: boolean;
+ holdReason?: string | null;
+}): { label: 'Ship this'; enabled: boolean; hint: string } {
+ const enabled = studioShipEnabled(input);
+ return {
+ label: 'Ship this',
+ enabled,
+ hint: enabled
+ ? 'Download the Video Pack ZIP. Named tools that UVAI cannot run are skip/unknown — they do not block ship.'
+ : 'Run analysis first so there is a pack to ship.',
+ };
+}
+
export function studioInvalidHandoffMessage(raw: string): string {
const preview = raw.trim() || 'that input';
return `Need a valid YouTube URL. "${preview}" is not a watchable video.`;
diff --git a/apps/web/src/lib/studio-playflow-PLAN.md b/apps/web/src/lib/studio-playflow-PLAN.md
index 8e94b8fc6..ab68c22ec 100644
--- a/apps/web/src/lib/studio-playflow-PLAN.md
+++ b/apps/web/src/lib/studio-playflow-PLAN.md
@@ -1,28 +1,29 @@
-# TASK: Studio playflow for empty events[] packs
+# TASK: Restore Studio Ship this playflow after IDE shell
## 1. Goal & Scope
-* **Objective:** Remove Studio dead ends when a live pack has transcript + architecture + artifacts + stack.tools but `events[]` is empty (`pBsT6v-ciO8` and similar).
-* **Context:** Hayden flagged `https://uvai.io/studio?video=https://www.youtube.com/watch?v=pBsT6v-ciO8`. Pack 200: transcript ~1767 chars, architecture + artifacts[2] + stack.tools present, events count = 0. Do not invent fake events from keyframes/concepts.
+* **Objective:** Get UVAI Studio playflow back toward ≥9/10 by restoring a discoverable **Ship this** path and rewiring leftover B/E/G helpers the IDE shell left unused.
+* **Context:** Kill A (#1692) merged. B–G helpers exist (`studioTranscriptStage`, `studioActionCard`, `deployHoldReason` skip/unknown, export toast, player overlay). The 2026-10-07 IDE refactor deleted `studio-deploy-button` and stopped rendering Ship this, stack-check honesty, review_action cards, and transcript retry. Dogfood kill F still stands: no Ship this; seadance/cluely-class checks must not hold anonymous ship.
* **Scope:**
- * `apps/web/src/lib/studio-handoff.ts` — reconstruct unencoded `?video=https://www.youtube.com/watch?v=ID`
- * `apps/web/src/lib/studio-pipeline-status.ts` — honest empty-events copy; promote pack workbench
- * `apps/web/src/lib/action-surface.ts` — export architecture/artifacts when events are empty
- * `apps/web/src/components/OneLoopStudio.tsx` — consume helpers; no fake events
- * *Initial check:* Extend existing helpers. No Origin G.A.T.E., ClipToAction, forge, or Ship checkout.
+ * `apps/web/src/lib/studio-pipeline-status.ts` — `studioShipEnabled` / `studioShipCta` (exportability gates ship; holdReason never does)
+ * `apps/web/src/lib/official-templates.ts` — stack-check row view (skip/unknown)
+ * `apps/web/src/components/OneLoopStudio.tsx` — Ship this CTA, transcript retry, toast position, pass checks/actions
+ * `apps/web/src/components/studio/StudioIdeOutput.tsx` — stack checks + `studioActionCard`
+ * *Initial check:* Extend existing helpers. Do not restore `studio-deploy-button`, origin deploy kickoff, fake events, or G.A.T.E. contract changes.
## 2. Execution Plan
-- [x] Failing tests: unencoded query, empty-events copy, export without events, no keyframe→event mapping
-- [x] Reconstruct `video` + sibling `v` for Home/share handoff
-- [x] Honest Events empty state + promote architecture/artifacts/stack
-- [x] Export pack files from formation when events[] is empty
-- [x] Verify focused Vitest
+- [x] Failing tests for Ship this enablement, skip/unknown checks, review cards, retry, toast position
+- [x] Ship this CTA enabled by `studioCanExport`, never by non-runnable stack checks
+- [x] Tools tab shows honest skip/unknown; Intent tab renders `review_action` cards
+- [x] Transcript retry control when `studioCanRetryTranscript`
+- [x] Export toast no longer uses sticky-footer `bottom-20`
+- [x] Focused Vitest
## 3. Definition of Done (Success Verification)
-* **Expected Outcome:** Studio for `pBsT6v-ciO8` loads the player from `?video=`, shows transcript, honest empty Events, prominent architecture/artifacts/stack, and Export is usable. No fake events.
-* **Verification Method:** `cd apps/web && npx vitest run src/lib/__tests__/studio-handoff.test.ts src/lib/__tests__/studio-pipeline-status.test.ts src/lib/__tests__/action-surface.test.ts`
-* **Proof Artifact:** `npx vitest run src/lib/__tests__/studio-handoff.test.ts src/lib/__tests__/studio-pipeline-status.test.ts src/lib/__tests__/action-surface.test.ts src/lib/__tests__/uvai-surface-unification.test.ts src/lib/__tests__/home-sell-surface.test.ts` → 5 files, 43 passed. Live GET `https://uvai.io/api/video/pack?url=https://www.youtube.com/watch?v=pBsT6v-ciO8` → 200; transcript 1767 chars; architecture present; artifacts 2; stack.tools 4; no `events` field; keyframes/concepts not mapped into Events.
+* **Expected Outcome:** Anonymous Studio on a seadance/cluely pack can click **Ship this** (ZIP export). Stack checks read skip/unknown and do not disable the button. No live hostname claim. Transcript retry and review cards are visible.
+* **Verification Method:** `cd apps/web && npx vitest run src/lib/__tests__/studio-pipeline-status.test.ts src/lib/__tests__/official-templates.test.ts src/lib/__tests__/uvai-surface-unification.test.ts src/components/__tests__/OneLoopStudio.three-panel.test.tsx src/components/studio/__tests__/StudioIdeOutput.playflow.test.tsx`
+* **Proof Artifact:** `cd apps/web && npm test -- src/lib/__tests__/studio-pipeline-status.test.ts src/lib/__tests__/official-templates.test.ts src/lib/__tests__/uvai-surface-unification.test.ts src/components/__tests__/OneLoopStudio.three-panel.test.tsx src/components/studio/__tests__/StudioIdeOutput.playflow.test.tsx` → **5 files, 49 passed**. Related: OneLoopStudio.build-live + job-strip + linked-sop → **3 files, 16 passed**. `tsc --noEmit` and eslint on touched files passed.
## 4. Post-Task Reflection
-* **What was done:** Honest empty Events copy after a completed run; promoted architecture/artifacts workbench when `events[]` is empty; Export writes ARCHITECTURE.md + artifacts.json; `?video=` handoff reads unencoded watch URLs and reconstructs split `video`+`v` params.
-* **Why it was needed:** Packs like `pBsT6v-ciO8` have useful formation but zero events. Studio previously left a dead “Events show up after Run.” rail and could refuse export.
-* **How it was tested:** RED then GREEN Vitest for handoff, empty-state copy, export without events, and OneLoopStudio source guards (no keyframe→event mapping). Live pack GET confirmed the empty-events shape.
+* **What was done:** Restored a discoverable **Ship this** toolbar CTA that downloads the pack ZIP. Anonymous/unknown SeaDance/Cluely stack checks render as skip/unknown and never disable the button. Intent tab renders `review_action` cards. Transcript retry is visible on retryable failure. Export toast sits at `bottom-4` (no sticky footer). Origin deploy button was not restored.
+* **Why it was needed:** IDE shell left playflow helpers unwired; dogfood kill F was a disabled Deploy with no Ship this.
+* **How it was tested:** Focused Vitest (49 + 16), type-check, eslint. No production live claim. GitHub issue create was 403 so this cut has no new issue number.