Niche Topic Discovery — Operator Guide
Day-to-day operator instructions for the niche-aware topic discovery + RAG writer modes shipped on 2026-04-30 (Glad-Labs/poindexter#278). For the architecture rationale (why this exists, table layout, decay math, provenance trail) seedocs/architecture/niches-and-rag-modes.md.
This page is the action surface — what to type, when to type it.
What this is, in 3 sentences
Topic discovery now runs per niche. You configure a niche once, then operate it as a stream of 5-candidate batches. The operator picks a winner from each batch; that handoff creates acontent_task that flows through
the regular pipeline.
Day-zero setup
The Glad Labs niche is pre-seeded (now folded into the baseline migration):- slug
glad-labs, writer modeTWO_PASS, batch size 5, cadence floor 60 minutes - 5 weighted goals —
AUTHORITY35,EDUCATION25,BRAND20,TRAFFIC15,REVENUE5 - 5 enabled sources —
internal_rag50,hackernews20,devto15,web_search10,knowledge5
poindexter niche create CLI yet (see “What’s not yet built” below).
Until that lands, insert via SQL or call services.niche_service.NicheService
from a Python shell:
The daily workflow
1. Trigger a sweep
Sweeps run one of four ways:poindexter topics sweep --niche <slug>— fires an immediate sweep on demand. Calls the sameTopicBatchService.run_sweepthat the scheduler uses, so cadence-floor + open-batch checks still apply. Prints the new batch id + candidate count or the reason a sweep was skipped. This is the recommended interactive path (Glad-Labs/poindexter#349).- Scheduled
run_niche_topic_sweepjob — fires on the scheduler’s default cadence (every 30 minutes, gated per-niche bydiscovery_cadence_minute_floor). Default for hands-off operation. - Idle worker auto-trigger — the legacy
IdleWorkerbackground loop fires periodically (subject to the niche’sdiscovery_cadence_minute_floorand thetopic_discovery_auto_enabledmaster switch — see kill-switch section). - Manual one-shot trigger — set
topic_discovery_manual_trigger=trueviapoindexter settings set; the next idle worker tick sees it and fires regardless of the auto switch.
2. View the open batch
external / internal), effective scores,
and titles.
3. Rank candidates
Pick your preferred order, best-first. The operator rank is the source of truth — it does not have to match the system rank.4. (Optional) Edit the winner’s title or angle
5. Resolve — push the winner into the pipeline
resolved, advances operator_rank=1 to a
content_task, and unblocks the next sweep for that niche (subject to the
cadence floor).
6. Or — reject everything and ask for a fresh sweep
niche_carry_forward_decay_factor (default 0.7).
The 4 writer RAG modes
Set per-niche onniches.writer_rag_mode. Tasks without a writer_rag_mode
fall back to the legacy generator, so pre-niche pipelines still work.
TWO_PASS revision loop is hard-capped at
writer_rag_two_pass_max_revision_loops (default 3), and each external
research call is capped at writer_rag_two_pass_research_max_sources
(default 2 sources per [EXTERNAL_NEEDED] marker).
All four modes use pipeline_writer_model for the LLM call (no per-mode
model override yet — see “What’s not yet built”).
The kill-switch for legacy auto-discovery
Thetopic_discovery_auto_enabled setting (seeded on by default):
- Default
true— the legacyIdleWorkerdiscovery loop keeps firing on its old signals (queue-low, stale-content, rejection-streak, 24h safety net). OSS installs without configured niches keep their existing behavior. - Set
false— all auto-firing branches bail out early with an INFO log. Manual triggers still work. Use this for any install where you want to drive everything throughpoindexter topics.
false.
Duplicate suppression — the dedup layers
Candidates are deduplicated at three seams before any GPU run starts:- Tap ingest + sweep intake — the engine selected by
topic_dedup_engine(defaultcontent_embedding) runs two passes:- Recent-coverage (
services/topic_recent_coverage.py): the candidate’s title+angle composite is embedded and compared against composites of recently published posts (title + source-task topic + winning candidate’s angle) and in-flight tasks. Like-for-like short text is what separates a re-tread from a same-domain neighbour — the 2026-07-23 incident pair (“Ditching Grafana for Native Telemetry” vs the published “The Shift to Native Telemetry”) scored 0.79–0.86 here while sitting below the content threshold on the title-vs-content signal. Matches are named: the sweep emits one aggregatedtopic_duplicate_suppressedfinding listing each dropped candidate and the post that blocked it. - Content-embedding: the bare title against published-post content
chunks at
topic_dedup_existing_threshold_content(catches re-treads whose angle is new but whose body would re-state an existing post).
- Recent-coverage (
- Batch handoff (
resolve-batch/ auto-resolve) — the winner that actually ships (operator edits included) is re-checked against recent coverage. A match raises a 400 (CLI/HTTP) naming the colliding post; the auto-resolver self-heals by expiring the batch (audit rowtopic_batch_auto_expired, reasonrecent_coverage) so the niche never wedges. This seam also protects batches swept before a similar post published, and operator picks made from the phone. - QA rail backstop —
qa.content_originalitycompares the finished draft’s chunks against published posts (advisory by default; see the anti-hallucination doc for graduation viaqa_gates).
Tuning knobs you’ll actually touch
Most of these live inapp_settings and are seeded with sane defaults. The
two niches table columns at the top are per-niche, not app_setting.
Set any app_setting via:
UPDATE niches SET ... WHERE slug=...
until the niche edit CLI lands — except the cadence target, which has its own
command:
niches.cadence_target_posts_per_day — a per-niche override that
probe_cadence_slo (brain/health_probes.py) checks in addition to the
site-wide cadence_slo_expected_posts_per_day target. A niche without this
set isn’t checked individually; it’s only covered by the site-wide check.
Troubleshooting
What’s not yet built
Honest gap list as of 2026-04-30:- No
poindexter niche create / edit / set-goal / enable-sourceCLI. New niches and goal/source edits go in via SQL or via theservices.niche_service.NicheServicePython API. The CLI subgroup exposesniche list,niche show, andniche set-cadence— everything else still needs SQL. - Writer-mode model selection is hardcoded to
pipeline_writer_model. Every niche writer mode uses the one writer pin — there’s no per-mode “a smaller model for the STORY_SPINE outline, a heavier one for the TWO_PASS revise” knob yet. (Thecost_tier.*tier ladder that might once have backed this was removed in PR #1907; a per-mode pin would be the replacement.) niche_goal_descriptionsis a single global JSON blob in app_settings. Per-niche goal prompt overrides need aniche_goal_promptstable keyed by(niche_id, goal_type). Filed as a follow-up; the JSON shape is the unblock-now compromise.- Some commands shown in
docs/architecture/niches-and-rag-modes.md(poindexter topics discover,poindexter niche create,poindexter niche set-goal,poindexter niche enable-source) are forward-looking — they describe the intended end state, not the current CLI surface.