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.
| Mode | Snippet limit | What it does | Use when |
|---|---|---|---|
TOPIC_ONLY | writer_rag_topic_only_snippet_limit = 8 | Top 8 internal pgvector snippets dropped into the writer prompt. Single pass, no enforcement. Cheapest mode. | Broad niche, no hard citation contract, lowest cost per draft. |
CITATION_BUDGET | writer_rag_citation_budget_snippet_limit = 12 | Top 12 snippets fetched. Writer must cite ≥ N (default 3, key writer_rag_citation_budget_min_citations). Drafts under-budget are rejected pre-QA. | Authority/depth posts where unsupported claims must be cut. |
STORY_SPINE | writer_rag_story_spine_snippet_limit = 15 | Outline preprocessing pass (5-beat: hook / what_happened / why_it_matters / what_we_learned / close), then writer expands to prose. | Long-form narrative, postmortems, decision history, journey-style retros. |
TWO_PASS | writer_rag_two_pass_snippet_limit = 20 | Internal-only first draft → writer marks [EXTERNAL_NEEDED: ...] for missing facts → bounded external research → revise. LangGraph state machine. Glad Labs default. | First-person reporting on something nobody else has covered. Most expensive. |
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.
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.
| Knob | Where | Default | What it controls |
|---|---|---|---|
niches.batch_size | per-niche column | 5 (Glad Labs) | Candidates per batch |
niches.discovery_cadence_minute_floor | per-niche column | 60 min (Glad Labs) | Minimum gap between sweeps for that niche |
niches.writer_rag_mode | per-niche column | TWO_PASS (Glad Labs) | Switch the writer mode |
topic_discovery_auto_enabled | app_settings | true | Legacy auto-discovery master kill-switch (see above) |
niche_carry_forward_decay_factor | app_settings | 0.7 | How fast unpicked candidates fade across batches |
niche_top_n_per_pool | app_settings | 5 | Top-N per pool fed to the LLM final-score stage |
niche_internal_rag_per_kind_limit | app_settings | 4 | Items per source-kind fed into the internal RAG candidate pool |
niche_batch_expires_days | app_settings | 7 | How long an open batch can sit before it expires |
niche_embedding_model | app_settings | nomic-embed-text | Ollama embedding model (must match embeddings dim) |
niche_ollama_chat_timeout_seconds | app_settings | 60 | Per-call HTTP timeout for the LLM scorer / distillation calls |
niche_goal_descriptions | app_settings (JSON) | (7-key dict) | Prose anchors for each goal_type — drives goal-vector embedding |
writer_rag_topic_only_snippet_limit | app_settings | 8 | Snippet count for TOPIC_ONLY |
writer_rag_citation_budget_snippet_limit | app_settings | 12 | Snippet count for CITATION_BUDGET |
writer_rag_citation_budget_min_citations | app_settings | 3 | Minimum internal citations the CITATION_BUDGET writer must hit |
writer_rag_story_spine_snippet_limit | app_settings | 15 | Snippet count for STORY_SPINE |
writer_rag_two_pass_snippet_limit | app_settings | 20 | Snippet count for TWO_PASS first draft |
writer_rag_two_pass_max_revision_loops | app_settings | 3 | Hard cap on the TWO_PASS revise loop |
writer_rag_two_pass_research_max_sources | app_settings | 2 | Sources fetched per [EXTERNAL_NEEDED] marker |
writer_rag_context_snippet_max_chars | app_settings | 500 | Per-snippet character cap when building the writer prompt block |
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
| Symptom | Likely cause | Fix |
|---|---|---|
Tasks land in awaiting_approval you never queued | Legacy auto-discovery still firing | poindexter settings set topic_discovery_auto_enabled false --category topic_discovery |
topics show-batch says “No open batch” | Either no sweep has run yet, or the last batch is still resolving | poindexter topics sweep --niche <slug> (see daily workflow §1) |
| Sweep runs but the batch is empty | internal_rag source disabled in niche_sources, or no recent embeddings to mine from | poindexter topics niche show <slug> and check sources; verify embeddings table has recent rows |
| Writer keeps citing the wrong source | TOPIC_ONLY has no enforcement; you may want CITATION_BUDGET | Switch the niche’s writer_rag_mode, or refine the niche’s prompt override (see follow-up table below) |
TWO_PASS keeps hitting the revision cap and ships drafts with [EXTERNAL_NEEDED] markers still in them | Hard cap of 3 loops reached | Bump writer_rag_two_pass_max_revision_loops cautiously — high values risk runaway cost |
| Scoring LLM call times out | Local Ollama model is slow or wedged | Bump niche_ollama_chat_timeout_seconds or check Ollama health |
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.