91 Commits

Author SHA1 Message Date
Matt Van Horn d05389d39b feat(telegram): add opt-in Telegram public channel source (#1035)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-08-21 22:59:01 +01:00
Sk 35ff7b0549 feat(perplexity): migrate Sonar integration to the Agent API (#1019) 2026-08-18 18:06:39 +01:00
Matt Van Horn 40b42b7bed feat(x): bird first, grok CLI opt-in only (#1005)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-08-14 10:16:04 -07:00
Matt Van Horn 698bfd0c3d feat(x): X search via Grok CLI plus first-party evidence fixes (#991)
Grok CLI X search with no X credential, first-party evidence fixes, skip-floor when unidentified, and thin-retry handle protection.
2026-08-14 00:55:57 -07:00
Matt Van Horn e93c8249d8 feat(amazon): buyer-signal source via Bright Data CLI (#988)
Adds an opt-in `amazon` source backed by the Bright Data CLI: product
discovery with live ratings and prices, plus a capped sample of recent
reviews woven in as buyer voice.

The signal it exists for is drift -- an all-time rating set against the
average of only the reviews inside the last 30 days. The emoji footer
names products and the direction they moved rather than counting
inventory, following the Polymarket shape:

  📦 Amazon: 12 products │ Spirit E-325 Gas 4.4★→4.8★, Genesis E-325 Gas 4.4★ quiet

Off by default and dual-gated: the CLI must be on PATH and logged in, and
the run must explicitly request the source. It never auto-fires from
inferred intent. A drift arrow requires n>=5 dated in-window reviews,
since a live date census showed Amazon's top-reviews ordering is only
weakly recency-biased.

Includes doctor registration, honest setup reporting, SKILL.md trigger
guidance with worked examples, and CONFIGURATION.md coverage.

Reviewed by three independent agents plus Greptile (5/5). Fixes landed
for: the API key being visible in argv, --amazon-query leaking into
competitor sub-runs, a lane deadline that did not bound wall clock, brand
inference that silently disabled the competitor guard, and a saved-report
source list that dropped whole sources from the per-source dump.

Verified with three live end-to-end runs against a funded account.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 14:40:01 -07:00
Trevin Chow ef6c279ceb fix(security): block SessionStart .env key RCE and align project trust (#914) 2026-07-30 10:14:55 -07:00
Trevin Chow 5cc8e7cceb fix(youtube): stop comparison-mode search timeouts under fan-out (#901) 2026-07-28 13:51:13 -07:00
Trevin Chow bfe1453c66 fix(trustpilot): auto-activate source when domain is explicitly pinned
Passing --trustpilot-domain (or a plan-level trustpilot_domain) was accepted
and then silently ignored unless INCLUDE_SOURCES/--search already opted in.
Treat an explicit domain pin as intent and activate the source for the run.

Fixes #873
2026-07-24 17:04:54 -07:00
Trevin Chow cda646a5a0 Merge origin/main into agent/youtube-transcript-fallback-spend
Resolve CHANGELOG.md conflict by keeping both the YouTube transcript
fallback fix and the #867/#868 Unreleased entries from main.
2026-07-24 15:06:42 -07:00
Trevin Chow 8a0fcf40fb fix(docs): align Reddit ScrapeCreators backup copy with empty-only behavior
SKILL.md and CONFIGURATION claimed rate-limit/unavailable escalation and SC
comment enrichment on the free path; document the real empty-only search
backup, thinness/primary knobs, and shreddit comments. Fixes #867.
2026-07-24 14:01:39 -07:00
terry-li-hm f780817a50 fix(youtube): avoid unnecessary transcript fallback spend 2026-07-24 20:47:37 +08:00
Matt Van Horn 5ccda4036b feat(discover): host-judged three-leg discovery protocol (#856)
* fix(discover): fold same-story duplicates, velocity-sorted ranks, loud fallback note

Survivors sharing evidence (identical top_comment or >=2 shared evidence
URLs) fold to the higher-velocity twin; display ranks and angle topic_ids
are assigned from the velocity-sorted order so rank order matches the
displayed velocity_score; non-mock runs with no reasoning provider emit
one loud stderr note. Mock fixture URLs/comments are namespaced per topic
slug so the fold cannot collapse distinct mock topics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BL9KsoytvnR1WfvZa7SEN7

* feat(discover): handoff contracts module - nominations bundle, judgments, angles

New lib/discovery_handoff.py owns the three-leg protocol file contracts:
versioned bundle writer/reader (full seed items as engine resume state,
bundle id, TTL, momentum window, leg-1 invocation context), strict-top/
lenient-row judgments reader with ported name sanitation and collision
disambiguation over host names, angles reader with the ported 200-char
cap, host-facing digest builder, and typed HandoffContractError for
exit-2 mapping that names both searched locations. schema.py gains the
discovery-nominations kind and duck-typed nomination serializers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BL9KsoytvnR1WfvZa7SEN7

* feat(discover): three-leg protocol CLI - nominate-only, judgments, finalize, angles flags

New flags with orphan/mutual-exclusion rejection (exit 2, message names
the combination), mock protocol legs require --save-dir to stay
side-effect-free, one handoff-state resolver (save-dir else config dir),
and a dispatch wrapper that maps HandoffContractError to stderr + exit 2.
Leg bodies are distinct NotImplementedError stubs replaced by U3-U5.
Bare --discover and --discover-shallow dispatch regression-pinned
unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BL9KsoytvnR1WfvZa7SEN7

* feat(discover): nominate leg - sweep to bundle, digest, nothing-solid short-circuit

run_discover_nominate shares the exact sweep/cluster/nominate code with
the one-shot path (factored _discovery_sweep, nominate_topic_pool,
shared source-boundary helpers), cuts at the full judge pool instead of
the enrichment limit, never resolves providers, and writes the versioned
bundle with heuristic fallback names, momentum window, and leg-1
invocation context. Zero nominations renders the nothing-solid brief in
leg 1 with no bundle. Digest names the bundle path and mandates reading
its evidence before judging.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BL9KsoytvnR1WfvZa7SEN7

* feat(discover): resume leg - host judgments, deep enrichment, pending report

run_discover_resume applies host judgments (per-row heuristic fallback,
collision resolution over the whole pool), excludes host-junk from
enrichment slots outright, skips sub-corroborated heuristic junk
pre-enrichment, blend-cuts to the slot limit, and runs tier-parameterized
enrichment (deep: default depth, 4 workers, LAST30DAYS_ENRICH_BUDGET_SECONDS
default 450 via the config allowlist; shallow and one-shot keep quick/240/3,
pinned both ways). Velocity scores against the bundle window. Floor, fold,
and velocity ranks share the one-shot code via extracted helpers. One
main-thread pending-report write (fresh TTL, run_ref, angle inputs keyed by
surviving nomination ids); stdout emits angle inputs plus instructions.
No queue writes on this leg.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BL9KsoytvnR1WfvZa7SEN7

* feat(discover): finalize leg - host angles, render, idempotent queue write

--discover --finalize loads the pending report (typed errors name both
searched locations and the resume-leg remedy; TTL from the leg-2 write),
applies host angles by nomination id, renders through the one-shot's
emit flow with O_EXCL artifact saving, and records the topic queue once,
guarded. record_discovery_surfacing gains a per-run idempotency guard:
a matched row already stamped with this run_ref neither increments nor
re-annotates, so finalize retries render byte-identical output. Mock
finalize stays queue-free. The full mock three-leg sequence is pinned
end-to-end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BL9KsoytvnR1WfvZa7SEN7

* feat(discover)!: delete the engine-side LLM judge - the host is the judge

Removes lib/discovery_judge.py and every provider touch in the discovery
path: no resolve_runtime in run_discover, no stage-1 verdict blending,
no stage-2 angle pass. One-shot discovery always names via topic_shape
heuristics, ranks velocity-only, renders no angles, and emits one loud
note pointing at the SKILL.md host-judged protocol (no provider-key
advice). Mock guard tightened to no-provider-client-constructed across
all legs; grep-level pins keep the judge from coming back. providers.py
untouched for the normal pipeline. Coverage 87.53% vs the 84 floor.

Keyed one-shot cron users lose LLM naming/angles by design; the
protocol replaces them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BL9KsoytvnR1WfvZa7SEN7

* feat(skill): LAW 11 - YOU ARE THE JUDGE; three-command discovery protocol in SKILL.md

New LAW 11 with the LAW-7-style anticipated-misread note (the one-shot
heuristics stderr line is a skipped-protocol signal, never a capability
constraint) and a pre-Bash self-check. Step 1 DISCOVERY branch rewritten
as the three-leg protocol: nominate-only (180s), judge via mktemp
heredoc judgments file, resume (600s), host angles file, finalize (60s)
relay-verbatim; identical --save-dir threaded through all legs;
fail-twice degradation to bare one-shot; shallow tier for sub-8-minute
shell caps. CONFIGURATION.md documents the four new flags; CONCEPTS.md
Discovery/Nomination name the host as judge; README trending mentions
updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BL9KsoytvnR1WfvZa7SEN7

* refactor(discover): consolidate handoff validation, fix leg-3 binding error text

Simplify pass on the branch: shared _parse_handoff_envelope for the two
engine-written files, one _search_paths helper, shared _known_rows gate
for host-file rows, schema._source_status_from_dict for both report
deserializers, schema._utc_now reuse, _discover_domain helper. Real fix:
_require_bundle_binding now names the pending report and the resume-leg
remedy when the finalize leg's angles file mismatches - it previously
pointed the host's retry at the nominations bundle; regression-pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BL9KsoytvnR1WfvZa7SEN7

* fix(review): handoff contract hardening - save-dir isolation, junk type-gate, fail-closed bundle, fenced digest

Explicit save-dir is now the single handoff store (no config-dir
fallback; matches scoped-db semantics and SKILL.md's own contract);
judgments junk accepts only real booleans (null/"false" fall back
per-row); non-list or zero-valid-row nominations fail closed instead of
rendering nothing-solid; bundle writes raise the protocol's exit-2 error
on OSError instead of a traceback; bundle_id mismatch remedy now says
fix-the-id-and-retry-this-leg; digest evidence rides inside the
untrusted-content fence the deleted judge used; SKILL.md documents the
leg-2 budget knob and data-not-instructions rule. Lenient-row paths
regression-pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BL9KsoytvnR1WfvZa7SEN7

* fix(review): protocol leg semantics - degraded-state plumbing, retry history, guard hoist, mock parity

Bundle and pending report now carry the leg-1 sweep's source_status and
a mock flag; the resume report restores degraded-source warnings and one
shared strict-exit helper gates every leg terminal (nothing-solid paths
included) exactly like the one-shot. Finalize retries reconstruct
pre-run queue history instead of dropping Pipeline lines; --as-of and
html-emit guards hoisted to all discover invocations; malformed pending
bodies and unwritable pending writes become clean exit-2 contract
errors; fresh rounds invalidate stale pending files; the same-story fold
re-scans to a fixpoint so three-way chains collapse; mock/real handoff
state cannot cross legs. Duplicated render/save and queue-warning blocks
extracted into shared helpers. Coverage 87.68%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BL9KsoytvnR1WfvZa7SEN7

* docs(changelog): unreleased entries for host-judged discovery protocol

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BL9KsoytvnR1WfvZa7SEN7

* docs(changelog): fill PR number for host-judged discovery entries

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BL9KsoytvnR1WfvZa7SEN7

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 14:14:41 -07:00
Matt Van Horn f9a3c919bc feat(discover): judged topic names, junk gate, angles, topic queue (#852)
* feat(discover): add topic_shape name distiller and junk-shape classifier

* feat(discover): stage-1 judge pass - short names, junk flags, worthiness-blended ranking

* feat(discover): add angle and topic-queue fields to DiscoveryTopic, bump export schema to 1.1

* feat(discover): junk-shape floor gate counts seed sources; weak_signal prefers non-junk failures

* feat(discover): stage-2 angle pass renders podcast and X-article hooks on trend cards

* feat(discover): persistent topic queue - surfacing memory, covered tracking, queue CLI

* feat(discover): SKILL.md trending trigger, relay contract for angle/pipeline lines, queue interactions

* chore(discover): drop unused query import in rerank

* refactor(discover): dedupe judge/angle plumbing, reuse shared normalizers, fix Pipeline line wording

* fix(review): guard queue hook (P0), two-phase annotation, domain-preserving upsert, queue-list message, ordinal tests, SKILL.md queue fast path (#1 #4 #6 #8 #9)

* fix(review): dict-payload guard, extract discovery_judge module, multi-token collision disambiguation (#2 #3 #5)

* fix(review): covered status survives judge naming drift - fuzzy-matched covered priors born covered (#7)

* docs(changelog): fill PR link for discovery content pipeline entries

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-20 23:01:34 -07:00
micmicalpha 665f9a893b feat(youtube): fetch comments free via yt-dlp, drop the ScrapeCreators requirement (#827)
YouTube comments previously required a paid ScrapeCreators key plus a
youtube_comments opt-in in INCLUDE_SOURCES. yt-dlp already backs YouTube
search and transcripts here and can fetch comments too, so the comment
lane no longer needs a credential or an opt-in.

- youtube_yt: new _ytdlp_comments_result() returns (comments, ran_cleanly)
  so a clean "video has zero comments" run never falls back to a paid SC
  call; the list-returning _fetch_video_comments_ytdlp() wraps it. yt-dlp
  is tried first; SC stays as the backstop only on genuine failure and only
  when a token is configured. Command requests top-sorted comments, bounded
  by _COMMENT_TIMEOUT=20s per video (3 videos, parallel).
- env.is_youtube_comments_available: True whenever yt-dlp is installed;
  legacy SC path still applies when yt-dlp is absent; EXCLUDE_SOURCES=
  youtube_comments remains a hard off-switch that wins over both.
- doctor: stop prescribing a paid SC key for comments when yt-dlp is
  present (was selling a fix for a non-problem); caveat now names yt-dlp
  (free) as the first way out.
- CONFIGURATION.md: YouTube comments row corrected to free/keyless.
- Tests: new tests/test_youtube_comments_ytdlp.py locks the command flags,
  the yt-dlp-first preference, the no-SC-on-clean-empty behavior, and the
  no-key availability gate; existing doctor/gating tests updated to the
  new contract and pinned hermetic (is_ytdlp_available derived from probe).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 16:53:22 -07:00
SAI MOHANEESH 39cca461aa fix: register LAST30DAYS_DEBUG in env keys, lazy resolution, fix xai_x crash (#770)
LAST30DAYS_DEBUG had three bugs:
1. Not registered in CONFIGURATION_KEYS - .env values were silently ignored
2. Eager module-level os.environ.get() in log.py meant --debug flag
   and .env values were never picked up by log.debug()
3. http.DEBUG in xai_x.py referenced a non-existent attribute,
   causing AttributeError on xAI API errors

Fixes:
- Add LAST30DAYS_DEBUG to the CONFIGURATION_KEYS tuple in env.py
- Export to os.environ after get_config() so log.py's lazy check
  picks up .env values
- Replace eager DEBUG constant with lazy is_debug() function
- Fix http.DEBUG -> log.is_debug() in xai_x.py
- Add CONFIGURATION.md section for --debug / LAST30DAYS_DEBUG
2026-07-15 16:47:25 -07:00
Matt Van Horn de3b692b1c docs(doctor): document four-state audit, --postmortem, --probe (U8)
SKILL.md doctor section + the source_status note, CONFIGURATION.md health
section (new commands, corrected network-note, LAST30DAYS_DOCTOR_PROBE_TIMEOUT),
and a CHANGELOG Unreleased entry for the audit redesign, post-mortem,
live probe, CLI health, new sources, sub-lanes, and the Threads fix.
2026-07-14 20:58:36 -07:00
Matt Van Horn d31d5f3b3a feat(discover): rebuild --discover as nominate -> enrich -> floor; add global trending (#816)
* feat(discover): U1 nominate stage with keyword-gate toggle for global vs domain

Extract the discovery fetch+normalize sweep into a reusable nominate_candidates()
and add a keyword_gate param to _fetch_discovery_source. Domain discovery keeps the
gate on; global trending (no domain) turns it off so the river feeds' own hot
ranking is the signal. run_discover now delegates its fetch loop to
nominate_candidates(keyword_gate=True), preserving existing behavior.

* feat(discover): U2 nominate_topics - named, seed-ranked candidate topics

Extract the cluster/rank/name block from run_discover into nominate_topics(),
returning Nomination objects (name, seed velocity score, cluster items, leader
summary). This is the contract between the nominate stage and the upcoming
enrichment fan-out: names deduped casefold, ranked by cheap seed velocity,
never padded past the evidence. run_discover consumes nominations with
identical output to before.

* feat(discover): U3 enrichment fan-out - full pipeline pass per nomination

enrich_nominations() runs the real research pipeline (run() with
internal_subrun=True, the comparison-mode lane) on each nominated topic in a
bounded ThreadPoolExecutor against a wall-clock batch budget. A topic whose
sub-run raises is kept as nomination-only with the error recorded; topics
unfinished at budget expiry are likewise dropped to nomination-only. The batch
never raises and preserves nomination order. This is what buys discovery the
whole multi-source corpus (Reddit+comments, X, YouTube, Techmeme, arXiv, HN,
Polymarket, web) with zero bespoke fetch code.

* feat(discover): U4 confidence floor - 'nothing solid' beats ranked noise

THE bug fix for the 2026-07-12 junk-trend regression (five 1-like tweets
ranked as a 'sports' trend list). passes_discovery_floor() gates every topic:
an absolute engagement floor kills junk outright, then a topic clears via
independent cross-source confirmation OR a genuinely strong single-source
spike (a 1,600-point HN thread is a story; a 30-upvote meme is not).

run_discover gains enrich= (full pipeline pass per nomination via U3) and
judges each topic on its enriched corpus when available, seed evidence
otherwise. Zero floor-clearing topics -> DiscoveryReport.outcome
'nothing-solid' with the strongest sub-floor name in weak_signal, exported
through to_discovery_export. Regression test: the junk corpus now returns an
honest empty result.

* feat(discover): U5 global trending mode - bare --discover with no domain

--discover DOMAIN is now nargs-optional: bare --discover sweeps every river
feed's own hot list (r/all, HN front page, Digg) with the keyword gate off -
the feeds' hot ranking is the signal and the confidence floor culls junk. X
sits out of the global nominate stage (its search lane needs a keyword) and
joins per-topic at enrichment. Dispatch keys on 'flag present' so the empty
domain is never mistaken for 'flag absent'.

Discovery now enriches by default (full research pass per nomination);
--discover-shallow is the fast escape hatch that ranks on listing evidence
only, still floored. Global saves slug as 'trending'.

* feat(discover): U6 rich trend cards + honest nothing-solid rendering

DiscoveryTopic gains top_comment (strongest verbatim community comment from
the enriched corpus, per-platform vote-normalized, with attribution) and
corroboration_count; both exported in to_discovery_export. Renderer upgrades:
'# Trending now' header for global runs, a 'confirmed across N sources' badge
on the momentum line, a '**Community voice:**' quote line on enriched cards,
and an explicit nothing-solid empty state naming the closest weak signal
instead of the old generic 'no clusters survived' line.

* feat(discover): U7 Techmeme + arXiv reach discovery through enrichment

Enrichment sub-runs get requested_sources=None by default, so every available
source - Techmeme, arXiv, YouTube, Polymarket, community comments - joins each
nominated topic's research pass despite having no river feed of its own. An
explicit user --search boundary now holds through enrichment too (the CLI
threads the original pre-narrowed list as enrich_requested_sources). The
keyword-source nominate gap (X/Techmeme/arXiv have no front-page lane) is
documented at the planner.

* docs(discover): U8 route global vs domain trending; document two-stage flow

SKILL.md discovery branch now routes bare trending asks (/last30days trending,
'what's hot') to bare --discover and domain asks to --discover DOMAIN,
documents the nominate-enrich timing (10-minute Bash timeout), the
--discover-shallow fast path, and pins 'Nothing solid this window' as a valid
verbatim pass-through outcome (never retry or fabricate around it).
CONFIGURATION.md and the JSON export reference document the new flag surface
and contract fields (outcome, weak_signal, top_comment, corroboration_count).

* fix(discover): strip quote chars from community-voice bodies to avoid doubled quotes

Live global-trending verification surfaced comments whose bodies start with a
quote character rendering as doubled quotes inside the card's wrapping quotes.

* fix(discover): address PR #816 review - real budget enforcement + shallow-flag guard

P1: enrichment workers now run as daemon threads behind a semaphore instead of
a ThreadPoolExecutor. Executor threads are non-daemon and joined at interpreter
shutdown, so one hung sub-run could keep the process alive long after its topic
was downgraded to nomination-only. Daemon workers make the wall-clock budget
real; abandonment is safe because internal_subrun passes write nothing to disk
and every fetch layer carries its own timeout.

P2: --discover-shallow without --discover now errors (exit 2) instead of
silently no-opping into a full research run.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-12 23:04:52 -07:00
Matt Van Horn 22e095fa0a fix(sources): honor INCLUDE_SOURCES for xiaohongshu and dripstack (#812)
* fix(sources): honor INCLUDE_SOURCES for xiaohongshu and dripstack

Both sources were per-run-request-only, so a persisted
INCLUDE_SOURCES=xiaohongshu or dripstack in .env was silently ignored -
the exact silent-ignore class documented from #707/#708 - and the only
persistent route (LAST30DAYS_DEFAULT_SEARCH) replaces the whole source
set instead of adding to it. Both gates now follow the established
LinkedIn/Perplexity/Trustpilot opt-in pattern: active when the source
is in INCLUDE_SOURCES or explicitly requested, otherwise off, with the
xiaohongshu availability probe still firing only after an opt-in.
Default behavior is unchanged; tests lock the checkbox on, the default
off, and the probe never firing without opt-in.

* fix(sources): strip whitespace in INCLUDE_SOURCES tokens

INCLUDE_SOURCES=linkedin, dripstack (space after the comma) produced a
' dripstack' token that failed the exact membership checks, silently
dropping the persisted opt-in - a pre-existing wart for every
INCLUDE_SOURCES consumer that this PR's hoisted computation now fixes
for all of them.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-12 08:48:10 -07:00
Charlie Ruiz 0a1a5799ae Add DripStack to related skills (#791)
* Add DripStack to related skills

DripStack is a complementary AI agent skill for premium financial
newsletter research via micropayments. Same audience, different data
layer — last30days covers public/social signal, DripStack covers
paywalled financial signal.

* Add DripStack as a source: premium financial newsletter search

DripStack indexes paid Substack newsletters, analyst writeups, and
financial podcasts. The search endpoint is free and public (no API key
required) — returns article metadata with relevance-scored snippets.

Integration:
- New source module: scripts/lib/dripstack.py
- Normalizer registered in normalize.py
- Wired into pipeline.py (always available, like GitHub/HN)
- Planner assigns it to opinion/prediction intents alongside StockTwits
- Mock data for tests
- SKILL.md display name mapping
- CONFIGURATION.md source table entry

Signal: complementary to StockTwits (retail sentiment) and Polymarket
(prediction odds) — DripStack covers what professional analysts and paid
newsletter authors are writing about. Publication attribution (e.g.
'SemiAnalysis', 'Bloomberg') is high-credibility signal for synthesis.

* fix(dripstack): requested-only gating, shared http path, window filtering

Maintainer hardening on #791 before merge (owner decision: every new
source ships default-off with an explicit toggle):

- DripStack activates only when explicitly requested (--search dripstack
  or LAST30DAYS_DEFAULT_SEARCH), mirroring the Xiaohongshu guard; default
  runs send zero traffic to dripstack.xyz and their available_sources set
  is unchanged.
- All requests route through the shared lib/http.py choke point so
  capture/replay, fixtures, and the failure taxonomy apply.
- Results honor the 30-day window at fetch (dated items outside the
  window are dropped; undated items are kept, not guessed).
- Docs describe the opt-in; the README Related Skills block recommending
  a separate skill install is removed (out of scope for a source PR).
- tests/test_dripstack.py covers gating, choke point, windowing, failure,
  and normalization.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-12 08:29:40 -07:00
yzy c410a2d141 [codex] Document and auto-detect Xiaohongshu source (#766)
* document xiaohongshu source

* auto-detect xiaohongshu local service
2026-07-12 08:12:33 -07:00
Matt Van Horn 3b0b021e2c feat(sources): local corpus source - your own files as a ranked signal (#808)
* feat(sources): local corpus source - your own files as a ranked signal

* fix: address self-review findings

* fix: address round-2 residual (surgical round)

* fix: defang corpus sentinels, matching-window snippets, exclusion-aware hosted gate, traversal notes, bounded discovery

* fix: keep absolute local paths out of corpus notes and coverage diagnostics

* fix(corpus): keep raw exception text out of coverage notes

OS and subprocess errors embed the failing absolute path in str(exc),
and scan/cache notes flow into source_status detail rendered in
coverage diagnostics outside the private corpus block. Notes now carry
the error's strerror (or class name) instead, so a permission failure
or file race can no longer leak a local path from a private run.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-12 07:33:01 -07:00
Matt Van Horn 9c020b1393 feat(grounding): per-claim freshness verdicts (--verify-freshness) (#805)
* feat(grounding): per-claim freshness verdicts (--verify-freshness)

* fix: address self-review findings

* fix: address round-2 residual (surgical round)

* fix: prefer active markets in mixed-event verification, plan-only cookies for cached verification

* fix: verify event identity on slug-fallback refetch before assigning verdicts

* test: use a real outcome datum key in slug-identity test

* fix(freshness): carry values in stale detail and announce zero-claim passes

A stale verdict's machine-readable detail said only that the value
moved; the rendered table already showed was/now, so agent-JSON
consumers saw less than readers. Compose the detail as
"moved: <original> -> <current>", formatting each value the way the
claim text does (percent for Polymarket/StockTwits, thousands
separators for stars).

A verification pass that extracts zero claims used to complete
silently, which misled a live post-hoc run. _verify_report_set now
prints one stderr note when no verified report produced any verdict,
aggregated across main and entity reports so multi-report passes note
it once; the drill path routes through the same helper.

* fix(freshness): verify star facts attached by candidate enrichment

A GitHub-flavored run produced zero verdicts live: star enrichment
attaches metadata["github_stars"] to candidates after reranking, but
claim extraction read only item-level engagement, and those candidates'
primary items are typically non-GitHub sources.

Extraction now emits one repo-keyed star claim per enriched repo
(skipping repos already claimed at item level). These claims cannot
ride the item-source dispatch - they would land unsupported as
"reddit"/"x" claims - so verify_report routes the repo-slug shape
straight to the GitHub refetcher, bypassing the grounding-item lookup
and the per-source outcome gate (the datum came from enrichment, not
the github search source). refetch_datum accepts an owner/repo slug as
the datum key, and the snapshot cache keys these claims by repo so
multiple candidates citing one repo share one request.

* fix(freshness): review fixes - honest evidence, joinable export, rendered detail

Four review findings on the hardening pass:

- Unsupported verdicts no longer fabricate evidence: evidence_url and
  evidence_timestamp stay empty when no fresh evidence was obtained;
  provenance remains on source_url/source_timestamp.
- Agent export results now carry candidate_id so consumers can join
  freshness_verdicts to the result they annotate (schema_version 1.2,
  golden snapshot and docs updated).
- The rendered verdict table shows the verifier's detail - formatted
  movement on stale rows, the un-checkable reason on unsupported rows -
  instead of reconstructing raw values.
- Item-level star dedup is scoped per candidate: another candidate's
  item-level claim no longer suppresses an enriched candidate's own
  verdict and inline flag; both share one repo snapshot.

* fix(freshness): require event-id identity on Polymarket slug fallback

The slug-fallback refetch verified the response slug but accepted a
slug-matched event whose id differed from the cached item's event id,
so a re-created event reusing a slug could produce current/stale
verdicts from another market's prices. When the cached item carries a
real numeric event id, the fallback now requires the response id to
match it (synthetic PM<N> parse ids carry no identity and skip the
check); mismatches raise and degrade to an unsupported verdict.

* fix(freshness): fail closed when the slug fallback has no event identity

A cached item with neither metadata.event_id nor a real numeric item id
(the synthetic PM<N> parse fallback) let the slug fallback accept
whatever event currently owns the slug, so a recreated or loosely
matched event could still drive a current/stale verdict. With no
identity to verify against, the refetch now raises before any request
and the claim degrades to unsupported.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-12 07:32:47 -07:00
Matt Van Horn a06e221552 feat(render): --register audience templates (exec/dev/creator), eli5 unified (#804)
* feat(render): --register audience templates (exec/dev/creator), eli5 unified

* fix: address self-review findings

* fix: apply audience emphasis weights to the lead Best Takes ranking

* fix: apply audience source weights inside the Best Takes ranking itself

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-12 07:30:30 -07:00
Matt Van Horn 6b62b867b5 feat(library): FTS search over saved briefs + self-citing library context (#803)
* feat(library): FTS search over saved briefs with self-citing library context

* fix: address self-review findings

* fix: safety note on search output, reject --output, repopulate FTS after table loss

* fix: per-library search index and scoped briefing archive for --save-dir libraries

* fix(library): keep scoped runs out of the shared store and index

Three leaks between --save-dir scopes and the shared research store:

- library search with --save-dir merged shared-store sightings into a
  scoped search, so one client's completed-run snippets could surface
  in another client's results. Scoped searches now read only a store
  inside the save dir (usually absent).
- The passive "From your library" context had the same fallback to
  the shared store on scoped runs; it now scopes identically.
- The markdown save hook synced the shared library.db from a scoped
  scan, and sync's stale-row prune could delete other scopes' (and the
  default library's) rows. Scoped saves now sync a per-directory index
  with the same paths scoped search uses.

* fix(store): write scoped-run findings to the save-dir store

Scoped reads (library search, library context) were fixed to stay
inside the save dir, but --store on a --save-dir run still wrote
findings through the shared research.db - so a scoped client's own
sightings never appeared in its scoped searches while remaining
visible to unscoped ones. persist_report now routes store access
through a scoped_db context to <save-dir>/research.db, the same path
the scoped read side uses; unscoped runs keep the shared store.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-12 07:30:14 -07:00
Matt Van Horn cb27c689b5 feat(library): index + Atom feed over the research library, publishable (#802)
* feat(library): publish saved research as index and Atom feed

Scan saved reports and briefing archives into deterministic, topic-grouped library pages with stable Atom entries. Add explicit opt-in multi-document publishing, publish-safe social IDs, slash-command guidance, configuration docs, and regression coverage.

* fix: address self-review findings

* fix: address round-2 residual (surgical round)

* fix: keep per-suffix reports distinct in library identity

* fix: scope briefing archive to the selected library, back up hand-written index/feed files

* fix(library): prune only marker-bearing briefs and never clobber a backup

A refresh deleted any orphaned briefs/ page whose filename looked
generated, so a hand-written page with a generated-looking name could
be removed. Rendered briefs now embed an ownership comment marker and
the prune requires it before unlink; name shape alone is no longer
grounds for deletion.

The hand-written index/feed backup used replace(), so a second refresh
overwrote the first .bak. Backups now pick the first free .bak/.bakN
name instead.

* fix(library): preserve hand-edited brief pages on refresh

The orphan prune checks the generator marker, but the brief write path
replaced briefs/<name>.html for current entries without any ownership
check, silently destroying a hand-edited or hand-written page whose
name matches a current report. Every library-feed write (briefs, index,
feed) now goes through one preservation helper: a page without the
generator marker is moved to the first free .bak/.bakN name before the
generated page is written.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-12 07:27:56 -07:00
Matt Van Horn 298310ca34 feat(pipeline): --discover mode for topic-less trending discovery (#801)
* feat(pipeline): add --discover mode for topic-less trending discovery

* fix: address self-review findings

* fix: address round-2 residual (surgical round)

* fix: boundary-aware category match, additive-only discovery totals, preserve discovery intent through onboarding

* fix: visible default-search fallback, non-plural domain anchors, clean X backend fallback

* fix(discover): apply domain filter to Digg listing clusters

The Digg source is an AI-focused leaderboard feed, so a discovery sweep
on a non-AI domain surfaced off-domain stories (observed live: a crypto
sweep returning model-release coverage). Filter parsed Digg clusters
with the same title-level _matches_discovery_domain check the adjacent
Hacker News branch already applies; an all-filtered result stays a
clean no-results outcome.

* fix(discover): surface the producing X backend's own error as partial

Failing over past a dead backend is a clean outcome, but when the
backend that actually produced items also returned an error, the sweep
is degraded; returning a clean state hid that from source_status and
strict-exit runs. Keep the producing backend's error; earlier fallback
errors remain stderr observability only.

* fix(discover): hold the configured source boundary instead of widening

When the configured source filter (LAST30DAYS_DEFAULT_SEARCH or
--search) contains no discovery-capable source, the sweep silently
widened to all discovery feeds with only a warning, querying sources
the user had filtered out. Discovery now exits 2 with the unsupported
sources and the discovery-capable set named, for both the config and
explicit-flag paths.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-12 07:27:39 -07:00
Matt Van Horn ffa2e31e9b feat(pipeline): --drill follow-up mode over the cached report (#800)
* feat(pipeline): add --drill follow-up mode over the cached report

* fix: address self-review findings

* fix: address round-2 residual (surgical round)

* fix: enforce drill source allowlist, exact-url merge collapse, window inheritance, skipped-source outcomes, cache-write verification

* fix: gate cached subreddit context on the drill source allowlist

* test: fix drill subreddit-gating regression fixture

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-12 07:24:40 -07:00
Matt Van Horn 0270c12352 feat(eval): research-quality regression harness with scored fixtures in CI (#799)
* feat(eval): research-quality regression harness with scored fixtures in CI

* fix: address self-review findings

* fix: harden eval replay (coherence floor, metadata-merge replay, offline star enrichment)

* fix: value-scrub module fixture recordings, per-fixture floors, pinned overlap predicate

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-12 07:22:01 -07:00
Matt Van Horn 7abcb9d108 feat(schema): versioned agent JSON export profile (--emit=json v1.0, raw fallback) (#810)
* feat(schema): versioned agent JSON export profile with --json-profile raw fallback

* fix: address self-review findings

* fix: exclude author-reach fields from headline engagement (stocktwits followers)

* fix: fail loudly when the evaluator receives the agent profile instead of raw Report

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-12 07:18:50 -07:00
Matt Van Horn 021bdbf60e feat(pipeline): typed per-run source outcomes with doctor-aligned failure states (#797)
* feat(pipeline): typed per-run source outcomes in the report

* fix: address self-review findings

* fix: propagate failure capture to nested pools, surface authed GitHub failures, classify wrapped timeouts, opt-in strict exit

* fix: pick the most specific captured failure, not the last-appended one

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-12 07:14:00 -07:00
Trevin Chow fc45949239 feat(grok): add native Grok plugin and marketplace support 2026-07-09 09:59:38 -07:00
Matt Van Horn 750176314a feat(doctor): unified doctor health command with fix prescriptions (#753)
* feat(health): dependency probe taxonomy with reinstall prescriptions

Uniform ok/missing/broken/timeout probes for yt-dlp, PP CLIs, node,
ffmpeg. broken = resolves on PATH but cannot exec (the #692 stale-shim
false-negative); prescriptions keyed by owning package manager;
off-PATH-but-on-disk reports missing with a PATH fix. (U1 of 5)

* feat(backends): chain descriptors with predicted will-use resolution

Alternative mode (X/YouTube/web) probes all candidates side-effect-free
then picks first fully-usable, so an unauthenticated preferred backend
cannot shadow a working fallback; Reddit renders conditional
public-default + ScrapeCreators-backfill wording instead of a fake
winner. Chains import env.py's own definitions; paid lanes probe key
presence only, guarded by no-network tests. (U2 of 5)

* feat(prescriptions): single fix-remediation registry shared by nudges

(source, failure) -> cause + natural-language fix + exact CLI fix +
config anchor, seeded with the known credential/tool failure inventory.
quality_nudge composes its fix text from registry entries (wording
byte-identical); backends.py setup-command prescriptions embed the same
strings, so doctor and nudges cannot drift. (U3 of 5)

* feat(doctor): unified doctor command with tier rollup and frozen aliases

/last30days doctor aggregates dependency probes, backend descriptors,
prescriptions, diagnose data, and the permission summary into one
grouped report (text + --json). Tier rollup per the plan's table;
per-source exception isolation; predicted will-use backend for chains;
no secrets, no cookie reads, no network; exit 0. --diagnose/--preflight
shapes characterization-frozen (pre-v3.9.0 baseline, re-record on
landing) with explicit MCP-passthrough and available_sources consumer
tests. (U4 of 5)

* feat(doctor): TTL cache, --cached read path, SKILL.md standing rule

Doctor persists its JSON beside last-run.json (doctor-cache.json);
--cached serves within LAST30DAYS_DOCTOR_TTL (900s default, registered
in env; 0 disables) and falls through live on stale/corrupt/absent.
Explicit doctor always runs live and refreshes. SKILL.md gains doctor
triggers plus the cached pre-research rule (2 lines); CONFIGURATION.md
documents the command, cache, TTL, and pin vars. (U5 of 5)

* refactor(doctor): simplification pass across the doctor stack

Concurrent per-source probes (order-preserving pool.map, worst case now
bounded by slowest probe); memoized xurl availability (kills a double
whoami spawn); INCLUDE_SOURCES token parsing matches the pipeline gate;
shared timestamp-freshness helper; tier/status constants unified;
public health accessors replace private cross-module reads; explicit
DependencyProbe.off_path replaces detail-string sniffing.

* fix(review): truthful probes and cache trust (wave 1)

Doctor's X probe is now genuinely local-only: xurl checked via its
token store, never whoami; the safe/diagnose path can no longer reach
the network through any of three vectors (forbid-network tests lock
it). Bird checks cookies before node so unconfigured X reports off,
not error. Cache layer: schema stamp, full shape validation with live
fall-through (fixes the reproduced KeyError crash), config fingerprint
invalidation, generated_at/from_cache staleness signals, and cache
write failures now warn on stderr.

* fix(review): single-source-of-truth pass (wave 2)

pipeline.py and providers.py read env's pin/floor constants instead of
restating the raw strings (parity tests guard it); env grows public
include_sources()/is_setup_complete() wrappers ending doctor's private
reaches; health.py and setup_wizard share one installer candidate-dir
list, restoring the Windows PrintingPress dir the mirror had dropped
(the documented Digg off-PATH failure mode), with Windows regression
tests.

* fix(review): doctor output fidelity (wave 3)

requires names the actually-failed backend; digg broken/timeout gets
reinstall-framed prescriptions; ok-tier fixes render in text (YouTube
transcription backstop now visible + covered); SKILL.md frontmatter
gains health-check discovery keywords and the standing rule is marked
mandatory, both locked by contract tests.

* fix(doctor): rebase onto v3.10.0 baseline + Greptile review fixes

Re-record the diagnose/preflight characterization snapshots against the
committed v3.10.0 baseline (new sources, x_pending_browser_auth key, PP
CLI external commands) per the planned re-record path; thread the
no-network gate through main's new x_pending precompute so the safe
path stays provably offline; tolerant alt_cli lookup in quality_nudge
(Greptile P1) with an arity pin test; autouse probe-cache reset fixture
(Greptile P2).

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-05 06:33:27 -07:00
Matt Van Horn 1e82659bb4 fix(techmeme): window Techmeme results to real dates, drop useless sync (#752)
* fix(techmeme): window results to real dates, drop useless sync, tolerate old-binary prose

The techmeme-pp-cli search command hits Techmeme's live archive (back to
~2005), not the synced cache, and until now the adapter stamped every hit
with today's date - so a Dec 2022 Parler headline could appear in a
last-30-days report as current news (observed in the 2026-07-04 Kanye West
run). Fixed CLI binaries now emit a per-record ISO date; the adapter windows
records to the research range on that date, keeps undated records as
honestly dateless (date_confidence: low downstream), deletes the pre-search
sync that search never read, and parses old binaries' zero-hit prose
('No results for ...') as an empty result set instead of a decode error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWuSdMPdQLnAeh65wTeG3L

* fix(techmeme): apply review findings - cap priority for dated records, windowing-inactive hint

Review pass caught: undated archive hits could evict confirmed in-window
stories from the depth cap (the inline comment overclaimed 'stale records
never consume cap slots' - only dated ones); and old-binary degraded mode
was invisible. Dated in-window records now take cap slots first, undated
fill the remainder; when zero records carry usable dates the adapter logs a
windowing-inactive hint with the upgrade command. Plus boundary/non-string/
whitespace date tests, the dropped-records log assertion, and a docstring
note on the deliberate divergence from arxiv.py's drop-on-unparseable
policy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWuSdMPdQLnAeh65wTeG3L

* fix(techmeme): correct npm package in the upgrade hint

Greptile review: the windowing-inactive hint named @mvanhorn/printing-press;
the repo's canonical installer (setup_wizard.PRINTING_PRESS_NPM and every
CONFIGURATION.md example) is @mvanhorn/printing-press-library with
--cli-only. A user running the hint verbatim would have hit the wrong
package.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWuSdMPdQLnAeh65wTeG3L

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 06:33:24 -07:00
Matt Van Horn 9e00fe5b6d feat: Instagram comments + comments-by-default + rank-based comment diversity (#751)
* feat(instagram): wire Instagram comments via ScrapeCreators

U1: instagram.enrich_with_comments + _fetch_post_comments hit
GET /v2/instagram/post/comments, sorted by comment_like_count;
env.is_instagram_comments_available gate (key + instagram_comments in
INCLUDE_SOURCES); pipeline enriches the Instagram source when available.
Mirrors the TikTok-comments implementation.

* feat(instagram): full vote-weighting participation for IG comments

U2: comment_like_count is remapped to the shared score field; a dedicated
_instagram_engagement mirrors _tiktok_engagement with the 0.10 top-comment
carve-out so highly-liked IG comments lift item ranking; render gains the
instagram vote label (likes) + min-score threshold. _VOTE_LOG_REFERENCE
already had instagram. IG comment votes now weight ranking/Best-Takes/render
like YouTube/TikTok.

* feat(render): rank-based cross-platform comment diversity

U3: _render_top_comments interleaves comments round-robin by within-platform
rank (every platform's #1, then every #2, then #3) instead of a global vote-
magnitude sort. Top-3-of-each-platform outranks 4th-of-any and each platform's
#1 is guaranteed a slot -- a viral platform can no longer sweep the list. The
cross-platform list drops the per-platform absolute floor (min_score=0) so a
less-watched video's killer low-vote comment surfaces too; the per-candidate
card still applies the floor. Vote strength only orders within a platform.

* feat(nux): comments-by-default Step 5 tier copy

U4: recommended tier is now posts AND top comments for TikTok + Instagram plus
YouTube comments (INCLUDE_SOURCES=tiktok,instagram,youtube_comments,
tiktok_comments,instagram_comments); Everything adds Threads + Pinterest. No
posts-only tier. Operator-approved copy; mirrored in the non-modal 5b flow.

* feat(nux): accurate ScrapeCreators offer copy + fix stale docstrings

U5: Step 4 offer states comments are on by default (posts AND top comments +
YouTube comments) and describes the real auto-enrichment (Reddit public+SC
merged, YouTube search backstop) instead of vague 'backup'. Fixes the
is_youtube_comments_available docstring that claimed comments are excluded
from the Recommended tier. (Xiaohongshu was never labeled ScrapeCreators in
the docs; no change needed there.)

* docs: comments-by-default tier + instagram_comments in AGENTS/CONFIGURATION

U6: AGENTS.md onboarding note now describes comments-on-by-default (Recommended
tier), IG-comments parity, and the rank-based diversity selection.
CONFIGURATION.md documents instagram_comments/tiktok_comments INCLUDE_SOURCES
keys and the new default string; corrects the youtube_comments row and the
Step 5 tier prose.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-04 08:28:14 -07:00
Matt Van Horn 6a1d93bdb5 fix(nux): deterministic first-run welcome + GitHub device code (engine-driven) (#748)
* fix(nux): make first-run welcome and GitHub device code deterministic

The v3.9.3 prose fixes for the welcome and the GitHub device code failed a
real cold run: the model skipped the welcome despite 'REQUIRED FIRST', and
never surfaced the device code (backgrounding + poll orchestration). Move
both from model-obeyed prose to engine-driven behavior.

- Welcome: new 'last30days.py --welcome' command prints the engine-owned
  welcome; SKILL.md Step 1 relays it verbatim. Single source of truth; the
  model reliably relays command output even when it skips prose.
- GitHub device code: split setup --github into --github-start (submit, copy
  to clipboard, print the code to stdout, open browser, return immediately)
  and --github-poll (wait, fetch, persist). run_github_start keeps the
  existing-key short-circuit; setup --github still chains both for back-compat.
- Code printed to stdout as a plain line so a foreground caller can't miss it;
  clipboard claim only made when pbcopy succeeded.

Updates onboarding contract tests, adds tests/test_welcome.py, updates
AGENTS.md and CONFIGURATION.md. Full suite green (coverage 84.06%).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBTxHmgTPg5xsgyUbgNdiz

* fix(nux): honest clipboard_ok in poll + in-memory handle for one-shot

Greptile findings on the device-auth split:
- run_github_poll hardcoded clipboard_ok=True, so the periodic poll reminder
  would falsely claim the code was on the clipboard on non-macOS or when
  pbcopy failed. Now clipboard_ok is carried in the handle (file + in-memory)
  and read back, defaulting to False when absent.
- run_full_device_auth (one-shot --github) relied on the persisted handle
  file, so a failed handle write stranded it. Extracted _start_device_flow
  returning (public, handle); the one-shot now passes the handle to poll
  in-memory (device_code never enters the public stdout dict), and
  run_github_start persists the file only for the separate --github-poll
  process.

Adds tests for both. Full suite green (coverage 84.09%).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBTxHmgTPg5xsgyUbgNdiz

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 23:39:03 -07:00
Matt Van Horn 76215629b9 feat(engine): optional remote research API backend (env-driven) (#747)
Adds an optional hosted-backend path: when both LAST30DAYS_API_KEY and
LAST30DAYS_API_BASE are set (and --mock is not passed), research runs
through the configured remote API instead of local sources - submit,
poll with progress on stderr, render the server's report. The endpoint
comes only from LAST30DAYS_API_BASE; there is no built-in default, so
with either variable unset the engine runs local sources unchanged.


Claude-Session: https://claude.ai/code/session_012gvxSQgfjp6RDyv6726VeB

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:39:16 -07:00
Matt Van Horn 443b05f330 fix(trustpilot): resolve name->domain, session warm-up, single-fetch cap (#745)
* fix(trustpilot): resolve name->domain, warm session pre-fanout, cap to one fetch

Trustpilot returned 0 items on company topics: the engine passed the raw
topic name to a CLI keyed by domain (info ThriftBooks -> HTTP 404), and N
parallel subqueries each raced their own Chrome WAF-cookie harvest.

- --trustpilot-domain flag (verbatim, bypasses the brand-shape gate; flows
  into competitors-plan sub-runs with provenance: user-set is final,
  resolved hints retry via search on a miss)
- name->domain resolution via the CLI's search, cached per topic; name-match
  mandatory, ambiguous multi-hit falls back rather than misattributing
- ensure_session_ready: one process-global, lock-serialized auth status /
  auth login before the fan-out; brand-gated so generic topics never launch
  Chrome; logs structured status strings only
- MAX_SOURCE_FETCHES trustpilot=1: N streams used identical identifiers

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhBKEWrZV9cpfzNu7rmRzR

* feat(resolve): auto-resolve a Trustpilot domain hint from official-site URLs

Headless --auto-resolve runs fill args.trustpilot_domain (hint provenance)
from news/handle search-result URLs whose registrable label matches the
topic. Hints are tier-2: the engine retries via CLI search when they miss.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhBKEWrZV9cpfzNu7rmRzR

* docs(skill): Step 0.5d Trustpilot-domain resolution + CONFIGURATION.md mirror

Pre-flight checklist row, Step 0.5d resolution subsection, Resolved-block
Trustpilot line, per-entity trustpilot_domain in --competitors-plan, and
the CONFIGURATION.md source-table row reflecting domain resolution and the
pre-fan-out session warm-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhBKEWrZV9cpfzNu7rmRzR

* fix(review): harden trustpilot resolution per 10-reviewer code review

- exclude trustpilot from the thin-source retry: it returns at most one
  item by design, so '<3 items' re-fetched it after every success --
  bypassing the fetch cap and re-resolving without --trustpilot-domain
  (lookalike-misattribution path) [cross-model adversarial, verified]
- move the session warm-up from pipeline.run's pre-fan-out slot into
  search_trustpilot's first fetch: never delays other sources' streams,
  never fires when the plan fetches no trustpilot [reliability, adversarial]
- replace the warm-up's boolean-forever flag with a 240s monotonic TTL:
  long-lived host processes re-check after the token window and retry a
  previously failed login [cross-model adversarial, correctness]
- only USER-set domains bypass the brand-shape gate; an auto-resolved hint
  no longer widens activation to generic topics [security]
- do not cache transient search errors as permanent negative resolutions
  [cross-model adversarial, correctness]
- bound the hint-retry chain: skip when the first lookup already consumed
  a full single-call budget [reliability]
- SKILL.md: --trustpilot-domain in the engine-command flag list
  [project-standards], vs-mode per-entity lookup type 5 + main-topic
  outer-flag clarification [agent-native, correctness]
- tests: thin-retry exclusion, main()-level flag plumbing, warm-up-at-
  first-touch, TTL lapse, hint-on-generic-topic quiet, transient-error
  not cached [testing]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhBKEWrZV9cpfzNu7rmRzR

* fix(trustpilot): address Greptile P2s - degenerate payloads uncached, retry timer after warm-up

- an empty-stdout CLI response ({}) is a degenerate payload, not a
  definitive no-match: skip the cache write so it retries next lookup
  (a well-formed empty hits list still caches)
- start the hint-retry budget timer after the warm-up so a slow Chrome
  harvest cannot consume the retry budget of a fast info call

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PhBKEWrZV9cpfzNu7rmRzR

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:06:10 -07:00
Matt Van Horn f857a5101f fix(setup-wizard): repair the first-run NUX (Chrome-first cookies, working GitHub signup, honest copy) (#744)
* fix(setup-wizard): scan Chrome/Chromium before Safari during onboarding

The wizard forced FROM_BROWSER=firefox,safari, so Chrome was never tried
and macOS users logged into X in Chrome fell through to Safari, whose
binarycookies read requires Full Disk Access — a dead-end. Chrome reads
via the Keychain with no FDA. Scan the Chromium family first (explicit
comma list preserves order; 'auto' is silent-first and would not).

Pin FROM_BROWSER only for a silent winner (firefox/safari); a Chromium
winner is left unpinned so steady-state runs never re-trigger the
Keychain prompt (U1 / R1 / R2 / OQ2 conservative default).

* fix(setup-wizard): surface device code on stdout, validate it, and short-circuit re-registration

Three fixes to the ScrapeCreators GitHub signup:
- U4: emit a structured device_code_ready line to stdout as soon as the
  code is available, so a backgrounded caller can show it immediately
  instead of the user seeing only a spinner until the 300s process exits.
- U5: validate user_code against ^[0-9A-Z]{4}-[0-9A-Z]{4}$ before copying,
  labeling, or emitting it; a key-shaped value is never mislabeled as a
  GitHub code. Scrub the incomplete-response log so a returning account's
  raw key can't land in logs.
- U6: an already-saved SCRAPECREATORS_API_KEY short-circuits to
  status=already_registered with no device dance; the CLI now masks the
  key for every status that carries one, not just success, so it never
  reaches captured stdout unmasked.

OQ3 conservative default: no key-shaped-user_code branch is built; the
.env-present check is the primary already-registered mechanism.

* fix(skill): Chrome-first cookie copy, honest gh signup, clearer sources, new-source surfacing

SKILL.md NUX copy, both Modal and Non-Modal flows:
- U2: cookie consent leads with Chrome (Keychain 'Always Allow' cue);
  Full Disk Access reframed as the Safari-only fallback.
- U3: StockTwits surfaced as a ticker/crypto conditional source; a
  power-user pointer to the Manual Setup guide for credential-gated
  opt-ins (LinkedIn, Bluesky, Perplexity) - Threads/Pinterest stay out
  of onboarding per the contract.
- U4 (orchestration): run setup --github in the background and poll
  stdout for the device_code_ready line so the code is shown immediately
  instead of a spinner; parse the LAST JSON line for final status; add a
  foreground fallback for hosts that can't background.
- U7: remove the false 'GitHub CLI ~2 seconds - no browser' promise;
  honest device-code copy; qualitative credit contrast (numberless,
  OQ1 fallback); already_registered + timeout/denied recovery.
- U8: clearer TikTok/Instagram opt-in (per-run cost vs backup-only).

Contract test locks the new copy (Chrome-first, StockTwits, no false
promise, device_code_ready in both flows, already_registered).

* fix(skill): drop StockTwits from the setup welcome

StockTwits needs zero setup (keyless) and only auto-fires on ticker/crypto
topics, so advertising it in the first-run welcome is noise for the typical
user whose topic is non-financial. It self-activates when relevant; the
welcome should show what needs a setup action or is broadly always-on.
Contract test now locks it OUT of the welcome.

* fix(skill): restore StockTwits bullet in the setup welcome

Keep StockTwits surfaced in the welcome as a ticker/crypto-gated source
(sits with arXiv/Techmeme as specialized auto-on). Reverts the prior
removal per maintainer call; contract test locks it back in.

* fix(sources): make Threads, YouTube comments, and Pinterest real INCLUDE_SOURCES opt-ins

The onboarding 'Recommended vs Everything' tiers were a no-op: a
ScrapeCreators key auto-ran TikTok, Instagram, Threads, and YouTube
comments regardless of INCLUDE_SOURCES, and Pinterest's opt-in only read
the per-run --sources list (a persisted INCLUDE_SOURCES=pinterest never
activated it). Gate the three Everything-tier sources properly:

- Threads: gated in available_sources on INCLUDE_SOURCES (perplexity
  pattern); is_threads_available stays an availability-only predicate.
- YouTube comments: is_youtube_comments_available flipped from opt-out
  (default-on) to opt-in, mirroring is_tiktok_comments_available.
- Pinterest: available_sources gate now honors a persisted
  INCLUDE_SOURCES=pinterest, not only requested_sources.

TikTok/Instagram stay auto-on with the key (the Recommended tier). New
TestScrapeCreatorsTierGating locks the tiers; YouTube-comments and
Threads tests reversed/clarified to the new contract.

Existing key-holders lose auto-on Threads/YouTube-comments until they
opt into Everything (additive sources; accepted, no migration).

* fix(skill): real Recommended vs Everything ScrapeCreators tiers + define backups

Step 4 offer defines 'backups' inline (keeps Reddit/YouTube working at
rate limits) instead of the undefined 'ScrapeCreators backups' jargon,
and drops the now-false 'YouTube comments auto-on' claim. Step 5 becomes
two honest tiers:
- Recommended: TikTok + Instagram (auto with key) + backups; writes no
  INCLUDE_SOURCES.
- Everything: also Threads, Pinterest, YouTube/TikTok/Instagram comments;
  writes the full INCLUDE_SOURCES list (incl. tiktok,instagram so
  quality_nudge doesn't treat them as excluded).
Mirrored into the Non-Modal flow (new step 5b).

Contract test resliced: Threads/Pinterest asserted present in the Step 5
Everything option and absent from the welcome/offer (reversing the old
blanket 'never in onboarding' lock); the Everything INCLUDE_SOURCES write
is locked. AGENTS.md and CONFIGURATION.md updated to match (Threads/
Pinterest/YouTube-comments are opt-in, offered in the Everything tier).

* test: address Greptile review nits on PR #744

- test_already_registered_status_handled now also asserts the prose flow
  (the handling was added to both flows but only the modal was checked).
- Rename mock_pbcopy -> mock_subprocess_run and add a note clarifying it
  patches subprocess.run (the pbcopy call), per the review.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-03 19:27:41 -07:00
Waqas Tiwana a28668ad30 feat: add StockTwits as a source, gated to ticker/crypto topics (#658)
* feat: add StockTwits as a source, gated to ticker/crypto topics

StockTwits is cashtag-native and carries self-reported Bullish/Bearish
tags, making it uniquely good at one thing the other sources can't
quantify for a stock/crypto query: a sentiment ratio and retail volume
on a specific symbol. On a "ServiceNow stock" run today the engine leans
on the product community (r/servicenow) for lack of an investor-voice
source; StockTwits fills that gap.

Design:
- New lib/stocktwits.py: public API (no auth), symbol resolution
  (cashtags, crypto .X aliases, name->symbol search), stream fetch with
  cursor pagination + date filter, and a bull/bear aggregate.
- Gating is a single chokepoint: run() sets config["_financial_topic"]
  via stocktwits.is_financial_topic(topic); available_sources() only
  registers "stocktwits" when that flag is set. Because
  eligible_sources = available ∩ capabilities, the planner can never
  assign it to a non-financial topic (verified: "Kanye West",
  "Apple pie recipe" -> no symbol, source absent).
- Items normalize like every other source; per-message sentiment + the
  symbol-level aggregate ride in metadata for synthesis to cite.
- Planner: capability entry + added to breaking_news/prediction/opinion
  priorities.

Output is a direction/volume signal only (retail-skewed, self-reported
tags) - not analysis. Documented in the module and CONFIGURATION.md.

Tests: tests/test_stocktwits.py (hermetic; HTTP patched) covers the
gate, name-resolution firing only for financial topics, parsing,
sentiment aggregation, normalize wiring, and the pipeline availability
gate. No network in tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Tighten the financial-topic gate and cap per-run fetches

- Drop ambiguous words (share/token/coin/bull/bear) from _FINANCE_HINTS:
  they misfired on general topics like 'share files' or 'bear attacks'
  and could inject stock chatter into non-financial runs
- Add unambiguous crypto names (bitcoin, btc, ethereum, solana, dogecoin,
  cardano, xrp) so 'bitcoin price' gates without a cashtag; short
  colliding aliases (eth, sol, ada, doge) stay out of the gate
- Register stocktwits in MAX_SOURCE_FETCHES so multiple subqueries don't
  re-fetch the same symbol stream and burn the unauthenticated IP quota
- Reword the stale wiring docstring; negative and positive gate tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBTxHmgTPg5xsgyUbgNdiz

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-03 15:25:03 -07:00
Matt Van Horn a9d2674147 feat: Add arXiv + Techmeme (default-on) and Trustpilot (opt-in) sources (#709)
* feat(arxiv): add arXiv as a default-on research source

Mirrors the digg adapter: lib/arxiv.py shells out to arxiv-pp-cli with a
quoted, relevance-sorted query and a 365-day recency cutoff (research does
not trend daily; the cutoff also keeps arXiv quiet on non-research topics
where the only keyword match is a stale paper). Registered across pipeline
(MOCK_AVAILABLE_SOURCES, available_sources gate on which(arxiv-pp-cli),
binary map, dispatch, mock fixture), normalize, planner, signals, render
(emoji 📄), and ui. 12 unit tests.

* feat(techmeme): add Techmeme as a default-on tech-news source

lib/techmeme.py shells out to techmeme-pp-cli: ensures a one-time cache sync
per run, then searches via 'search <topic> --json' (not --agent, which implies
--compact and blanked records on pre-PR-1383 binaries). Filters publication-
name header rows, dates headlines to the sync time (the cache is the current
news cycle), and ranks on topic relevance plus source quality. Registered
across pipeline, normalize, planner, signals, render (emoji 📰), and ui.
11 unit tests.

* feat(trustpilot): add Trustpilot as a default-on brand-sentiment source

lib/trustpilot.py shells out to trustpilot-pp-cli 'info <company>' for
TrustScore + AI review summary. Three default-on safety gates: (1) brand-shape
gate -- the CLI is invoked only for company-like topics (domain token or a
<=2-word capitalized proper noun), so it stays quiet and never harvests Chrome
on generic topics; (2) LAST30DAYS_TRUSTPILOT_NO_BROWSER opt-out for automated
contexts; (3) graceful degradation to empty on any CLI failure. Registered
across pipeline (dispatch passes config), normalize, planner, signals
(reviews engagement), render (emoji ), and ui. 19 unit tests.

* feat(setup): auto-install arxiv/techmeme/trustpilot from Printing Press

Generalize the digg install path into _install_pp_cli(slug, bin_name) and
install_default_pp_sources(), mirroring digg's action taxonomy
(already_installed | installed | installed_off_path | install_failed | no_npx)
and PATH-candidate detection. run_auto_setup now best-effort-installs the three
new default-on sources via npx @mvanhorn/printing-press-library install <slug>
--cli-only and reports per-CLI status under results['pp_sources']. 7 new tests;
updated the digg install test to assert_any_call (digg is now one of several
install calls).

* fix(techmeme): drop unsupported --max-results flag, cap client-side

Live evaluation surfaced 'Error: unknown flag: --max-results' on every
techmeme search -- the search subcommand has no result-limit flag, so the flag
broke the source entirely (0 records). Remove it and apply the depth cap
client-side after parsing. Verified live: searches now return real headlines.

* feat(sources): document the three default-on sources + add eval harness

SKILL.md auto-setup list now names arXiv (📄), Techmeme (📰), and Trustpilot
() with their gating notes. test_new_sources_eval.py drives the three across
five diverse real topics: a deterministic fire/quiet gating matrix (offline,
CI-safe) plus an opt-in LAST30DAYS_EVAL_LIVE mode that exercises the real CLIs
and asserts the negative controls stay quiet.

* fix(review): address code-review findings (opt-out, sync race, gating edges, docs)

- P1 Trustpilot browser opt-out was inert: LAST30DAYS_TRUSTPILOT_NO_BROWSER is
  read from config, but config is built from an env.py allowlist that omitted
  it, so the documented kill-switch never fired in production. Add an
  os.environ fallback in _harvest_allowed and add the key to env.py's
  allowlist; new test exercises the env-var path env.get_config uses.
- P1 CONFIGURATION.md: document the three new sources and the
  LAST30DAYS_TRUSTPILOT_NO_BROWSER env var (AGENTS.md hard rule).
- P2 Techmeme sync: replace the unlocked _SYNCED bool with a lock + monotonic
  TTL, fixing the concurrent-double-sync race under ThreadPoolExecutor fan-out
  and the never-reset staleness in long-lived/multi-report processes.
- P2 Techmeme: test the client-side depth cap (the compensating half of the
  --max-results removal) plus sync-failure swallow and headline-word boundary.
- P3 arXiv: one-day future-date grace (same-UTC-day papers no longer dropped),
  empty-phrase guard (quote-only topic no longer searches all:""), and
  end-to-end _run_cli tests (envelope flatten, non-zero exit, bad JSON).

Accepted residual (follow-up): adapter-level URL-scheme guard parity with digg
-- the actual XSS surface (HTML output) is already centrally guarded by
html_render._is_safe_link_url, and these sources emit URLs from trusted
first-party CLIs scraping known domains.

* change(trustpilot): make Trustpilot opt-in instead of default-on

Trustpilot's headless-Chrome WAF-cookie harvest is heavier than the zero-auth
arXiv/Techmeme sources, so gate it behind INCLUDE_SOURCES=trustpilot (the same
opt-in pattern as perplexity/linkedin) rather than activating on binary
presence. The setup wizard no longer auto-installs its binary (off-by-default
source); opting in installs it on demand. arXiv and Techmeme remain default-on.
Docs (SKILL.md, CONFIGURATION.md) updated; new tests assert off-by-default,
on-with-INCLUDE_SOURCES, and on-when-requested.

* fix(review): exclude single-word tech names from Trustpilot brand gate

Greptile P1: is_brand_shaped admitted bare capitalized language/framework/tool
names (Python, React, Docker, Rust, Linux, Swift, Java) because _GENERIC_TOKENS
only covered AI/query vocabulary. An opted-in user querying a single tech term
would trigger the Chrome WAF harvest and risk surfacing an unrelated company's
reviews. Add a _TECH_TOKENS exclusion set; explicit company intent still
resolves via the domain branch (e.g. docker.com). Parametrized tests lock the
behavior.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-06-30 08:38:12 -07:00
Matt Van Horn 732d4ae76b fix(linkedin): correct field mapping + surface articles as high signal
The ScrapeCreators /v1/linkedin/search/posts parser was keyed on field
names the live API doesn't return (`text`/`date`), so every post was
dropped (10 raw -> 0 items). Map the real fields (`description`,
`datePublished`) into the existing fallback chains; the unit tests now
encode the real response shape with a regression guard for this exact bug.

Also honor "a LinkedIn article is high signal": articles never appear in
post search (only `/posts/` URLs come back), so add a bounded profile-
enrichment lane. On person topics (a returned post author matches the
topic) we make one `/v1/linkedin/profile` call and surface the author's
`articles[]` as high-signal items (relevance 0.9, ranked above posts).
Keyword topics make no profile call.

Off-by-default opt-in gating (INCLUDE_SOURCES=linkedin) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012iWk4GFaH6dHjUEwsZneiB
2026-06-26 19:05:24 -07:00
Ravi Shrestha ad52872ecf Address review feedback: gate LinkedIn behind INCLUDE_SOURCES, add date-range filtering, update docs
- Gate linkedin in available_sources() behind INCLUDE_SOURCES/requested_sources,
  matching the perplexity opt-in pattern, so existing SCRAPECREATORS_API_KEY
  holders don't silently start spending credits on LinkedIn.
- Add instagram-style graceful date-range filtering to
  parse_linkedin_response(), with fallback to keep all results if the
  filter would otherwise return zero.
- Remove unused sys import in linkedin.py.
- Document linkedin in CONFIGURATION.md's source table and SKILL.md's
  tags list, power-user opt-in note, and ACTIVE_SOURCES_LIST mapping.
- Add TestLinkedinAvailability (pipeline) and TestDateRangeFiltering
  (linkedin) test coverage.
2026-06-26 17:31:27 -07:00
Trevin Chow 65951b3bd7 Merge pull request #680 from mvanhorn/codex/optional-html-publishing
Add optional HTML publishing
2026-06-24 23:34:55 -07:00
Trevin Chow 287327df60 Merge pull request #686 from rfoust/codex/add-codex-plugin-manifest 2026-06-24 23:03:15 -07:00
Trevin Chow 6db296ca68 docs: describe Codex root plugin source directly 2026-06-24 22:31:03 -07:00
Trevin Chow 1e365e573b fix: point Codex marketplace at repo root URL 2026-06-24 22:30:43 -07:00
Robbie Foust e219c65ea7 Address Codex plugin review feedback 2026-06-25 00:23:36 -04:00
Trevin Chow 91dba43d1f merge main into optional html publishing 2026-06-24 21:15:47 -07:00
Trevin Chow 979e6dece3 Merge pull request #679 from mvanhorn/codex/cached-html-rendering 2026-06-24 21:10:43 -07:00
Trevin Chow a824fa3c3a merge main into preflight permission plan 2026-06-24 21:04:03 -07:00