[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M69A39Z22W1T779DY7P9MJAC":3,"skill-same:01M69A39Z22W1T779DY7P9MJAC":124},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":26,"publishedAt":30,"linkOnly":31,"markdownContent":32,"markdownBody":33,"frontMatter":34,"refName":35,"commitSha":36,"skillDirectory":37,"forksCount":38,"repositoryHtmlUrl":39,"repositoryHomepageUrl":6,"ownerHtmlUrl":40,"ownerBlogUrl":41,"ownerDescription":42,"updatedAt":43,"install":44,"resources":57},"01M69A39Z22W1T779DY7P9MJAC","clipchamp-video",null,"Create narrated demo\u002Fexplainer videos of a live web app or agent, with real screen-flow footage under an AI (Ava neural) voiceover. Use whenever the user wants to \"make a video\", \"record a demo\", \"create a walkthrough\", \"screen record an agent\u002Fapp and narrate it\", \"build a Clipchamp video\", \"produce a product demo\", or turn a Copilot Studio \u002F web-app flow into a narrated MP4. FIRST ask the user which assembly method to use: (A) headless ffmpeg + edge-tts Ava — fast, scripted, produces a finished MP4 directly — or (B) the Clipchamp web UI — slower, but leaves an editable Clipchamp project for hand-tweaking later.","microsoft\u002Fcat-agent-skills","microsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F6154722?v=4","01M698VYDXZ6JW5WH3CRV650R8","Microsoft","01M69A1AHE1MABT7GVP5143M5Z",105,"MIT","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fclipchamp-video\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000016","图像与多媒体","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[27,28,29],"clipchamp video","web","copilot","2026-11-10 10:12:30",false,"---\nname: \"clipchamp-video\"\ndescription: >-\n  Create narrated demo\u002Fexplainer videos of a live web app or agent, with real\n  screen-flow footage under an AI (Ava neural) voiceover. Use whenever the user\n  wants to \"make a video\", \"record a demo\", \"create a walkthrough\", \"screen\n  record an agent\u002Fapp and narrate it\", \"build a Clipchamp video\", \"produce a\n  product demo\", or turn a Copilot Studio \u002F web-app flow into a narrated MP4.\n  FIRST ask the user which assembly method to use: (A) headless ffmpeg +\n  edge-tts Ava — fast, scripted, produces a finished MP4 directly — or (B) the\n  Clipchamp web UI — slower, but leaves an editable Clipchamp project for\n  hand-tweaking later.\n---\n\n# Clipchamp Narrated Demo Video Skill\n\nProduce a polished, narrated demo video: **real screen-flow footage of a live app\u002Fagent** under an **AI voiceover**. This captures hard-won lessons — read it fully before starting.\n\n## STEP 0 — ASK THE USER: which assembly method? (do this FIRST, before recording)\n\nThe recording step is identical for both. The **assembly + narration** step has two paths — ask the user to pick before you start, e.g. via `m_ask_user`:\n\n> \"How should I assemble the video — **(A) Headless (ffmpeg + Ava neural TTS)** — faster (~2–3 min), scripted, precise, produces a finished MP4; or **(B) Clipchamp web UI** — slower (~15–25 min of browser clicks) but gives you an **editable Clipchamp project** to hand-tweak later (transitions, captions, music)?\"\n\nGuidance to offer with the question:\n- **Default \u002F recommended: (A) Headless** — faster, deterministic, exact narration timing, no session-drop\u002Fupload-hook risk. Best when the deliverable is just the finished MP4.\n- **Choose (B) Clipchamp** only if the user wants an editable project living in their Clipchamp account, or plans manual edits (transitions, captions, background music, hand re-timing).\n- **(B) requires the `playwright-browser_*` MCP tools.** If those tools are not available in the session (they can be disabled\u002Fdropped), (B) is impossible — tell the user and either have them re-enable the Browser Control tools \u002F start a new chat, or fall back to (A).\n\nRecord the flow the same way either path (§1), then follow **§2 + §2b (headless)** or **§3 (Clipchamp)** based on their choice.\n\n## STEP 0b — ASK: page-only capture, or \"your actions + the chat\" (dual capture)?\n\nIf the user says anything like *\"record your actions and the chat at the same time\"*, *\"show what you're doing while it runs\"*, *\"a making-of\"*, or *\"I want to see both\"* — they want **the Scout\u002Fassistant conversation visible alongside the app**, not just the app. That is a **different capture mode** and you must pick it BEFORE recording:\n\n- **Page-only (default, §1).** Playwright `recordVideo` captures the page's internal buffer. Clean, no browser chrome, immune to other windows \u002F screen switches. **It physically cannot capture the Scout window** — `recordVideo` only ever sees the one page.\n- **Dual capture (§1b).** OS-level desktop capture with **ffmpeg `gdigrab`** while Scout and the automated browser sit **side by side**. One capture, so the two halves are perfectly in sync for free.\n\nDo NOT try to satisfy a dual-capture request by recording the page and hoping to add the chat later — you cannot recover the chat pixels after the fact.\n\n## §1b — Dual capture: agent actions + the app, side by side\n\nVerified working (`ffmpeg 8.x`, Windows): `-f gdigrab -framerate 15 -i desktop` produced a clean 1920×1280 H.264 capture.\n\n**Ready-made scripts:** `scripts\u002Farrange.ps1` (side-by-side window placement) and `scripts\u002Fdual-capture.js` (starts gdigrab, runs `recorder.js`, stops ffmpeg cleanly, probes the result). Run `node dual-capture.js out.mp4`.\n\n1. **Arrange the two windows first.** Put the automated Edge on one half and the Scout window on the other — see `scripts\u002Farrange.ps1`. It matches Edge by command line (`*edge-tenant*`) and Scout by process name \u002F window title, then `MoveWindow`s each to half the working area.\n   **Recompute the device-scale factor for the narrower window** — half-width means the CSS viewport halves too. Re-run the empirical calibration from \"Make the window fill the user's screen\" against the half-width, or the chat input bar will be cut off. The MANDATORY GATE still applies, at the half-width geometry.\n2. **Start ffmpeg detached, before the recorder**, and give it a couple of seconds of lead-in:\n   ```powershell\n   ffmpeg -y -f gdigrab -framerate 15 -i desktop -c:v libx264 -pix_fmt yuv420p -crf 23 -preset veryfast dual.mp4\n   ```\n   Capture `desktop` (the whole screen) rather than `-i title=\u003Cwindow>`: per-window gdigrab breaks whenever the window is occluded, moved, or redrawn, and it silently drops frames. Crop later with ffmpeg if you want a tighter frame.\n3. **Stop ffmpeg gracefully** — write `q` to its stdin, or it may leave the MP4 without a moov atom (unplayable). If you must kill it, use `Stop-Process -Id \u003Cpid>` and then remux: `ffmpeg -y -i broken.mp4 -c copy fixed.mp4`.\n4. **Consequences of desktop capture — tell the user up front:**\n   - **They must not use the machine while it records.** Unlike `recordVideo`, everything on screen ends up in the video: notifications, other windows, the taskbar.\n   - **Toasts and popups will appear.** Enable Focus Assist \u002F Do Not Disturb first.\n   - **Anything private on screen is captured.** Close unrelated mail\u002Fchat windows before starting.\n5. **Alternative when the user wants both but can't stop working:** record the page-only video (§1), then composite the chat as a separate pass afterwards (screenshots of the conversation as an intro\u002Foutro, or a picture-in-picture strip). Sync will be approximate — say so rather than implying frame accuracy.\n6. **Compositing two separate sources** (only if you truly captured them separately) — normalize both to the same height and fps first, then stack:\n   ```powershell\n   ffmpeg -y -i app.mp4 -i chat.mp4 -filter_complex `\n     \"[0:v]scale=-2:1080,setsar=1,fps=30[l];[1:v]scale=-2:1080,setsar=1,fps=30[r];[l][r]hstack=inputs=2,pad=ceil(iw\u002F2)*2:ceil(ih\u002F2)*2\" `\n     -c:v libx264 -pix_fmt yuv420p -crf 20 composite.mp4\n   ```\n   `hstack` **requires identical heights** or it errors out; the trailing `pad=ceil(iw\u002F2)*2:...` keeps dimensions even for yuv420p. For picture-in-picture use `overlay=W-w-40:H-h-40` instead of `hstack`.\n\n## The winning approach (and why)\n\nA slideshow of static screenshots is almost never what the user wants. They want to **see the app move** — cursor, typing, live responses. The reliable way to get that autonomously:\n\n1. **Drive the real app with Playwright** and record **page-only video** (`recordVideo`). This is clean (no browser chrome) and hands-off. Raw OS screen capture is worse: synthetic automation cursors often don't render, and it captures the whole desktop. **Exception:** if the user explicitly wants the assistant conversation in-frame too, `recordVideo` cannot do it — switch to §1b dual capture.\n2. **Reuse the user's logged-in browser profile** so auth is preserved (e.g. Copilot Studio, M365). Never spin up a fresh profile — you'll lose the session. **If the target lives in a different tenant than the MCP browser's signed-in account, use the real Edge profile for that tenant instead of signing in interactively** — see \"Pick the right profile \u002F tenant\" below.\n3. **Cut the auth\u002Floading head** from the recording with ffmpeg — the account picker, \"Stay signed in\", and app \"Initializing…\" spinner are dead air.\n4. **Narrate with the Ava neural voice** — either Clipchamp's built-in Text to speech or edge-tts `en-US-AvaNeural` — split into **several short blocks aligned to on-screen moments**, not one long block. Silence between events feels broken; short blocks with technical call-outs (\"here it invokes a skill to query the table\") make it feel alive.\n5. **Name the export after the agent\u002Fproduct**, not \"Video Project 1\".\n\n## Prerequisites\n\n**Windows only** (PowerShell, Win32 window APIs, `msedge.exe`, ffmpeg `gdigrab` desktop capture) — this skill does not currently have macOS\u002FLinux capture backends. See `README.md` for the one-time global install steps (Node + Playwright, ffmpeg\u002Fffprobe, edge-tts) and how to check each is already present before installing.\n\n### Ready-made scripts (`scripts\u002F`)\n\nWorking, battle-tested versions live next to this file. Copy them into the task's work dir and edit the constants at the top of `common.js` (clone path, env id, agent id, target account) rather than writing new ones from scratch.\n\n| Script | Purpose |\n| ------ | ------- |\n| `clone.ps1` | Kill real Edge, clone the tenant profile to a non-default root (caches excluded), regex-patch `Preferences`. **Re-run whenever auth breaks.** |\n| `prep.ps1` | Between runs: kill stale Edge (cloned profile only) and this workflow's own Node processes, clear `Singleton*` locks, reset `exit_type`\u002F`exited_cleanly`. Run before every launch. |\n| `maximize.ps1` | Win32 `MoveWindow`+`SW_MAXIMIZE` on the cloned-profile Edge window (the only thing that reliably resizes it). |\n| `arrange.ps1` | Side-by-side placement (browser left, Scout right) for §1b dual capture. |\n| `common.js` | Shared launch `ARGS`, the `settle()` auth\u002Fstability loop, `dismissPopups()`, `logViewport()`, `maximizeWindow()`. |\n| `verify.js` | **The MANDATORY GATE** — loads the target, opens the test pane, sends one real prompt, screenshots the frame. |\n| `recorder.js` | The real run: overview → instructions → skills\u002Ftools scroll → N live test prompts, with `progress.log` beat markers. |\n| `dual-capture.js` | §1b: starts gdigrab, runs `recorder.js`, stops ffmpeg cleanly, probes the output. |\n| `findagent.js` | Scrape the agents grid (`[role=\"row\"]`) for names, then open one and harvest its real id from the URL. |\n| `diag.js` | When a page renders blank: timed screenshots + console errors + HTTP ≥400 logging. |\n\n### Pick the right profile \u002F tenant (do this BEFORE anything else)\n\nThe MCP browser is usually signed in to the user's **primary** work account. If the agent\u002Fapp lives in a **different tenant** (e.g. a `*.onmicrosoft.com` dev\u002Fdemo tenant), that session will 404 or bounce to the wrong environment. **Do not try to sign in interactively** and do not ask the user to type a password — the user almost always already has a **second Edge profile** signed in to that tenant. Find it and drive that.\n\n1. **Detect the mismatch early.** Open the target URL in the MCP browser; a blank page, `404` on `api.bap.microsoft.com\u002F...\u002Fenvironments\u002F\u003Cid>`, or a redirect to `~personal`\u002Fa different environment ID means wrong tenant. Confirm the signed-in identity (in Copilot Studio: click `[data-testid=\"account-profile-button-trigger\"]`).\n2. **Enumerate the real Edge profiles and their accounts** — map `Default`, `Profile 1`, `Profile 2`… to emails by reading each profile's `Preferences`:\n   ```powershell\n   $ud = \"$env:LOCALAPPDATA\\Microsoft\\Edge\\User Data\"\n   Get-ChildItem $ud -Directory | Where-Object { $_.Name -eq 'Default' -or $_.Name -like 'Profile*' } | ForEach-Object {\n     $p = Join-Path $_.FullName 'Preferences'\n     if (Test-Path $p) { try { $j = Get-Content $p -Raw -Encoding UTF8 | ConvertFrom-Json\n       [PSCustomObject]@{ Dir=$_.Name; Name=$j.profile.name; Email=$j.account_info[0].email } } catch {} }\n   } | Format-Table -AutoSize\n   ```\n   Note the **directory name** (`Profile 1`), *not* the display name (`Profile 2`) — they routinely disagree, and Playwright needs the directory.\n3. **CLONE the profile to a non-default User Data root — do NOT drive the real one.** Edge **refuses remote debugging on its default data directory** and `launchPersistentContext` dies after 180 s with:\n   `Timeout 180000ms exceeded` + `[err] DevTools remote debugging requires a non-default data directory. Specify this using --user-data-dir.`\n   Cloning also frees you from the real-Edge profile lock. Copy only what carries auth — skip the caches, which are the bulk (a 3 GB profile drops to ~300 MB, and `Service Worker` alone was 2 GB):\n   ```powershell\n   $src = \"$env:LOCALAPPDATA\\Microsoft\\Edge\\User Data\"; $dst = \"C:\\Users\\\u003Cuser>\\.copilot\\video-work\\edge-tenant\"\n   New-Item -ItemType Directory -Force -Path $dst | Out-Null\n   Copy-Item \"$src\\Local State\" \"$dst\\Local State\" -Force\n   robocopy \"$src\\Profile 1\" \"$dst\\Profile 1\" \u002FE \u002FNFL \u002FNDL \u002FNJH \u002FNJS \u002FR:0 \u002FW:0 \u002FXJ `\n     \u002FXD \"Service Worker\" \"Cache\" \"Code Cache\" \"GPUCache\" \"Shared Dictionary\" \"DawnGraphiteCache\" \"DawnWebGPUCache\" \"Crashpad\" \"optimization_guide_model_store\"\n   ```\n   Keep `Network\\Cookies`, `Login Data`, `Local Storage`, `IndexedDB`, `WebStorage`, `Preferences` — that's what preserves the session. Then launch `launchPersistentContext(\u003Cclone root>, { args: ['--profile-directory=Profile 1', ...] })` — still the **root**, never the profile subfolder.\n   **The clone is a snapshot, not a link.** If auth later breaks, have the user load the target URL in their own Edge and then **re-clone** — that is the fastest repair, and far more reliable than any scripted sign-in (see the passkey pitfall below). Close the real Edge only while cloning (files are locked mid-copy).\n4. **Close the real Edge only while cloning** (files are locked mid-copy). Kill only the non-Playwright `msedge.exe` processes, delete `Singleton*` under the target root, and patch `Profile 1\\Preferences` (`exit_type`→`Normal`, `exited_cleanly`→`true`) so no \"Restore pages?\" bubble appears in-frame.\n   ```powershell\n   Get-CimInstance Win32_Process -Filter \"Name='msedge.exe'\" |\n     Where-Object { $_.CommandLine -notlike '*m-playwright-profiles*' -and $_.CommandLine -notlike '*ms-playwright*' } |\n     ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }\n   ```\n   Filtering by command line matters: killing everything also kills the MCP browser you may still need.\n5. Real profiles can be **multi-GB** and slower to boot than `mcp-msedge` — allow generous first-navigation timeouts (90–120 s) and a long post-load settle before recording.\n6. **Wait for auth with a stability loop, never a fixed sleep.** MSAL bounces *back* to `login.microsoftonline.com\u002F…\u002Foauth2\u002Fv2.0\u002Fauthorize` for a silent token refresh **after** you appear to have landed on the app — a one-shot \"am I on the app host?\" check passes, then the very next screenshot is a login URL with 200 chars of body text. Poll every 2 s and require **N consecutive good polls** (host matches AND `document.body.innerText.length` is large AND no `Initializing|Loading your|Getting things ready` spinner). Handle the sign-in UI inside the same loop:\n   ```js\n   async function settle(page, budgetMs = 240000, needStable = 4) {\n     const t0 = Date.now(); let stable = 0;\n     while (Date.now() - t0 \u003C budgetMs) {\n       const u = page.url();\n       if (u.includes('login.microsoftonline.com')) {           \u002F\u002F \"Stay signed in?\"\n         stable = 0;\n         const cb = page.locator('#KmsiCheckboxField').first();\n         if (await cb.isVisible({ timeout: 400 }).catch(() => false)) await cb.check({ timeout: 2000 }).catch(() => {});\n         const yes = page.locator('#idSIButton9').first();       \u002F\u002F the Yes\u002FNext button\n         if (await yes.isVisible({ timeout: 800 }).catch(() => false)) { await yes.click({ timeout: 3000 }); await page.waitForTimeout(4000); continue; }\n         const tile = page.locator('div[role=\"button\"]:has-text(\"user@\"), small:has-text(\"user@\")').first();\n         if (await tile.isVisible({ timeout: 600 }).catch(() => false)) { await tile.click({ timeout: 3000 }); await page.waitForTimeout(4000); continue; }\n         await page.waitForTimeout(2000); continue;\n       }\n       if (u.includes('\u003Capp-host>')) {\n         const len = await page.evaluate(() => document.body.innerText.length).catch(() => 0);\n         const busy = await page.locator('text=\u002FInitializing|Loading your|Getting things ready\u002Fi').count().catch(() => 0);\n         if (len > 400 && !busy) { stable++; if (stable >= needStable) return true; } else stable = 0;\n       } else stable = 0;\n       await page.waitForTimeout(2000);\n     }\n     return false;\n   }\n   ```\n   Use the account **tile** selectors (`div[role=\"button\"]:has-text(\"user@\")`, `small:has-text(\"user@\")`) — `[data-test-id*=\"…\"]` attributes on the account picker are unstable. Check `#KmsiCheckboxField` (\"Don't show this again\") before clicking `#idSIButton9` so the prompt stops recurring on later runs.\n\n### Make the window fill the user's screen (they WILL be watching it)\n\nThe user sees the automated window while it records; a tiny 800×600 window that can't be moved or resized is a real complaint. Three separate traps:\n\n1. **`viewport: {width, height}` locks the window.** Playwright *emulates* a fixed viewport and the window becomes unresizable. Use **`viewport: null`** so the page tracks the real window — then control size via launch args. (`recordVideo` still captures the page buffer, so the recording stays clean.)\n2. **`--start-maximized` and `--window-size` are BOTH silently ignored on a profile that has saved window bounds** — you get a stubborn 800×600. Removing `browser.window_placement` from the clone's `Preferences` is *not* reliable either, and CDP `Browser.setWindowBounds` fails under `launchPersistentContext` (`Protocol error: Browser window not found`). **What actually works: resize the real Win32 window after launch.** Call this right after launch, and again after each navigation (the app can re-trigger a resize):\n   ```powershell\n   # maximize.ps1 - maximize the Edge window belonging to our cloned profile\n   Add-Type @\"\n   using System; using System.Runtime.InteropServices;\n   public class Win {\n     [DllImport(\"user32.dll\")] public static extern bool ShowWindow(IntPtr h, int c);\n     [DllImport(\"user32.dll\")] public static extern bool MoveWindow(IntPtr h, int x, int y, int w, int t, bool r);\n     [DllImport(\"user32.dll\")] public static extern bool IsWindowVisible(IntPtr h);\n   }\n   \"@ -ErrorAction SilentlyContinue\n   Add-Type -AssemblyName System.Windows.Forms\n   $wa = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea\n   Get-CimInstance Win32_Process -Filter \"Name='msedge.exe'\" |\n     Where-Object { $_.CommandLine -like '*edge-tenant*' } | ForEach-Object {\n       $p = Get-Process -Id $_.ProcessId -ErrorAction SilentlyContinue\n       if ($p -and $p.MainWindowHandle -ne 0 -and [Win]::IsWindowVisible($p.MainWindowHandle)) {\n         [Win]::ShowWindow($p.MainWindowHandle, 9) | Out-Null              # SW_RESTORE\n         [Win]::MoveWindow($p.MainWindowHandle, $wa.X, $wa.Y, $wa.Width, $wa.Height, $true) | Out-Null\n         [Win]::ShowWindow($p.MainWindowHandle, 3) | Out-Null              # SW_MAXIMIZE\n       } }\n   ```\n   Filter by command line (`*edge-tenant*`) so you only touch your own window, and invoke it from Node with `child_process.execSync('powershell -NoProfile -ExecutionPolicy Bypass -File maximize.ps1')`.\n3. **Measure the screen correctly — logical ≠ physical.** `[System.Windows.Forms.Screen]` reports **logical** px (already divided by the Windows display scale), while `screen.width` in the browser reports **physical**. A machine reporting `1536×1024` from PowerShell was really `2400×1600` at 156 % scale. Reconcile before computing sizes, or you will undersize the window by the scale factor.\n\n   **Calibrate empirically instead of deriving it.** Launch once at a known `--force-device-scale-factor`, maximize, and read the CSS width; CSS width scales as `1\u002Fdsf`, so:\n   `dsf_target = dsf_measured × (cssWidth_measured \u002F cssWidth_wanted)`\n   Worked example: at `dsf 0.8` the maximized window measured **2376 CSS px** wide. For a 1920-wide capture: `0.8 × 2376\u002F1920 = 0.99`. Relaunching at `--force-device-scale-factor=0.99` produced **1916×1162** — a full-screen window *and* a ~1920 px capture buffer.\n   ```\n   '--force-device-scale-factor=0.99', '--start-maximized',\n   ```\n   Read the real numbers from the browser, not from PowerShell, and log them every run:\n   ```js\n   await page.evaluate(() => ({ w: innerWidth, h: innerHeight, dpr: devicePixelRatio, sw: screen.width, sh: screen.height }))\n   ```\n\n## Workflow\n\n### 1. Record the real flow\n\n> ⛔ **MANDATORY GATE — DO NOT RECORD until the chat input bar is verified fully visible.**\n> The single most common defect is the chat input bar (and the end of the last message) being cut off at the bottom of the frame. You **MUST** pass this gate before every recording run — no exceptions, even if a previous run \"looked fine\":\n> 1. Run **`scripts\u002Fverify.js`** (same launch args\u002Fviewport as the recorder). It opens the target, dismisses popups, handles the account picker, sends ONE test message, and screenshots the **viewport buffer** (which is exactly what gets recorded — NOT the on-screen window).\n> 2. **View that screenshot** and confirm ALL of these are visible in-frame: the header, the latest message bubble's END, the chat **input bar** (\"Ask a question…\"), and the disclaimer line. Do not rely on the on-screen window (it may be cut by the physical screen\u002Ftaskbar); judge only from the screenshot.\n> 3. If ANY of them is cut off → **adjust the viewport height and re-run verify.js. Repeat until the screenshot passes.** Never proceed to the real recording on a failed\u002Funcertain frame.\n> 4. Use the SAME `viewport` and launch `args` in `recorder.js` that passed the gate.\n>\n> Reference that passes for Copilot Studio at 1920 wide: viewport **1920×880**. Treat this as a starting point, still verify per app\u002Fscreen.\n\n- Confirm the target URL and the scenario steps (prompts to type \u002F buttons to click) with the user.\n- **Free the browser profile first.** Only one process can lock the persistent profile. Kill any Edge processes using the profile dir before launching the recorder (see `scripts\u002Fprep.ps1`, which also clears stale `Singleton*` lock files and normalizes the crash-restore flags), otherwise `launchPersistentContext` fails with \"Failed to launch… Opening in existing browser session\".\n- Run `scripts\u002Frecorder.js` (template provided) **only after the visibility gate passes**. It launches the logged-in profile, records page video, drives the chat\u002Fapp with human-like typing, waits for each response to stabilize, holds for readability, then closes to flush the `.webm`.\n- **Submit reliably:** prefer clicking the Send button and confirm the input box empties; pressing Enter alone can concatenate prompts into one bubble. The template's `submit()` handles this.\n- **Copilot Studio's Build\u002FPreview\u002FEvaluate\u002FMonitor switcher is a `menuitemradio` DROPDOWN behind a single \"Build\" button, not tabs.** `getByRole('tab', {name: \u002FPreview\u002Fi})` will silently find nothing and `openTestPane()` will time out waiting for the chat input. Two fixes, prefer the first:\n  1. **Just navigate directly to `${AGENT_URL}\u002Fpreview`** (`page.goto`) — clicking the option once reveals the URL pattern is stable (`\u002Fagents\u002F\u003Cid>\u002Fpreview`), and a direct `goto` is far more reliable than menu-click choreography. This is what `openTestPane()` in the shared `recorder.js`\u002F`verify.js` templates now does.\n  2. If you must click through the UI: click the button whose accessible name is the *current* tab label (e.g. `Build`) to open the menu, then click the `menuitemradio` named `Preview` inside it (`page.getByRole('menuitemradio', {name: \u002F^Preview$\u002Fi})`).\n  - **Diagnose this class of bug fast:** if a locator search for the expected tab\u002Fbutton comes back empty, don't guess — take a `browser_snapshot`\u002Fscreenshot of the live page (via the `playwright-browser_*` MCP tools, reusing the same signed-in tenant profile) to see the actual accessibility tree before patching the recorder script blind.\n\n#### Robust, clean capture (learned the hard way — bake these into the recorder)\n- **Suppress Edge nag UI with launch args** so no banners\u002Fbubbles pollute the frame:\n  `--test-type` (kills the yellow \"unsupported command-line flag\" infobar), `--disable-infobars`, `--hide-crash-restore-bubble`, `--disable-session-crashed-bubble`, `--no-first-run`.\n- **Kill the \"Restore pages?\" bubble at the source:** after force-killing Edge between runs, patch the profile `Default\u002FPreferences` before relaunch — replace `\"exit_type\":\"...\"` → `\"exit_type\":\"Normal\"` and `\"exited_cleanly\":false` → `\"exited_cleanly\":true`.\n- **Auto-handle the Microsoft account picker.** If a sign-in\u002Faccount chooser appears, click the correct account by regex on its email\u002Ftenant (ask the user which account once, then match it) — otherwise the run stalls. See `pickAccountIfNeeded()` in the template.\n- **Dismiss product coach-marks \u002F NPS \u002F cookie popups.** Loop over buttons named `Close`, `Got it`, `Dismiss`, `No thanks`, `Skip`, `Not now`, `Accept`, `OK`, then press `Escape`. Call it after load AND after opening the chat\u002Fpreview pane. See `dismissPopups()`.\n- **Size the viewport so the chat input bar is fully in-frame (see the MANDATORY GATE above).** The recorded frame is the *viewport buffer*, not the on-screen window, so it can exceed the physical screen. For Copilot Studio at 1920-wide, a viewport of **1920×880** shows the header, messages, the \"Ask a question\" input bar, and the disclaimer line. This is NOT optional guesswork — you must prove it with `verify.js` and a screenshot before recording, and re-run until it passes.\n- **Scroll to the END of each response before typing the next turn** so the full answer is on screen, and pin to bottom during holds — this also hides the floating \"Scroll to bottom\" down-arrow. See `scrollBottom()` + `readHold()`.\n- **You can keep using the laptop \u002F switch screens during recording.** `recordVideo` captures the page's internal buffer, not your monitor, so other windows, screen switches, or minimizing never appear. Only avoid closing the automated Edge window or sleeping the machine.\n- **Run detached with synchronous file logging.** Long recordings (slow tool calls, timeouts) can outlive a tool call; launch the recorder detached so it survives, and have it `fs.appendFileSync` a timestamped `progress.log` per step (console output to a redirected file is block-buffered and looks \"stuck\"). Poll `progress.log` + node\u002Fedge process counts to track real progress.\n\n### 2. Post-process with ffmpeg\n- Probe duration & sample frames every few seconds to **map when each scenario happens** and **find where auth ends**:\n  ```powershell\n  ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 in.webm\n  ffmpeg -y -i in.webm -vf \"fps=1\u002F5\" frame_%03d.png    # then view frames\n  ```\n- **Cut auth + crop empty margins**, convert to H.264 MP4 (Clipchamp-friendly). Example: drop first 30s, crop to content:\n  ```powershell\n  ffmpeg -y -ss 30 -i in.webm -vf \"crop=1280:600:0:0,setsar=1\" -c:v libx264 -pix_fmt yuv420p -crf 20 -preset medium out.mp4\n  ```\n- **If `maximize.ps1` couldn't fill the physical screen** (small\u002Flow-res display, or a screen-resolution change since the empirical DSF calibration), the recorded buffer will show the real app content only in a small corner with a large flat-gray dead zone around it — check a sampled frame BEFORE narrating, not after. Fix by cropping to the actual content bounding box (read it off a sample frame — e.g. `crop=782:600:0:0`), then `scale` up to a target size and `pad` onto a white\u002Fblack canvas to reach a standard resolution (16:9 for a normal video deliverable):\n  ```powershell\n  ffmpeg -y -i in.webm -vf \"crop=782:600:0:0,scale=1303:1000,pad=1920:1080:(ow-iw)\u002F2:(oh-ih)\u002F2:color=white\" -c:v libx264 -pix_fmt yuv420p -crf 20 out.mp4\n  ```\n  Do this crop\u002Fscale\u002Fpad **per trimmed segment inside the same `trim`+`concat` filter_complex** (not as a separate pass) so segments stay in sync — see the worked multi-segment example below.\n- **Hit a target length (e.g. ≤ 2 min) by cutting DEAD TIME, not content.** Agent demos have long dead gaps during slow tool calls \u002F timeouts. Don't speed-ramp the whole clip — instead pick keep-windows around each meaningful beat (question asked, skill loads, result appears) and drop the multi-second waits between them. Extract each window with `-ss \u003Cstart> -t \u003Cdur>`, normalize them all to the same size\u002Ffps (`scale=...,pad=1920:1080,setsar=1,fps=30`), then concat. Use the recorder's `progress.log` timestamps to locate beats fast.\n  - concat gotcha: `ffmpeg -f concat` resolves `file '...'` paths **relative to the concat file's own directory** — list bare filenames and run from that dir, or use absolute paths.\n- Note the trimmed clip's exact duration and each segment's start offset — you'll align narration to these.\n\n### 2b. Narration without the Clipchamp UI (headless, reliable fallback)\n### 2b. PATH (A) — Narration without the Clipchamp UI (headless, fast, recommended default)\nWhen the browser\u002FClipchamp UI isn't available (or you just want a deterministic build), generate the voiceover with **edge-tts** (same Ava neural voice) and mux with ffmpeg:\n- Write one text block per beat with `(start_time, max_dur, text)`. Generate each: `python -m edge_tts --voice en-US-AvaNeural --text \"...\" --write-media bNN.mp3` (in code, bump `rate` `+8%`\u002F`+15%`\u002F… until a block fits its `max_dur`).\n- Build a single voice track by delaying each block to its start: `[i:a]adelay=START_ms|START_ms,apad[ai]` then `amix=inputs=N:normalize=0` and `atrim=0:TOTAL`.\n- Mux onto the trimmed video: `ffmpeg -i cut.mp4 -i voice.m4a -c:v copy -c:a aac -map 0:v:0 -map 1:a:0 -shortest final.mp4`.\n- Name the output after the agent\u002Fproduct and copy it into the user's workspace\u002FOneDrive folder so it renders inline and syncs.\n\n### 3. PATH (B) — Assemble in Clipchamp (browser automation; editable project)\nOnly if the user chose (B) in STEP 0 AND the `playwright-browser_*` tools are available.\nRead **[clipchamp-ui.md](clipchamp-ui.md)** for the exact click targets, selectors, and gotchas. High level:\n1. Open https:\u002F\u002Fapp.clipchamp.com → open (or recover) the project, or create a new one.\n2. **Import media** → file chooser → `browser_file_upload` the trimmed MP4 (+ any intro screenshot). If a permission hook blocks uploads, ask the user to enable **Settings → Permissions → Allow agent file uploads**.\n3. Add media to the timeline via each item's \"Add to timeline\" button (drops at the playhead).\n4. Optionally add an intro still (e.g. a \"Build\"\u002Farchitecture screenshot) so the video opens on context.\n5. **Voiceover:** open **Record & create → Text to speech** (or select an existing TTS clip's **Text to speech** tab), pick **Ava Multilingual**, type the block text, **Save** to render. New TTS blocks drop **at the playhead** — set the playhead precisely first via the time input spinbuttons (Minutes\u002FSeconds), then create the block.\n6. Cover any black tail after the video ends with a closing still so the final voiceover has a visual.\n\n### 4. Export\n- **Export → set the File name to the agent\u002Fproduct** (e.g. `Contoso IT Assistant - Demo`), 1080p, keep \"Store in the cloud\" → **Export**. If prompted about an existing name, choose **Keep both** or **Replace** as the user prefers.\n- Output lands in OneDrive: `Videos\u002FClipchamp\u002F\u003Cproject>\u002FExports\u002F\u003Cname>.mp4`. It may not sync to the local folder immediately; the export screen's **Copy link** \u002F **Save to your computer** are the fallbacks.\n\n## Narration authoring tips\n- One block per on-screen beat; ~1–3 short sentences each. Keep total pacing tight — aim for little dead air.\n- Add **technical call-outs** that describe the mechanics the viewer is seeing: which skill\u002Ftool fired, what data source it hit (\"queries the knowledge table\", \"calls the create-record tool\", \"reads the live schedule\").\n- Keep it **generic** if asked — strip customer\u002Fcompany names.\n- End on a **hero voiceover line** (no card needed) that names the value prop and the tech, e.g. \"That's the power of AI, combining X and Y to turn Z into one seamless, autonomous experience.\"\n- Remove leftover title-text overlays if the intro screenshot already shows the name.\n\n## Common pitfalls (all learned the hard way)\n- **`Timeout 180000ms exceeded` on `launchPersistentContext` + `DevTools remote debugging requires a non-default data directory`** → you pointed Playwright at Edge's **default** `User Data` root. Edge blocks remote debugging there. **Clone the profile to a separate root** (see \"Pick the right profile \u002F tenant\" step 3) and launch against the clone.\n- **App renders a totally blank page, `document.body.innerText.length === 0`, `outerHTML` is ~1 MB, and the network log shows `404` on `api.bap.microsoft.com\u002F...\u002Fenvironments\u002F\u003Cid>`** → the app shell loaded but **MSAL silently issued a token for the wrong tenant**. Confirm by reading the MSAL cache instead of guessing:\n  ```js\n  await page.evaluate(() => { const o = [];\n    for (let i = 0; i \u003C localStorage.length; i++) { const k = localStorage.key(i);\n      try { const j = JSON.parse(localStorage.getItem(k)); if (j && (j.username || j.tenantId)) o.push({ u: j.username, t: j.tenantId }); } catch {} }\n    return o; });\n  ```\n  A `tenantId` of `72f988bf-86f1-41af-91ab-2d7cd011db47` is the **Microsoft corp** tenant — i.e. it authenticated as the primary work account, not the dev\u002Fdemo tenant. Clearing `localStorage`\u002F`sessionStorage` and the app's cookies does **not** fix it: the `login.microsoftonline.com` session cookie still defaults to the primary account, so it silently re-SSOs to the same wrong identity.\n  **Fix:** force the picker by navigating to the tenant-scoped authorize endpoint with `prompt=select_account` + `login_hint` before loading the app:\n  ```\n  https:\u002F\u002Flogin.microsoftonline.com\u002F\u003Ctenant>.onmicrosoft.com\u002Foauth2\u002Fv2.0\u002Fauthorize\n    ?client_id=\u003Capp spa client id>&response_type=code\n    &redirect_uri=\u003Curl-encoded app redirect>&response_mode=fragment\n    &scope=openid%20profile%20offline_access\n    &prompt=select_account&login_hint=\u003Cuser@tenant>\n  ```\n  Read the SPA `client_id` straight out of the MSAL localStorage key (`msal.\u003Cclient-id>.active-account-filters`). Then click the tenant tile — `div[data-test-id=\"\u003Cfull upn>\"]` is the reliable selector on the \"Pick an account\" screen.\n- **Never test \"did we land on the app?\" with `url.includes('\u003Capp-host>')`** → the authorize URL embeds the app host inside its own `redirect_uri` query param, so `includes()` returns true while you are still sitting on the login page, and your picker loop exits before clicking anything. Compare `new URL(u).host` instead. Apply this to `settle()` too.\n- **Sign-in loop spins forever re-clicking the account tile and the host becomes `login.microsoft.com\u002F\u003Ctenant>\u002Fbridge\u002Ffido`** → the account requires **passkey \u002F Windows Hello**, which cannot be automated. The cookie-based SSO you cloned has expired.\n  **Do not burn time scripting around this.** An automated `prompt=select_account` flow will *still* silently resolve back to the Windows-connected primary account (`Connected to Windows` on the picker always wins), so the MSAL cache stays on the wrong tenant no matter how many times you clear storage.\n  **The reliable fix — always prefer it:** ask the user to open **their own normal Edge** on the target URL and confirm it loads. Then **re-clone the profile** (the clone captures the freshly-minted cookies) and every subsequent automated run works. This takes one message and ~2 minutes; driving an in-Playwright interactive sign-in is slower, and the user may not notice the window at all — an unattended \"waiting for user\" poller will just time out.\n  Note `login.microsoft.com` and `login.microsoftonline.com` are **different hosts** — match both when detecting auth pages. Treat the clone as a **cache of the user's live session**: whenever auth looks wrong, re-run the clone step before debugging anything else.\n- **Do NOT round-trip Chromium `Preferences` through `ConvertFrom-Json`\u002F`ConvertTo-Json`** → it reorders keys and retypes values, and can leave the profile in a broken state. Patch it with **targeted regex string replacement** only:\n  ```powershell\n  $c = Get-Content $pf -Raw -Encoding UTF8\n  $c = $c -replace '\"exit_type\":\"[^\"]*\"','\"exit_type\":\"Normal\"' -replace '\"exited_cleanly\":false','\"exited_cleanly\":true'\n  [System.IO.File]::WriteAllText($pf, $c, (New-Object System.Text.UTF8Encoding($false)))\n  ```\n- **User asks for \"your actions and the chat at the same time\" and you already recorded page-only** → `recordVideo` captured only the page buffer; the assistant conversation was never in those pixels and cannot be added retroactively. You must re-record with **§1b dual capture** (gdigrab + side-by-side windows). Ask which capture mode they want *before* recording.\n- **gdigrab MP4 won't play \u002F has no duration** → ffmpeg was killed instead of being asked to stop, so the moov atom was never written. Send `q` to stdin to stop it; recover an existing file with `ffmpeg -i broken.mp4 -c copy fixed.mp4`.\n- **`hstack` fails with \"Input link parameters do not match\"** → the two sources have different heights or SAR. Scale both to the same height and `setsar=1` before stacking.\n- **Chat input bar cut off after switching to side-by-side windows** → halving the window width changes the CSS viewport, so the previously-calibrated device-scale factor no longer holds. Re-calibrate and re-run the visibility gate at the new geometry.\n- **Copilot Studio agent tabs are `Build | Preview | Evaluate | Monitor`** → the test chat is under **Preview** (not \"Test\"). The agent's overview page under **Build** already lists Instructions, Model, **Skills**, **Tools**, and **Knowledge** in one scrollable column — one slow scroll covers the whole \"what this agent is\" section, no tab-hopping needed.\n- **A tool call in the demo returns a red error card (e.g. `Request validation · 400`)** → the backing system may be asleep (some dev\u002Fsandbox backends hibernate) or the action is genuinely misconfigured. **Check this during the visibility gate, not after recording** — send one real test prompt and read the answer. If the backend is down, tell the user and get it woken up before the real run; a demo video full of error cards is worthless.\n- **The agent you want isn't in the agents list** → the list only shows agents in *that* environment, and a broken-tenant session renders an empty body so **every** agent looks missing. Confirm auth first, then try the direct `\u002Fagents\u002F\u003Cid>` URL — an agent can load fine by id even when you can't spot it in the list. Harvest names\u002Fids by scraping `[role=\"row\"]` from the grid.\n- **Window is tiny (800×600), can't be moved or resized, user complains they can't see it** → three causes, check all: (a) `viewport: {w,h}` locks the window — use `viewport: null`; (b) saved `browser.window_placement` in the cloned `Preferences` overrides `--window-size` **and** `--start-maximized` — delete that key before launch; (c) you sized from `[System.Windows.Forms.Screen]` (logical px) instead of `screen.width` (physical px) and undersized by the display-scale factor. Always log `innerWidth\u002FinnerHeight\u002FdevicePixelRatio` from inside the page to confirm.\n- **`Browser.getWindowForTarget` → `Protocol error: Browser window not found`** → CDP window control is unreliable under `launchPersistentContext`; fix the profile `Preferences` + launch args instead.\n- **App \"loads\", then the next screenshot is a login page with ~200 chars of text** → MSAL redirected back to `\u002Foauth2\u002Fv2.0\u002Fauthorize` for a silent token refresh after your one-shot check passed. Use the **N-consecutive-stable-polls `settle()` loop**, never a fixed `waitForTimeout`.\n- **Wrong tenant \u002F blank page \u002F 404 on the environment** → the MCP browser is signed in to a different account than the one that owns the agent. Don't sign in interactively; enumerate the real Edge profiles, find the one whose `account_info[0].email` matches the tenant, clone it, and relaunch with `--profile-directory=\u003Cdir>` against the clone root (see \"Pick the right profile \u002F tenant\").\n- **`--profile-directory` picked the wrong profile** → the profile's *display name* and its *folder name* differ (folder `Profile 1` can be shown as \"Profile 2\"). Always use the folder name from `Get-ChildItem`.\n- **Profile lock** → recorder can't launch. Kill stale Edge procs on the profile dir first, and remove any `Singleton*` lock files under the profile dir. Filter by command line so you don't also kill the MCP browser.\n- **Recorder looks \"stuck\" at an early turn** → it's usually block-buffered redirected console output, not a hang. Use synchronous `fs.appendFileSync` logging to `progress.log` and check node\u002Fedge process counts to see real progress.\n- **Yellow \"unsupported command-line flag\" banner in-frame** → add `--test-type` (+ `--disable-infobars`).\n- **\"Restore pages?\" bubble in-frame** → patch the profile `Preferences` (`exit_type`→`Normal`, `exited_cleanly`→`true`) before relaunch, and pass `--hide-crash-restore-bubble --disable-session-crashed-bubble`.\n- **Account picker \u002F sign-in stalls the run** → auto-click the right account by email\u002Ftenant regex (`pickAccountIfNeeded`).\n- **Chat input bar half-cut \u002F off-screen** → the recorded frame is the viewport buffer, not the physical window; size the viewport tall enough (e.g. 1920×880 for Copilot Studio) and verify with a pre-record viewport screenshot.\n- **Video too long** → cut dead tool-call\u002Ftimeout gaps into per-beat keep-windows and concat; don't speed-ramp the whole thing.\n- **Browser session drops mid-session** → the MCP browser can be relaunched with `browser_navigate`; the Clipchamp project autosaves to the cloud, so reopen it from the home page. If the browser tools are removed entirely mid-task, fall back to the **§2b headless edge-tts + ffmpeg** narration path — same narrated-MP4 deliverable.\n- **Enter-to-send concatenation** → use the Send button + confirm the box cleared.\n- **TTS block lands in the wrong place** (Clipchamp UI) → it drops at the playhead; set the time input first. Delete a misplaced block and recreate rather than dragging.\n- **Black tail** at the end when the last voiceover outlasts the footage → extend the last clip or add a closing still.\n- **ffmpeg not on PATH** → install once to `C:\\Users\\\u003Cuser>\\.copilot\\bin` and add it to the User PATH (persists across chats); in the current shell refresh `$env:Path` from Machine+User.\n","# Clipchamp Narrated Demo Video Skill\n\nProduce a polished, narrated demo video: **real screen-flow footage of a live app\u002Fagent** under an **AI voiceover**. This captures hard-won lessons — read it fully before starting.\n\n## STEP 0 — ASK THE USER: which assembly method? (do this FIRST, before recording)\n\nThe recording step is identical for both. The **assembly + narration** step has two paths — ask the user to pick before you start, e.g. via `m_ask_user`:\n\n> \"How should I assemble the video — **(A) Headless (ffmpeg + Ava neural TTS)** — faster (~2–3 min), scripted, precise, produces a finished MP4; or **(B) Clipchamp web UI** — slower (~15–25 min of browser clicks) but gives you an **editable Clipchamp project** to hand-tweak later (transitions, captions, music)?\"\n\nGuidance to offer with the question:\n- **Default \u002F recommended: (A) Headless** — faster, deterministic, exact narration timing, no session-drop\u002Fupload-hook risk. Best when the deliverable is just the finished MP4.\n- **Choose (B) Clipchamp** only if the user wants an editable project living in their Clipchamp account, or plans manual edits (transitions, captions, background music, hand re-timing).\n- **(B) requires the `playwright-browser_*` MCP tools.** If those tools are not available in the session (they can be disabled\u002Fdropped), (B) is impossible — tell the user and either have them re-enable the Browser Control tools \u002F start a new chat, or fall back to (A).\n\nRecord the flow the same way either path (§1), then follow **§2 + §2b (headless)** or **§3 (Clipchamp)** based on their choice.\n\n## STEP 0b — ASK: page-only capture, or \"your actions + the chat\" (dual capture)?\n\nIf the user says anything like *\"record your actions and the chat at the same time\"*, *\"show what you're doing while it runs\"*, *\"a making-of\"*, or *\"I want to see both\"* — they want **the Scout\u002Fassistant conversation visible alongside the app**, not just the app. That is a **different capture mode** and you must pick it BEFORE recording:\n\n- **Page-only (default, §1).** Playwright `recordVideo` captures the page's internal buffer. Clean, no browser chrome, immune to other windows \u002F screen switches. **It physically cannot capture the Scout window** — `recordVideo` only ever sees the one page.\n- **Dual capture (§1b).** OS-level desktop capture with **ffmpeg `gdigrab`** while Scout and the automated browser sit **side by side**. One capture, so the two halves are perfectly in sync for free.\n\nDo NOT try to satisfy a dual-capture request by recording the page and hoping to add the chat later — you cannot recover the chat pixels after the fact.\n\n## §1b — Dual capture: agent actions + the app, side by side\n\nVerified working (`ffmpeg 8.x`, Windows): `-f gdigrab -framerate 15 -i desktop` produced a clean 1920×1280 H.264 capture.\n\n**Ready-made scripts:** `scripts\u002Farrange.ps1` (side-by-side window placement) and `scripts\u002Fdual-capture.js` (starts gdigrab, runs `recorder.js`, stops ffmpeg cleanly, probes the result). Run `node dual-capture.js out.mp4`.\n\n1. **Arrange the two windows first.** Put the automated Edge on one half and the Scout window on the other — see `scripts\u002Farrange.ps1`. It matches Edge by command line (`*edge-tenant*`) and Scout by process name \u002F window title, then `MoveWindow`s each to half the working area.\n   **Recompute the device-scale factor for the narrower window** — half-width means the CSS viewport halves too. Re-run the empirical calibration from \"Make the window fill the user's screen\" against the half-width, or the chat input bar will be cut off. The MANDATORY GATE still applies, at the half-width geometry.\n2. **Start ffmpeg detached, before the recorder**, and give it a couple of seconds of lead-in:\n   ```powershell\n   ffmpeg -y -f gdigrab -framerate 15 -i desktop -c:v libx264 -pix_fmt yuv420p -crf 23 -preset veryfast dual.mp4\n   ```\n   Capture `desktop` (the whole screen) rather than `-i title=\u003Cwindow>`: per-window gdigrab breaks whenever the window is occluded, moved, or redrawn, and it silently drops frames. Crop later with ffmpeg if you want a tighter frame.\n3. **Stop ffmpeg gracefully** — write `q` to its stdin, or it may leave the MP4 without a moov atom (unplayable). If you must kill it, use `Stop-Process -Id \u003Cpid>` and then remux: `ffmpeg -y -i broken.mp4 -c copy fixed.mp4`.\n4. **Consequences of desktop capture — tell the user up front:**\n   - **They must not use the machine while it records.** Unlike `recordVideo`, everything on screen ends up in the video: notifications, other windows, the taskbar.\n   - **Toasts and popups will appear.** Enable Focus Assist \u002F Do Not Disturb first.\n   - **Anything private on screen is captured.** Close unrelated mail\u002Fchat windows before starting.\n5. **Alternative when the user wants both but can't stop working:** record the page-only video (§1), then composite the chat as a separate pass afterwards (screenshots of the conversation as an intro\u002Foutro, or a picture-in-picture strip). Sync will be approximate — say so rather than implying frame accuracy.\n6. **Compositing two separate sources** (only if you truly captured them separately) — normalize both to the same height and fps first, then stack:\n   ```powershell\n   ffmpeg -y -i app.mp4 -i chat.mp4 -filter_complex `\n     \"[0:v]scale=-2:1080,setsar=1,fps=30[l];[1:v]scale=-2:1080,setsar=1,fps=30[r];[l][r]hstack=inputs=2,pad=ceil(iw\u002F2)*2:ceil(ih\u002F2)*2\" `\n     -c:v libx264 -pix_fmt yuv420p -crf 20 composite.mp4\n   ```\n   `hstack` **requires identical heights** or it errors out; the trailing `pad=ceil(iw\u002F2)*2:...` keeps dimensions even for yuv420p. For picture-in-picture use `overlay=W-w-40:H-h-40` instead of `hstack`.\n\n## The winning approach (and why)\n\nA slideshow of static screenshots is almost never what the user wants. They want to **see the app move** — cursor, typing, live responses. The reliable way to get that autonomously:\n\n1. **Drive the real app with Playwright** and record **page-only video** (`recordVideo`). This is clean (no browser chrome) and hands-off. Raw OS screen capture is worse: synthetic automation cursors often don't render, and it captures the whole desktop. **Exception:** if the user explicitly wants the assistant conversation in-frame too, `recordVideo` cannot do it — switch to §1b dual capture.\n2. **Reuse the user's logged-in browser profile** so auth is preserved (e.g. Copilot Studio, M365). Never spin up a fresh profile — you'll lose the session. **If the target lives in a different tenant than the MCP browser's signed-in account, use the real Edge profile for that tenant instead of signing in interactively** — see \"Pick the right profile \u002F tenant\" below.\n3. **Cut the auth\u002Floading head** from the recording with ffmpeg — the account picker, \"Stay signed in\", and app \"Initializing…\" spinner are dead air.\n4. **Narrate with the Ava neural voice** — either Clipchamp's built-in Text to speech or edge-tts `en-US-AvaNeural` — split into **several short blocks aligned to on-screen moments**, not one long block. Silence between events feels broken; short blocks with technical call-outs (\"here it invokes a skill to query the table\") make it feel alive.\n5. **Name the export after the agent\u002Fproduct**, not \"Video Project 1\".\n\n## Prerequisites\n\n**Windows only** (PowerShell, Win32 window APIs, `msedge.exe`, ffmpeg `gdigrab` desktop capture) — this skill does not currently have macOS\u002FLinux capture backends. See `README.md` for the one-time global install steps (Node + Playwright, ffmpeg\u002Fffprobe, edge-tts) and how to check each is already present before installing.\n\n### Ready-made scripts (`scripts\u002F`)\n\nWorking, battle-tested versions live next to this file. Copy them into the task's work dir and edit the constants at the top of `common.js` (clone path, env id, agent id, target account) rather than writing new ones from scratch.\n\n| Script | Purpose |\n| ------ | ------- |\n| `clone.ps1` | Kill real Edge, clone the tenant profile to a non-default root (caches excluded), regex-patch `Preferences`. **Re-run whenever auth breaks.** |\n| `prep.ps1` | Between runs: kill stale Edge (cloned profile only) and this workflow's own Node processes, clear `Singleton*` locks, reset `exit_type`\u002F`exited_cleanly`. Run before every launch. |\n| `maximize.ps1` | Win32 `MoveWindow`+`SW_MAXIMIZE` on the cloned-profile Edge window (the only thing that reliably resizes it). |\n| `arrange.ps1` | Side-by-side placement (browser left, Scout right) for §1b dual capture. |\n| `common.js` | Shared launch `ARGS`, the `settle()` auth\u002Fstability loop, `dismissPopups()`, `logViewport()`, `maximizeWindow()`. |\n| `verify.js` | **The MANDATORY GATE** — loads the target, opens the test pane, sends one real prompt, screenshots the frame. |\n| `recorder.js` | The real run: overview → instructions → skills\u002Ftools scroll → N live test prompts, with `progress.log` beat markers. |\n| `dual-capture.js` | §1b: starts gdigrab, runs `recorder.js`, stops ffmpeg cleanly, probes the output. |\n| `findagent.js` | Scrape the agents grid (`[role=\"row\"]`) for names, then open one and harvest its real id from the URL. |\n| `diag.js` | When a page renders blank: timed screenshots + console errors + HTTP ≥400 logging. |\n\n### Pick the right profile \u002F tenant (do this BEFORE anything else)\n\nThe MCP browser is usually signed in to the user's **primary** work account. If the agent\u002Fapp lives in a **different tenant** (e.g. a `*.onmicrosoft.com` dev\u002Fdemo tenant), that session will 404 or bounce to the wrong environment. **Do not try to sign in interactively** and do not ask the user to type a password — the user almost always already has a **second Edge profile** signed in to that tenant. Find it and drive that.\n\n1. **Detect the mismatch early.** Open the target URL in the MCP browser; a blank page, `404` on `api.bap.microsoft.com\u002F...\u002Fenvironments\u002F\u003Cid>`, or a redirect to `~personal`\u002Fa different environment ID means wrong tenant. Confirm the signed-in identity (in Copilot Studio: click `[data-testid=\"account-profile-button-trigger\"]`).\n2. **Enumerate the real Edge profiles and their accounts** — map `Default`, `Profile 1`, `Profile 2`… to emails by reading each profile's `Preferences`:\n   ```powershell\n   $ud = \"$env:LOCALAPPDATA\\Microsoft\\Edge\\User Data\"\n   Get-ChildItem $ud -Directory | Where-Object { $_.Name -eq 'Default' -or $_.Name -like 'Profile*' } | ForEach-Object {\n     $p = Join-Path $_.FullName 'Preferences'\n     if (Test-Path $p) { try { $j = Get-Content $p -Raw -Encoding UTF8 | ConvertFrom-Json\n       [PSCustomObject]@{ Dir=$_.Name; Name=$j.profile.name; Email=$j.account_info[0].email } } catch {} }\n   } | Format-Table -AutoSize\n   ```\n   Note the **directory name** (`Profile 1`), *not* the display name (`Profile 2`) — they routinely disagree, and Playwright needs the directory.\n3. **CLONE the profile to a non-default User Data root — do NOT drive the real one.** Edge **refuses remote debugging on its default data directory** and `launchPersistentContext` dies after 180 s with:\n   `Timeout 180000ms exceeded` + `[err] DevTools remote debugging requires a non-default data directory. Specify this using --user-data-dir.`\n   Cloning also frees you from the real-Edge profile lock. Copy only what carries auth — skip the caches, which are the bulk (a 3 GB profile drops to ~300 MB, and `Service Worker` alone was 2 GB):\n   ```powershell\n   $src = \"$env:LOCALAPPDATA\\Microsoft\\Edge\\User Data\"; $dst = \"C:\\Users\\\u003Cuser>\\.copilot\\video-work\\edge-tenant\"\n   New-Item -ItemType Directory -Force -Path $dst | Out-Null\n   Copy-Item \"$src\\Local State\" \"$dst\\Local State\" -Force\n   robocopy \"$src\\Profile 1\" \"$dst\\Profile 1\" \u002FE \u002FNFL \u002FNDL \u002FNJH \u002FNJS \u002FR:0 \u002FW:0 \u002FXJ `\n     \u002FXD \"Service Worker\" \"Cache\" \"Code Cache\" \"GPUCache\" \"Shared Dictionary\" \"DawnGraphiteCache\" \"DawnWebGPUCache\" \"Crashpad\" \"optimization_guide_model_store\"\n   ```\n   Keep `Network\\Cookies`, `Login Data`, `Local Storage`, `IndexedDB`, `WebStorage`, `Preferences` — that's what preserves the session. Then launch `launchPersistentContext(\u003Cclone root>, { args: ['--profile-directory=Profile 1', ...] })` — still the **root**, never the profile subfolder.\n   **The clone is a snapshot, not a link.** If auth later breaks, have the user load the target URL in their own Edge and then **re-clone** — that is the fastest repair, and far more reliable than any scripted sign-in (see the passkey pitfall below). Close the real Edge only while cloning (files are locked mid-copy).\n4. **Close the real Edge only while cloning** (files are locked mid-copy). Kill only the non-Playwright `msedge.exe` processes, delete `Singleton*` under the target root, and patch `Profile 1\\Preferences` (`exit_type`→`Normal`, `exited_cleanly`→`true`) so no \"Restore pages?\" bubble appears in-frame.\n   ```powershell\n   Get-CimInstance Win32_Process -Filter \"Name='msedge.exe'\" |\n     Where-Object { $_.CommandLine -notlike '*m-playwright-profiles*' -and $_.CommandLine -notlike '*ms-playwright*' } |\n     ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }\n   ```\n   Filtering by command line matters: killing everything also kills the MCP browser you may still need.\n5. Real profiles can be **multi-GB** and slower to boot than `mcp-msedge` — allow generous first-navigation timeouts (90–120 s) and a long post-load settle before recording.\n6. **Wait for auth with a stability loop, never a fixed sleep.** MSAL bounces *back* to `login.microsoftonline.com\u002F…\u002Foauth2\u002Fv2.0\u002Fauthorize` for a silent token refresh **after** you appear to have landed on the app — a one-shot \"am I on the app host?\" check passes, then the very next screenshot is a login URL with 200 chars of body text. Poll every 2 s and require **N consecutive good polls** (host matches AND `document.body.innerText.length` is large AND no `Initializing|Loading your|Getting things ready` spinner). Handle the sign-in UI inside the same loop:\n   ```js\n   async function settle(page, budgetMs = 240000, needStable = 4) {\n     const t0 = Date.now(); let stable = 0;\n     while (Date.now() - t0 \u003C budgetMs) {\n       const u = page.url();\n       if (u.includes('login.microsoftonline.com')) {           \u002F\u002F \"Stay signed in?\"\n         stable = 0;\n         const cb = page.locator('#KmsiCheckboxField').first();\n         if (await cb.isVisible({ timeout: 400 }).catch(() => false)) await cb.check({ timeout: 2000 }).catch(() => {});\n         const yes = page.locator('#idSIButton9').first();       \u002F\u002F the Yes\u002FNext button\n         if (await yes.isVisible({ timeout: 800 }).catch(() => false)) { await yes.click({ timeout: 3000 }); await page.waitForTimeout(4000); continue; }\n         const tile = page.locator('div[role=\"button\"]:has-text(\"user@\"), small:has-text(\"user@\")').first();\n         if (await tile.isVisible({ timeout: 600 }).catch(() => false)) { await tile.click({ timeout: 3000 }); await page.waitForTimeout(4000); continue; }\n         await page.waitForTimeout(2000); continue;\n       }\n       if (u.includes('\u003Capp-host>')) {\n         const len = await page.evaluate(() => document.body.innerText.length).catch(() => 0);\n         const busy = await page.locator('text=\u002FInitializing|Loading your|Getting things ready\u002Fi').count().catch(() => 0);\n         if (len > 400 && !busy) { stable++; if (stable >= needStable) return true; } else stable = 0;\n       } else stable = 0;\n       await page.waitForTimeout(2000);\n     }\n     return false;\n   }\n   ```\n   Use the account **tile** selectors (`div[role=\"button\"]:has-text(\"user@\")`, `small:has-text(\"user@\")`) — `[data-test-id*=\"…\"]` attributes on the account picker are unstable. Check `#KmsiCheckboxField` (\"Don't show this again\") before clicking `#idSIButton9` so the prompt stops recurring on later runs.\n\n### Make the window fill the user's screen (they WILL be watching it)\n\nThe user sees the automated window while it records; a tiny 800×600 window that can't be moved or resized is a real complaint. Three separate traps:\n\n1. **`viewport: {width, height}` locks the window.** Playwright *emulates* a fixed viewport and the window becomes unresizable. Use **`viewport: null`** so the page tracks the real window — then control size via launch args. (`recordVideo` still captures the page buffer, so the recording stays clean.)\n2. **`--start-maximized` and `--window-size` are BOTH silently ignored on a profile that has saved window bounds** — you get a stubborn 800×600. Removing `browser.window_placement` from the clone's `Preferences` is *not* reliable either, and CDP `Browser.setWindowBounds` fails under `launchPersistentContext` (`Protocol error: Browser window not found`). **What actually works: resize the real Win32 window after launch.** Call this right after launch, and again after each navigation (the app can re-trigger a resize):\n   ```powershell\n   # maximize.ps1 - maximize the Edge window belonging to our cloned profile\n   Add-Type @\"\n   using System; using System.Runtime.InteropServices;\n   public class Win {\n     [DllImport(\"user32.dll\")] public static extern bool ShowWindow(IntPtr h, int c);\n     [DllImport(\"user32.dll\")] public static extern bool MoveWindow(IntPtr h, int x, int y, int w, int t, bool r);\n     [DllImport(\"user32.dll\")] public static extern bool IsWindowVisible(IntPtr h);\n   }\n   \"@ -ErrorAction SilentlyContinue\n   Add-Type -AssemblyName System.Windows.Forms\n   $wa = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea\n   Get-CimInstance Win32_Process -Filter \"Name='msedge.exe'\" |\n     Where-Object { $_.CommandLine -like '*edge-tenant*' } | ForEach-Object {\n       $p = Get-Process -Id $_.ProcessId -ErrorAction SilentlyContinue\n       if ($p -and $p.MainWindowHandle -ne 0 -and [Win]::IsWindowVisible($p.MainWindowHandle)) {\n         [Win]::ShowWindow($p.MainWindowHandle, 9) | Out-Null              # SW_RESTORE\n         [Win]::MoveWindow($p.MainWindowHandle, $wa.X, $wa.Y, $wa.Width, $wa.Height, $true) | Out-Null\n         [Win]::ShowWindow($p.MainWindowHandle, 3) | Out-Null              # SW_MAXIMIZE\n       } }\n   ```\n   Filter by command line (`*edge-tenant*`) so you only touch your own window, and invoke it from Node with `child_process.execSync('powershell -NoProfile -ExecutionPolicy Bypass -File maximize.ps1')`.\n3. **Measure the screen correctly — logical ≠ physical.** `[System.Windows.Forms.Screen]` reports **logical** px (already divided by the Windows display scale), while `screen.width` in the browser reports **physical**. A machine reporting `1536×1024` from PowerShell was really `2400×1600` at 156 % scale. Reconcile before computing sizes, or you will undersize the window by the scale factor.\n\n   **Calibrate empirically instead of deriving it.** Launch once at a known `--force-device-scale-factor`, maximize, and read the CSS width; CSS width scales as `1\u002Fdsf`, so:\n   `dsf_target = dsf_measured × (cssWidth_measured \u002F cssWidth_wanted)`\n   Worked example: at `dsf 0.8` the maximized window measured **2376 CSS px** wide. For a 1920-wide capture: `0.8 × 2376\u002F1920 = 0.99`. Relaunching at `--force-device-scale-factor=0.99` produced **1916×1162** — a full-screen window *and* a ~1920 px capture buffer.\n   ```\n   '--force-device-scale-factor=0.99', '--start-maximized',\n   ```\n   Read the real numbers from the browser, not from PowerShell, and log them every run:\n   ```js\n   await page.evaluate(() => ({ w: innerWidth, h: innerHeight, dpr: devicePixelRatio, sw: screen.width, sh: screen.height }))\n   ```\n\n## Workflow\n\n### 1. Record the real flow\n\n> ⛔ **MANDATORY GATE — DO NOT RECORD until the chat input bar is verified fully visible.**\n> The single most common defect is the chat input bar (and the end of the last message) being cut off at the bottom of the frame. You **MUST** pass this gate before every recording run — no exceptions, even if a previous run \"looked fine\":\n> 1. Run **`scripts\u002Fverify.js`** (same launch args\u002Fviewport as the recorder). It opens the target, dismisses popups, handles the account picker, sends ONE test message, and screenshots the **viewport buffer** (which is exactly what gets recorded — NOT the on-screen window).\n> 2. **View that screenshot** and confirm ALL of these are visible in-frame: the header, the latest message bubble's END, the chat **input bar** (\"Ask a question…\"), and the disclaimer line. Do not rely on the on-screen window (it may be cut by the physical screen\u002Ftaskbar); judge only from the screenshot.\n> 3. If ANY of them is cut off → **adjust the viewport height and re-run verify.js. Repeat until the screenshot passes.** Never proceed to the real recording on a failed\u002Funcertain frame.\n> 4. Use the SAME `viewport` and launch `args` in `recorder.js` that passed the gate.\n>\n> Reference that passes for Copilot Studio at 1920 wide: viewport **1920×880**. Treat this as a starting point, still verify per app\u002Fscreen.\n\n- Confirm the target URL and the scenario steps (prompts to type \u002F buttons to click) with the user.\n- **Free the browser profile first.** Only one process can lock the persistent profile. Kill any Edge processes using the profile dir before launching the recorder (see `scripts\u002Fprep.ps1`, which also clears stale `Singleton*` lock files and normalizes the crash-restore flags), otherwise `launchPersistentContext` fails with \"Failed to launch… Opening in existing browser session\".\n- Run `scripts\u002Frecorder.js` (template provided) **only after the visibility gate passes**. It launches the logged-in profile, records page video, drives the chat\u002Fapp with human-like typing, waits for each response to stabilize, holds for readability, then closes to flush the `.webm`.\n- **Submit reliably:** prefer clicking the Send button and confirm the input box empties; pressing Enter alone can concatenate prompts into one bubble. The template's `submit()` handles this.\n- **Copilot Studio's Build\u002FPreview\u002FEvaluate\u002FMonitor switcher is a `menuitemradio` DROPDOWN behind a single \"Build\" button, not tabs.** `getByRole('tab', {name: \u002FPreview\u002Fi})` will silently find nothing and `openTestPane()` will time out waiting for the chat input. Two fixes, prefer the first:\n  1. **Just navigate directly to `${AGENT_URL}\u002Fpreview`** (`page.goto`) — clicking the option once reveals the URL pattern is stable (`\u002Fagents\u002F\u003Cid>\u002Fpreview`), and a direct `goto` is far more reliable than menu-click choreography. This is what `openTestPane()` in the shared `recorder.js`\u002F`verify.js` templates now does.\n  2. If you must click through the UI: click the button whose accessible name is the *current* tab label (e.g. `Build`) to open the menu, then click the `menuitemradio` named `Preview` inside it (`page.getByRole('menuitemradio', {name: \u002F^Preview$\u002Fi})`).\n  - **Diagnose this class of bug fast:** if a locator search for the expected tab\u002Fbutton comes back empty, don't guess — take a `browser_snapshot`\u002Fscreenshot of the live page (via the `playwright-browser_*` MCP tools, reusing the same signed-in tenant profile) to see the actual accessibility tree before patching the recorder script blind.\n\n#### Robust, clean capture (learned the hard way — bake these into the recorder)\n- **Suppress Edge nag UI with launch args** so no banners\u002Fbubbles pollute the frame:\n  `--test-type` (kills the yellow \"unsupported command-line flag\" infobar), `--disable-infobars`, `--hide-crash-restore-bubble`, `--disable-session-crashed-bubble`, `--no-first-run`.\n- **Kill the \"Restore pages?\" bubble at the source:** after force-killing Edge between runs, patch the profile `Default\u002FPreferences` before relaunch — replace `\"exit_type\":\"...\"` → `\"exit_type\":\"Normal\"` and `\"exited_cleanly\":false` → `\"exited_cleanly\":true`.\n- **Auto-handle the Microsoft account picker.** If a sign-in\u002Faccount chooser appears, click the correct account by regex on its email\u002Ftenant (ask the user which account once, then match it) — otherwise the run stalls. See `pickAccountIfNeeded()` in the template.\n- **Dismiss product coach-marks \u002F NPS \u002F cookie popups.** Loop over buttons named `Close`, `Got it`, `Dismiss`, `No thanks`, `Skip`, `Not now`, `Accept`, `OK`, then press `Escape`. Call it after load AND after opening the chat\u002Fpreview pane. See `dismissPopups()`.\n- **Size the viewport so the chat input bar is fully in-frame (see the MANDATORY GATE above).** The recorded frame is the *viewport buffer*, not the on-screen window, so it can exceed the physical screen. For Copilot Studio at 1920-wide, a viewport of **1920×880** shows the header, messages, the \"Ask a question\" input bar, and the disclaimer line. This is NOT optional guesswork — you must prove it with `verify.js` and a screenshot before recording, and re-run until it passes.\n- **Scroll to the END of each response before typing the next turn** so the full answer is on screen, and pin to bottom during holds — this also hides the floating \"Scroll to bottom\" down-arrow. See `scrollBottom()` + `readHold()`.\n- **You can keep using the laptop \u002F switch screens during recording.** `recordVideo` captures the page's internal buffer, not your monitor, so other windows, screen switches, or minimizing never appear. Only avoid closing the automated Edge window or sleeping the machine.\n- **Run detached with synchronous file logging.** Long recordings (slow tool calls, timeouts) can outlive a tool call; launch the recorder detached so it survives, and have it `fs.appendFileSync` a timestamped `progress.log` per step (console output to a redirected file is block-buffered and looks \"stuck\"). Poll `progress.log` + node\u002Fedge process counts to track real progress.\n\n### 2. Post-process with ffmpeg\n- Probe duration & sample frames every few seconds to **map when each scenario happens** and **find where auth ends**:\n  ```powershell\n  ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 in.webm\n  ffmpeg -y -i in.webm -vf \"fps=1\u002F5\" frame_%03d.png    # then view frames\n  ```\n- **Cut auth + crop empty margins**, convert to H.264 MP4 (Clipchamp-friendly). Example: drop first 30s, crop to content:\n  ```powershell\n  ffmpeg -y -ss 30 -i in.webm -vf \"crop=1280:600:0:0,setsar=1\" -c:v libx264 -pix_fmt yuv420p -crf 20 -preset medium out.mp4\n  ```\n- **If `maximize.ps1` couldn't fill the physical screen** (small\u002Flow-res display, or a screen-resolution change since the empirical DSF calibration), the recorded buffer will show the real app content only in a small corner with a large flat-gray dead zone around it — check a sampled frame BEFORE narrating, not after. Fix by cropping to the actual content bounding box (read it off a sample frame — e.g. `crop=782:600:0:0`), then `scale` up to a target size and `pad` onto a white\u002Fblack canvas to reach a standard resolution (16:9 for a normal video deliverable):\n  ```powershell\n  ffmpeg -y -i in.webm -vf \"crop=782:600:0:0,scale=1303:1000,pad=1920:1080:(ow-iw)\u002F2:(oh-ih)\u002F2:color=white\" -c:v libx264 -pix_fmt yuv420p -crf 20 out.mp4\n  ```\n  Do this crop\u002Fscale\u002Fpad **per trimmed segment inside the same `trim`+`concat` filter_complex** (not as a separate pass) so segments stay in sync — see the worked multi-segment example below.\n- **Hit a target length (e.g. ≤ 2 min) by cutting DEAD TIME, not content.** Agent demos have long dead gaps during slow tool calls \u002F timeouts. Don't speed-ramp the whole clip — instead pick keep-windows around each meaningful beat (question asked, skill loads, result appears) and drop the multi-second waits between them. Extract each window with `-ss \u003Cstart> -t \u003Cdur>`, normalize them all to the same size\u002Ffps (`scale=...,pad=1920:1080,setsar=1,fps=30`), then concat. Use the recorder's `progress.log` timestamps to locate beats fast.\n  - concat gotcha: `ffmpeg -f concat` resolves `file '...'` paths **relative to the concat file's own directory** — list bare filenames and run from that dir, or use absolute paths.\n- Note the trimmed clip's exact duration and each segment's start offset — you'll align narration to these.\n\n### 2b. Narration without the Clipchamp UI (headless, reliable fallback)\n### 2b. PATH (A) — Narration without the Clipchamp UI (headless, fast, recommended default)\nWhen the browser\u002FClipchamp UI isn't available (or you just want a deterministic build), generate the voiceover with **edge-tts** (same Ava neural voice) and mux with ffmpeg:\n- Write one text block per beat with `(start_time, max_dur, text)`. Generate each: `python -m edge_tts --voice en-US-AvaNeural --text \"...\" --write-media bNN.mp3` (in code, bump `rate` `+8%`\u002F`+15%`\u002F… until a block fits its `max_dur`).\n- Build a single voice track by delaying each block to its start: `[i:a]adelay=START_ms|START_ms,apad[ai]` then `amix=inputs=N:normalize=0` and `atrim=0:TOTAL`.\n- Mux onto the trimmed video: `ffmpeg -i cut.mp4 -i voice.m4a -c:v copy -c:a aac -map 0:v:0 -map 1:a:0 -shortest final.mp4`.\n- Name the output after the agent\u002Fproduct and copy it into the user's workspace\u002FOneDrive folder so it renders inline and syncs.\n\n### 3. PATH (B) — Assemble in Clipchamp (browser automation; editable project)\nOnly if the user chose (B) in STEP 0 AND the `playwright-browser_*` tools are available.\nRead **[clipchamp-ui.md](clipchamp-ui.md)** for the exact click targets, selectors, and gotchas. High level:\n1. Open https:\u002F\u002Fapp.clipchamp.com → open (or recover) the project, or create a new one.\n2. **Import media** → file chooser → `browser_file_upload` the trimmed MP4 (+ any intro screenshot). If a permission hook blocks uploads, ask the user to enable **Settings → Permissions → Allow agent file uploads**.\n3. Add media to the timeline via each item's \"Add to timeline\" button (drops at the playhead).\n4. Optionally add an intro still (e.g. a \"Build\"\u002Farchitecture screenshot) so the video opens on context.\n5. **Voiceover:** open **Record & create → Text to speech** (or select an existing TTS clip's **Text to speech** tab), pick **Ava Multilingual**, type the block text, **Save** to render. New TTS blocks drop **at the playhead** — set the playhead precisely first via the time input spinbuttons (Minutes\u002FSeconds), then create the block.\n6. Cover any black tail after the video ends with a closing still so the final voiceover has a visual.\n\n### 4. Export\n- **Export → set the File name to the agent\u002Fproduct** (e.g. `Contoso IT Assistant - Demo`), 1080p, keep \"Store in the cloud\" → **Export**. If prompted about an existing name, choose **Keep both** or **Replace** as the user prefers.\n- Output lands in OneDrive: `Videos\u002FClipchamp\u002F\u003Cproject>\u002FExports\u002F\u003Cname>.mp4`. It may not sync to the local folder immediately; the export screen's **Copy link** \u002F **Save to your computer** are the fallbacks.\n\n## Narration authoring tips\n- One block per on-screen beat; ~1–3 short sentences each. Keep total pacing tight — aim for little dead air.\n- Add **technical call-outs** that describe the mechanics the viewer is seeing: which skill\u002Ftool fired, what data source it hit (\"queries the knowledge table\", \"calls the create-record tool\", \"reads the live schedule\").\n- Keep it **generic** if asked — strip customer\u002Fcompany names.\n- End on a **hero voiceover line** (no card needed) that names the value prop and the tech, e.g. \"That's the power of AI, combining X and Y to turn Z into one seamless, autonomous experience.\"\n- Remove leftover title-text overlays if the intro screenshot already shows the name.\n\n## Common pitfalls (all learned the hard way)\n- **`Timeout 180000ms exceeded` on `launchPersistentContext` + `DevTools remote debugging requires a non-default data directory`** → you pointed Playwright at Edge's **default** `User Data` root. Edge blocks remote debugging there. **Clone the profile to a separate root** (see \"Pick the right profile \u002F tenant\" step 3) and launch against the clone.\n- **App renders a totally blank page, `document.body.innerText.length === 0`, `outerHTML` is ~1 MB, and the network log shows `404` on `api.bap.microsoft.com\u002F...\u002Fenvironments\u002F\u003Cid>`** → the app shell loaded but **MSAL silently issued a token for the wrong tenant**. Confirm by reading the MSAL cache instead of guessing:\n  ```js\n  await page.evaluate(() => { const o = [];\n    for (let i = 0; i \u003C localStorage.length; i++) { const k = localStorage.key(i);\n      try { const j = JSON.parse(localStorage.getItem(k)); if (j && (j.username || j.tenantId)) o.push({ u: j.username, t: j.tenantId }); } catch {} }\n    return o; });\n  ```\n  A `tenantId` of `72f988bf-86f1-41af-91ab-2d7cd011db47` is the **Microsoft corp** tenant — i.e. it authenticated as the primary work account, not the dev\u002Fdemo tenant. Clearing `localStorage`\u002F`sessionStorage` and the app's cookies does **not** fix it: the `login.microsoftonline.com` session cookie still defaults to the primary account, so it silently re-SSOs to the same wrong identity.\n  **Fix:** force the picker by navigating to the tenant-scoped authorize endpoint with `prompt=select_account` + `login_hint` before loading the app:\n  ```\n  https:\u002F\u002Flogin.microsoftonline.com\u002F\u003Ctenant>.onmicrosoft.com\u002Foauth2\u002Fv2.0\u002Fauthorize\n    ?client_id=\u003Capp spa client id>&response_type=code\n    &redirect_uri=\u003Curl-encoded app redirect>&response_mode=fragment\n    &scope=openid%20profile%20offline_access\n    &prompt=select_account&login_hint=\u003Cuser@tenant>\n  ```\n  Read the SPA `client_id` straight out of the MSAL localStorage key (`msal.\u003Cclient-id>.active-account-filters`). Then click the tenant tile — `div[data-test-id=\"\u003Cfull upn>\"]` is the reliable selector on the \"Pick an account\" screen.\n- **Never test \"did we land on the app?\" with `url.includes('\u003Capp-host>')`** → the authorize URL embeds the app host inside its own `redirect_uri` query param, so `includes()` returns true while you are still sitting on the login page, and your picker loop exits before clicking anything. Compare `new URL(u).host` instead. Apply this to `settle()` too.\n- **Sign-in loop spins forever re-clicking the account tile and the host becomes `login.microsoft.com\u002F\u003Ctenant>\u002Fbridge\u002Ffido`** → the account requires **passkey \u002F Windows Hello**, which cannot be automated. The cookie-based SSO you cloned has expired.\n  **Do not burn time scripting around this.** An automated `prompt=select_account` flow will *still* silently resolve back to the Windows-connected primary account (`Connected to Windows` on the picker always wins), so the MSAL cache stays on the wrong tenant no matter how many times you clear storage.\n  **The reliable fix — always prefer it:** ask the user to open **their own normal Edge** on the target URL and confirm it loads. Then **re-clone the profile** (the clone captures the freshly-minted cookies) and every subsequent automated run works. This takes one message and ~2 minutes; driving an in-Playwright interactive sign-in is slower, and the user may not notice the window at all — an unattended \"waiting for user\" poller will just time out.\n  Note `login.microsoft.com` and `login.microsoftonline.com` are **different hosts** — match both when detecting auth pages. Treat the clone as a **cache of the user's live session**: whenever auth looks wrong, re-run the clone step before debugging anything else.\n- **Do NOT round-trip Chromium `Preferences` through `ConvertFrom-Json`\u002F`ConvertTo-Json`** → it reorders keys and retypes values, and can leave the profile in a broken state. Patch it with **targeted regex string replacement** only:\n  ```powershell\n  $c = Get-Content $pf -Raw -Encoding UTF8\n  $c = $c -replace '\"exit_type\":\"[^\"]*\"','\"exit_type\":\"Normal\"' -replace '\"exited_cleanly\":false','\"exited_cleanly\":true'\n  [System.IO.File]::WriteAllText($pf, $c, (New-Object System.Text.UTF8Encoding($false)))\n  ```\n- **User asks for \"your actions and the chat at the same time\" and you already recorded page-only** → `recordVideo` captured only the page buffer; the assistant conversation was never in those pixels and cannot be added retroactively. You must re-record with **§1b dual capture** (gdigrab + side-by-side windows). Ask which capture mode they want *before* recording.\n- **gdigrab MP4 won't play \u002F has no duration** → ffmpeg was killed instead of being asked to stop, so the moov atom was never written. Send `q` to stdin to stop it; recover an existing file with `ffmpeg -i broken.mp4 -c copy fixed.mp4`.\n- **`hstack` fails with \"Input link parameters do not match\"** → the two sources have different heights or SAR. Scale both to the same height and `setsar=1` before stacking.\n- **Chat input bar cut off after switching to side-by-side windows** → halving the window width changes the CSS viewport, so the previously-calibrated device-scale factor no longer holds. Re-calibrate and re-run the visibility gate at the new geometry.\n- **Copilot Studio agent tabs are `Build | Preview | Evaluate | Monitor`** → the test chat is under **Preview** (not \"Test\"). The agent's overview page under **Build** already lists Instructions, Model, **Skills**, **Tools**, and **Knowledge** in one scrollable column — one slow scroll covers the whole \"what this agent is\" section, no tab-hopping needed.\n- **A tool call in the demo returns a red error card (e.g. `Request validation · 400`)** → the backing system may be asleep (some dev\u002Fsandbox backends hibernate) or the action is genuinely misconfigured. **Check this during the visibility gate, not after recording** — send one real test prompt and read the answer. If the backend is down, tell the user and get it woken up before the real run; a demo video full of error cards is worthless.\n- **The agent you want isn't in the agents list** → the list only shows agents in *that* environment, and a broken-tenant session renders an empty body so **every** agent looks missing. Confirm auth first, then try the direct `\u002Fagents\u002F\u003Cid>` URL — an agent can load fine by id even when you can't spot it in the list. Harvest names\u002Fids by scraping `[role=\"row\"]` from the grid.\n- **Window is tiny (800×600), can't be moved or resized, user complains they can't see it** → three causes, check all: (a) `viewport: {w,h}` locks the window — use `viewport: null`; (b) saved `browser.window_placement` in the cloned `Preferences` overrides `--window-size` **and** `--start-maximized` — delete that key before launch; (c) you sized from `[System.Windows.Forms.Screen]` (logical px) instead of `screen.width` (physical px) and undersized by the display-scale factor. Always log `innerWidth\u002FinnerHeight\u002FdevicePixelRatio` from inside the page to confirm.\n- **`Browser.getWindowForTarget` → `Protocol error: Browser window not found`** → CDP window control is unreliable under `launchPersistentContext`; fix the profile `Preferences` + launch args instead.\n- **App \"loads\", then the next screenshot is a login page with ~200 chars of text** → MSAL redirected back to `\u002Foauth2\u002Fv2.0\u002Fauthorize` for a silent token refresh after your one-shot check passed. Use the **N-consecutive-stable-polls `settle()` loop**, never a fixed `waitForTimeout`.\n- **Wrong tenant \u002F blank page \u002F 404 on the environment** → the MCP browser is signed in to a different account than the one that owns the agent. Don't sign in interactively; enumerate the real Edge profiles, find the one whose `account_info[0].email` matches the tenant, clone it, and relaunch with `--profile-directory=\u003Cdir>` against the clone root (see \"Pick the right profile \u002F tenant\").\n- **`--profile-directory` picked the wrong profile** → the profile's *display name* and its *folder name* differ (folder `Profile 1` can be shown as \"Profile 2\"). Always use the folder name from `Get-ChildItem`.\n- **Profile lock** → recorder can't launch. Kill stale Edge procs on the profile dir first, and remove any `Singleton*` lock files under the profile dir. Filter by command line so you don't also kill the MCP browser.\n- **Recorder looks \"stuck\" at an early turn** → it's usually block-buffered redirected console output, not a hang. Use synchronous `fs.appendFileSync` logging to `progress.log` and check node\u002Fedge process counts to see real progress.\n- **Yellow \"unsupported command-line flag\" banner in-frame** → add `--test-type` (+ `--disable-infobars`).\n- **\"Restore pages?\" bubble in-frame** → patch the profile `Preferences` (`exit_type`→`Normal`, `exited_cleanly`→`true`) before relaunch, and pass `--hide-crash-restore-bubble --disable-session-crashed-bubble`.\n- **Account picker \u002F sign-in stalls the run** → auto-click the right account by email\u002Ftenant regex (`pickAccountIfNeeded`).\n- **Chat input bar half-cut \u002F off-screen** → the recorded frame is the viewport buffer, not the physical window; size the viewport tall enough (e.g. 1920×880 for Copilot Studio) and verify with a pre-record viewport screenshot.\n- **Video too long** → cut dead tool-call\u002Ftimeout gaps into per-beat keep-windows and concat; don't speed-ramp the whole thing.\n- **Browser session drops mid-session** → the MCP browser can be relaunched with `browser_navigate`; the Clipchamp project autosaves to the cloud, so reopen it from the home page. If the browser tools are removed entirely mid-task, fall back to the **§2b headless edge-tts + ffmpeg** narration path — same narrated-MP4 deliverable.\n- **Enter-to-send concatenation** → use the Send button + confirm the box cleared.\n- **TTS block lands in the wrong place** (Clipchamp UI) → it drops at the playhead; set the time input first. Delete a misplaced block and recreate rather than dragging.\n- **Black tail** at the end when the last voiceover outlasts the footage → extend the last clip or add a closing still.\n- **ffmpeg not on PATH** → install once to `C:\\Users\\\u003Cuser>\\.copilot\\bin` and add it to the User PATH (persists across chats); in the current shell refresh `$env:Path` from Machine+User.\n",{"name":5,"description":7},"main","3678d23ad8fb8d098c428302a98044ac9514d5a1","submissions\u002Fclipchamp-video",120,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills","https:\u002F\u002Fgithub.com\u002Fmicrosoft","https:\u002F\u002Fopensource.microsoft.com","Open source projects and samples from Microsoft","2026-11-13 01:39:10",{"source":8,"skillName":5,"commands":45,"prompt":55,"zipAvailable":56},[46,49,52],{"tool":47,"command":48},"npx","npx skills add microsoft\u002Fcat-agent-skills --skill clipchamp-video",{"tool":50,"command":51},"bunx","bunx skills add microsoft\u002Fcat-agent-skills --skill clipchamp-video",{"tool":53,"command":54},"pnpm","pnpm dlx skills add microsoft\u002Fcat-agent-skills --skill clipchamp-video","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fclipchamp-video\u002FSKILL.md",true,[58,64,70,74,78,84,88,92,96,100,104,108,112,116,120],{"path":59,"resourceType":60,"resourceTypeDesc":61,"sizeBytes":62,"sourceHtmlUrl":63},"agents\u002Fopenai.yaml","REFERENCE","参考文档",198,"https:\u002F\u002Fgithub.com\u002Fgoogle\u002Felephant-goldfish-model\u002Fblob\u002Fmain\u002Fdistributions\u002Fclaude\u002Fskills\u002Fegm-review\u002Fagents\u002Fopenai.yaml",{"path":65,"resourceType":66,"resourceTypeDesc":67,"sizeBytes":68,"sourceHtmlUrl":69},"assets\u002Ficon.svg","ASSET","资产文件",530,"https:\u002F\u002Fgithub.com\u002Fgoogle\u002Felephant-goldfish-model\u002Fblob\u002Fmain\u002Fdistributions\u002Fclaude\u002Fskills\u002Fegm-review\u002Fassets\u002Ficon.svg",{"path":71,"resourceType":60,"resourceTypeDesc":61,"sizeBytes":72,"sourceHtmlUrl":73},"metadata.json",603,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fclipchamp-video\u002Fmetadata.json",{"path":75,"resourceType":60,"resourceTypeDesc":61,"sizeBytes":76,"sourceHtmlUrl":77},"README.md",5218,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fclipchamp-video\u002FREADME.md",{"path":79,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":82,"sourceHtmlUrl":83},"scripts\u002Farrange.ps1","SCRIPT","脚本",2194,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fclipchamp-video\u002Fscripts\u002Farrange.ps1",{"path":85,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":86,"sourceHtmlUrl":87},"scripts\u002Fclone.ps1",1697,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fclipchamp-video\u002Fscripts\u002Fclone.ps1",{"path":89,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":90,"sourceHtmlUrl":91},"scripts\u002Fcommon.js",5625,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fclipchamp-video\u002Fscripts\u002Fcommon.js",{"path":93,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":94,"sourceHtmlUrl":95},"scripts\u002Fdiag.js",1525,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fclipchamp-video\u002Fscripts\u002Fdiag.js",{"path":97,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":98,"sourceHtmlUrl":99},"scripts\u002Fdual-capture.js",2894,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fclipchamp-video\u002Fscripts\u002Fdual-capture.js",{"path":101,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":102,"sourceHtmlUrl":103},"scripts\u002Ffindagent.js",2418,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fclipchamp-video\u002Fscripts\u002Ffindagent.js",{"path":105,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":106,"sourceHtmlUrl":107},"scripts\u002Fmaximize.ps1",1422,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fclipchamp-video\u002Fscripts\u002Fmaximize.ps1",{"path":109,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":110,"sourceHtmlUrl":111},"scripts\u002Fprep.ps1",1359,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fclipchamp-video\u002Fscripts\u002Fprep.ps1",{"path":113,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":114,"sourceHtmlUrl":115},"scripts\u002Frecorder.js",5920,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fclipchamp-video\u002Fscripts\u002Frecorder.js",{"path":117,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":118,"sourceHtmlUrl":119},"scripts\u002Frequire-playwright.js",958,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fclipchamp-video\u002Fscripts\u002Frequire-playwright.js",{"path":121,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":122,"sourceHtmlUrl":123},"scripts\u002Fverify.js",3625,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fclipchamp-video\u002Fscripts\u002Fverify.js",[125,152,177,213,231,247],{"skillNo":126,"name":127,"nameZh":6,"description":128,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":129,"categories":130,"keywords":131,"publishedAt":30},"01M6X3P09R8SJH3QJGMTAA5N90","stakeholder-and-decision-map","Mines customer interactions to identify stakeholders, infer their likely roles and engagement, map the decision process, and highlight who or what may be missing. Use when asked to \"map a deal's buying group\", identify influential or missing stakeholders, \"assess who has gone quiet\", or \"explain how the customer will decide\". Do not use for phased engagement plans, contact sequences, choosing or drafting outreach to the single next contact, seller or team performance reviews, or contacting, scheduling, or changing records.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fstakeholder-and-decision-map\u002FSKILL.md",[],[132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151],"decision","map","decision map","stakeholder and decision map","performance","customer","team","how","decide","do","next","infer","missing","records","assess","reviews","for","single","when","sequences",{"skillNo":153,"name":154,"nameZh":6,"description":155,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":156,"categories":157,"keywords":158,"publishedAt":30},"01M6X3NRES2XSZQCH015WD4JN6","sales-progress-update","Creates or refreshes a sales progress update for a manager or team about work done, deal movement, pending outcomes, supported forecast position, and help needed. Use when asked to \"create my weekly sales update\", \"refresh my sales progress deck\", \"report my forecast position\", \"where do I stand against my quarter forecast\", or \"catch my manager up on committed deals\". Do NOT use for non-sales project updates, a since-date deal change digest, momentum or blocker analysis, meeting prep, or outreach drafting.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Fsales-progress-update\u002FSKILL.md",[],[159,160,161,162,163,164,165,166,138,167,168,169,170,171,172,173,174,175,176,150],"update","sales","progress","sales progress update","change","create","meeting prep","analysis","work","up","done","digest","project","my","updates","manager","weekly","catch",{"skillNo":178,"name":179,"nameZh":6,"description":180,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":181,"categories":182,"keywords":194,"publishedAt":30},"01M6X3MSDTB1E5H6DQAFAY41YP","submission-assemble","Put the checked CSR or 510(k) sections together in template order with a reviewer-facing open-items register, as a draft package for human sign-off. Use when the user asks \"assemble the CSR draft\", \"put the module together for review\", \"build the review package with the open items on top\", \"compile the 510(k) sections in order\" or \"package this up for the submission lead\". Do NOT use for assembling an unchecked draft - use consistency-check instead. Do NOT use for loading protocol, SAP or TLF sources - use source-ingest instead. Do NOT use for compiling general documents or slide decks - use the built-in docx or pptx skill instead. Do NOT use for filing or submitting to a regulator - use the sponsor's validated gateway, operated by a human, instead. Guardrail: draft for human approval only (L1); status is draft_clean or draft_open_items, never submitted.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Freg-submission-authoring\u002Fsubmission-assemble\u002FSKILL.md",[183,186,189],{"categoryNo":184,"categoryName":185,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000002","文档处理",{"categoryNo":187,"categoryName":188,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000003","演示文稿",{"categoryNo":190,"categoryName":191,"taxonomyType":192,"taxonomyTypeDesc":193},"01JBSEEDCAT000000000000034","行政与办公人员","PROFESSION","适用职业",[195,196,197,198,199,200,201,202,203,204,205,206,168,207,208,209,210,211,212,150],"assemble","submission","submission assemble","docx","pptx","writing","documents","healthcare","open","reviewer","never","only","clean","guardrail","slide decks","sources","filing","loading",{"skillNo":214,"name":215,"nameZh":6,"description":216,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":217,"categories":218,"keywords":219,"publishedAt":30},"01M6X3MM9TVWDQ716QGNV4DS25","source-ingest","Pull every number, date and population definition out of a study's protocol, SAP, TLF tables, investigator brochure and prior approved text into one cited fact map, keyed per config\u002Ffield-dictionary.json, before any CSR or 510(k) section is written. Use when the user asks \"here's the locked TLF set and SAP, set up the CSR\", \"load the protocol and tables before we start drafting\", \"what figures do we have from the database lock?\", \"pull the disposition counts out of Table 14.1.1\" or \"ingest the IB for the 510(k)\". Do NOT use for drafting or assembling submission sections - use section-draft or submission-assemble instead. Do NOT use for reconciling a patient's medications - use the discharge-care-transition plugin instead. Guardrail: reads and cites sources only; never estimates, rounds or invents a fact.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Freg-submission-authoring\u002Fsource-ingest\u002FSKILL.md",[],[220,221,222,223,224,166,202,225,205,206,168,133,226,208,210,227,228,229,230,150],"source ingest","ingest","source","database","SET","text","patient","number","lock","definition","transition",{"skillNo":232,"name":233,"nameZh":6,"description":234,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":235,"categories":236,"keywords":238,"publishedAt":30},"01M6X3M7ZH5ZNEHP56BQ74HAJR","section-plan","Work out which sections a CSR or 510(k) requires, from the templates in config, check each one against the cited fact map, and name what is missing before a word is drafted. Use when the user asks \"what do we still need before we can draft the CSR?\", \"which sections are ready to start?\", \"what's blocking the performance data section?\", \"do we have everything for the synopsis?\" or \"map our tables to the E3 structure\". Do NOT use for writing section language - use section-draft instead. Do NOT use for cross-checking a finished draft - use consistency-check instead. Do NOT use for outlining general Word documents or business reports - use the built-in docx skill instead. Only structures with a template in config are supported. Guardrail: plans only; never marks a section ready on a missing fact and never invents a structure.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Freg-submission-authoring\u002Fsection-plan\u002FSKILL.md",[237],{"categoryNo":184,"categoryName":185,"taxonomyType":21,"taxonomyTypeDesc":22},[239,240,241,198,200,136,201,166,202,167,205,206,242,243,244,133,208,245,246,150],"plan","section","section plan","ready","reports","structure","word documents","which",{"skillNo":248,"name":249,"nameZh":6,"description":250,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":251,"categories":252,"keywords":253,"publishedAt":30},"01M6X3M0V1MJNCR653ES9XGVMS","section-draft","Write or revise the language for one planned CSR or 510(k) section - synopsis, efficacy, safety, discussion, device description - using only figures already in the cited fact map, each with its TLF or SAP citation. Use when the user asks \"draft the Synopsis from our locked tables\", \"write up safety from Table 14.3.1\", \"give me a first pass at the discussion\", \"draft the device description for the 510(k)\" or \"tighten the wording of the Discussion section\". Do NOT use for supplying a number not already in source_facts - use source-ingest instead. Do NOT use for verifying a draft's figures - use consistency-check instead. Do NOT use for appeal letters, clinical notes or discharge summaries - use the matching HLS plugin instead. Guardrail: drafts for human approval only (L1); never asserts an unsourced number or harmonises two figures.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fcat-agent-skills\u002Fblob\u002Fmain\u002Fsubmissions\u002Freg-submission-authoring\u002Fsection-draft\u002FSKILL.md",[],[254,240,255,200,256,202,257,205,206,168,258,133,259,260,261,208,262,227,263,150],"draft","section draft","notes","write","me","human","life","clinical","drafts","at"]