fix(x): release-post drafts stop parroting changelog bullets

Highlights reorder to marketing order (Added/Changed before Fixed/Security
— the drafting model anchors on highlight #1 and the changelog opens with
Security), the prompt bans verbatim highlight copying and internal plumbing
jargon, and the deterministic fallback becomes a generic announcement that
can never quote a raw bullet.
This commit is contained in:
Renn F
2026-07-25 03:04:35 +02:00
parent 29243771e4
commit 037114338a
3 changed files with 57 additions and 28 deletions
+1
View File
@@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
### Fixed
- **release-post drafts stop parroting changelog bullets.** The X announcement drafter fed the changelog's bold leads to the local model in document order — Security first, so the WAF plumbing line became the tweeted headline — and its deterministic fallback template quoted the first raw bullet verbatim. Highlights now reorder to marketing order (Added/Changed features before Fixed/Security plumbing), the prompt bans verbatim highlight copying and internal jargon, and the fallback is a generic ships-announcement that can never quote a bullet.
- **docs-redirects-only commits fire a CI run so the release gate gets a verdict.** The CI paths filter covered docs/panel/motion-only slave commits but not `docs-redirects/**`, so a redirect-stub commit landing as the slave tip produced no CI run at all — and the fail-closed release-readiness gate read the missing conclusion as "unknown", silently holding back every release proposal until an unrelated code commit happened to land.
- **Mix-mode routing gained its escape hatch (#663).** A fully-pinned fleet had no way back to a global mode: mode switches deliberately spare per-agent pins, but the server refused an empty mix map and the panel refused an empty save — so with every agent pinned, the mode buttons were permanent no-ops behind a success toast and the mode label read "mix" forever. An empty per-agent map is now the explicit clear-all, Save mix with nothing picked confirms and clears instead of erroring, a Clear-all button resets every agent to inherit-global in one click, and the Routing-mode section warns when per-agent overrides outrank the mode buttons.
- **The lazy DB engine rebinds per event loop (#663).** The global engine cache was loop-agnostic while its pooled connections are loop-bound, so a second event loop touching it (a test's `asyncio.run`, a second server thread, the eval bench's disposable stack) eventually drew a foreign-loop connection and died with "Future attached to a different loop" — the eval-bench smoke's flaky CI timeout. The engine/factory accessors now stamp the owning loop and discard-and-rebuild on cross-loop access; production runs one loop, so nothing changes there.
+49 -23
View File
@@ -169,11 +169,13 @@ def _clamp_tweet(text: str) -> str:
return collapsed[: MAX_TWEET_CHARS - 1].rstrip() + ""
def _fallback_release_body(
version: str, highlights: list[str], product_name: str
) -> str:
lead = highlights[0] if highlights else "assorted improvements"
return f"{product_name} v{version} is out: {lead}"
def _fallback_release_body(version: str, product_name: str) -> str:
# Deliberately generic: the deterministic fallback must never quote a raw
# changelog bullet — internal plumbing jargon read as the release headline.
return (
f"{product_name} v{version} just shipped — new features, fixes, and "
"performance work across the board. Full release notes on GitHub."
)
# Bold feature leads in a Keep-a-Changelog release body: "- **Headline (#N).**"
@@ -186,6 +188,13 @@ _CHANGELOG_REF_RE = re.compile(
)
# Marketing order for the announcement prompt. The drafting model anchors on
# highlight #1, and a Keep-a-Changelog body opens with Security — document
# order once tweeted a WAF internals bullet as the release headline.
_SECTION_ORDER = ("added", "changed", "performance", "fixed", "security")
_SECTION_SPLIT_RE = re.compile(r"^### +(?P<name>[A-Za-z ]+?)\s*$", re.MULTILINE)
def changelog_highlights(entry: str, *, limit: int = 8) -> list[str]:
"""Human-readable feature headlines from a curated CHANGELOG release body.
@@ -193,17 +202,33 @@ def changelog_highlights(entry: str, *, limit: int = 8) -> list[str]:
("docs: curate the full 0.26.0 Unreleased body (#601)") — feeding those to
the announcement model produces a lame parroted caption. The curated
changelog's bold leads ARE the feature story ("Telegram Mini App V5 — brand
voice…"), so use those instead. Pure + best-effort: a body with no bold
leads yields an empty list and the caller falls back to change_summary.
voice…"), so use those instead — reordered so Added/Changed feature
headlines precede Fixed/Security plumbing. Pure + best-effort: a body with
no bold leads yields an empty list and the caller falls back to
change_summary.
"""
out: list[str] = []
for m in _CHANGELOG_LEAD_RE.finditer(entry):
lead = _CHANGELOG_REF_RE.sub("", m.group("lead")).strip().rstrip(".").strip()
if lead:
out.append(lead)
if len(out) >= limit:
break
return out
def leads(chunk: str) -> list[str]:
found: list[str] = []
for m in _CHANGELOG_LEAD_RE.finditer(chunk):
lead = (
_CHANGELOG_REF_RE.sub("", m.group("lead")).strip().rstrip(".").strip()
)
if lead:
found.append(lead)
return found
parts = _SECTION_SPLIT_RE.split(entry)
if len(parts) == 1: # no section headings — keep document order
return leads(entry)[:limit]
rank = {name: i for i, name in enumerate(_SECTION_ORDER)}
sections = [
(rank.get(parts[i].strip().lower(), len(rank)), leads(parts[i + 1]))
for i in range(1, len(parts) - 1, 2)
]
sections.sort(key=lambda s: s[0]) # stable: unknown sections keep doc order
ordered = leads(parts[0]) + [lead for _, chunk in sections for lead in chunk]
return ordered[:limit]
def _release_prompt(
@@ -214,11 +239,14 @@ def _release_prompt(
f"{voice}\n\n"
f"Draft ONE tweet (max 280 characters) announcing that {product_name} "
f"v{version} just shipped. Lead with the single most user-visible "
f"change: name both it and {product_name} in the first sentence. One "
"concrete detail (a real feature name, a real number) beats three "
"vague adjectives. Aim well under 240 characters so the 280 clamp "
"never has to truncate mid-sentence. End on substance, not a "
"slogan.\n\n"
f"change: name both it and {product_name} in the first sentence. "
"Rewrite highlights in plain language a non-engineer follows — NEVER "
"copy a highlight's wording verbatim, and skip internal plumbing "
"jargon (middleware, proxy hops, branch mechanics, CI) unless it IS "
"the user-facing story. One concrete detail (a real feature name, a "
"real number) beats three vague adjectives. Aim well under 240 "
"characters so the 280 clamp never has to truncate mid-sentence. End "
"on substance, not a slogan.\n\n"
f"Highlights:\n{bullets}\n"
)
@@ -496,9 +524,7 @@ class XEngine(BaseService):
error=str(exc),
)
draft = None
body = (draft or "").strip() or _fallback_release_body(
version, highlights, product_name
)
body = (draft or "").strip() or _fallback_release_body(version, product_name)
return _clamp_tweet(body)
# ---- mentions (periodic poll) ------------------------------------------
+7 -5
View File
@@ -1696,11 +1696,13 @@ def test_changelog_highlights_extracts_feature_headlines() -> None:
"- **Forge program: GitHub, Gitea, and GitLab (#575, #581).** One API.\n"
)
hl = x_engine_module.changelog_highlights(entry)
# A GHSA advisory ref is stripped exactly like a PR ref — neither belongs
# in a caption prompt's feature headline.
assert hl[0] == "Orchestrator API is off the public internet"
assert hl[1] == "Telegram Mini App V5 — brand voice and an operations ring"
assert hl[2] == "Forge program: GitHub, Gitea, and GitLab"
# Marketing order: Added feature headlines precede the Security plumbing
# even though Security comes first in the document — the drafting model
# anchors on highlight #1. A GHSA advisory ref is stripped exactly like a
# PR ref — neither belongs in a caption prompt's feature headline.
assert hl[0] == "Telegram Mini App V5 — brand voice and an operations ring"
assert hl[1] == "Forge program: GitHub, Gitea, and GitLab"
assert hl[2] == "Orchestrator API is off the public internet"
# No raw commit-subject noise, no trailing PR/GHSA refs or periods.
assert all("#" not in h and "GHSA" not in h for h in hl)