{"slug": "how-i-triage-release-notes-before-claude-code-turns-them-into-a-product-video", "title": "How I Triage Release Notes Before Claude Code Turns Them Into a Product Video", "summary": "A developer wrote a small standard-library Python script that triages a Keep a Changelog-formatted CHANGELOG.md before an AI agent turns it into a product video, sorting each entry into keep, cut, or human-review verdicts. The script flags internal work like dependency bumps for cutting, vague or jargon-heavy entries for rewriting, and security items for human review, aiming to catch coverage and overconfidence errors in a table rather than in a finished render. The developer notes the sorting itself is the boring part, and the rows the script refuses to decide are the interesting ones.", "body_md": "Ask an agent to make a video from `CHANGELOG.md` and it will often treat every line as content, including the dependency bump. Nothing looks wrong until the render, where the React upgrade has six seconds of screen time.\n\nI would rather argue about a cut list than about a finished video. So I wrote a small script that sorts a changelog into keep, cut and \"a human should look\" before the agent sees it. The sorting turned out to be the boring part. The rows the script refuses to decide are the interesting ones.\n\nA changelog is written for the team that shipped the release. It records what happened, in the order a maintainer thinks about it, and nothing in the file says which entries a customer cares about.\n\nThat leaves an agent two ways to go wrong. The first is coverage: it can treat every line as content. The second is confidence: when a line is thin, a model tends to fill the gap with a stronger headline than the line supports. A fix for one crash turns into \"crashes are gone\". Both mistakes are cheap to catch in a table and expensive to catch in a render.\n\nIt reads a changelog in the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) layout, takes the newest dated release, and prints a verdict for every entry. It uses only the standard library.\n\n``` bash\n#!/usr/bin/env python3\n\"\"\"Draft a keep/cut table for the newest dated release in a CHANGELOG.md.\"\"\"\nimport argparse\nimport re\nimport sys\nfrom pathlib import Path\n\nRELEASE = re.compile(r\"^##\\s+\\[?(?P<ver>[^\\]\\s]+)\\]?\\s*(?:-\\s*(?P<date>\\d{4}-\\d{2}-\\d{2}))?(?P<tail>.*)$\")\nINTERNAL = re.compile(r\"\\b(bump|upgrade[sd]?|dependenc\\w+|deps?|refactor\\w*|lint\\w*|chore|typo|tests?|ci|rename[sd]?)\\b\", re.I)\nJARGON = re.compile(r\"\\b(race condition|deadlock|mutex|queue|worker|thread|pool|cache|index|orm|migration script)\\b\", re.I)\n\ndef newest_release(lines):\n    \"\"\"Return (version, date, {group: [entries]}) for the first dated, non-yanked release.\"\"\"\n    release, group, found = None, None, []\n    for line in lines:\n        m = RELEASE.match(line)\n        if m:\n            release, group = m, None\n            if m[\"date\"] and \"yanked\" not in m[\"tail\"].lower():\n                found.append((m[\"ver\"], m[\"date\"], {}))\n            continue\n        if line.startswith(\"### \"):\n            group = line[4:].strip()\n        elif re.match(r\"^[-*]\\s+\", line) and release and found and release[\"date\"] \\\n                and \"yanked\" not in release[\"tail\"].lower() and group:\n            found[-1][2].setdefault(group, []).append(re.sub(r\"^[-*]\\s+\", \"\", line).strip())\n    return found[0] if found else None\n\ndef triage(group, entry):\n    if group == \"Security\":\n        return \"ASK\", \"a person decides if this goes on screen\"\n    if INTERNAL.search(entry):\n        return \"CUT\", \"reads as internal work\"\n    if len(entry.split()) < 4:\n        return \"REWRITE\", \"too vague to turn into a headline\"\n    if JARGON.search(entry):\n        return \"REWRITE\", \"names a component customers never see\"\n    if group in (\"Deprecated\", \"Removed\"):\n        return \"SCENE\", \"customers lose something, say it plainly\"\n    return \"SCENE\", \"\"\n\ndef main():\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"path\")\n    ap.add_argument(\"--title\", type=int, default=3, help=\"title card seconds\")\n    ap.add_argument(\"--per\", type=int, default=6, help=\"seconds per scene\")\n    ap.add_argument(\"--cap\", type=int, default=40, help=\"runtime cap in seconds\")\n    ap.add_argument(\"--strict\", action=\"store_true\", help=\"exit 1 if ASK or REWRITE rows remain\")\n    args = ap.parse_args()\n\n    rel = newest_release(Path(args.path).read_text().splitlines())\n    if not rel:\n        sys.exit(\"No dated, non-yanked release found.\")\n    ver, date, groups = rel\n\n    print(f\"Release {ver} ({date})\\n\")\n    print(\"| Group | Entry | Verdict | Why |\")\n    print(\"|---|---|---|---|\")\n    scenes, open_rows = 0, 0\n    for group, entries in groups.items():\n        for e in entries:\n            verdict, why = triage(group, e)\n            scenes += verdict == \"SCENE\"\n            open_rows += verdict in (\"ASK\", \"REWRITE\")\n            print(f\"| {group} | {e} | {verdict} | {why} |\")\n\n    runtime = args.title + scenes * args.per\n    flag = \"  <- over the cap\" if runtime > args.cap else \"\"\n    print(f\"\\n{scenes} scenes, about {runtime}s of {args.cap}s{flag}\")\n    if args.strict and open_rows:\n        sys.exit(1)\n\nif __name__ == \"__main__\":\n    main()\n```\n\nThe rules are deliberately dumb:\n\n`[Unreleased]`, because those lines haven't shipped. It also ignores any release marked `[YANKED]`, because that release was pulled.\nHere is the file I tested with. The app, Fieldnote, is made up so there is no real customer data in it.\n\n```\n## [Unreleased]\n\n### Added\n- Shared folders (behind a feature flag).\n\n## [2.8.0] - 2026-10-14\n\n### Added\n- Offline mode for iOS.\n- Tag filters in search.\n\n### Changed\n- Upgrade React to 19.1.\n- Search results open faster on large libraries.\n\n### Removed\n- Dropped the legacy Markdown importer.\n\n### Fixed\n- Fix race condition in sync queue.\n- Fix crash when opening notes over 10,000 characters.\n- Misc fixes.\n\n### Security\n- Sanitize HTML in the note preview.\n\n## [2.7.1] - 2026-09-30 [YANKED]\n\n### Fixed\n- Fix login redirect.\n```\n\nAnd what `python changelog_triage.py CHANGELOG.md` printed:\n\n| Group | Entry | Verdict | Why | \n|---|---|---|---|\n| Added | Offline mode for iOS. | SCENE |  | \n| Added | Tag filters in search. | SCENE |  | \n| Changed | Upgrade React to 19.1. | CUT | reads as internal work | \n| Changed | Search results open faster on large libraries. | SCENE |  | \n| Removed | Dropped the legacy Markdown importer. | SCENE | customers lose something, say it plainly | \n| Fixed | Fix race condition in sync queue. | REWRITE | names a component customers never see | \n| Fixed | Fix crash when opening notes over 10,000 characters. | SCENE |  | \n| Fixed | Misc fixes. | REWRITE | too vague to turn into a headline | \n| Security | Sanitize HTML in the note preview. | ASK | a person decides if this goes on screen | \n\nThe footer reads `5 scenes, about 33s of 40s`. The shared-folders flag and the yanked 2.7.1 never appear, which is the point.\n\nTwo rows deserve a second look. \"Search results open faster on large libraries\" got a SCENE verdict, and the script is right to let it through. But a speed claim invites a number, and the note doesn't contain one. If an agent writes \"twice as fast\" on that scene, it made the number up. I would catch that in the brief, not in the render.\n\nThe \"Misc fixes\" row is the other. No script can fix it. Someone who knows what shipped has to write the real line, and that is the whole argument for doing this triage before the agent runs.\n\nSetup is the boring part and the docs cover it better than I would. In the CoAnimator app, Settings, General, Command line and agent skill has an install button for each, and the [CLI docs](https://coanimator.com/docs/cli) cover the command. Claude Code comes from [Anthropic's setup guide](https://code.claude.com/docs/en/setup). If you want background on what the agent can do inside a project, [this post](https://coanimator.com/blog/can-claude-code-make-videos) covers it.\n\nThen I would send the dated section, the table, and a brief like this:\n\nHere is a triage table for Fieldnote 2.8.0 and the dated changelog section it came from. Treat the table as a proposal. For each row, confirm or overrule the verdict and give one sentence of reasoning. For REWRITE rows, propose a customer-facing line and quote the source words it came from. Do not state any number, speed or percentage that is not in the notes. Build nothing until I reply.\n\nThe last two instructions do most of the work. \"Quote the source words\" forces every headline to point at its entry, and \"build nothing until I reply\" turns the first answer into something you can correct for the price of one sentence. Each SCENE row also needs a screenshot or recording to attach, or the scene will be text on a background.\n\nOnce the build is approved, the render step is a single `coa render <project>`. For a quick look at the title card, `--start 0 --end 6` renders only the first six seconds. The script can't check the rest, so I go through these:\n\n`git describe --tags --abbrev=0` prints the latest tag in one second.\n`--strict` makes the script exit 1 while any ASK or REWRITE row is unresolved. I tested the exit code locally, and I haven't wired it into a release pipeline yet. The step itself is two lines:\n\n```\n- name: Changelog triage\n  run: python changelog_triage.py CHANGELOG.md --strict\n```\n\nAny Security entry fails strict mode on purpose, so a person has to look at it. Once someone has, run that release without `--strict`.\n\nThe table prints to the job log, so whoever reviews the failed run sees exactly which rows need a person.\n\nIt matches words, not meaning, and it will be wrong in both directions:\n\nThat is why the script proposes and the agent and I decide. It also can't tell whether a change is visible to a customer, because it only sees the words.\n\nOne privacy point. The script runs locally, but whatever you paste into an agent can leave your machine for its provider. CoAnimator's [quickstart](https://coanimator.com/docs/quickstart) says the same about files and frames sent to an AI provider. Delete embargoed rows from the table before pasting, and crop customer data out of screenshots.\n\n**Does it work with changelogs that aren't in Keep a Changelog format?**\n\nIt needs `## [version] - date` headings and `###` groups. GitHub's generated release notes have neither, so the parser would need a small change.\n\n**Does the script replace reading the notes?**\n\nNo. It's a first draft of the cut list so the conversation with the agent starts from something concrete.\n\n**Do I need CoAnimator for this?**\n\nNo. The table works with any agent or video workflow. I use CoAnimator for the render step.\n\nRegex is a fallback. The better signal is the person who wrote the change. A `customer-facing` label on the pull request, or a one-line release-note field in the PR template, would let the table come from labels and leave the regex to catch whatever nobody labeled. Then the triage happens at merge time, when the author still remembers why the change matters.\n\nTry the script on your last release and see which rows you disagree with. Those are the decisions your changelog never recorded. If you want to turn the approved rows into a video, the [CoAnimator desktop app](https://coanimator.com/download) is where I'd render them or you can use your existing video workflow.", "url": "https://wpnews.pro/news/how-i-triage-release-notes-before-claude-code-turns-them-into-a-product-video", "canonical_source": "https://dev.to/sheen_417f0f3a7f4/how-i-triage-release-notes-before-claude-code-turns-them-into-a-product-video-1h3", "published_at": "2026-10-21 12:35:31+00:00", "updated_at": "2026-10-21 12:40:05.581012+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools"], "entities": ["Claude Code", "Keep a Changelog"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-i-triage-release-notes-before-claude-code-turns-them-into-a-product-video", "markdown": "https://wpnews.pro/news/how-i-triage-release-notes-before-claude-code-turns-them-into-a-product-video.md", "text": "https://wpnews.pro/news/how-i-triage-release-notes-before-claude-code-turns-them-into-a-product-video.txt", "jsonld": "https://wpnews.pro/news/how-i-triage-release-notes-before-claude-code-turns-them-into-a-product-video.jsonld"}}