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)` | | `notify_ack` | `notify_ack(notification_id: UUID)` |
| `read_messages` | `read_messages()` | | `read_messages` | `read_messages()` |
| `read_a2a` | `read_a2a(see do_server)` | | `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)` | | `notify_ack` | `notify_ack(notification_id: UUID)` |
| `read_messages` | `read_messages()` | | `read_messages` | `read_messages()` |
| `read_a2a` | `read_a2a(see do_server)` | | `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 ## auditor
+2
View File
@@ -176,6 +176,8 @@ async def do_propose_feature_spotlight(
body=body.body, body=body.body,
wants_video=body.wants_video, wants_video=body.wants_video,
video_script=body.video_script, video_script=body.video_script,
skip=body.skip,
skip_reason=body.skip_reason,
) )
return envelope_to_response(env, request) return envelope_to_response(env, request)
+10 -4
View File
@@ -130,13 +130,19 @@ class ProposeRoadmapRequest(BaseModel):
class ProposeFeatureSpotlightRequest(BaseModel): class ProposeFeatureSpotlightRequest(BaseModel):
"""Head of Marketing's feature-spotlight draft: a picked feature + a """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_slug: str = Field(default="", max_length=128)
feature_title: str = Field(..., min_length=1) feature_title: str = ""
body: str = Field(..., min_length=1) body: str = ""
wants_video: bool = False wants_video: bool = False
video_script: str = "" video_script: str = ""
skip: bool = False
skip_reason: str = ""
class ProposeVideoRequest(BaseModel): class ProposeVideoRequest(BaseModel):
+5 -3
View File
@@ -953,9 +953,11 @@ class Settings(BaseSettings):
), ),
) )
x_feature_spotlight_interval_seconds: int = Field( x_feature_spotlight_interval_seconds: int = Field(
default=259200, # 3 days — tunable; marketing cadence is a CEO call, not a default=86400, # 1 day — tunable; marketing cadence is a CEO call, not a
# technical constant. Sits between the 30-min mentions poll # technical constant. This is the BASE loop tick only: the engine's own
# and the weekly roadmap cycle. # 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, ge=3600,
description="Seconds between feature-spotlight exploration cycles.", 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_POSTED_TWEET_ID = "x_posted_tweet_id"
X_FEATURE_REF = "x_feature_ref" X_FEATURE_REF = "x_feature_ref"
X_SEEN_FEATURES = "x_seen_features" X_SEEN_FEATURES = "x_seen_features"
X_SPOTLIGHT_BRIEF = "x_spotlight_brief"
X_SPOTLIGHT_SKIP_REASON = "x_spotlight_skip_reason"
ROADMAP_CYCLE = "roadmap_cycle" ROADMAP_CYCLE = "roadmap_cycle"
VIDEO_DRAFT = "video_draft" VIDEO_DRAFT = "video_draft"
VIDEO_REJECT_REASON = "video_reject_reason" 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]) 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 --------------------------------------------------- # --- board roadmap cycle ---------------------------------------------------
# The themed cycle (goal + item drafts) the Product Owner authors via # The themed cycle (goal + item drafts) the Product Owner authors via
# ``propose_roadmap`` onto the exploration task the roadmap engine opened. # ``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( def propose_feature_spotlight(
feature_slug: str, feature_slug: str = "",
feature_title: str, feature_title: str = "",
body: str, body: str = "",
wants_video: bool = False, wants_video: bool = False,
video_script: str = "", video_script: str = "",
skip: bool = False,
skip_reason: str = "",
) -> dict[str, Any]: ) -> 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 Call this exactly ONCE per exploration cycle, after investigating the
CHANGELOG, feature-flags ledger, docs/map, charter, and KB to pick a real, 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 under-publicized capability. The draft is held in the X post queue for the
CEO to edit/approve nothing auto-posts. 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: Args:
feature_slug: Stable slug identifying the feature (the dedup key). feature_slug: Stable slug identifying the feature (the dedup key).
feature_title: Short human title of the feature. Ignored when skip=True.
body: The tweet text (plain, <=280 chars, no invented facts). 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 wants_video: Also request a companion video (held separately for CEO
approval, when the video engine is armed for spotlights). approval, when the video engine is armed for spotlights).
video_script: Optional script for that video; falls back to the video_script: Optional script for that video; falls back to the
feature title/body when omitted. 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( return _post(
"/api/v1/do/propose_feature_spotlight", "/api/v1/do/propose_feature_spotlight",
@@ -616,6 +632,8 @@ def propose_feature_spotlight(
"body": body, "body": body,
"wants_video": wants_video, "wants_video": wants_video,
"video_script": video_script, "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 _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: class AgentOrchestrator:
""" """
Manages Claude Code containers for all agents. 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.""" """Prompt for the Head of Marketing's one-shot feature-spotlight cycle."""
task_id = task.get("id", "unknown") task_id = task.get("id", "unknown")
markers_dict = task.get("orchestration_markers") or {} markers_dict = task.get("orchestration_markers") or {}
seen = markers_dict.get(_markers.X_SEEN_FEATURES) or [] seen_line = _format_seen_features(markers_dict)
seen_line = ", ".join(seen) if seen else "(none yet — this is the first cycle)" shipped_line = _format_shipped_since(markers_dict)
rejected_line = _format_rejected_spotlights(markers_dict)
return f"""\ return f"""\
You are the Head of Marketing. It's time for your periodic feature-spotlight cycle. 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 RoboCo markets its own capabilities, not just releases. Investigate what the
company has actually shipped and draft ONE marketing post about a genuinely company has actually shipped and draft ONE marketing post about a genuinely
useful, under-publicized capability something a user or prospect would not 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} 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 == == WHAT TO DO ==
1. triage() see your board-level context. 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>", 5. propose_feature_spotlight(feature_slug="<a short stable slug>",
feature_title="<human-readable feature name>", body="<the post>") feature_title="<human-readable feature name>", body="<the post>")
call this EXACTLY ONCE. 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 If nothing shipped is genuinely worth spotlighting this cycle, call
approval. 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 Do NOT claim, plan, delegate, or attempt to post anything yourself that is
not your job here, and the gateway will reject those. not your job here, and the gateway will reject those.
+51 -4
View File
@@ -1297,22 +1297,67 @@ class ContentActions:
) )
return None 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( async def propose_feature_spotlight(
self, self,
*, *,
agent_id: UUID, agent_id: UUID,
feature_slug: str, feature_slug: str = "",
feature_title: str, feature_title: str = "",
body: str, body: str = "",
wants_video: bool = False, wants_video: bool = False,
video_script: str = "", video_script: str = "",
skip: bool = False,
skip_reason: str = "",
) -> Envelope: ) -> 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 Validates role, field lengths, the 280-char tweet limit, and that the
feature hasn't already been covered, then materializes the held X-queue feature hasn't already been covered, then materializes the held X-queue
draft and completes the caller's exploration task. One call per cycle. 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 ``wants_video`` optionally requests a companion video. The video
authoring task no longer opens here it opens later, at CEO-approve authoring task no longer opens here it opens later, at CEO-approve
time (``XPostService._open_spotlight_video``, gated on time (``XPostService._open_spotlight_video``, gated on
@@ -1332,6 +1377,8 @@ class ContentActions:
remediate="this verb is Head-of-Marketing-only", remediate="this verb is Head-of-Marketing-only",
context_briefing={}, context_briefing={},
) )
if skip:
return await self._propose_feature_spotlight_skip(agent_id, skip_reason)
if rej := self._reject_feature_spotlight_fields( if rej := self._reject_feature_spotlight_fields(
feature_slug, feature_title, body feature_slug, feature_title, body
): ):
+17
View File
@@ -1588,6 +1588,23 @@ class TaskService(BaseService):
) )
return list(result.scalars().all()) 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]: async def list_external_pr_reviews_awaiting_decision(self) -> list[TaskTable]:
"""Completed external-PR reviews still awaiting the CEO's decision. """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 from __future__ import annotations
import re
from datetime import UTC, datetime, timedelta 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 httpx
import redis.asyncio as redis import redis.asyncio as redis
from sqlalchemy import select from sqlalchemy import func, select
from roboco.config import settings from roboco.config import settings
from roboco.db.tables import ( from roboco.db.tables import (
AgentSpawnSessionTable, AgentSpawnSessionTable,
TaskTable,
XSeenFeatureTable, XSeenFeatureTable,
XSeenMentionTable, XSeenMentionTable,
) )
@@ -61,7 +64,7 @@ if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession 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 from roboco.services.task import TaskService
_CHAT_TIMEOUT_SECONDS = 60.0 _CHAT_TIMEOUT_SECONDS = 60.0
@@ -147,11 +150,65 @@ _FEATURE_EXPLORATION_TITLE = "X feature-spotlight exploration"
_FEATURE_EXPLORATION_DESCRIPTION = ( _FEATURE_EXPLORATION_DESCRIPTION = (
"Investigate RoboCo's own shipped capabilities — CHANGELOG.md, the " "Investigate RoboCo's own shipped capabilities — CHANGELOG.md, the "
"feature-flags ledger, docs/map, the company charter, and the knowledge " "feature-flags ledger, docs/map, the company charter, and the knowledge "
"base — and pick ONE under-publicized feature not already covered (see " "base — and pick ONE under-publicized, fresh-but-unspotlighted feature "
"the seen-features list on this task). Draft ONE marketing post via " "not already covered (see the seen-features list on this task). Draft "
"propose_feature_spotlight()." "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): class XEngine(BaseService):
"""Draft release posts (event hook) + mention replies (poll), both held.""" """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 No-ops when the flags are off, no X credentials are configured (drafting
content nobody can ever post is pointless mirrors the release/mentions 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 guard), a materialized spotlight draft is still awaiting the CEO (never
the RoboCo project isn't resolvable. Never authors content itself — HoM stack a second one), nothing has shipped since the last spotlight
does, via propose_feature_spotlight() once the dispatcher spawns it. 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): if not await self._feature_spotlight_may_proceed():
return None
client = await self._client()
if not client.configured:
return None return None
task_svc = get_task_service(self.session) task_svc = get_task_service(self.session)
# Cancel a stale + spawnless exploration so the engine can re-arm; a # 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) 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: async def _cancel_stale_exploration(self, task_svc: TaskService) -> bool:
"""Return False to block re-arm (fresh cycle or live HoM spawn); return """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 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 self, task_svc: TaskService, project_id: UUID
) -> TaskTable: ) -> TaskTable:
seen = await self._seen_feature_slugs() seen = await self._seen_feature_slugs()
brief = await self._gather_spotlight_brief()
task = await task_svc.create( task = await task_svc.create(
TaskCreateRequest( TaskCreateRequest(
title=_FEATURE_EXPLORATION_TITLE, title=_FEATURE_EXPLORATION_TITLE,
description=_FEATURE_EXPLORATION_DESCRIPTION, description=_FEATURE_EXPLORATION_DESCRIPTION,
acceptance_criteria=[ acceptance_criteria=[
"propose_feature_spotlight() is called once with an " "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, team=Team.BOARD,
assigned_to=_foundation.AGENTS["head-marketing"].uuid, assigned_to=_foundation.AGENTS["head-marketing"].uuid,
@@ -472,6 +708,7 @@ class XEngine(BaseService):
) )
) )
markers.set_x_seen_features(task, seen) markers.set_x_seen_features(task, seen)
markers.set_x_spotlight_brief(task, brief)
await self.session.flush() await self.session.flush()
self.log.info( self.log.info(
"feature-spotlight exploration opened (Head of Marketing)", "feature-spotlight exploration opened (Head of Marketing)",
@@ -486,6 +723,29 @@ class XEngine(BaseService):
async def is_feature_seen(self, feature_slug: str) -> bool: async def is_feature_seen(self, feature_slug: str) -> bool:
return await self.session.get(XSeenFeatureTable, feature_slug) is not None 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 ------------------------------------------------- # ---- shared origination -------------------------------------------------
async def _originate_post( async def _originate_post(
+11
View File
@@ -10,6 +10,7 @@ from roboco.services.settings import FEATURE_FLAGS, validate_setting
_DEFAULT_INTERVAL = 1800 _DEFAULT_INTERVAL = 1800
_DEFAULT_MAX_OPEN = 10 _DEFAULT_MAX_OPEN = 10
_DEFAULT_SPOTLIGHT_INTERVAL_SECONDS = 86400 # 1 day
def test_x_engine_disabled_by_default() -> None: 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: def test_x_feature_spotlight_flag_registered_in_feature_flags() -> None:
assert "x_feature_spotlight_enabled" in [key for key, _ in FEATURE_FLAGS] 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.error is None
assert env.status == "feature_spotlight_proposed" assert env.status == "feature_spotlight_proposed"
assert markers.get_x_feature_ref(materialized) is None 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()) empty_prompt = orch._build_feature_spotlight_prompt(_feature_task())
assert "none yet" in empty_prompt.lower() 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() await db_session.flush()
_enable(monkeypatch, x_feature_spotlight_enabled=True) _enable(monkeypatch, x_feature_spotlight_enabled=True)
engine = x_engine_module.XEngine(db_session, client=_FakeClient()) 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() task = await engine.open_feature_spotlight_exploration()
assert task is not None assert task is not None
assert set(markers.get_x_seen_features(task)) == { 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 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) # Voice guide (feeds release/reply prompts + the HoM identity's briefing claim)
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #