feat(x-engine): smart spotlight cadence — daily when there's news, quiet when there isn't (#374)

CEO verdict on the blind 3-day timer: 'It should default to 1 day and
be more like... smart.' Now: interval defaults to 1 day; the cycle
skips (with logged reasons) while a spotlight draft is still awaiting
the CEO, and stretches to 3x the interval when nothing has shipped
(CHANGELOG sections via the read clone) since the last spotlight
activity — where activity is a materialized draft's seen_at or a
completed exploration's updated_at, deliberately excluding the stale-
cycle janitor's cancels. The HoM gains an explicit skip exit
(propose_feature_spotlight skip=true + reason: completes the
exploration, no draft, no seen-slug, still counts as activity), and
its spawn prompt now carries the seen ledger WITH dates, what shipped
since the last spotlight, and recently rejected drafts with the CEO's
reasons — fresh-but-unspotlighted first. Fail-open on changelog read
errors so a signal outage never starves the engine.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-09 20:59:53 +02:00
committed by GitHub
co-authored by Renn F
parent 3cb32e3c5f
commit 0450ec9e89
15 changed files with 957 additions and 39 deletions
+1 -1
View File
@@ -25,4 +25,4 @@
| `notify_ack` | `notify_ack(notification_id: UUID)` |
| `read_messages` | `read_messages()` |
| `read_a2a` | `read_a2a(see do_server)` |
| `propose_feature_spotlight` | `propose_feature_spotlight(feature_slug: str, feature_title: str, body: str, wants_video: bool = False, video_script: str = '')` |
| `propose_feature_spotlight` | `propose_feature_spotlight(feature_slug: str = '', feature_title: str = '', body: str = '', wants_video: bool = False, video_script: str = '', skip: bool = False, skip_reason: str = '')` |
+1 -1
View File
@@ -231,7 +231,7 @@ real tools live in their agent_sdk drivers, not role_config.
| `notify_ack` | `notify_ack(notification_id: UUID)` |
| `read_messages` | `read_messages()` |
| `read_a2a` | `read_a2a(see do_server)` |
| `propose_feature_spotlight` | `propose_feature_spotlight(feature_slug: str, feature_title: str, body: str, wants_video: bool = False, video_script: str = '')` |
| `propose_feature_spotlight` | `propose_feature_spotlight(feature_slug: str = '', feature_title: str = '', body: str = '', wants_video: bool = False, video_script: str = '', skip: bool = False, skip_reason: str = '')` |
## auditor
+2
View File
@@ -176,6 +176,8 @@ async def do_propose_feature_spotlight(
body=body.body,
wants_video=body.wants_video,
video_script=body.video_script,
skip=body.skip,
skip_reason=body.skip_reason,
)
return envelope_to_response(env, request)
+10 -4
View File
@@ -130,13 +130,19 @@ class ProposeRoadmapRequest(BaseModel):
class ProposeFeatureSpotlightRequest(BaseModel):
"""Head of Marketing's feature-spotlight draft: a picked feature + a
ready-to-post body, plus an optional companion-video request."""
ready-to-post body, plus an optional companion-video request — or a
``skip`` verdict when nothing is worth spotlighting this cycle (the
feature/title/body fields are then ignored; the handler enforces the
real "required unless skipping" rule since a plain Field(..., min_length)
can't express that conditional)."""
feature_slug: str = Field(..., min_length=1, max_length=128)
feature_title: str = Field(..., min_length=1)
body: str = Field(..., min_length=1)
feature_slug: str = Field(default="", max_length=128)
feature_title: str = ""
body: str = ""
wants_video: bool = False
video_script: str = ""
skip: bool = False
skip_reason: str = ""
class ProposeVideoRequest(BaseModel):
+5 -3
View File
@@ -953,9 +953,11 @@ class Settings(BaseSettings):
),
)
x_feature_spotlight_interval_seconds: int = Field(
default=259200, # 3 days — tunable; marketing cadence is a CEO call, not a
# technical constant. Sits between the 30-min mentions poll
# and the weekly roadmap cycle.
default=86400, # 1 day — tunable; marketing cadence is a CEO call, not a
# technical constant. This is the BASE loop tick only: the engine's own
# smart-cadence guard (XEngine._feature_activity_stretch_skip) stretches
# the effective cadence to 3x this whenever nothing has shipped since
# the last spotlight activity, so a quiet week doesn't fire daily.
ge=3600,
description="Seconds between feature-spotlight exploration cycles.",
)
@@ -41,6 +41,8 @@ X_REJECT_REASON = "x_reject_reason"
X_POSTED_TWEET_ID = "x_posted_tweet_id"
X_FEATURE_REF = "x_feature_ref"
X_SEEN_FEATURES = "x_seen_features"
X_SPOTLIGHT_BRIEF = "x_spotlight_brief"
X_SPOTLIGHT_SKIP_REASON = "x_spotlight_skip_reason"
ROADMAP_CYCLE = "roadmap_cycle"
VIDEO_DRAFT = "video_draft"
VIDEO_REJECT_REASON = "video_reject_reason"
@@ -208,6 +210,24 @@ def set_x_seen_features(task: HasMarkers, slugs: list[str]) -> None:
set_marker(task, X_SEEN_FEATURES, [str(s) for s in slugs])
def get_x_spotlight_brief(task: HasMarkers) -> dict[str, Any] | None:
val = get_marker(task, X_SPOTLIGHT_BRIEF)
return val if isinstance(val, dict) else None
def set_x_spotlight_brief(task: HasMarkers, brief: dict[str, Any]) -> None:
set_marker(task, X_SPOTLIGHT_BRIEF, brief)
def get_x_spotlight_skip_reason(task: HasMarkers) -> str | None:
val = get_marker(task, X_SPOTLIGHT_SKIP_REASON)
return str(val) if val else None
def set_x_spotlight_skip_reason(task: HasMarkers, reason: str) -> None:
set_marker(task, X_SPOTLIGHT_SKIP_REASON, reason)
# --- board roadmap cycle ---------------------------------------------------
# The themed cycle (goal + item drafts) the Product Owner authors via
# ``propose_roadmap`` onto the exploration task the roadmap engine opened.
+24 -6
View File
@@ -586,27 +586,43 @@ def propose_roadmap(cycle_goal: str, items: list[dict[str, Any]]) -> dict[str, A
def propose_feature_spotlight(
feature_slug: str,
feature_title: str,
body: str,
feature_slug: str = "",
feature_title: str = "",
body: str = "",
wants_video: bool = False,
video_script: str = "",
skip: bool = False,
skip_reason: str = "",
) -> dict[str, Any]:
"""Head of Marketing: draft ONE feature-spotlight marketing post.
"""Head of Marketing: draft ONE feature-spotlight marketing post, or skip.
Call this exactly ONCE per exploration cycle, after investigating the
CHANGELOG, feature-flags ledger, docs/map, charter, and KB to pick a real,
under-publicized capability. The draft is held in the X post queue for the
CEO to edit/approve nothing auto-posts.
If nothing shipped is genuinely worth spotlighting this cycle, pass
skip=True with a substantive skip_reason instead of forcing a weak post
a forced spotlight is worse than skipping. A skip still completes the
exploration task (no draft materialized, no feature marked seen) and
counts as this cycle's activity for the engine's cadence, so it won't just
re-fire daily into the same quiet period.
Args:
feature_slug: Stable slug identifying the feature (the dedup key).
feature_title: Short human title of the feature.
body: The tweet text (plain, <=280 chars, no invented facts).
Ignored when skip=True.
feature_title: Short human title of the feature. Ignored when
skip=True.
body: The tweet text (plain, <=280 chars, no invented facts). Ignored
when skip=True.
wants_video: Also request a companion video (held separately for CEO
approval, when the video engine is armed for spotlights).
video_script: Optional script for that video; falls back to the
feature title/body when omitted.
skip: True to declare "nothing worth spotlighting this cycle" instead
of authoring a draft.
skip_reason: Required (non-empty, >=8 chars) explanation when
skip=True.
"""
return _post(
"/api/v1/do/propose_feature_spotlight",
@@ -616,6 +632,8 @@ def propose_feature_spotlight(
"body": body,
"wants_video": wants_video,
"video_script": video_script,
"skip": skip,
"skip_reason": skip_reason,
},
)
+65 -6
View File
@@ -799,6 +799,53 @@ def _is_non_dev_dispatch_source(task: dict[str, Any]) -> bool:
_MAX_VIDEO_RENDER_ATTEMPTS = _markers.MAX_VIDEO_RENDER_ATTEMPTS
def _format_seen_features(markers_dict: dict[str, Any]) -> str:
"""Render the seen-features ledger with dates when the enriched
``x_spotlight_brief`` marker carries them, falling back to the plain
slug list (a pre-brief exploration, or a brief-gather failure) the
prompt must never break on a missing/partial marker. Module-level (not a
method), mirroring ``_is_non_dev_dispatch_source``, so this is unit
testable without a wholesale-mocked ``self`` (xenon budget: keeps
``_build_feature_spotlight_prompt`` a flat render instead of inlining
this branching)."""
brief = markers_dict.get(_markers.X_SPOTLIGHT_BRIEF) or {}
seen = brief.get("seen")
if isinstance(seen, list) and seen:
return ", ".join(
f"{s.get('slug')} (seen {str(s.get('seen_at') or '')[:10]})" for s in seen
)
slugs = markers_dict.get(_markers.X_SEEN_FEATURES) or []
return ", ".join(slugs) if slugs else "(none yet — this is the first cycle)"
def _format_shipped_since(markers_dict: dict[str, Any]) -> str:
"""Render the CHANGELOG sections shipped since the last spotlight
activity empty/missing brief renders as "nothing new" rather than
breaking the prompt (see ``_format_seen_features``)."""
brief = markers_dict.get(_markers.X_SPOTLIGHT_BRIEF) or {}
shipped = brief.get("shipped_since")
if not isinstance(shipped, list) or not shipped:
return "(nothing new since the last cycle, per CHANGELOG.md)"
parts = [
f"v{entry.get('version')} ({entry.get('date')}): "
f"{', '.join(entry.get('titles') or []) or 'no subsections'}"
for entry in shipped
]
return "; ".join(parts)
def _format_rejected_spotlights(markers_dict: dict[str, Any]) -> str:
"""Render recently CEO-rejected x_feature drafts + their reasons, so HoM
steers away from ground the CEO already turned down."""
brief = markers_dict.get(_markers.X_SPOTLIGHT_BRIEF) or {}
rejected = brief.get("rejected")
if not isinstance(rejected, list) or not rejected:
return "(none)"
return "; ".join(
f"{r.get('title') or r.get('slug')}{r.get('reason')}" for r in rejected
)
class AgentOrchestrator:
"""
Manages Claude Code containers for all agents.
@@ -13657,8 +13704,9 @@ that is not your job here, and the gateway will reject those verbs.
"""Prompt for the Head of Marketing's one-shot feature-spotlight cycle."""
task_id = task.get("id", "unknown")
markers_dict = task.get("orchestration_markers") or {}
seen = markers_dict.get(_markers.X_SEEN_FEATURES) or []
seen_line = ", ".join(seen) if seen else "(none yet — this is the first cycle)"
seen_line = _format_seen_features(markers_dict)
shipped_line = _format_shipped_since(markers_dict)
rejected_line = _format_rejected_spotlights(markers_dict)
return f"""\
You are the Head of Marketing. It's time for your periodic feature-spotlight cycle.
@@ -13667,10 +13715,15 @@ TASK: {task_id}
RoboCo markets its own capabilities, not just releases. Investigate what the
company has actually shipped and draft ONE marketing post about a genuinely
useful, under-publicized capability something a user or prospect would not
already know from the last release announcement.
already know from the last release announcement. Prefer something fresh but
not yet spotlighted over stale already-covered ground.
ALREADY COVERED do not repeat: {seen_line}
SHIPPED SINCE THE LAST CYCLE (CHANGELOG.md): {shipped_line}
RECENTLY REJECTED BY THE CEO avoid repeating these angles: {rejected_line}
== WHAT TO DO ==
1. triage() see your board-level context.
@@ -13690,9 +13743,15 @@ ALREADY COVERED — do not repeat: {seen_line}
5. propose_feature_spotlight(feature_slug="<a short stable slug>",
feature_title="<human-readable feature name>", body="<the post>")
call this EXACTLY ONCE.
6. i_am_idle() once proposed. The CEO reviews, edits, approves, or rejects
the draft in the X post queue; nothing posts without that explicit
approval.
If nothing shipped is genuinely worth spotlighting this cycle, call
propose_feature_spotlight(skip=True, skip_reason="<why nothing qualifies>")
instead a weak, forced spotlight is worse than skipping a cycle, and the
next cycle will see this skip as recent activity (the cadence won't just
re-fire into the same quiet period tomorrow).
6. i_am_idle() once proposed (or skipped). The CEO reviews, edits, approves,
or rejects the draft in the X post queue; nothing posts without that
explicit approval.
Do NOT claim, plan, delegate, or attempt to post anything yourself that is
not your job here, and the gateway will reject those.
+51 -4
View File
@@ -1297,22 +1297,67 @@ class ContentActions:
)
return None
async def _propose_feature_spotlight_skip(
self, agent_id: UUID, skip_reason: str
) -> Envelope:
"""``skip=True`` branch of ``propose_feature_spotlight``: validate the
reason, find the caller's open exploration, and record the skip.
Split out to keep the caller's return-statement count under the
xenon/PLR0911 budget."""
if rej := self._reject_soup(skip_reason, field="skip_reason", min_chars=8):
return rej
from roboco.services.task import get_task_service
from roboco.services.x_engine import get_x_engine
task_svc = get_task_service(self.task.session)
explorations = await task_svc.list_open_feature_explorations()
task = next((t for t in explorations if t.assigned_to == agent_id), None)
if task is None:
return Envelope.invalid_state(
message="no open feature-spotlight exploration task assigned to you",
remediate=(
"propose_feature_spotlight only runs against an active "
"exploration spawned by the X engine; wait for the next cycle"
),
context_briefing={},
)
engine = get_x_engine(self.task.session)
await engine.skip_feature_spotlight(exploration_task=task, reason=skip_reason)
return Envelope.ok(
status="feature_spotlight_skipped",
task_id=str(task.id),
next="i_am_idle() — no draft was materialized this cycle",
context_briefing={"skip_reason": skip_reason},
)
async def propose_feature_spotlight(
self,
*,
agent_id: UUID,
feature_slug: str,
feature_title: str,
body: str,
feature_slug: str = "",
feature_title: str = "",
body: str = "",
wants_video: bool = False,
video_script: str = "",
skip: bool = False,
skip_reason: str = "",
) -> Envelope:
"""Head of Marketing authors ONE feature-spotlight draft.
"""Head of Marketing authors ONE feature-spotlight draft, or skips.
Validates role, field lengths, the 280-char tweet limit, and that the
feature hasn't already been covered, then materializes the held X-queue
draft and completes the caller's exploration task. One call per cycle.
``skip=True`` is the "nothing worth spotlighting this cycle" exit a
forced, weak spotlight is worse than skipping one. It requires a
substantive ``skip_reason`` and ignores ``feature_slug``/
``feature_title``/``body``/``wants_video``/``video_script`` entirely:
no draft is materialized, no feature is marked seen, but the
exploration task still completes (``XEngine.skip_feature_spotlight``)
so the skip counts as this cycle's activity for the engine's
smart-cadence guard.
``wants_video`` optionally requests a companion video. The video
authoring task no longer opens here it opens later, at CEO-approve
time (``XPostService._open_spotlight_video``, gated on
@@ -1332,6 +1377,8 @@ class ContentActions:
remediate="this verb is Head-of-Marketing-only",
context_briefing={},
)
if skip:
return await self._propose_feature_spotlight_skip(agent_id, skip_reason)
if rej := self._reject_feature_spotlight_fields(
feature_slug, feature_title, body
):
+17
View File
@@ -1588,6 +1588,23 @@ class TaskService(BaseService):
)
return list(result.scalars().all())
async def list_open_feature_spotlight_drafts(self) -> list[TaskTable]:
"""Non-terminal MATERIALIZED spotlight drafts only (source=x_feature,
excludes the exploration source) the "never stack a second draft"
basis for XEngine.open_feature_spotlight_exploration. Distinct from
list_open_x_posts' shared numeric cap: this is a one-open-draft rule
specific to the spotlight source, mirroring list_open_video_post_drafts'
narrower filter over list_open_video_posts. Ordered oldest-first."""
result = await self.session.execute(
select(TaskTable)
.where(
TaskTable.source == X_FEATURE_SOURCE,
TaskTable.status.notin_([TaskStatus.COMPLETED, TaskStatus.CANCELLED]),
)
.order_by(TaskTable.created_at)
)
return list(result.scalars().all())
async def list_external_pr_reviews_awaiting_decision(self) -> list[TaskTable]:
"""Completed external-PR reviews still awaiting the CEO's decision.
+274 -14
View File
@@ -21,16 +21,19 @@ from ``ReleaseProposalService.approve()``'s publish success branch;
from __future__ import annotations
import re
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING, cast
from pathlib import Path
from typing import TYPE_CHECKING, Any, cast
import httpx
import redis.asyncio as redis
from sqlalchemy import select
from sqlalchemy import func, select
from roboco.config import settings
from roboco.db.tables import (
AgentSpawnSessionTable,
TaskTable,
XSeenFeatureTable,
XSeenMentionTable,
)
@@ -61,7 +64,7 @@ if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
from roboco.db.tables import ProjectTable, TaskTable
from roboco.db.tables import ProjectTable
from roboco.services.task import TaskService
_CHAT_TIMEOUT_SECONDS = 60.0
@@ -147,11 +150,65 @@ _FEATURE_EXPLORATION_TITLE = "X feature-spotlight exploration"
_FEATURE_EXPLORATION_DESCRIPTION = (
"Investigate RoboCo's own shipped capabilities — CHANGELOG.md, the "
"feature-flags ledger, docs/map, the company charter, and the knowledge "
"base — and pick ONE under-publicized feature not already covered (see "
"the seen-features list on this task). Draft ONE marketing post via "
"propose_feature_spotlight()."
"base — and pick ONE under-publicized, fresh-but-unspotlighted feature "
"not already covered (see the seen-features list on this task). Draft "
"ONE marketing post via propose_feature_spotlight(). If nothing shipped "
"is genuinely worth spotlighting this cycle, call "
"propose_feature_spotlight(skip=True, skip_reason='<why>') instead — a "
"weak, forced spotlight is worse than skipping a cycle; the skip still "
"counts as this cycle's activity so the engine doesn't re-fire daily "
"into the same quiet period."
)
# --- CHANGELOG.md parsing (activity-stretch signal + brief enrichment) -----
# Keep-a-Changelog headers are regular enough for a small regex split instead
# of a markdown-parser dependency: "## [X.Y.Z] - YYYY-MM-DD" release headers,
# "### Added/Fixed/Changed/..." subsection headers within each release body.
_CHANGELOG_VERSION_RE = re.compile(
r"^## \[(?P<version>[^\]]+)\] - (?P<date>\d{4}-\d{2}-\d{2})\s*$", re.MULTILINE
)
_CHANGELOG_SUBSECTION_RE = re.compile(r"^### (?P<title>.+?)\s*$", re.MULTILINE)
def _parse_changelog_sections(text: str) -> list[dict[str, Any]]:
"""Split a Keep-a-Changelog file into per-release sections: version, date
(the file's own day granularity), and subsection titles. Pure + best-
effort a malformed/missing header just yields fewer/no sections, never
raises, so a hand-edited CHANGELOG can't break the engine."""
headers = list(_CHANGELOG_VERSION_RE.finditer(text))
sections: list[dict[str, Any]] = []
for i, m in enumerate(headers):
start = m.end()
end = headers[i + 1].start() if i + 1 < len(headers) else len(text)
titles = [t.strip() for t in _CHANGELOG_SUBSECTION_RE.findall(text[start:end])]
sections.append(
{"version": m.group("version"), "date": m.group("date"), "titles": titles}
)
return sections
def _sections_since(
sections: list[dict[str, Any]], cutoff: datetime
) -> list[dict[str, Any]]:
"""Sections dated strictly after ``cutoff``'s calendar date.
The CHANGELOG only carries day granularity, so a section dated the same
day as an intra-day cutoff can't be ordered against it and is
conservatively treated as not-new (a false "nothing shipped" costs one
extra quiet cycle; a false "something shipped" would let a weak forced
spotlight through the former is the cheaper mistake).
"""
cutoff_date = cutoff.date()
out: list[dict[str, Any]] = []
for section in sections:
try:
sec_date = datetime.strptime(section["date"], "%Y-%m-%d").date()
except ValueError:
continue
if sec_date > cutoff_date:
out.append(section)
return out
class XEngine(BaseService):
"""Draft release posts (event hook) + mention replies (poll), both held."""
@@ -383,14 +440,15 @@ class XEngine(BaseService):
No-ops when the flags are off, no X credentials are configured (drafting
content nobody can ever post is pointless mirrors the release/mentions
guard), a cycle is already open, the shared open-post cap is reached, or
the RoboCo project isn't resolvable. Never authors content itself — HoM
does, via propose_feature_spotlight() once the dispatcher spawns it.
guard), a materialized spotlight draft is still awaiting the CEO (never
stack a second one), nothing has shipped since the last spotlight
activity and the stretched cadence hasn't elapsed yet (the "smart
cadence" guard — see ``_feature_activity_stretch_skip``), a cycle is
already open, the shared open-post cap is reached, or the RoboCo
project isn't resolvable. Never authors content itself — HoM does, via
propose_feature_spotlight() once the dispatcher spawns it.
"""
if not (settings.x_engine_enabled and settings.x_feature_spotlight_enabled):
return None
client = await self._client()
if not client.configured:
if not await self._feature_spotlight_may_proceed():
return None
task_svc = get_task_service(self.session)
# Cancel a stale + spawnless exploration so the engine can re-arm; a
@@ -409,6 +467,181 @@ class XEngine(BaseService):
task_svc, cast("UUID", project.id)
)
async def _feature_spotlight_may_proceed(self) -> bool:
"""Bundles the flags/creds/pending-draft/activity-stretch guards into
one boolean so ``open_feature_spotlight_exploration``'s own
return-statement count stays under the xenon/PLR0911 budget each
sub-guard still logs its own skip reason."""
if not (settings.x_engine_enabled and settings.x_feature_spotlight_enabled):
return False
client = await self._client()
if not client.configured:
return False
task_svc = get_task_service(self.session)
if await self._pending_spotlight_draft_open(task_svc):
return False
return not await self._feature_activity_stretch_skip()
async def _pending_spotlight_draft_open(self, task_svc: TaskService) -> bool:
"""True when a materialized x_feature draft is still open (PENDING,
awaiting the CEO in the X post queue) never stack a second spotlight
draft while one is unreviewed. Distinct from the shared numeric
open-post cap below: this is a one-open-draft rule specific to the
spotlight source."""
drafts = await task_svc.list_open_feature_spotlight_drafts()
if not drafts:
return False
self.log.info(
"x-engine: feature-spotlight draft still open; skipping this cycle",
open_draft_id=str(drafts[0].id),
)
return True
async def _feature_activity_stretch_skip(self) -> bool:
"""True to skip this cycle under the smart-cadence guard.
Stretches the effective cadence to 3x the base interval whenever
nothing has shipped (per CHANGELOG.md) since the last spotlight
activity, so a quiet stretch doesn't fire the Head of Marketing daily
into nothing. Always False on no activity history yet (a first-ever
cycle is never stretched) or on a changelog-read failure (fail open
a signal outage must never silently starve the engine of cycles).
"""
last_activity = await self._last_spotlight_activity()
if last_activity is None:
return False
try:
shipped = bool(await self._shipped_sections_since(last_activity))
except Exception as exc:
self.log.warning(
"x-engine: changelog activity check failed (fail open)",
error=str(exc),
)
return False
if shipped:
return False
stretched_seconds = 3 * settings.x_feature_spotlight_interval_seconds
quiet = (datetime.now(UTC) - last_activity).total_seconds() < stretched_seconds
if quiet:
self.log.info(
"x-engine: feature-spotlight cadence stretched "
"(nothing shipped since last activity)",
last_activity=last_activity.isoformat(),
stretched_seconds=stretched_seconds,
)
return quiet
async def _last_spotlight_activity(self) -> datetime | None:
"""Latest genuine spotlight-cycle activity, or None with no history yet.
Two sources, taking the max: a materialized draft's ``seen_at``
(XSeenFeatureTable), or a COMPLETED exploration task's ``updated_at``
a HoM "skip" verdict completes the exploration with no
seen-features row, so ``updated_at`` is the only signal that advances
on a skip (mirrors ``list_x_post_history``'s use of ``updated_at`` as
"when this was acted on"). Deliberately excludes CANCELLED
explorations: the stale-cycle janitor (``_cancel_stale_exploration``)
cancels an abandoned cycle with no HoM action at all, which must not
look like activity or the stretch guard would wrongly suppress the
very next real cycle.
"""
seen_max = (
await self.session.execute(select(func.max(XSeenFeatureTable.seen_at)))
).scalar_one_or_none()
explo_max = (
await self.session.execute(
select(func.max(TaskTable.updated_at)).where(
TaskTable.source == X_FEATURE_EXPLORATION_SOURCE,
TaskTable.status == TaskStatus.COMPLETED,
)
)
).scalar_one_or_none()
candidates = [v for v in (seen_max, explo_max) if v is not None]
return max(candidates) if candidates else None
async def _shipped_sections_since(self, cutoff: datetime) -> list[dict[str, Any]]:
"""CHANGELOG.md release sections dated after ``cutoff`` — the shared
"did anything ship" signal for both the activity-stretch gate and the
exploration brief (``_gather_spotlight_brief``).
Reads the RoboCo project's read clone (``WorkspaceService.
ensure_read_clone``) rather than running a full
``ReleaseReadinessService.assess``: a single small-file read + regex
split is far cheaper than the multi-subprocess git snapshot the
release manager needs, and this runs on every feature-spotlight loop
tick (up to daily), not once per release.
"""
from roboco.services.release_readiness import _read_changelog
from roboco.services.workspace import get_workspace_service
slug = (settings.self_heal_project_slug or "roboco-api").strip()
root = await get_workspace_service(self.session).ensure_read_clone(slug)
text = _read_changelog(Path(root))
return _sections_since(_parse_changelog_sections(text), cutoff)
async def _recent_rejected_spotlights(
self, *, limit: int = 5
) -> list[dict[str, str]]:
"""Recently CEO-rejected x_feature drafts + their reject reasons, so
HoM doesn't re-propose ground the CEO already turned down. Reuses
``list_x_post_history`` (both sources + terminal statuses) and
filters in Python a rejected-spotlight-only query isn't worth a
dedicated task-service method for a bounded top-N read."""
history = await get_task_service(self.session).list_x_post_history(limit=50)
rejected: list[dict[str, str]] = []
for t in history:
if t.source != X_FEATURE_SOURCE or t.status != TaskStatus.CANCELLED:
continue
reason = markers.get_x_reject_reason(t)
if not reason:
continue
ref = markers.get_x_feature_ref(t) or {}
rejected.append(
{
"slug": str(ref.get("slug", "")),
"title": str(ref.get("title", "")),
"reason": reason,
}
)
if len(rejected) >= limit:
break
return rejected
async def _gather_spotlight_brief(self) -> dict[str, Any]:
"""Everything the spawn prompt's brief-enrichment needs beyond the
plain seen-slugs list: the seen ledger WITH dates, what shipped since
the last spotlight activity (same changelog signal as the
activity-stretch gate), and recently CEO-rejected x_feature drafts
with their reasons so HoM can prefer fresh-but-unspotlighted
material over stale or already-rejected ground. Every piece is
best-effort; a changelog-read failure yields an empty
``shipped_since`` rather than blocking origination.
"""
seen_rows = (
await self.session.execute(
select(XSeenFeatureTable).order_by(XSeenFeatureTable.seen_at)
)
).scalars()
seen = [
{"slug": row.feature_slug, "seen_at": row.seen_at.isoformat()}
for row in seen_rows
]
last_activity = await self._last_spotlight_activity()
shipped_since: list[dict[str, Any]] = []
if last_activity is not None:
try:
shipped_since = await self._shipped_sections_since(last_activity)
except Exception as exc:
self.log.warning(
"x-engine: changelog brief read failed (best-effort)",
error=str(exc),
)
return {
"seen": seen,
"shipped_since": shipped_since,
"rejected": await self._recent_rejected_spotlights(),
}
async def _cancel_stale_exploration(self, task_svc: TaskService) -> bool:
"""Return False to block re-arm (fresh cycle or live HoM spawn); return
True to proceed either no open exploration, or a stale+spawnless one
@@ -451,13 +684,16 @@ class XEngine(BaseService):
self, task_svc: TaskService, project_id: UUID
) -> TaskTable:
seen = await self._seen_feature_slugs()
brief = await self._gather_spotlight_brief()
task = await task_svc.create(
TaskCreateRequest(
title=_FEATURE_EXPLORATION_TITLE,
description=_FEATURE_EXPLORATION_DESCRIPTION,
acceptance_criteria=[
"propose_feature_spotlight() is called once with an "
"under-publicized, not-yet-covered feature"
"under-publicized, not-yet-covered feature, OR skip=True "
"with a substantive skip_reason when nothing is worth "
"spotlighting this cycle"
],
team=Team.BOARD,
assigned_to=_foundation.AGENTS["head-marketing"].uuid,
@@ -472,6 +708,7 @@ class XEngine(BaseService):
)
)
markers.set_x_seen_features(task, seen)
markers.set_x_spotlight_brief(task, brief)
await self.session.flush()
self.log.info(
"feature-spotlight exploration opened (Head of Marketing)",
@@ -486,6 +723,29 @@ class XEngine(BaseService):
async def is_feature_seen(self, feature_slug: str) -> bool:
return await self.session.get(XSeenFeatureTable, feature_slug) is not None
async def skip_feature_spotlight(
self, *, exploration_task: TaskTable, reason: str
) -> TaskTable:
"""Complete a HoM "nothing worth spotlighting this cycle" verdict.
No draft is materialized and no feature slug is marked seen (a skip
covers nothing). The exploration task completes exactly like a
materialized spotlight does, so its ``updated_at`` feeds
``_last_spotlight_activity`` a skip counts as this cycle's activity,
which is what keeps the smart-cadence guard from re-firing daily into
the same quiet period. Called only from the propose_feature_spotlight
content verb's skip=True branch.
"""
markers.set_x_spotlight_skip_reason(exploration_task, reason)
exploration_task.status = TaskStatus.COMPLETED
await self.session.flush()
self.log.info(
"x-engine: feature spotlight skipped (nothing to spotlight)",
task_id=str(exploration_task.id),
reason=reason,
)
return exploration_task
# ---- shared origination -------------------------------------------------
async def _originate_post(
+11
View File
@@ -10,6 +10,7 @@ from roboco.services.settings import FEATURE_FLAGS, validate_setting
_DEFAULT_INTERVAL = 1800
_DEFAULT_MAX_OPEN = 10
_DEFAULT_SPOTLIGHT_INTERVAL_SECONDS = 86400 # 1 day
def test_x_engine_disabled_by_default() -> None:
@@ -46,3 +47,13 @@ def test_x_feature_spotlight_disabled_by_default() -> None:
def test_x_feature_spotlight_flag_registered_in_feature_flags() -> None:
assert "x_feature_spotlight_enabled" in [key for key, _ in FEATURE_FLAGS]
def test_x_feature_spotlight_interval_defaults_to_one_day() -> None:
"""CEO directive: default cadence is 1 day (was 3 days) — the engine's
own smart-cadence guard stretches this to 3x when quiet instead of the
interval itself defaulting slower."""
assert (
Settings().x_feature_spotlight_interval_seconds
== _DEFAULT_SPOTLIGHT_INTERVAL_SECONDS
)
@@ -300,3 +300,81 @@ async def test_propose_feature_spotlight_default_wants_video_false_leaves_marker
assert env.error is None
assert env.status == "feature_spotlight_proposed"
assert markers.get_x_feature_ref(materialized) is None
# --------------------------------------------------------------------------- #
# skip — the HoM "nothing worth spotlighting this cycle" exit
# --------------------------------------------------------------------------- #
@pytest.mark.asyncio
async def test_propose_feature_spotlight_skip_forbidden_for_product_owner() -> None:
env = await _actions("product_owner").propose_feature_spotlight(
agent_id=uuid4(), skip=True, skip_reason="nothing shipped worth spotlighting"
)
assert env.error == "not_authorized"
@pytest.mark.asyncio
async def test_propose_feature_spotlight_skip_requires_reason() -> None:
env = await _actions("head_marketing").propose_feature_spotlight(
agent_id=uuid4(), skip=True
)
assert env.error == "invalid_state"
@pytest.mark.asyncio
async def test_propose_feature_spotlight_skip_rejects_short_reason() -> None:
env = await _actions("head_marketing").propose_feature_spotlight(
agent_id=uuid4(), skip=True, skip_reason="meh"
)
assert env.error == "invalid_state"
@pytest.mark.asyncio
async def test_propose_feature_spotlight_skip_no_open_exploration_is_invalid_state(
monkeypatch: pytest.MonkeyPatch,
) -> None:
task_svc = MagicMock()
task_svc.list_open_feature_explorations = AsyncMock(return_value=[])
monkeypatch.setattr("roboco.services.task.get_task_service", lambda _s: task_svc)
env = await _actions("head_marketing").propose_feature_spotlight(
agent_id=uuid4(), skip=True, skip_reason="nothing shipped worth spotlighting"
)
assert env.error == "invalid_state"
@pytest.mark.asyncio
async def test_propose_feature_spotlight_skip_completes_exploration_without_draft(
monkeypatch: pytest.MonkeyPatch,
) -> None:
agent_id = uuid4()
exploration = _FakeTask(assigned_to=agent_id)
task_svc = MagicMock()
task_svc.list_open_feature_explorations = AsyncMock(return_value=[exploration])
monkeypatch.setattr("roboco.services.task.get_task_service", lambda _s: task_svc)
engine = MagicMock()
engine.skip_feature_spotlight = AsyncMock(return_value=exploration)
monkeypatch.setattr("roboco.services.x_engine.get_x_engine", lambda _s: engine)
reason = "nothing shipped worth spotlighting this cycle"
env = await _actions("head_marketing").propose_feature_spotlight(
agent_id=agent_id, skip=True, skip_reason=reason
)
assert env.error is None
assert env.status == "feature_spotlight_skipped"
assert env.task_id == str(exploration.id)
engine.skip_feature_spotlight.assert_awaited_once_with(
exploration_task=exploration, reason=reason
)
engine.materialize_feature_spotlight.assert_not_called()
@pytest.mark.asyncio
async def test_propose_feature_spotlight_missing_fields_is_invalid_state() -> None:
"""Defaulting feature_slug/feature_title/body to "" (so skip=True never
forces dummy values) must not weaken non-skip validation."""
env = await _actions("head_marketing").propose_feature_spotlight(agent_id=uuid4())
assert env.error == "invalid_state"
@@ -142,3 +142,50 @@ def test_feature_spotlight_prompt_names_real_verbs_and_seen_features() -> None:
empty_prompt = orch._build_feature_spotlight_prompt(_feature_task())
assert "none yet" in empty_prompt.lower()
def test_feature_spotlight_prompt_names_the_skip_exit() -> None:
"""The HoM must know the skip=True exit exists and that it beats a weak
forced spotlight otherwise the smart-cadence work is pointless."""
orch = _make_orch()
prompt = orch._build_feature_spotlight_prompt(_feature_task())
assert "skip=True" in prompt
assert "skip_reason=" in prompt
def test_feature_spotlight_prompt_renders_enriched_brief_sections() -> None:
"""The x_spotlight_brief marker's seen-dates, shipped-since, and rejected
sections must reach the spawn prompt HoM actually sees."""
orch = _make_orch()
markers_dict = {
"x_seen_features": ["org-memory"],
"x_spotlight_brief": {
"seen": [{"slug": "org-memory", "seen_at": "2026-07-01T00:00:00+00:00"}],
"shipped_since": [
{
"version": "0.21.0",
"date": "2026-07-09",
"titles": ["Added", "Fixed"],
}
],
"rejected": [
{"slug": "old-one", "title": "Old Feature", "reason": "too niche"}
],
},
}
prompt = orch._build_feature_spotlight_prompt(
_feature_task(orchestration_markers=markers_dict)
)
assert "org-memory (seen 2026-07-01)" in prompt
assert "0.21.0" in prompt
assert "Added, Fixed" in prompt
assert "too niche" in prompt
def test_feature_spotlight_prompt_brief_fallbacks_when_marker_missing() -> None:
"""No x_spotlight_brief marker (a pre-brief exploration) must never break
prompt rendering every section falls back to a friendly placeholder."""
orch = _make_orch()
prompt = orch._build_feature_spotlight_prompt(_feature_task())
assert "nothing new since the last cycle" in prompt
assert "(none)" in prompt
+351
View File
@@ -791,6 +791,13 @@ async def test_feature_spotlight_exploration_carries_seen_features_marker(
await db_session.flush()
_enable(monkeypatch, x_feature_spotlight_enabled=True)
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
# These seen rows are unrelated dedup fixtures, not a real "recent
# activity" signal for the smart-cadence guard — bypass it here (a
# dedicated no-network test covers the guard itself below) so this test
# stays about the seen-features marker only.
monkeypatch.setattr(
engine, "_last_spotlight_activity", AsyncMock(return_value=None)
)
task = await engine.open_feature_spotlight_exploration()
assert task is not None
assert set(markers.get_x_seen_features(task)) == {
@@ -876,6 +883,350 @@ async def test_materialize_feature_spotlight_enforces_280_chars(
assert len(body) <= MAX_TWEET_CHARS
# --------------------------------------------------------------------------- #
# CHANGELOG.md parsing — pure functions, no DB/network needed
# --------------------------------------------------------------------------- #
_CHANGELOG_FIXTURE = (
"# Changelog\n\n"
"## [0.21.0] - 2026-07-09\n\n"
"### Added\n\n- thing one\n\n### Fixed\n\n- thing two\n\n"
"## [0.20.0] - 2026-07-01\n\n"
"### Added\n\n- older thing\n"
)
def test_parse_changelog_sections_extracts_version_date_titles() -> None:
sections = x_engine_module._parse_changelog_sections(_CHANGELOG_FIXTURE)
assert sections[0] == {
"version": "0.21.0",
"date": "2026-07-09",
"titles": ["Added", "Fixed"],
}
assert sections[1] == {
"version": "0.20.0",
"date": "2026-07-01",
"titles": ["Added"],
}
def test_parse_changelog_sections_empty_text_yields_no_sections() -> None:
assert x_engine_module._parse_changelog_sections("no headers here") == []
def test_sections_since_excludes_earlier_and_includes_later() -> None:
sections = x_engine_module._parse_changelog_sections(_CHANGELOG_FIXTURE)
cutoff = datetime(2026, 7, 8, 12, 0, tzinfo=UTC)
result = x_engine_module._sections_since(sections, cutoff)
assert [s["version"] for s in result] == ["0.21.0"]
def test_sections_since_same_calendar_day_as_cutoff_is_not_new() -> None:
"""Day-granularity conservatism: a section dated the same day as the
cutoff can't be ordered against it, so it's treated as not-new."""
sections = [{"version": "0.21.0", "date": "2026-07-09", "titles": ["Added"]}]
cutoff = datetime(2026, 7, 9, 1, 0, tzinfo=UTC)
assert x_engine_module._sections_since(sections, cutoff) == []
# --------------------------------------------------------------------------- #
# Smart spotlight cadence: pending-draft guard, activity-stretch, skip verb
# --------------------------------------------------------------------------- #
async def _seed_completed_exploration(
session: AsyncSession, *, project_id: UUID, age: timedelta = timedelta(seconds=0)
) -> TaskTable:
"""A COMPLETED x_feature_exploration row with an explicit updated_at —
``onupdate`` only fires on a real UPDATE, not this direct INSERT, so the
activity timestamp must be set here rather than relying on the column
default."""
now = datetime.now(UTC)
task = TaskTable(
id=uuid4(),
title="X feature-spotlight exploration",
description="seed",
acceptance_criteria=["x"],
status=TS.COMPLETED,
task_type=TaskType.ADMINISTRATIVE,
nature=TaskNature.NON_TECHNICAL,
project_id=project_id,
created_by=SYSTEM_UUID,
assigned_to=HOM_UUID,
team=Team.BOARD,
source=X_FEATURE_EXPLORATION_SOURCE,
confirmed_by_human=False,
created_at=now - age,
updated_at=now - age,
)
session.add(task)
await session.flush()
return task
@pytest.mark.asyncio
async def test_feature_spotlight_pending_draft_blocks_new_cycle(
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A still-open materialized x_feature draft blocks a new exploration —
never stack a second spotlight draft while one awaits the CEO."""
await _seed(db_session)
_enable(monkeypatch, x_feature_spotlight_enabled=True)
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
exploration = await engine.open_feature_spotlight_exploration()
assert exploration is not None
await engine.materialize_feature_spotlight(
exploration_task=exploration,
feature_slug="org-memory",
feature_title="Organizational Memory Loop",
body="Did you know RoboCo agents learn from every completed task?",
)
second = await engine.open_feature_spotlight_exploration()
assert second is None
assert await get_task_service(db_session).list_open_feature_explorations() == []
@pytest.mark.asyncio
async def test_feature_spotlight_new_cycle_resumes_once_draft_acted_on(
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Once the CEO acts on the draft (cancelled here, mirroring reject), the
pending-draft guard no longer blocks a fresh cycle."""
await _seed(db_session)
_enable(monkeypatch, x_feature_spotlight_enabled=True)
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
exploration = await engine.open_feature_spotlight_exploration()
assert exploration is not None
draft = await engine.materialize_feature_spotlight(
exploration_task=exploration,
feature_slug="org-memory",
feature_title="Organizational Memory Loop",
body="Did you know RoboCo agents learn from every completed task?",
)
draft.status = TS.CANCELLED # mirrors XPostService.reject
await db_session.flush()
# The activity-stretch guard is covered separately below; bypass it here
# so this test is only about the pending-draft guard clearing.
monkeypatch.setattr(
engine, "_feature_activity_stretch_skip", AsyncMock(return_value=False)
)
second = await engine.open_feature_spotlight_exploration()
assert second is not None
@pytest.mark.asyncio
async def test_feature_spotlight_activity_stretch_skips_when_quiet(
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Nothing shipped since the last (recent) spotlight activity and less
than 3x the interval has elapsed -> the cycle is skipped."""
await _seed(db_session)
_enable(
monkeypatch,
x_feature_spotlight_enabled=True,
x_feature_spotlight_interval_seconds=3600,
)
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
project = await engine._roboco_project()
assert project is not None and project.id is not None
await _seed_completed_exploration(db_session, project_id=cast("UUID", project.id))
monkeypatch.setattr(engine, "_shipped_sections_since", AsyncMock(return_value=[]))
task = await engine.open_feature_spotlight_exploration()
assert task is None
assert await get_task_service(db_session).list_open_feature_explorations() == []
@pytest.mark.asyncio
async def test_feature_spotlight_activity_stretch_fires_when_something_shipped(
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Even right after the last activity, a newer CHANGELOG section clears
the stretch guard and the cycle proceeds."""
await _seed(db_session)
_enable(
monkeypatch,
x_feature_spotlight_enabled=True,
x_feature_spotlight_interval_seconds=3600,
)
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
project = await engine._roboco_project()
assert project is not None and project.id is not None
await _seed_completed_exploration(db_session, project_id=cast("UUID", project.id))
monkeypatch.setattr(
engine,
"_shipped_sections_since",
AsyncMock(
return_value=[
{"version": "9.9.9", "date": "2099-01-01", "titles": ["Added"]}
]
),
)
task = await engine.open_feature_spotlight_exploration()
assert task is not None
@pytest.mark.asyncio
async def test_feature_spotlight_activity_stretch_fires_after_stretched_window(
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Nothing shipped, but the 3x-stretched window has already elapsed since
the last activity -> the cycle proceeds anyway (the stretch has a
ceiling, it doesn't silence the engine forever)."""
await _seed(db_session)
_enable(
monkeypatch,
x_feature_spotlight_enabled=True,
x_feature_spotlight_interval_seconds=10,
)
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
project = await engine._roboco_project()
assert project is not None and project.id is not None
await _seed_completed_exploration(
db_session,
project_id=cast("UUID", project.id),
age=timedelta(seconds=100), # > 3 * 10s stretched window
)
monkeypatch.setattr(engine, "_shipped_sections_since", AsyncMock(return_value=[]))
task = await engine.open_feature_spotlight_exploration()
assert task is not None
@pytest.mark.asyncio
async def test_feature_spotlight_no_activity_history_never_stretched(
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
"""First-ever cycle (no seen rows, no completed explorations): the
activity-stretch guard never even reads the changelog."""
await _seed(db_session)
_enable(monkeypatch, x_feature_spotlight_enabled=True)
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
spy = AsyncMock(return_value=[])
monkeypatch.setattr(engine, "_shipped_sections_since", spy)
task = await engine.open_feature_spotlight_exploration()
assert task is not None
spy.assert_not_awaited()
@pytest.mark.asyncio
async def test_feature_spotlight_activity_stretch_fails_open_on_changelog_error(
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A changelog-read failure must never silently starve the engine of
cycles fail open (proceed) rather than skip."""
await _seed(db_session)
_enable(
monkeypatch,
x_feature_spotlight_enabled=True,
x_feature_spotlight_interval_seconds=3600,
)
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
project = await engine._roboco_project()
assert project is not None and project.id is not None
await _seed_completed_exploration(db_session, project_id=cast("UUID", project.id))
monkeypatch.setattr(
engine,
"_shipped_sections_since",
AsyncMock(side_effect=RuntimeError("clone failed")),
)
task = await engine.open_feature_spotlight_exploration()
assert task is not None
@pytest.mark.asyncio
async def test_skip_feature_spotlight_completes_without_draft_or_seen_slug(
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
await _seed(db_session)
_enable(monkeypatch, x_feature_spotlight_enabled=True)
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
exploration = await engine.open_feature_spotlight_exploration()
assert exploration is not None
reason = "nothing shipped this cycle worth a spotlight"
result = await engine.skip_feature_spotlight(
exploration_task=exploration, reason=reason
)
assert result.status == TS.COMPLETED
assert markers.get_x_spotlight_skip_reason(result) == reason
assert await get_task_service(db_session).list_open_feature_explorations() == []
assert await get_task_service(db_session).list_open_x_posts() == []
@pytest.mark.asyncio
async def test_skip_feature_spotlight_counts_as_activity(
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A skip (no seen-features row at all) still advances
_last_spotlight_activity via the completed exploration's updated_at."""
await _seed(db_session)
_enable(monkeypatch, x_feature_spotlight_enabled=True)
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
exploration = await engine.open_feature_spotlight_exploration()
assert exploration is not None
assert await engine._last_spotlight_activity() is None
await engine.skip_feature_spotlight(
exploration_task=exploration, reason="quiet week, nothing shipped"
)
assert await engine._last_spotlight_activity() is not None
@pytest.mark.asyncio
async def test_gather_spotlight_brief_includes_dates_shipped_and_rejected(
db_session: AsyncSession, monkeypatch: pytest.MonkeyPatch
) -> None:
await _seed(db_session)
db_session.add(XSeenFeatureTable(feature_slug="org-memory"))
await db_session.flush()
_enable(monkeypatch, x_feature_spotlight_enabled=True)
engine = x_engine_module.XEngine(db_session, client=_FakeClient())
project = await engine._roboco_project()
assert project is not None and project.id is not None
rejected_task = await engine._originate_post(
title="X post: feature spotlight — Old Feature",
body="a previously drafted spotlight body",
source=X_FEATURE_SOURCE,
project_id=cast("UUID", project.id),
)
markers.set_x_feature_ref(
rejected_task, {"slug": "old-one", "title": "Old Feature"}
)
markers.set_x_reject_reason(rejected_task, "too niche for the audience")
rejected_task.status = TS.CANCELLED
await db_session.flush()
monkeypatch.setattr(
engine,
"_last_spotlight_activity",
AsyncMock(return_value=datetime.now(UTC) - timedelta(days=1)),
)
monkeypatch.setattr(
engine,
"_shipped_sections_since",
AsyncMock(
return_value=[
{"version": "1.2.3", "date": "2026-07-09", "titles": ["Added"]}
]
),
)
brief = await engine._gather_spotlight_brief()
assert brief["seen"] == [
{"slug": "org-memory", "seen_at": brief["seen"][0]["seen_at"]}
]
assert brief["shipped_since"] == [
{"version": "1.2.3", "date": "2026-07-09", "titles": ["Added"]}
]
assert brief["rejected"] == [
{
"slug": "old-one",
"title": "Old Feature",
"reason": "too niche for the audience",
}
]
# --------------------------------------------------------------------------- #
# Voice guide (feeds release/reply prompts + the HoM identity's briefing claim)
# --------------------------------------------------------------------------- #