SKILL.md
2,812 tokens · o200k_base · 12,059 bytes
Source excerpt starting at line 1.---name: product-update-newsletterdescription: Draft, update, or audit a crisp, changelog-grounded Anarlog product-update newsletter in Loops (app.loops.so) for a desktop release. Use after the changelog is merged, when asked to draft, revise, or pre-send check the release announcement email.metadata: internal: true--- # Product Update Newsletter Announce a desktop release to users through the "Anarlog update (...)" campaign in Loops. Run this after `../new-changelog/SKILL.md` produces the entry and the version is released via`../release-new-version/SKILL.md`. The published changelog is the copy source, not the raw commit log. ## Nightly announcement The newsletter follows stable releases. Do not send a newsletter for eachNightly. In the next stable update, include the opt-in Nightly announcementfrom [nightly-announcement.md](nightly-announcement.md), alongside that stablerelease's changelog. Existing users remain on stable automatically. Before describing Nightly as available, verify its published installers, updatefeed, and `https://anarlog.so/download/nightly/`. Use the released stablechangelog as the main source; Nightly-only changes are not shipped stablefeatures. Nightly release notes live in the app and GitHub prereleases, not thewebsite changelog. Prepare copy locally before publication is ready; create orupdate the Loops campaign once the stable release and linked Nightly are live. ## Source of Truth Read `https://anarlog.so/changelog/<version>`, which renders `packages/changelog/content/<version>.md`. The changelog is frequently edited after first publish. When told it changed, re-fetch and diffagainst the current email body, then apply only the delta. Do not reapply the whole rewrite. Map items to the existing structure rather than appending a dump: - Feature-level items become or extend an H3 section.- Minor UX items become bullets under "Small things that add up".- Reliability and privacy items append to the fixes paragraph after the bullets. Keep beta or partially shipped work honestly qualified. If execution is not connected yet, say so. ## Copy Style Contract Write a product email, not an essay. Keep it crisp, concrete, and easy to scan. - Open with what shipped and when. Prefer `We shipped four releases since last week. Anarlog 1.4.8 is here.` over scene-setting such as `If you've been putting off updating, this is the one to take` or `It folds in four releases`.- If encouraging an update, use explicit loss framing tied to named features: `If you aren't updating, you're missing out on Automations that run after every meeting, editable transcripts, meeting imports, and near-instant sync.` Do not use vague hype, guilt, or blame.- Lead with outcomes and specific numbers: `30 assistants`, `eight connect directly`, `about a second`. Cut adjectives when a fact can do the work.- Use short, active H3 headings such as `Automations do the work now`, `Transcripts you can fix`, and `Sync you stop thinking about`.- Keep most paragraphs to one to three sentences. Split long chains, remove repeated setup, and do not restate the same framing in both the intro and fixes paragraph.- Use contractions and direct verbs. `Last week ... This week ...` is a useful cadence when it connects one update to the next. Avoid cutesy transitions such as `Now they earn it` and vague phrases such as `Accuracy got quieter improvements`.- Keep subjects compact and noun-led: `Anarlog 1.4.8: automations, meeting imports, instant sync`. Preview text should front-load concrete outcomes and stay near 90 to 140 characters.- Self-critical language belongs only when Anarlog or the company was actually at fault. Never add an apology merely to sound personal.- Read the final copy aloud and cut anything that can disappear without losing a fact, instruction, or point of view. Changelog lists are allowed; do not force prose just to avoid a list. ## Campaign Setup Duplicate the most recent "Anarlog update (...)" campaign from the Loops Home page so sender,reply-to, section headings, Download button, and footer are inherited, then rename to the new date. Campaign metrics pages show only send/open/click stats. Use the compose view to edit content. ## Body Structure Contract Every section is exactly this block sequence, and edits must preserve it: ```textH3 headingP (empty spacer)DIV.editor-imageP (empty spacer)P (body text)``` Verify before finishing: ```jsawait page.evaluate(() => { const blocks = [...document.querySelector(".ContentEditable__root").children]; return blocks .filter((b) => b.tagName === "H3") .map((h) => { const i = blocks.indexOf(h); return ( h.innerText + " -> " + blocks .slice(i + 1, i + 4) .map((x) => x.className.includes("editor-image") ? "IMG" : x.innerText.trim() === "" ? "SP" : "TXT", ) .join(",") ); });});``` ## Editing the Lexical Body The body is `div.ContentEditable__root`. The page has several `[contenteditable="true"]` elements(Sender, From, Reply, Subject, Preview, body). Match on distinctive existing text, never a cachedindex or ref. Before any multi-block pass, rescan the full block map and resolve targets again. Theuser may edit concurrently, so a previously correct fixed index can shift and corrupt headings,bullets, or the greeting. `###` creates an H3 and `-` creates a bullet. Reliable mid-document replacement is Range-select then type: ```jsasync function selectTextInBody(searchStr) { return await page.evaluate((str) => { const root = document.querySelector(".ContentEditable__root"); const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); let node; while ((node = walker.nextNode())) { const idx = node.textContent.indexOf(str); if (idx !== -1) { root.focus(); const range = document.createRange(); range.setStart(node, idx); range.setEnd(node, idx + str.length); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); node.parentElement.scrollIntoView({ block: "center" }); return true; } } return false; }, searchStr);}``` Repeat per occurrence for multi-hit edits such as version bumps. ### Failure Modes These corrupt the document silently. All four were observed in practice. - Range selection on the **first block** reverses typed characters, so "Hi," becomes ",iH". Lexical resets the caret to offset 0 per keystroke there. Use a native mouse triple-click, then type.- `Meta+ArrowUp` does not reliably reach document start. It has appended to the end of the current block instead. Triple-click the target block.- Coordinates read in the same `evaluate()` as `scrollIntoView` are stale, so clicks land in the wrong block and keystrokes corrupt unrelated sections. Scroll, `sleep(600)`, then re-read the rect in a second `evaluate()`.- Pressing Enter with an active Range selection deletes the selected text. Press `End` or `Home` first. Verify caret placement before typing: ```jsconst ok = await page.evaluate(() => { const sel = window.getSelection(); const blocks = [...document.querySelector(".ContentEditable__root").children]; const target = blocks[TARGET_INDEX]; const anchorBlock = sel.anchorNode ? (sel.anchorNode.nodeType === 1 ? sel.anchorNode : sel.anchorNode.parentElement ).closest(".ContentEditable__root > *") : null; return anchorBlock === target;});``` If false, click the horizontal middle of the block's text rather than its left edge. After any structural repair, scan all blocks for stray empty paragraphs, duplicated greetings, andleftover `/` or `/image` text. ## Inserting an Image The slash menu triggers a native file chooser. Arm the listener before opening the menu. ```jsconst fcPromise = page .waitForEvent("filechooser", { timeout: 15000 }) .catch(() => null);await page.keyboard.type("/", { delay: 200 });await sleep(700);await page.keyboard.type("image", { delay: 120 });await sleep(700);const btn = await page.evaluate(() => { const menu = [...document.querySelectorAll("div.fixed")].find((el) => el.className.toString().includes("z-[100]"), ); const img = menu && [...menu.querySelectorAll("button")].find( (b) => b.textContent.trim() === "Image", ); const r = img.getBoundingClientRect(); return { cx: r.x + r.width / 2, cy: r.y + r.height / 2 };});await page.mouse.click(btn.cx, btn.cy);const fc = await fcPromise;await fc.setFiles("/abs/path/to/image.png");``` - The caret must sit in an empty paragraph, and `/` needs `delay >= 150` or a literal `/` is typed.- The menu is a scrollable `div.fixed.z-[100]`. Unfiltered, the Image item under MEDIA sits below the visible scroll area, so clicking its reported rect hits the page behind and silently closes the menu. Typing `image` filters the list and brings it into view.- `Enter` and `ArrowDown`+`Enter` do not activate the item. Only a mouse click works.- Poll until `img.src` contains `images.vialoops.com` to confirm the upload finished.- Insertion consumes the empty paragraph, so re-add the trailing spacer. Replace an image by clicking it to select the block, pressing Backspace, then re-running the flow.Capture existing `images.vialoops.com` URLs before editing so a deleted image can be re-downloadedand restored. ## Section Art Generate with Midjourney using the established house style: ```textdithered. [short concept]. bright white sunshine, cheerful, high key. --ar 4:3 --profile aofpoq2``` - Target bright, high-key, airy, light-background results. Reject dim or dystopian output.- Prefer scene compositions, an object in a sunlit room, over flat grids or UI mockups. Grids do not match the existing set.- The Personalize "P" toggle may fail to activate, leaving `data-active="false"`. Appending `--profile aofpoq2` to the prompt text works and produces the profile chip.- Poll for completion by watching for `\d+% Complete` to disappear from `document.body.innerText`. `cdn.midjourney.com` returns a Cloudflare challenge to Node-side `fetch()`. Download full resolutioninside the authenticated tab, where `jobId` comes from the grid thumbnail`cdn.midjourney.com/<jobId>/0_<n>_640_N.webp`: ```jsconst b64 = await mjPage.evaluate(async (u) => { const r = await fetch(u); const blob = await r.blob(); return await new Promise((res) => { const fr = new FileReader(); fr.onload = () => res(fr.result.split(",")[1]); fr.readAsDataURL(blob); });}, `https://cdn.midjourney.com/${jobId}/0_${variant}.png`);``` ## Pre-Send Audit Run before sending and report pass or fail per item. Automated: - Version string consistent across subject and body, with no prior version anywhere.- Every changelog item is represented in the copy, including contributor thanks when a change came from someone outside the Fastrepl org.- All H3 sections match the `SP,IMG,SP` pattern.- All images report `complete && naturalWidth > 0`.- No stray artifacts: `/image`, orphan `/`, double spaces, duplicated greeting.- Sign-off block intact. Manual judgment: - Sender, From, and Reply fields correct. Flag any reply-to change, since the body promises a personal reply and any call for testers routes to the same address.- Preview text length. Clients truncate near 90 to 140 characters, so front-load the important content.- The Download button URL lives in editor state, not the DOM. Click the button block and read the Link textbox in the right sidebar.- State explicitly that only the Compose step was audited unless Audience, Schedule, and Goals were also checked. ## Reporting Restate what changed in plain language, quote the new copy, and attach a screenshot of editedsections. Disclose any self-inflicted document damage and its repair rather than hiding it.
Discovery context
Discovered by repository scan. No exact path reference found in the snapshot’s root AGENTS.md.