# news-watcher / news.yaml
#
# Tunable strategy for the news watch engine. This is the GLOBAL config.
# Per-KB topic configuration lives in each KB's SCHEMA.md under a
# `news_watch:` block; see the SCHEMA snippet below for the shape.
#
# Override: copy to {vault}/agntspace/system/skills/core/news-watcher/
# config/news.yaml.

# Master switch. When false, the engine never registers cron jobs and
# never fires manual ticks. Lets a user kill all news watchers without
# editing per-KB SCHEMA blocks.
enabled: true

# Cron expression DEFAULT for any topic that doesn't declare its own
# `cadence_cron`. Standard 5-field cron syntax. The default is every
# 6 hours at the top of the hour, which is the OpenClaw-validated
# cadence for news-shaped intake.
default_cadence_cron: "0 */6 * * *"

# How many search results to fetch per topic per tick. Smaller = less
# bandwidth + faster cycles. Larger = more candidates but more LLM
# review calls (each survivor goes through fact-checker).
default_max_search_results: 8

# How many candidates make it through deterministic gates and reach
# the LLM relevance reviewer per tick. Hard cap so a noisy news day
# can't blow the LLM budget. Set to 0 to disable cap.
default_max_reviewed_per_tick: 5

# ── "Is this news?" — recency signals ────────────────────────────────
#
# Real news services (Google News, Apple News) lean on TWO signals:
# (1) a search-time recency filter, and (2) per-page publish-date
# extraction. We replicate both. Without them, the watcher would
# happily re-scrape 2018 listicles and litter the KB with articles
# the user already read years ago.

# Search-time recency filter passed to DuckDuckGo as `timelimit`.
# Allowed values: "d" (past day), "w" (past week), "m" (past month),
# "y" (past year), null (no filter). Most real news watching wants
# "w" — narrow enough to filter listicles, wide enough to catch
# weekend-published articles by Monday morning.
default_search_time_window: "w"

# Hard age cap on the article's own published_at. Engine extracts
# publish_date from JSON-LD (schema.org NewsArticle / Article) →
# OpenGraph (article:published_time) → <time datetime=> → meta
# (DC.date / pubdate) → URL date patterns (/2026/04/27/), in that
# order. Sources older than this are rejected at the deterministic
# stage BEFORE fact-checker review (saves LLM tokens).
default_max_age_days: 14

# When true, a source that has NO extractable published_at is
# rejected. When false (default), un-dated sources go through to
# fact-checker — useful for evergreen topic pages, encyclopedia
# entries, or sources that genuinely lack metadata. Tighten to
# `true` for hard news watching where un-dated content is suspicious.
require_publish_date: false

# Hard quality threshold. The fact-checker scores 0.0–1.0; we only keep
# sources at or above this. 0.65 is the empirical sweet spot for
# general-topic news on capable-tier models. Drift below 0.55 and you
# start admitting clickbait; drift above 0.80 and you reject most
# legitimate secondary coverage.
relevance_threshold: 0.65

# LLM tier for the news-watcher skill itself. The skill prompt itself
# refuses `fast` (small models can't reliably score relevance + flag
# clickbait), but this knob is the single source of truth for the
# engine's invocation. NEVER set to `fast` — see the long note in
# `news-watcher/SKILL.md` about why.
llm_tier: capable

# Reviewer agent. The skill always delegates the per-source review
# here. Do not change unless you are introducing a new fact-checker
# variant — the prompt is tuned for the existing fact-checker agent.
reviewer_agent: fact-checker

# What the engine writes when a tick produces ZERO keepers. Set to
# `true` to log a low-importance "news_watch_empty_tick" event so the
# user can confirm the watcher is alive but legitimately empty (vs.
# disabled or broken). false hides empty ticks from the activity feed.
log_empty_ticks: true

# Domains the deterministic search-result filter ALWAYS skips, in
# addition to research_scrape's existing filter (PDF / social media).
# News-specific noise: video aggregators, content farms.
extra_skip_domains:
  - "youtube.com"
  - "tiktok.com"
  - "reddit.com"
  - "medium.com"  # too many AI-generated rewrites; trust upstream sources
  - "buzzfeed.com"

# When relevance_threshold rejects a source, also blacklist its URL
# for the next N hours so the search doesn't re-scrape and re-reject
# the same noise on the next tick. 0 to disable.
url_cooldown_hours: 24

# Hard cap on URLs in the cooldown blacklist (per KB+topic) so the
# memory footprint doesn't grow unboundedly over years. Oldest first
# eviction.
max_cooldown_entries: 500

# Per-KB override format (FYI — not read from this file).
#
# Each Dedicated Base can declare news-watch topics in its own
# `wiki/_index.md` or `SCHEMA.md` like this:
#
#   news_watch:
#     enabled: true
#     topics:
#       - name: "AI safety incidents"
#         enabled: true
#         query_variants:
#           - "frontier model safety incident 2026"
#           - "LLM jailbreak disclosure"
#         cadence_cron: "0 */4 * * *"     # optional, override default
#         max_search_results: 10          # optional, override default
#         relevance_threshold: 0.7        # optional, override default
#         search_time_window: "d"         # optional — past day for hot topics
#         max_age_days: 3                 # optional — strict recency
#         require_publish_date: true      # optional — strict, reject undated
#       - name: "EU AI Act enforcement"
#         enabled: false                  # paused without losing config
#         query_variants:
#           - "EU AI Act fine"
#           - "European AI regulator action"
#
# A KB with no `news_watch:` block is simply not a news watcher. The
# engine scans every KB SCHEMA at startup and registers one cron job
# per (KB, enabled topic) pair.
