mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -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
@@ -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) ------------------------------------------
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user