luckiest-co 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (618) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/LICENSE +24 -0
  4. package/bin/install.js +7 -5
  5. package/commands/{luckiest/finish.md → finish.md} +2 -0
  6. package/commands/go.md +53 -0
  7. package/commands/plan.md +66 -0
  8. package/package.json +16 -5
  9. package/skills/brainstorm/ATTRIBUTION.md +27 -0
  10. package/skills/brainstorm/SKILL.md +139 -0
  11. package/skills/luckiest-ab-testing/ATTRIBUTION.md +35 -0
  12. package/skills/luckiest-ab-testing/CHANGELOG.md +27 -0
  13. package/skills/luckiest-ab-testing/SKILL.md +379 -0
  14. package/skills/luckiest-ab-testing/evals/evals.json +118 -0
  15. package/skills/luckiest-ab-testing/references/sample-size-guide.md +290 -0
  16. package/skills/luckiest-ab-testing/references/test-templates.md +284 -0
  17. package/skills/luckiest-ad-creative/ATTRIBUTION.md +35 -0
  18. package/skills/luckiest-ad-creative/CHANGELOG.md +23 -0
  19. package/skills/luckiest-ad-creative/SKILL.md +393 -0
  20. package/skills/luckiest-ad-creative/evals/evals.json +90 -0
  21. package/skills/luckiest-ad-creative/references/generative-tools.md +637 -0
  22. package/skills/luckiest-ad-creative/references/platform-specs.md +213 -0
  23. package/skills/luckiest-ads/ATTRIBUTION.md +36 -0
  24. package/skills/luckiest-ads/CHANGELOG.md +24 -0
  25. package/skills/luckiest-ads/LICENSE +21 -0
  26. package/skills/luckiest-ads/SKILL.md +434 -0
  27. package/skills/luckiest-ads/evals/evals.json +90 -0
  28. package/skills/luckiest-ads/references/ad-copy-templates.md +207 -0
  29. package/skills/luckiest-ads/references/audience-targeting.md +243 -0
  30. package/skills/luckiest-ads/references/conversion-tracking.md +361 -0
  31. package/skills/luckiest-ads/references/platform-setup-checklists.md +277 -0
  32. package/skills/luckiest-advisors/ATTRIBUTION.md +16 -0
  33. package/skills/luckiest-advisors/CHANGELOG.md +99 -0
  34. package/skills/luckiest-advisors/SKILL.md +151 -0
  35. package/skills/luckiest-aeo-grader/ATTRIBUTION.md +19 -0
  36. package/skills/luckiest-aeo-grader/CHANGELOG.md +52 -0
  37. package/skills/luckiest-aeo-grader/SKILL.md +200 -0
  38. package/skills/luckiest-aeo-grader/evals/evals.json +19 -0
  39. package/skills/luckiest-aeo-grader/references/rubric.md +72 -0
  40. package/skills/luckiest-aeo-grader/scripts/scan.py +265 -0
  41. package/skills/luckiest-ai-seo/ATTRIBUTION.md +38 -0
  42. package/skills/luckiest-ai-seo/CHANGELOG.md +23 -0
  43. package/skills/luckiest-ai-seo/SKILL.md +510 -0
  44. package/skills/luckiest-ai-seo/evals/evals.json +90 -0
  45. package/skills/luckiest-ai-seo/references/content-patterns.md +285 -0
  46. package/skills/luckiest-ai-seo/references/content-types.md +71 -0
  47. package/skills/luckiest-ai-seo/references/okf.md +104 -0
  48. package/skills/luckiest-ai-seo/references/platform-ranking-factors.md +154 -0
  49. package/skills/luckiest-analytics/ATTRIBUTION.md +42 -0
  50. package/skills/luckiest-analytics/CHANGELOG.md +36 -0
  51. package/skills/luckiest-analytics/SKILL.md +380 -0
  52. package/skills/luckiest-analytics/evals/evals.json +90 -0
  53. package/skills/luckiest-analytics/references/event-library.md +260 -0
  54. package/skills/luckiest-analytics/references/ga4-implementation.md +300 -0
  55. package/skills/luckiest-analytics/references/gtm-implementation.md +390 -0
  56. package/skills/luckiest-aso/ATTRIBUTION.md +40 -0
  57. package/skills/luckiest-aso/CHANGELOG.md +33 -0
  58. package/skills/luckiest-aso/SKILL.md +327 -0
  59. package/skills/luckiest-aso/evals/evals.json +91 -0
  60. package/skills/luckiest-aso/references/apple-specs.md +107 -0
  61. package/skills/luckiest-aso/references/benchmarks.md +129 -0
  62. package/skills/luckiest-aso/references/google-play-specs.md +131 -0
  63. package/skills/luckiest-aso/references/report-template.md +213 -0
  64. package/skills/luckiest-aso/references/scoring-criteria.md +213 -0
  65. package/skills/luckiest-churn-prevention/ATTRIBUTION.md +41 -0
  66. package/skills/luckiest-churn-prevention/CHANGELOG.md +24 -0
  67. package/skills/luckiest-churn-prevention/SKILL.md +445 -0
  68. package/skills/luckiest-churn-prevention/evals/evals.json +93 -0
  69. package/skills/luckiest-churn-prevention/references/cancel-flow-patterns.md +316 -0
  70. package/skills/luckiest-churn-prevention/references/dunning-playbook.md +408 -0
  71. package/skills/luckiest-co-marketing/ATTRIBUTION.md +38 -0
  72. package/skills/luckiest-co-marketing/CHANGELOG.md +25 -0
  73. package/skills/luckiest-co-marketing/SKILL.md +331 -0
  74. package/skills/luckiest-co-marketing/evals/evals.json +84 -0
  75. package/skills/luckiest-coder/ATTRIBUTION.md +26 -0
  76. package/skills/luckiest-coder/CHANGELOG.md +30 -0
  77. package/skills/luckiest-coder/SKILL.md +77 -0
  78. package/skills/luckiest-coder/references/antigravity-tools.md +23 -0
  79. package/skills/luckiest-coder/references/codex-tools.md +39 -0
  80. package/skills/luckiest-coder/references/pi-tools.md +16 -0
  81. package/skills/luckiest-coder-brainstorming/ATTRIBUTION.md +26 -0
  82. package/skills/luckiest-coder-brainstorming/CHANGELOG.md +31 -0
  83. package/skills/luckiest-coder-brainstorming/SKILL.md +173 -0
  84. package/skills/luckiest-coder-brainstorming/scripts/frame-template.html +213 -0
  85. package/skills/luckiest-coder-brainstorming/scripts/helper.js +168 -0
  86. package/skills/luckiest-coder-brainstorming/scripts/server.cjs +721 -0
  87. package/skills/luckiest-coder-brainstorming/scripts/start-server.sh +209 -0
  88. package/skills/luckiest-coder-brainstorming/scripts/stop-server.sh +120 -0
  89. package/skills/luckiest-coder-brainstorming/spec-document-reviewer-prompt.md +49 -0
  90. package/skills/luckiest-coder-brainstorming/visual-companion.md +291 -0
  91. package/skills/luckiest-coder-consistency/ATTRIBUTION.md +30 -0
  92. package/skills/luckiest-coder-consistency/CHANGELOG.md +26 -0
  93. package/skills/luckiest-coder-consistency/SKILL.md +98 -0
  94. package/skills/luckiest-coder-constitution/ATTRIBUTION.md +28 -0
  95. package/skills/luckiest-coder-constitution/CHANGELOG.md +25 -0
  96. package/skills/luckiest-coder-constitution/SKILL.md +115 -0
  97. package/skills/luckiest-coder-debugging/ATTRIBUTION.md +26 -0
  98. package/skills/luckiest-coder-debugging/CHANGELOG.md +22 -0
  99. package/skills/luckiest-coder-debugging/CREATION-LOG.md +119 -0
  100. package/skills/luckiest-coder-debugging/SKILL.md +309 -0
  101. package/skills/luckiest-coder-debugging/condition-based-waiting-example.ts +158 -0
  102. package/skills/luckiest-coder-debugging/condition-based-waiting.md +115 -0
  103. package/skills/luckiest-coder-debugging/defense-in-depth.md +122 -0
  104. package/skills/luckiest-coder-debugging/find-polluter.sh +63 -0
  105. package/skills/luckiest-coder-debugging/root-cause-tracing.md +169 -0
  106. package/skills/luckiest-coder-debugging/test-academic.md +14 -0
  107. package/skills/luckiest-coder-debugging/test-pressure-1.md +58 -0
  108. package/skills/luckiest-coder-debugging/test-pressure-2.md +68 -0
  109. package/skills/luckiest-coder-debugging/test-pressure-3.md +69 -0
  110. package/skills/luckiest-coder-dispatching-parallel-agents/ATTRIBUTION.md +26 -0
  111. package/skills/luckiest-coder-dispatching-parallel-agents/CHANGELOG.md +23 -0
  112. package/skills/luckiest-coder-dispatching-parallel-agents/SKILL.md +198 -0
  113. package/skills/luckiest-coder-executing-plans/ATTRIBUTION.md +26 -0
  114. package/skills/luckiest-coder-executing-plans/CHANGELOG.md +22 -0
  115. package/skills/luckiest-coder-executing-plans/SKILL.md +83 -0
  116. package/skills/luckiest-coder-finishing-a-branch/ATTRIBUTION.md +26 -0
  117. package/skills/luckiest-coder-finishing-a-branch/CHANGELOG.md +22 -0
  118. package/skills/luckiest-coder-finishing-a-branch/SKILL.md +258 -0
  119. package/skills/luckiest-coder-git-worktrees/ATTRIBUTION.md +26 -0
  120. package/skills/luckiest-coder-git-worktrees/CHANGELOG.md +22 -0
  121. package/skills/luckiest-coder-git-worktrees/SKILL.md +215 -0
  122. package/skills/luckiest-coder-guard/ATTRIBUTION.md +29 -0
  123. package/skills/luckiest-coder-guard/CHANGELOG.md +48 -0
  124. package/skills/luckiest-coder-guard/LICENSE +21 -0
  125. package/skills/luckiest-coder-guard/SKILL.md +126 -0
  126. package/skills/luckiest-coder-guard/bin/check-careful.sh +65 -0
  127. package/skills/luckiest-coder-guard/bin/check-freeze.sh +48 -0
  128. package/skills/luckiest-coder-qa/ATTRIBUTION.md +28 -0
  129. package/skills/luckiest-coder-qa/CHANGELOG.md +49 -0
  130. package/skills/luckiest-coder-qa/LICENSE +21 -0
  131. package/skills/luckiest-coder-qa/SKILL.md +160 -0
  132. package/skills/luckiest-coder-qa/references/issue-taxonomy.md +85 -0
  133. package/skills/luckiest-coder-qa/references/qa-report-template.md +126 -0
  134. package/skills/luckiest-coder-receiving-code-review/ATTRIBUTION.md +26 -0
  135. package/skills/luckiest-coder-receiving-code-review/CHANGELOG.md +28 -0
  136. package/skills/luckiest-coder-receiving-code-review/SKILL.md +226 -0
  137. package/skills/luckiest-coder-requesting-code-review/ATTRIBUTION.md +26 -0
  138. package/skills/luckiest-coder-requesting-code-review/CHANGELOG.md +31 -0
  139. package/skills/luckiest-coder-requesting-code-review/SKILL.md +118 -0
  140. package/skills/luckiest-coder-requesting-code-review/code-reviewer.md +172 -0
  141. package/skills/luckiest-coder-shipping/ATTRIBUTION.md +26 -0
  142. package/skills/luckiest-coder-shipping/CHANGELOG.md +48 -0
  143. package/skills/luckiest-coder-shipping/LICENSE +21 -0
  144. package/skills/luckiest-coder-shipping/SKILL.md +148 -0
  145. package/skills/luckiest-coder-shipping/references/canary.md +44 -0
  146. package/skills/luckiest-coder-shipping/references/test-and-coverage.md +91 -0
  147. package/skills/luckiest-coder-subagent-driven-development/ATTRIBUTION.md +26 -0
  148. package/skills/luckiest-coder-subagent-driven-development/CHANGELOG.md +28 -0
  149. package/skills/luckiest-coder-subagent-driven-development/SKILL.md +439 -0
  150. package/skills/luckiest-coder-subagent-driven-development/implementer-prompt.md +139 -0
  151. package/skills/luckiest-coder-subagent-driven-development/scripts/review-package +44 -0
  152. package/skills/luckiest-coder-subagent-driven-development/scripts/sdd-workspace +22 -0
  153. package/skills/luckiest-coder-subagent-driven-development/scripts/task-brief +40 -0
  154. package/skills/luckiest-coder-subagent-driven-development/task-reviewer-prompt.md +188 -0
  155. package/skills/luckiest-coder-tdd/ATTRIBUTION.md +26 -0
  156. package/skills/luckiest-coder-tdd/CHANGELOG.md +26 -0
  157. package/skills/luckiest-coder-tdd/SKILL.md +384 -0
  158. package/skills/luckiest-coder-tdd/testing-anti-patterns.md +299 -0
  159. package/skills/luckiest-coder-verification/ATTRIBUTION.md +26 -0
  160. package/skills/luckiest-coder-verification/CHANGELOG.md +22 -0
  161. package/skills/luckiest-coder-verification/SKILL.md +152 -0
  162. package/skills/luckiest-coder-writing-plans/ATTRIBUTION.md +26 -0
  163. package/skills/luckiest-coder-writing-plans/CHANGELOG.md +22 -0
  164. package/skills/luckiest-coder-writing-plans/SKILL.md +191 -0
  165. package/skills/luckiest-coder-writing-plans/plan-document-reviewer-prompt.md +49 -0
  166. package/skills/luckiest-coder-writing-skills/ATTRIBUTION.md +26 -0
  167. package/skills/luckiest-coder-writing-skills/CHANGELOG.md +31 -0
  168. package/skills/luckiest-coder-writing-skills/SKILL.md +702 -0
  169. package/skills/luckiest-coder-writing-skills/anthropic-best-practices.md +1150 -0
  170. package/skills/luckiest-coder-writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  171. package/skills/luckiest-coder-writing-skills/graphviz-conventions.dot +172 -0
  172. package/skills/luckiest-coder-writing-skills/persuasion-principles.md +187 -0
  173. package/skills/luckiest-coder-writing-skills/render-graphs.js +168 -0
  174. package/skills/luckiest-coder-writing-skills/testing-skills-with-subagents.md +384 -0
  175. package/skills/luckiest-cold-email/ATTRIBUTION.md +36 -0
  176. package/skills/luckiest-cold-email/CHANGELOG.md +24 -0
  177. package/skills/luckiest-cold-email/SKILL.md +213 -0
  178. package/skills/luckiest-cold-email/evals/evals.json +94 -0
  179. package/skills/luckiest-cold-email/references/benchmarks.md +83 -0
  180. package/skills/luckiest-cold-email/references/follow-up-sequences.md +81 -0
  181. package/skills/luckiest-cold-email/references/frameworks.md +90 -0
  182. package/skills/luckiest-cold-email/references/personalization.md +79 -0
  183. package/skills/luckiest-cold-email/references/subject-lines.md +53 -0
  184. package/skills/luckiest-community-marketing/ATTRIBUTION.md +41 -0
  185. package/skills/luckiest-community-marketing/CHANGELOG.md +35 -0
  186. package/skills/luckiest-community-marketing/SKILL.md +200 -0
  187. package/skills/luckiest-community-marketing/evals/evals.json +89 -0
  188. package/skills/luckiest-competitor-profiling/ATTRIBUTION.md +36 -0
  189. package/skills/luckiest-competitor-profiling/CHANGELOG.md +24 -0
  190. package/skills/luckiest-competitor-profiling/SKILL.md +443 -0
  191. package/skills/luckiest-competitor-profiling/evals/evals.json +85 -0
  192. package/skills/luckiest-competitor-profiling/references/templates.md +167 -0
  193. package/skills/luckiest-competitor-profiling/references/tool-reference.md +179 -0
  194. package/skills/luckiest-competitors/ATTRIBUTION.md +36 -0
  195. package/skills/luckiest-competitors/CHANGELOG.md +25 -0
  196. package/skills/luckiest-competitors/SKILL.md +171 -0
  197. package/skills/luckiest-competitors/evals/evals.json +93 -0
  198. package/skills/luckiest-competitors/references/content-architecture.md +271 -0
  199. package/skills/luckiest-competitors/references/templates.md +223 -0
  200. package/skills/luckiest-content-strategy/ATTRIBUTION.md +39 -0
  201. package/skills/luckiest-content-strategy/CHANGELOG.md +23 -0
  202. package/skills/luckiest-content-strategy/SKILL.md +395 -0
  203. package/skills/luckiest-content-strategy/evals/evals.json +90 -0
  204. package/skills/luckiest-content-strategy/references/headless-cms.md +194 -0
  205. package/skills/luckiest-copy-editing/ATTRIBUTION.md +41 -0
  206. package/skills/luckiest-copy-editing/CHANGELOG.md +31 -0
  207. package/skills/luckiest-copy-editing/SKILL.md +491 -0
  208. package/skills/luckiest-copy-editing/evals/evals.json +89 -0
  209. package/skills/luckiest-copy-editing/references/checklist.md +66 -0
  210. package/skills/luckiest-copy-editing/references/content-refresh.md +38 -0
  211. package/skills/luckiest-copy-editing/references/plain-english-alternatives.md +394 -0
  212. package/skills/luckiest-copywriting/ATTRIBUTION.md +35 -0
  213. package/skills/luckiest-copywriting/CHANGELOG.md +23 -0
  214. package/skills/luckiest-copywriting/SKILL.md +296 -0
  215. package/skills/luckiest-copywriting/evals/evals.json +111 -0
  216. package/skills/luckiest-copywriting/references/copy-frameworks.md +344 -0
  217. package/skills/luckiest-copywriting/references/natural-transitions.md +272 -0
  218. package/skills/luckiest-cro/ATTRIBUTION.md +34 -0
  219. package/skills/luckiest-cro/CHANGELOG.md +23 -0
  220. package/skills/luckiest-cro/SKILL.md +215 -0
  221. package/skills/luckiest-cro/evals/evals.json +111 -0
  222. package/skills/luckiest-cro/references/experiments.md +248 -0
  223. package/skills/luckiest-cro/references/form.md +422 -0
  224. package/skills/luckiest-customer-research/ATTRIBUTION.md +40 -0
  225. package/skills/luckiest-customer-research/CHANGELOG.md +26 -0
  226. package/skills/luckiest-customer-research/SKILL.md +293 -0
  227. package/skills/luckiest-customer-research/evals/evals.json +162 -0
  228. package/skills/luckiest-customer-research/references/source-guides.md +401 -0
  229. package/skills/luckiest-directory-submissions/ATTRIBUTION.md +40 -0
  230. package/skills/luckiest-directory-submissions/CHANGELOG.md +33 -0
  231. package/skills/luckiest-directory-submissions/SKILL.md +419 -0
  232. package/skills/luckiest-directory-submissions/evals/evals.json +94 -0
  233. package/skills/luckiest-directory-submissions/references/directory-list.md +477 -0
  234. package/skills/luckiest-directory-submissions/references/positioning-variations.md +232 -0
  235. package/skills/luckiest-directory-submissions/references/submission-tracker-template.csv +266 -0
  236. package/skills/luckiest-emails/ATTRIBUTION.md +42 -0
  237. package/skills/luckiest-emails/CHANGELOG.md +33 -0
  238. package/skills/luckiest-emails/SKILL.md +220 -0
  239. package/skills/luckiest-emails/evals/evals.json +93 -0
  240. package/skills/luckiest-emails/references/copy-guidelines.md +120 -0
  241. package/skills/luckiest-emails/references/email-types.md +515 -0
  242. package/skills/luckiest-emails/references/sequence-templates.md +168 -0
  243. package/skills/luckiest-extract-design-system/ATTRIBUTION.md +8 -0
  244. package/skills/luckiest-extract-design-system/CHANGELOG.md +37 -0
  245. package/skills/luckiest-extract-design-system/SKILL.md +83 -0
  246. package/skills/luckiest-free-tools/ATTRIBUTION.md +43 -0
  247. package/skills/luckiest-free-tools/CHANGELOG.md +25 -0
  248. package/skills/luckiest-free-tools/SKILL.md +220 -0
  249. package/skills/luckiest-free-tools/evals/evals.json +90 -0
  250. package/skills/luckiest-free-tools/references/tool-types.md +217 -0
  251. package/skills/luckiest-image/ATTRIBUTION.md +43 -0
  252. package/skills/luckiest-image/CHANGELOG.md +23 -0
  253. package/skills/luckiest-image/SKILL.md +376 -0
  254. package/skills/luckiest-image/evals/evals.json +89 -0
  255. package/skills/luckiest-image/references/ai-image-prompting.md +229 -0
  256. package/skills/luckiest-launch/ATTRIBUTION.md +40 -0
  257. package/skills/luckiest-launch/CHANGELOG.md +43 -0
  258. package/skills/luckiest-launch/SKILL.md +385 -0
  259. package/skills/luckiest-launch/evals/evals.json +91 -0
  260. package/skills/luckiest-lead-magnets/ATTRIBUTION.md +35 -0
  261. package/skills/luckiest-lead-magnets/CHANGELOG.md +23 -0
  262. package/skills/luckiest-lead-magnets/SKILL.md +331 -0
  263. package/skills/luckiest-lead-magnets/evals/evals.json +87 -0
  264. package/skills/luckiest-lead-magnets/references/benchmarks.md +129 -0
  265. package/skills/luckiest-lead-magnets/references/format-guide.md +196 -0
  266. package/skills/luckiest-marketing-ideas/ATTRIBUTION.md +39 -0
  267. package/skills/luckiest-marketing-ideas/CHANGELOG.md +33 -0
  268. package/skills/luckiest-marketing-ideas/SKILL.md +191 -0
  269. package/skills/luckiest-marketing-ideas/evals/evals.json +90 -0
  270. package/skills/luckiest-marketing-ideas/references/ideas-by-category.md +366 -0
  271. package/skills/luckiest-marketing-plan/ATTRIBUTION.md +40 -0
  272. package/skills/luckiest-marketing-plan/CHANGELOG.md +23 -0
  273. package/skills/luckiest-marketing-plan/SKILL.md +285 -0
  274. package/skills/luckiest-marketing-plan/evals/evals.json +96 -0
  275. package/skills/luckiest-marketing-plan/references/aarrr-framework.md +180 -0
  276. package/skills/luckiest-marketing-plan/references/budget-planning.md +168 -0
  277. package/skills/luckiest-marketing-plan/references/client-types.md +373 -0
  278. package/skills/luckiest-marketing-plan/references/current-state-rubric.md +255 -0
  279. package/skills/luckiest-marketing-plan/references/example-quietude.md +972 -0
  280. package/skills/luckiest-marketing-plan/references/funding-stage-unlocks.md +230 -0
  281. package/skills/luckiest-marketing-plan/references/growth-patterns.md +148 -0
  282. package/skills/luckiest-marketing-plan/references/idea-cross-reference.md +265 -0
  283. package/skills/luckiest-marketing-plan/references/measurement-framework.md +213 -0
  284. package/skills/luckiest-marketing-plan/references/methodology.md +363 -0
  285. package/skills/luckiest-marketing-plan/references/ops-stack-mapping.md +197 -0
  286. package/skills/luckiest-marketing-plan/references/plan-template.md +494 -0
  287. package/skills/luckiest-marketing-plan/references/team-and-agency-model.md +278 -0
  288. package/skills/luckiest-marketing-psychology/ATTRIBUTION.md +31 -0
  289. package/skills/luckiest-marketing-psychology/CHANGELOG.md +32 -0
  290. package/skills/luckiest-marketing-psychology/SKILL.md +202 -0
  291. package/skills/luckiest-marketing-psychology/evals.json +88 -0
  292. package/skills/luckiest-model-router/ATTRIBUTION.md +11 -0
  293. package/skills/luckiest-model-router/CHANGELOG.md +45 -0
  294. package/skills/luckiest-model-router/SKILL.md +74 -0
  295. package/skills/luckiest-model-router/scripts/route.py +145 -0
  296. package/skills/luckiest-offers/ATTRIBUTION.md +39 -0
  297. package/skills/luckiest-offers/CHANGELOG.md +23 -0
  298. package/skills/luckiest-offers/SKILL.md +167 -0
  299. package/skills/luckiest-offers/references/bonus-stacking.md +150 -0
  300. package/skills/luckiest-offers/references/examples.md +215 -0
  301. package/skills/luckiest-offers/references/guarantee-design.md +172 -0
  302. package/skills/luckiest-offers/references/offer-anatomy.md +203 -0
  303. package/skills/luckiest-offers/references/offer-formats.md +240 -0
  304. package/skills/luckiest-offers/references/scarcity-urgency.md +175 -0
  305. package/skills/luckiest-offers/references/value-equation.md +134 -0
  306. package/skills/luckiest-onboarding/ATTRIBUTION.md +43 -0
  307. package/skills/luckiest-onboarding/CHANGELOG.md +31 -0
  308. package/skills/luckiest-onboarding/SKILL.md +243 -0
  309. package/skills/luckiest-onboarding/references/experiments.md +258 -0
  310. package/skills/luckiest-paywalls/ATTRIBUTION.md +39 -0
  311. package/skills/luckiest-paywalls/CHANGELOG.md +25 -0
  312. package/skills/luckiest-paywalls/SKILL.md +256 -0
  313. package/skills/luckiest-paywalls/evals/evals.json +93 -0
  314. package/skills/luckiest-paywalls/references/experiments.md +164 -0
  315. package/skills/luckiest-popups/ATTRIBUTION.md +35 -0
  316. package/skills/luckiest-popups/CHANGELOG.md +27 -0
  317. package/skills/luckiest-popups/SKILL.md +477 -0
  318. package/skills/luckiest-popups/evals/evals.json +107 -0
  319. package/skills/luckiest-pricing/ATTRIBUTION.md +35 -0
  320. package/skills/luckiest-pricing/CHANGELOG.md +31 -0
  321. package/skills/luckiest-pricing/SKILL.md +283 -0
  322. package/skills/luckiest-pricing/evals/evals.json +90 -0
  323. package/skills/luckiest-pricing/references/research-methods.md +152 -0
  324. package/skills/luckiest-pricing/references/tier-structure.md +232 -0
  325. package/skills/luckiest-product-marketing/ATTRIBUTION.md +38 -0
  326. package/skills/luckiest-product-marketing/CHANGELOG.md +23 -0
  327. package/skills/luckiest-product-marketing/SKILL.md +256 -0
  328. package/skills/luckiest-product-marketing/evals/evals.json +85 -0
  329. package/skills/luckiest-programmatic-seo/ATTRIBUTION.md +35 -0
  330. package/skills/luckiest-programmatic-seo/CHANGELOG.md +24 -0
  331. package/skills/luckiest-programmatic-seo/SKILL.md +286 -0
  332. package/skills/luckiest-programmatic-seo/evals/evals.json +95 -0
  333. package/skills/luckiest-programmatic-seo/references/playbooks.md +308 -0
  334. package/skills/luckiest-prompt-rewrite/ATTRIBUTION.md +39 -0
  335. package/skills/luckiest-prompt-rewrite/CHANGELOG.md +70 -0
  336. package/skills/luckiest-prompt-rewrite/LICENSE +21 -0
  337. package/skills/luckiest-prompt-rewrite/SKILL.md +494 -0
  338. package/skills/luckiest-prompt-rewrite/references/patterns.md +82 -0
  339. package/skills/luckiest-prompt-rewrite/references/templates.md +454 -0
  340. package/skills/luckiest-prospecting/ATTRIBUTION.md +29 -0
  341. package/skills/luckiest-prospecting/CHANGELOG.md +24 -0
  342. package/skills/luckiest-prospecting/SKILL.md +275 -0
  343. package/skills/luckiest-prospecting/evals/evals.json +107 -0
  344. package/skills/luckiest-prospecting/references/b2b-prospecting.md +106 -0
  345. package/skills/luckiest-prospecting/references/compliance.md +123 -0
  346. package/skills/luckiest-prospecting/references/data-sources.md +287 -0
  347. package/skills/luckiest-prospecting/references/local-prospecting.md +165 -0
  348. package/skills/luckiest-prospecting/references/saas-prospecting.md +123 -0
  349. package/skills/luckiest-public-relations/ATTRIBUTION.md +34 -0
  350. package/skills/luckiest-public-relations/CHANGELOG.md +23 -0
  351. package/skills/luckiest-public-relations/SKILL.md +154 -0
  352. package/skills/luckiest-public-relations/references/journalist-pitching.md +368 -0
  353. package/skills/luckiest-public-relations/references/media-outlets.md +207 -0
  354. package/skills/luckiest-public-relations/references/newsjacking.md +218 -0
  355. package/skills/luckiest-public-relations/references/press-platforms.md +188 -0
  356. package/skills/luckiest-referrals/ATTRIBUTION.md +41 -0
  357. package/skills/luckiest-referrals/CHANGELOG.md +23 -0
  358. package/skills/luckiest-referrals/SKILL.md +292 -0
  359. package/skills/luckiest-referrals/evals/evals.json +90 -0
  360. package/skills/luckiest-referrals/references/affiliate-programs.md +167 -0
  361. package/skills/luckiest-referrals/references/program-examples.md +143 -0
  362. package/skills/luckiest-revops/ATTRIBUTION.md +40 -0
  363. package/skills/luckiest-revops/CHANGELOG.md +24 -0
  364. package/skills/luckiest-revops/SKILL.md +362 -0
  365. package/skills/luckiest-revops/evals/evals.json +91 -0
  366. package/skills/luckiest-revops/references/automation-playbooks.md +290 -0
  367. package/skills/luckiest-revops/references/lifecycle-definitions.md +278 -0
  368. package/skills/luckiest-revops/references/routing-rules.md +203 -0
  369. package/skills/luckiest-revops/references/scoring-models.md +247 -0
  370. package/skills/luckiest-sales-enablement/ATTRIBUTION.md +39 -0
  371. package/skills/luckiest-sales-enablement/CHANGELOG.md +23 -0
  372. package/skills/luckiest-sales-enablement/SKILL.md +379 -0
  373. package/skills/luckiest-sales-enablement/evals/evals.json +91 -0
  374. package/skills/luckiest-sales-enablement/references/deck-frameworks.md +263 -0
  375. package/skills/luckiest-sales-enablement/references/demo-scripts.md +355 -0
  376. package/skills/luckiest-sales-enablement/references/objection-library.md +270 -0
  377. package/skills/luckiest-sales-enablement/references/one-pager-templates.md +208 -0
  378. package/skills/luckiest-schema/ATTRIBUTION.md +39 -0
  379. package/skills/luckiest-schema/CHANGELOG.md +25 -0
  380. package/skills/luckiest-schema/SKILL.md +209 -0
  381. package/skills/luckiest-schema/evals/evals.json +88 -0
  382. package/skills/luckiest-schema/references/schema-examples.md +398 -0
  383. package/skills/luckiest-seo-audit/ATTRIBUTION.md +39 -0
  384. package/skills/luckiest-seo-audit/CHANGELOG.md +23 -0
  385. package/skills/luckiest-seo-audit/SKILL.md +529 -0
  386. package/skills/luckiest-seo-audit/evals/evals.json +136 -0
  387. package/skills/luckiest-seo-audit/references/ai-writing-detection.md +200 -0
  388. package/skills/luckiest-seo-audit/references/international-seo.md +230 -0
  389. package/skills/luckiest-session-handoff/ATTRIBUTION.md +14 -0
  390. package/skills/luckiest-session-handoff/CHANGELOG.md +30 -0
  391. package/skills/luckiest-session-handoff/SKILL.md +96 -0
  392. package/skills/luckiest-signup/ATTRIBUTION.md +48 -0
  393. package/skills/luckiest-signup/CHANGELOG.md +24 -0
  394. package/skills/luckiest-signup/SKILL.md +382 -0
  395. package/skills/luckiest-signup/evals/evals.json +88 -0
  396. package/skills/luckiest-site-architecture/ATTRIBUTION.md +38 -0
  397. package/skills/luckiest-site-architecture/CHANGELOG.md +23 -0
  398. package/skills/luckiest-site-architecture/SKILL.md +383 -0
  399. package/skills/luckiest-site-architecture/evals/evals.json +88 -0
  400. package/skills/luckiest-site-architecture/references/mermaid-templates.md +216 -0
  401. package/skills/luckiest-site-architecture/references/navigation-patterns.md +305 -0
  402. package/skills/luckiest-site-architecture/references/site-type-templates.md +293 -0
  403. package/skills/luckiest-sms/ATTRIBUTION.md +27 -0
  404. package/skills/luckiest-sms/CHANGELOG.md +24 -0
  405. package/skills/luckiest-sms/SKILL.md +400 -0
  406. package/skills/luckiest-sms/evals/evals.json +100 -0
  407. package/skills/luckiest-sms/references/compliance.md +202 -0
  408. package/skills/luckiest-sms/references/platforms.md +318 -0
  409. package/skills/luckiest-sms/references/sequence-templates.md +282 -0
  410. package/skills/luckiest-social/ATTRIBUTION.md +27 -0
  411. package/skills/luckiest-social/CHANGELOG.md +24 -0
  412. package/skills/luckiest-social/SKILL.md +430 -0
  413. package/skills/luckiest-social/evals/evals.json +92 -0
  414. package/skills/luckiest-social/references/listening-sources-template.md +123 -0
  415. package/skills/luckiest-social/references/listening.md +283 -0
  416. package/skills/luckiest-social/references/platform-limits.md +110 -0
  417. package/skills/luckiest-social/references/platforms.md +170 -0
  418. package/skills/luckiest-social/references/post-templates.md +177 -0
  419. package/skills/luckiest-social/references/reverse-engineering.md +195 -0
  420. package/skills/luckiest-social/references/short-form-video.md +237 -0
  421. package/skills/luckiest-thinker/ATTRIBUTION.md +16 -0
  422. package/skills/luckiest-thinker/CHANGELOG.md +20 -0
  423. package/skills/luckiest-thinker/LICENSE +21 -0
  424. package/skills/luckiest-thinker/SKILL.md +92 -0
  425. package/skills/luckiest-thinker/references/distribution-calibration.md +106 -0
  426. package/skills/luckiest-thinker/references/research-foundations.md +214 -0
  427. package/skills/luckiest-thinker/references/thinking-patterns.md +138 -0
  428. package/skills/luckiest-trends/.skillignore +10 -0
  429. package/skills/luckiest-trends/ATTRIBUTION.md +43 -0
  430. package/skills/luckiest-trends/CHANGELOG.md +86 -0
  431. package/skills/luckiest-trends/LICENSE +21 -0
  432. package/skills/luckiest-trends/SKILL.md +194 -0
  433. package/skills/luckiest-trends/agents/openai.yaml +8 -0
  434. package/skills/luckiest-trends/references/engine-contract.md +2041 -0
  435. package/skills/luckiest-trends/references/save-html-brief.md +206 -0
  436. package/skills/luckiest-trends/scripts/briefing.py +272 -0
  437. package/skills/luckiest-trends/scripts/build-skill.sh +39 -0
  438. package/skills/luckiest-trends/scripts/compare.sh +61 -0
  439. package/skills/luckiest-trends/scripts/evaluate_search_quality.py +573 -0
  440. package/skills/luckiest-trends/scripts/last30days.py +1467 -0
  441. package/skills/luckiest-trends/scripts/lib/__init__.py +1 -0
  442. package/skills/luckiest-trends/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  443. package/skills/luckiest-trends/scripts/lib/__pycache__/arxiv.cpython-313.pyc +0 -0
  444. package/skills/luckiest-trends/scripts/lib/__pycache__/bird_x.cpython-313.pyc +0 -0
  445. package/skills/luckiest-trends/scripts/lib/__pycache__/bluesky.cpython-313.pyc +0 -0
  446. package/skills/luckiest-trends/scripts/lib/__pycache__/categories.cpython-313.pyc +0 -0
  447. package/skills/luckiest-trends/scripts/lib/__pycache__/cjk.cpython-313.pyc +0 -0
  448. package/skills/luckiest-trends/scripts/lib/__pycache__/cluster.cpython-313.pyc +0 -0
  449. package/skills/luckiest-trends/scripts/lib/__pycache__/dates.cpython-313.pyc +0 -0
  450. package/skills/luckiest-trends/scripts/lib/__pycache__/dedupe.cpython-313.pyc +0 -0
  451. package/skills/luckiest-trends/scripts/lib/__pycache__/digg.cpython-313.pyc +0 -0
  452. package/skills/luckiest-trends/scripts/lib/__pycache__/entity_extract.cpython-313.pyc +0 -0
  453. package/skills/luckiest-trends/scripts/lib/__pycache__/env.cpython-313.pyc +0 -0
  454. package/skills/luckiest-trends/scripts/lib/__pycache__/fusion.cpython-313.pyc +0 -0
  455. package/skills/luckiest-trends/scripts/lib/__pycache__/github.cpython-313.pyc +0 -0
  456. package/skills/luckiest-trends/scripts/lib/__pycache__/grounding.cpython-313.pyc +0 -0
  457. package/skills/luckiest-trends/scripts/lib/__pycache__/hackernews.cpython-313.pyc +0 -0
  458. package/skills/luckiest-trends/scripts/lib/__pycache__/hiring_signals.cpython-313.pyc +0 -0
  459. package/skills/luckiest-trends/scripts/lib/__pycache__/html_render.cpython-313.pyc +0 -0
  460. package/skills/luckiest-trends/scripts/lib/__pycache__/http.cpython-313.pyc +0 -0
  461. package/skills/luckiest-trends/scripts/lib/__pycache__/instagram.cpython-313.pyc +0 -0
  462. package/skills/luckiest-trends/scripts/lib/__pycache__/jobs.cpython-313.pyc +0 -0
  463. package/skills/luckiest-trends/scripts/lib/__pycache__/linkedin.cpython-313.pyc +0 -0
  464. package/skills/luckiest-trends/scripts/lib/__pycache__/log.cpython-313.pyc +0 -0
  465. package/skills/luckiest-trends/scripts/lib/__pycache__/normalize.cpython-313.pyc +0 -0
  466. package/skills/luckiest-trends/scripts/lib/__pycache__/permission_preflight.cpython-313.pyc +0 -0
  467. package/skills/luckiest-trends/scripts/lib/__pycache__/perplexity.cpython-313.pyc +0 -0
  468. package/skills/luckiest-trends/scripts/lib/__pycache__/pinterest.cpython-313.pyc +0 -0
  469. package/skills/luckiest-trends/scripts/lib/__pycache__/pipeline.cpython-313.pyc +0 -0
  470. package/skills/luckiest-trends/scripts/lib/__pycache__/planner.cpython-313.pyc +0 -0
  471. package/skills/luckiest-trends/scripts/lib/__pycache__/polymarket.cpython-313.pyc +0 -0
  472. package/skills/luckiest-trends/scripts/lib/__pycache__/preflight.cpython-313.pyc +0 -0
  473. package/skills/luckiest-trends/scripts/lib/__pycache__/providers.cpython-313.pyc +0 -0
  474. package/skills/luckiest-trends/scripts/lib/__pycache__/quality_nudge.cpython-313.pyc +0 -0
  475. package/skills/luckiest-trends/scripts/lib/__pycache__/query.cpython-313.pyc +0 -0
  476. package/skills/luckiest-trends/scripts/lib/__pycache__/reddit.cpython-313.pyc +0 -0
  477. package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_arctic.cpython-313.pyc +0 -0
  478. package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_enrich.cpython-313.pyc +0 -0
  479. package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_keyless.cpython-313.pyc +0 -0
  480. package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_listing.cpython-313.pyc +0 -0
  481. package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_public.cpython-313.pyc +0 -0
  482. package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_rss.cpython-313.pyc +0 -0
  483. package/skills/luckiest-trends/scripts/lib/__pycache__/reddit_shreddit.cpython-313.pyc +0 -0
  484. package/skills/luckiest-trends/scripts/lib/__pycache__/relevance.cpython-313.pyc +0 -0
  485. package/skills/luckiest-trends/scripts/lib/__pycache__/render.cpython-313.pyc +0 -0
  486. package/skills/luckiest-trends/scripts/lib/__pycache__/rerank.cpython-313.pyc +0 -0
  487. package/skills/luckiest-trends/scripts/lib/__pycache__/resolve.cpython-313.pyc +0 -0
  488. package/skills/luckiest-trends/scripts/lib/__pycache__/schema.cpython-313.pyc +0 -0
  489. package/skills/luckiest-trends/scripts/lib/__pycache__/signals.cpython-313.pyc +0 -0
  490. package/skills/luckiest-trends/scripts/lib/__pycache__/skill_meta.cpython-313.pyc +0 -0
  491. package/skills/luckiest-trends/scripts/lib/__pycache__/snippet.cpython-313.pyc +0 -0
  492. package/skills/luckiest-trends/scripts/lib/__pycache__/subproc.cpython-313.pyc +0 -0
  493. package/skills/luckiest-trends/scripts/lib/__pycache__/techmeme.cpython-313.pyc +0 -0
  494. package/skills/luckiest-trends/scripts/lib/__pycache__/threads.cpython-313.pyc +0 -0
  495. package/skills/luckiest-trends/scripts/lib/__pycache__/tiktok.cpython-313.pyc +0 -0
  496. package/skills/luckiest-trends/scripts/lib/__pycache__/trustpilot.cpython-313.pyc +0 -0
  497. package/skills/luckiest-trends/scripts/lib/__pycache__/truthsocial.cpython-313.pyc +0 -0
  498. package/skills/luckiest-trends/scripts/lib/__pycache__/ui.cpython-313.pyc +0 -0
  499. package/skills/luckiest-trends/scripts/lib/__pycache__/web_search_keyless.cpython-313.pyc +0 -0
  500. package/skills/luckiest-trends/scripts/lib/__pycache__/xai_x.cpython-313.pyc +0 -0
  501. package/skills/luckiest-trends/scripts/lib/__pycache__/xiaohongshu_api.cpython-313.pyc +0 -0
  502. package/skills/luckiest-trends/scripts/lib/__pycache__/xquik.cpython-313.pyc +0 -0
  503. package/skills/luckiest-trends/scripts/lib/__pycache__/xurl_x.cpython-313.pyc +0 -0
  504. package/skills/luckiest-trends/scripts/lib/__pycache__/youtube_yt.cpython-313.pyc +0 -0
  505. package/skills/luckiest-trends/scripts/lib/arxiv.py +290 -0
  506. package/skills/luckiest-trends/scripts/lib/bird_x.py +639 -0
  507. package/skills/luckiest-trends/scripts/lib/bluesky.py +323 -0
  508. package/skills/luckiest-trends/scripts/lib/categories.py +283 -0
  509. package/skills/luckiest-trends/scripts/lib/chrome_cookies.py +414 -0
  510. package/skills/luckiest-trends/scripts/lib/cjk.py +107 -0
  511. package/skills/luckiest-trends/scripts/lib/cluster.py +271 -0
  512. package/skills/luckiest-trends/scripts/lib/competitors.py +198 -0
  513. package/skills/luckiest-trends/scripts/lib/cookie_extract.py +508 -0
  514. package/skills/luckiest-trends/scripts/lib/dates.py +169 -0
  515. package/skills/luckiest-trends/scripts/lib/dedupe.py +145 -0
  516. package/skills/luckiest-trends/scripts/lib/digg.py +440 -0
  517. package/skills/luckiest-trends/scripts/lib/entity_extract.py +127 -0
  518. package/skills/luckiest-trends/scripts/lib/env.py +1073 -0
  519. package/skills/luckiest-trends/scripts/lib/fanout.py +84 -0
  520. package/skills/luckiest-trends/scripts/lib/fusion.py +207 -0
  521. package/skills/luckiest-trends/scripts/lib/github.py +996 -0
  522. package/skills/luckiest-trends/scripts/lib/grounding.py +331 -0
  523. package/skills/luckiest-trends/scripts/lib/hackernews.py +357 -0
  524. package/skills/luckiest-trends/scripts/lib/health.py +92 -0
  525. package/skills/luckiest-trends/scripts/lib/hiring_signals.py +326 -0
  526. package/skills/luckiest-trends/scripts/lib/html_publish.py +73 -0
  527. package/skills/luckiest-trends/scripts/lib/html_render.py +738 -0
  528. package/skills/luckiest-trends/scripts/lib/http.py +372 -0
  529. package/skills/luckiest-trends/scripts/lib/instagram.py +521 -0
  530. package/skills/luckiest-trends/scripts/lib/jobs.py +708 -0
  531. package/skills/luckiest-trends/scripts/lib/linkedin.py +363 -0
  532. package/skills/luckiest-trends/scripts/lib/log.py +35 -0
  533. package/skills/luckiest-trends/scripts/lib/normalize.py +759 -0
  534. package/skills/luckiest-trends/scripts/lib/permission_preflight.py +242 -0
  535. package/skills/luckiest-trends/scripts/lib/perplexity.py +745 -0
  536. package/skills/luckiest-trends/scripts/lib/pinterest.py +154 -0
  537. package/skills/luckiest-trends/scripts/lib/pipeline.py +1720 -0
  538. package/skills/luckiest-trends/scripts/lib/planner.py +763 -0
  539. package/skills/luckiest-trends/scripts/lib/polymarket.py +794 -0
  540. package/skills/luckiest-trends/scripts/lib/preflight.py +119 -0
  541. package/skills/luckiest-trends/scripts/lib/providers.py +380 -0
  542. package/skills/luckiest-trends/scripts/lib/quality_nudge.py +383 -0
  543. package/skills/luckiest-trends/scripts/lib/query.py +202 -0
  544. package/skills/luckiest-trends/scripts/lib/reddit.py +731 -0
  545. package/skills/luckiest-trends/scripts/lib/reddit_arctic.py +92 -0
  546. package/skills/luckiest-trends/scripts/lib/reddit_enrich.py +322 -0
  547. package/skills/luckiest-trends/scripts/lib/reddit_keyless.py +329 -0
  548. package/skills/luckiest-trends/scripts/lib/reddit_listing.py +188 -0
  549. package/skills/luckiest-trends/scripts/lib/reddit_public.py +275 -0
  550. package/skills/luckiest-trends/scripts/lib/reddit_rss.py +224 -0
  551. package/skills/luckiest-trends/scripts/lib/reddit_shreddit.py +184 -0
  552. package/skills/luckiest-trends/scripts/lib/relevance.py +200 -0
  553. package/skills/luckiest-trends/scripts/lib/render.py +2130 -0
  554. package/skills/luckiest-trends/scripts/lib/rerank.py +708 -0
  555. package/skills/luckiest-trends/scripts/lib/resolve.py +345 -0
  556. package/skills/luckiest-trends/scripts/lib/safari_cookies.py +193 -0
  557. package/skills/luckiest-trends/scripts/lib/schema.py +319 -0
  558. package/skills/luckiest-trends/scripts/lib/setup_wizard.py +843 -0
  559. package/skills/luckiest-trends/scripts/lib/signals.py +334 -0
  560. package/skills/luckiest-trends/scripts/lib/skill_meta.py +33 -0
  561. package/skills/luckiest-trends/scripts/lib/snippet.py +51 -0
  562. package/skills/luckiest-trends/scripts/lib/subproc.py +116 -0
  563. package/skills/luckiest-trends/scripts/lib/techmeme.py +239 -0
  564. package/skills/luckiest-trends/scripts/lib/threads.py +207 -0
  565. package/skills/luckiest-trends/scripts/lib/tiktok.py +596 -0
  566. package/skills/luckiest-trends/scripts/lib/transcribe.py +235 -0
  567. package/skills/luckiest-trends/scripts/lib/trustpilot.py +277 -0
  568. package/skills/luckiest-trends/scripts/lib/truthsocial.py +162 -0
  569. package/skills/luckiest-trends/scripts/lib/ui.py +626 -0
  570. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/LICENSE +21 -0
  571. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/bird-search.mjs +147 -0
  572. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/cookies.js +220 -0
  573. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/features.json +17 -0
  574. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/paginate-cursor.js +37 -0
  575. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/query-ids.json +20 -0
  576. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/runtime-features.js +151 -0
  577. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/runtime-query-ids.js +264 -0
  578. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-base.js +129 -0
  579. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-constants.js +50 -0
  580. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-features.js +347 -0
  581. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-search.js +157 -0
  582. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-types.js +2 -0
  583. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/lib/twitter-client-utils.js +511 -0
  584. package/skills/luckiest-trends/scripts/lib/vendor/bird-search/package.json +13 -0
  585. package/skills/luckiest-trends/scripts/lib/web_fetch_keyless.py +105 -0
  586. package/skills/luckiest-trends/scripts/lib/web_search_keyless.py +158 -0
  587. package/skills/luckiest-trends/scripts/lib/xai_x.py +236 -0
  588. package/skills/luckiest-trends/scripts/lib/xiaohongshu_api.py +162 -0
  589. package/skills/luckiest-trends/scripts/lib/xquik.py +404 -0
  590. package/skills/luckiest-trends/scripts/lib/xurl_x.py +171 -0
  591. package/skills/luckiest-trends/scripts/lib/youtube_yt.py +1365 -0
  592. package/skills/luckiest-trends/scripts/setup-keychain.sh +123 -0
  593. package/skills/luckiest-trends/scripts/setup-pass.sh +132 -0
  594. package/skills/luckiest-trends/scripts/store.py +1020 -0
  595. package/skills/luckiest-trends/scripts/test-v1-vs-v2.sh +220 -0
  596. package/skills/luckiest-trends/scripts/test_device_auth.py +128 -0
  597. package/skills/luckiest-trends/scripts/verify_v3.py +195 -0
  598. package/skills/luckiest-trends/scripts/watchlist.py +318 -0
  599. package/skills/luckiest-video/ATTRIBUTION.md +27 -0
  600. package/skills/luckiest-video/CHANGELOG.md +23 -0
  601. package/skills/luckiest-video/SKILL.md +371 -0
  602. package/skills/luckiest-video/evals/evals.json +88 -0
  603. package/skills/luckiest-video/references/ai-video-prompting.md +175 -0
  604. package/skills/remix/SKILL.md +32 -0
  605. package/commands/luckiest/go.md +0 -45
  606. package/commands/luckiest/plan.md +0 -55
  607. /package/commands/{luckiest/charms.md → charms.md} +0 -0
  608. /package/commands/{luckiest/helpers.md → helpers.md} +0 -0
  609. /package/commands/{luckiest/home.md → home.md} +0 -0
  610. /package/commands/{luckiest/leaderboard.md → leaderboard.md} +0 -0
  611. /package/commands/{luckiest/merge.md → merge.md} +0 -0
  612. /package/commands/{luckiest/overlaps.md → overlaps.md} +0 -0
  613. /package/commands/{luckiest/skills.md → skills.md} +0 -0
  614. /package/commands/{luckiest/start.md → start.md} +0 -0
  615. /package/commands/{luckiest/status.md → status.md} +0 -0
  616. /package/commands/{luckiest/updates.md → updates.md} +0 -0
  617. /package/commands/{luckiest/vouch.md → vouch.md} +0 -0
  618. /package/commands/{luckiest/wishes.md → wishes.md} +0 -0
@@ -0,0 +1,1720 @@
1
+ """v3.0.0 orchestration pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import sys
7
+ import threading
8
+ import time
9
+ from concurrent.futures import ThreadPoolExecutor, as_completed
10
+ from datetime import datetime, timezone
11
+ from shutil import which
12
+ from typing import Any
13
+
14
+ from . import (
15
+ arxiv,
16
+ bird_x,
17
+ bluesky,
18
+ dates,
19
+ dedupe,
20
+ digg,
21
+ entity_extract,
22
+ env,
23
+ github,
24
+ grounding,
25
+ hackernews,
26
+ hiring_signals,
27
+ instagram,
28
+ jobs,
29
+ linkedin,
30
+ normalize,
31
+ permission_preflight,
32
+ perplexity,
33
+ pinterest,
34
+ planner,
35
+ polymarket,
36
+ providers,
37
+ query,
38
+ reddit,
39
+ reddit_public,
40
+ relevance,
41
+ rerank,
42
+ schema,
43
+ signals,
44
+ snippet,
45
+ techmeme,
46
+ threads,
47
+ tiktok,
48
+ truthsocial,
49
+ trustpilot,
50
+ xai_x,
51
+ xiaohongshu_api,
52
+ xquik,
53
+ xurl_x,
54
+ youtube_yt,
55
+ )
56
+ from .cluster import cluster_candidates
57
+ from .fusion import weighted_rrf
58
+
59
+ DEPTH_SETTINGS = {
60
+ "quick": {"per_stream_limit": 6, "pool_limit": 15, "rerank_limit": 12},
61
+ "default": {"per_stream_limit": 12, "pool_limit": 40, "rerank_limit": 40},
62
+ "deep": {"per_stream_limit": 20, "pool_limit": 60, "rerank_limit": 60},
63
+ }
64
+
65
+ SEARCH_ALIAS = {
66
+ "hn": "hackernews",
67
+ "bsky": "bluesky",
68
+ "truth": "truthsocial",
69
+ "web": "grounding",
70
+ "xhs": "xiaohongshu",
71
+ "xquik": "x", # xquik is a backend of the single "x" source, not its own source
72
+ }
73
+
74
+ MAX_SOURCE_FETCHES: dict[str, int] = {"x": 2, "jobs": 1, "linkedin": 1}
75
+
76
+ # Per-handle result caps for the X handle-search lanes. The FROM lane (the
77
+ # subject's own timeline) is the single best source for a person topic, so it
78
+ # gets the highest cap; the ABOUT (mention) and related-handle lanes stay
79
+ # modest so total volume and request budget don't balloon.
80
+ FROM_LANE_COUNT_PER = 8
81
+ MENTION_LANE_COUNT_PER = 5
82
+ RELATED_HANDLE_COUNT_PER = 3
83
+
84
+
85
+ def _has_perplexity_provider(config: dict[str, Any]) -> bool:
86
+ return bool(config.get("PERPLEXITY_API_KEY") or config.get("OPENROUTER_API_KEY"))
87
+
88
+ MOCK_AVAILABLE_SOURCES = [
89
+ "reddit",
90
+ "x",
91
+ "youtube",
92
+ "tiktok",
93
+ "instagram",
94
+ "hackernews",
95
+ "bluesky",
96
+ "truthsocial",
97
+ "polymarket",
98
+ "grounding",
99
+ "xiaohongshu",
100
+ "github",
101
+ "perplexity",
102
+ "threads",
103
+ "pinterest",
104
+ "digg",
105
+ "arxiv",
106
+ "techmeme",
107
+ "trustpilot",
108
+ "jobs",
109
+ "linkedin",
110
+ ]
111
+
112
+
113
+ def normalize_requested_sources(sources: list[str] | None) -> list[str] | None:
114
+ if not sources:
115
+ return None
116
+ normalized = []
117
+ for source in sources:
118
+ key = SEARCH_ALIAS.get(source.lower(), source.lower())
119
+ if key not in normalized:
120
+ normalized.append(key)
121
+ return normalized
122
+
123
+
124
+ def available_sources(
125
+ config: dict[str, Any],
126
+ requested_sources: list[str] | None = None,
127
+ *,
128
+ x_pending: bool | None = None,
129
+ ) -> list[str]:
130
+ available: list[str] = []
131
+ # reddit_public needs no API key - always available
132
+ available.append("reddit")
133
+ if config.get("SCRAPECREATORS_API_KEY"):
134
+ available.extend(["tiktok", "instagram"])
135
+ if env.get_x_source(config):
136
+ available.append("x")
137
+ else:
138
+ # Safe inspection (--diagnose/--preflight) skips browser-cookie
139
+ # extraction, so get_x_source is None even though a real run would
140
+ # authenticate X via FROM_BROWSER. Report it as available so consumers
141
+ # of available_sources (SKILL.md ACTIVE_SOURCES_LIST) don't under-report.
142
+ # diagnose() precomputes the predicate and passes it via x_pending to
143
+ # avoid evaluating it twice in one diagnose() call.
144
+ if x_pending is None:
145
+ x_pending = env.x_pending_browser_auth(config)
146
+ if x_pending:
147
+ available.append("x")
148
+ if which("yt-dlp") or env.is_youtube_sc_available(config):
149
+ available.append("youtube")
150
+ available.extend(["hackernews", "polymarket"])
151
+ # GitHub is reachable via the unauthenticated REST tier too, so it is
152
+ # available even without a token/gh CLI (a token only raises rate limits).
153
+ available.append("github")
154
+ if which("digg-pp-cli"):
155
+ available.append("digg")
156
+ # arXiv is default-on when its Printing Press CLI is installed (zero auth).
157
+ # The adapter relevance-and-recency gates so it stays quiet off-topic.
158
+ if which("arxiv-pp-cli"):
159
+ available.append("arxiv")
160
+ # Techmeme is default-on when its CLI is installed (zero auth; sub-second
161
+ # local sync before each run's first search).
162
+ if which("techmeme-pp-cli"):
163
+ available.append("techmeme")
164
+ if env.is_bluesky_available(config):
165
+ available.append("bluesky")
166
+ if env.is_truthsocial_available(config):
167
+ available.append("truthsocial")
168
+ # Grounding (general web) is available when a paid backend is configured OR
169
+ # the keyless floor is permitted (i.e. the host has no native search). On a
170
+ # native-search host with no paid key, keyless_web_allowed is False and the
171
+ # engine leaves general web to the model's own search.
172
+ if (config.get("BRAVE_API_KEY") or config.get("EXA_API_KEY")
173
+ or config.get("SERPER_API_KEY") or config.get("PARALLEL_API_KEY")
174
+ or env.keyless_web_allowed(config)):
175
+ available.append("grounding")
176
+ if requested_sources and "jobs" in requested_sources:
177
+ available.append("jobs")
178
+ # Perplexity Sonar: opt-in additive source via INCLUDE_SOURCES=perplexity
179
+ include_sources = (config.get("INCLUDE_SOURCES") or "").lower().split(",")
180
+ if _has_perplexity_provider(config) and (
181
+ "perplexity" in include_sources or (requested_sources and "perplexity" in requested_sources)
182
+ ):
183
+ available.append("perplexity")
184
+ # LinkedIn: opt-in additive source via INCLUDE_SOURCES=linkedin (same
185
+ # consent pattern as Perplexity). Unlike tiktok/instagram, which are
186
+ # offered during SKILL.md Step 0 onboarding, LinkedIn is power-user-only
187
+ # and must not silently activate for existing SCRAPECREATORS_API_KEY
188
+ # holders.
189
+ if config.get("SCRAPECREATORS_API_KEY") and (
190
+ "linkedin" in include_sources or (requested_sources and "linkedin" in requested_sources)
191
+ ):
192
+ available.append("linkedin")
193
+ # Trustpilot: opt-in additive source via INCLUDE_SOURCES=trustpilot (same
194
+ # consent pattern as Perplexity/LinkedIn). Off by default -- unlike arXiv and
195
+ # Techmeme, which are zero-auth, it can spawn a one-time headless-Chrome WAF
196
+ # cookie harvest on a brand topic, so activating it is the user's choice.
197
+ if which("trustpilot-pp-cli") and (
198
+ "trustpilot" in include_sources or (requested_sources and "trustpilot" in requested_sources)
199
+ ):
200
+ available.append("trustpilot")
201
+ if requested_sources and "xiaohongshu" in requested_sources and env.is_xiaohongshu_available(config):
202
+ available.append("xiaohongshu")
203
+ if env.is_threads_available(config):
204
+ available.append("threads")
205
+ if requested_sources and "pinterest" in requested_sources and env.is_pinterest_available(config):
206
+ available.append("pinterest")
207
+ # xquik is a backend of the single "x" source (see env.x_backend_chain),
208
+ # not a separate parallel source — registered via the "x" entry above.
209
+ exclude = {s.strip().lower() for s in (config.get("EXCLUDE_SOURCES") or "").split(",") if s.strip()}
210
+ if exclude:
211
+ available = [s for s in available if s not in exclude]
212
+ return available
213
+
214
+
215
+ def diagnose(
216
+ config: dict[str, Any],
217
+ requested_sources: list[str] | None = None,
218
+ *,
219
+ safe: bool = False,
220
+ ) -> dict[str, Any]:
221
+ requested_sources = normalize_requested_sources(requested_sources)
222
+ google_key = _google_key(config)
223
+ x_status = env.get_x_source_status(config, probe=not safe)
224
+ # Compute once and reuse for both the diag flag and available_sources below.
225
+ x_pending = env.x_pending_browser_auth(config)
226
+ native_web_backend = None
227
+ if config.get("BRAVE_API_KEY"):
228
+ native_web_backend = "brave"
229
+ elif config.get("EXA_API_KEY"):
230
+ native_web_backend = "exa"
231
+ elif config.get("SERPER_API_KEY"):
232
+ native_web_backend = "serper"
233
+ elif config.get("PARALLEL_API_KEY"):
234
+ native_web_backend = "parallel"
235
+ providers_status = {
236
+ "google": bool(google_key),
237
+ "openai": bool(config.get("OPENAI_API_KEY")) and config.get("OPENAI_AUTH_STATUS") == env.AUTH_STATUS_OK,
238
+ "xai": bool(config.get("XAI_API_KEY")),
239
+ "openrouter": bool(config.get("OPENROUTER_API_KEY")),
240
+ "perplexity": bool(config.get("PERPLEXITY_API_KEY")),
241
+ }
242
+ reasoning_provider_available = any(
243
+ providers_status[name] for name in ("google", "openai", "xai", "openrouter")
244
+ )
245
+ external_commands = {
246
+ "yt-dlp": bool(which("yt-dlp")),
247
+ "digg-pp-cli": bool(which("digg-pp-cli")),
248
+ "arxiv-pp-cli": bool(which("arxiv-pp-cli")),
249
+ "techmeme-pp-cli": bool(which("techmeme-pp-cli")),
250
+ "trustpilot-pp-cli": bool(which("trustpilot-pp-cli")),
251
+ "gh": bool(which("gh")),
252
+ }
253
+ credential_destinations = {
254
+ "global_env": str(env.CONFIG_FILE) if env.CONFIG_FILE else None,
255
+ }
256
+ browser_cookies = {
257
+ "mode": config.get("_BROWSER_COOKIE_MODE", "off"),
258
+ "browsers": list(config.get("_BROWSER_COOKIE_BROWSERS") or []),
259
+ "reads_values": False if safe else config.get("_BROWSER_COOKIE_MODE") == "read",
260
+ }
261
+ ignored_project_keys = list(config.get("_IGNORED_PROJECT_CONFIG_KEYS") or [])
262
+ ignored_endpoint_overrides = [
263
+ key for key in ignored_project_keys if key in permission_preflight.ENDPOINT_OVERRIDE_KEYS
264
+ ]
265
+ local_writes: list[dict[str, str]] = []
266
+ if config.get("LAST30DAYS_MEMORY_DIR"):
267
+ local_writes.append({"kind": "report", "path": str(config.get("LAST30DAYS_MEMORY_DIR"))})
268
+ diag = {
269
+ "providers": providers_status,
270
+ "local_mode": not reasoning_provider_available,
271
+ "reasoning_provider": (config.get("LAST30DAYS_REASONING_PROVIDER") or "auto").lower(),
272
+ "x_backend": x_status["source"],
273
+ "bird_installed": x_status["bird_installed"],
274
+ "bird_authenticated": x_status["bird_authenticated"],
275
+ "bird_username": x_status["bird_username"],
276
+ "x_pending_browser_auth": x_pending,
277
+ "xquik_available": x_status.get("xquik_available", False),
278
+ "xquik_working": x_status.get("xquik_working"),
279
+ "xquik_status": x_status.get("xquik_status", ""),
280
+ "native_web_backend": native_web_backend,
281
+ "native_search": env.is_native_search(config),
282
+ "has_scrapecreators": bool(config.get("SCRAPECREATORS_API_KEY")),
283
+ "has_github": bool(config.get("GITHUB_TOKEN") or which("gh")),
284
+ "available_sources": available_sources(config, requested_sources, x_pending=x_pending),
285
+ "safe": safe,
286
+ "config_source": config.get("_CONFIG_SOURCE"),
287
+ "ignored_project_config": config.get("_IGNORED_PROJECT_CONFIG"),
288
+ "ignored_project_config_keys": ignored_project_keys,
289
+ "ignored_endpoint_overrides": ignored_endpoint_overrides,
290
+ "browser_cookies": browser_cookies,
291
+ "external_commands": external_commands,
292
+ "credential_destinations": credential_destinations,
293
+ "local_writes": local_writes,
294
+ }
295
+ diag["permission_preflight"] = permission_preflight.build(config, diag)
296
+ return diag
297
+
298
+
299
+ def _inner_max_workers(stream_count: int, *, internal_subrun: bool) -> int:
300
+ """Worker-pool size for the per-stream fanout inside a single pipeline run.
301
+
302
+ Top-level runs use up to 16 workers. Subruns of ``run_competitor_fanout``
303
+ cap the inner pool to 4 so a six-way competitor fan-out stays below
304
+ roughly 30 worker threads in aggregate instead of ~96.
305
+ """
306
+ if internal_subrun:
307
+ return max(2, min(4, stream_count or 1))
308
+ return max(4, min(16, stream_count or 1))
309
+
310
+
311
+ def run(
312
+ *,
313
+ topic: str,
314
+ config: dict[str, Any],
315
+ depth: str,
316
+ requested_sources: list[str] | None = None,
317
+ mock: bool = False,
318
+ x_handle: str | None = None,
319
+ x_related: list[str] | None = None,
320
+ web_backend: str = "auto",
321
+ external_plan: dict | None = None,
322
+ subreddits: list[str] | None = None,
323
+ tiktok_hashtags: list[str] | None = None,
324
+ tiktok_creators: list[str] | None = None,
325
+ ig_creators: list[str] | None = None,
326
+ lookback_days: int = 30,
327
+ as_of_date: str | None = None,
328
+ github_user: str | None = None,
329
+ github_repos: list[str] | None = None,
330
+ hiring_signals_mode: bool = False,
331
+ internal_subrun: bool = False,
332
+ ) -> schema.Report:
333
+ settings = DEPTH_SETTINGS[depth]
334
+ requested_sources = normalize_requested_sources(requested_sources)
335
+ from_date, to_date = dates.get_date_range(lookback_days, as_of_date=as_of_date)
336
+
337
+ if mock:
338
+ runtime = providers.mock_runtime(config, depth)
339
+ reasoning_provider = None
340
+ available = list(requested_sources or MOCK_AVAILABLE_SOURCES)
341
+ if not requested_sources and not hiring_signals_mode and not _company_topic_likely(topic):
342
+ available = [source for source in available if source != "jobs"]
343
+ else:
344
+ runtime, reasoning_provider = providers.resolve_runtime(config, depth)
345
+ available = available_sources(config, requested_sources)
346
+ if requested_sources:
347
+ available = [source for source in available if source in requested_sources]
348
+ if web_backend == "none":
349
+ available = [s for s in available if s != "grounding"]
350
+ elif web_backend in ("brave", "exa", "serper", "parallel", "keyless") and "grounding" not in available:
351
+ available.append("grounding")
352
+ if (hiring_signals_mode or _company_topic_likely(topic)) and "jobs" not in available:
353
+ available.append("jobs")
354
+ if hiring_signals_mode:
355
+ config = dict(config)
356
+ config["_hiring_signals_mode"] = True
357
+ if not requested_sources:
358
+ available = ["jobs"]
359
+ if not available:
360
+ raise RuntimeError("No sources are available for this run.")
361
+
362
+ planner_requested_sources = requested_sources
363
+ if hiring_signals_mode and not planner_requested_sources:
364
+ planner_requested_sources = ["jobs"]
365
+
366
+ if external_plan:
367
+ # External plan provided (e.g., from Claude Code via --plan flag).
368
+ # Parse it through the same sanitizer to validate structure.
369
+ plan = planner._sanitize_plan(
370
+ external_plan, topic, available, planner_requested_sources, depth,
371
+ )
372
+ plan_source = "external"
373
+ else:
374
+ plan = planner.plan_query(
375
+ topic=topic,
376
+ available_sources=available,
377
+ requested_sources=planner_requested_sources,
378
+ depth=depth,
379
+ provider=None if mock else reasoning_provider,
380
+ model=None if mock else runtime.planner_model,
381
+ context=config.get("_auto_resolve_context", ""),
382
+ internal_subrun=internal_subrun,
383
+ )
384
+ # Source labelling: the fallback path annotates notes with "fallback-plan"
385
+ # or "deterministic-comparison-plan"; anything else came from the LLM.
386
+ if any("fallback" in note or "deterministic" in note for note in (plan.notes or [])):
387
+ plan_source = "deterministic"
388
+ elif not mock and reasoning_provider and runtime.planner_model:
389
+ plan_source = "llm"
390
+ else:
391
+ plan_source = "deterministic"
392
+
393
+ # Safety net: ensure grounding appears in all subqueries even if the planner
394
+ # omits it. This is redundant when the planner includes grounding via
395
+ # SOURCE_CAPABILITIES, but kept as a fallback.
396
+ if web_backend != "none" and "grounding" in available:
397
+ for sq in plan.subqueries:
398
+ if "grounding" not in sq.sources:
399
+ sq.sources.append("grounding")
400
+ _ensure_jobs_in_plan(plan, available, explicit=hiring_signals_mode, topic=topic)
401
+
402
+ # Always-on planner trace. Emits one summary line plus one per subquery
403
+ # so retrieval-breadth failures like the 2026-04-19 Hermes Agent Use Cases
404
+ # disaster are visible without --debug. Stderr only; does not leak into
405
+ # the user-facing stdout synthesis.
406
+ print(
407
+ f"[Planner] Plan: intent={plan.intent}, freshness={plan.freshness_mode}, "
408
+ f"cluster_mode={plan.cluster_mode}, subqueries={len(plan.subqueries)}, "
409
+ f"source={plan_source}",
410
+ file=sys.stderr,
411
+ )
412
+ if plan.subqueries:
413
+ for index, sq in enumerate(plan.subqueries, start=1):
414
+ sources_str = ",".join(sq.sources) if sq.sources else "(none)"
415
+ print(
416
+ f"[Planner] sq{index} label={sq.label} "
417
+ f'search="{sq.search_query}" sources=[{sources_str}]',
418
+ file=sys.stderr,
419
+ )
420
+ else:
421
+ print("[Planner] (no subqueries in plan)", file=sys.stderr)
422
+
423
+ bundle = schema.RetrievalBundle(artifacts={"grounding": []})
424
+ # Expose plan_source to the renderer so render_compact can emit the
425
+ # DEGRADED RUN banner when a named-entity topic was invoked bare
426
+ # (source=deterministic AND no pre-research flags). LAW 7 backstop.
427
+ bundle.artifacts["plan_source"] = plan_source
428
+ # Hiring-signals is deliberately jobs-only with no multi-source --plan, so
429
+ # the LAW 7 degraded-run and Step 0.55 pre-research banners do not apply -
430
+ # they would contradict the documented jobs-scoped flow. Suppress them.
431
+ bundle.artifacts["hiring_signals_mode"] = hiring_signals_mode
432
+
433
+ # Project-mode or person-mode GitHub: run once before the main subquery loop
434
+ _github_custom_done = False
435
+ _github_enriched_repos: set[str] = set()
436
+
437
+ # Project mode takes priority over person mode
438
+ if github_repos and "github" in available:
439
+ try:
440
+ project_items = github.search_github_project(
441
+ github_repos, from_date, to_date,
442
+ depth=depth, token=config.get("GITHUB_TOKEN"),
443
+ )
444
+ if project_items:
445
+ normalized = _normalize_score_dedupe(
446
+ "github", project_items, from_date, to_date,
447
+ freshness_mode=plan.freshness_mode,
448
+ ranking_query=f"What are {', '.join(github_repos)} doing on GitHub?",
449
+ )
450
+ primary_label = plan.subqueries[0].label if plan.subqueries else "primary"
451
+ bundle.add_items(primary_label, "github", normalized)
452
+ _github_custom_done = True
453
+ _github_enriched_repos = {r.lower() for r in github_repos}
454
+ except Exception as exc:
455
+ bundle.errors_by_source["github"] = f"Project-mode failed: {exc}"
456
+
457
+ _github_person_done = False
458
+ if github_user and "github" in available and not _github_custom_done:
459
+ try:
460
+ person_items = github.search_github_person(
461
+ github_user, from_date, to_date,
462
+ depth=depth, token=config.get("GITHUB_TOKEN"),
463
+ )
464
+ if person_items:
465
+ normalized = _normalize_score_dedupe(
466
+ "github", person_items, from_date, to_date,
467
+ freshness_mode=plan.freshness_mode,
468
+ ranking_query=f"What is @{github_user} doing on GitHub?",
469
+ )
470
+ # Use the first subquery's label so RRF can look up the weight
471
+ primary_label = plan.subqueries[0].label if plan.subqueries else "primary"
472
+ bundle.add_items(primary_label, "github", normalized)
473
+ _github_person_done = True
474
+ except Exception as exc:
475
+ bundle.errors_by_source["github"] = f"Person-mode failed: {exc}"
476
+
477
+ # Thread-safe set prevents redundant fetches after a source returns 429
478
+ rate_limited_sources: set[str] = set()
479
+ rate_limit_lock = threading.Lock()
480
+
481
+ futures = {}
482
+ # Per-source fetch budget prevents redundant API calls
483
+ source_fetch_count: dict[str, int] = {}
484
+ stream_count = sum(
485
+ 1
486
+ for subquery in plan.subqueries
487
+ for source in subquery.sources
488
+ if source in available
489
+ )
490
+ max_workers = _inner_max_workers(stream_count, internal_subrun=internal_subrun)
491
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
492
+ for subquery in plan.subqueries:
493
+ for source in subquery.sources:
494
+ if source not in available:
495
+ continue
496
+ # Skip GitHub keyword search if person-mode already ran
497
+ if source == "github" and (_github_person_done or _github_custom_done):
498
+ continue
499
+ # Enforce per-source fetch cap
500
+ cap = MAX_SOURCE_FETCHES.get(source)
501
+ if cap is not None:
502
+ current = source_fetch_count.get(source, 0)
503
+ if current >= cap:
504
+ continue
505
+ source_fetch_count[source] = current + 1
506
+ futures[
507
+ executor.submit(
508
+ _retrieve_stream,
509
+ topic=topic,
510
+ subquery=subquery,
511
+ source=source,
512
+ config=config,
513
+ depth=depth,
514
+ date_range=(from_date, to_date),
515
+ runtime=runtime,
516
+ mock=mock,
517
+ rate_limited_sources=rate_limited_sources,
518
+ rate_limit_lock=rate_limit_lock,
519
+ web_backend=web_backend,
520
+ raw_topic=topic,
521
+ subreddits=subreddits,
522
+ tiktok_hashtags=tiktok_hashtags,
523
+ tiktok_creators=tiktok_creators,
524
+ ig_creators=ig_creators,
525
+ )
526
+ ] = (subquery, source)
527
+
528
+ for future in as_completed(futures):
529
+ subquery, source = futures[future]
530
+ try:
531
+ raw_items, artifact = future.result()
532
+ except Exception as exc:
533
+ # Share 429 signal so pending futures skip this source
534
+ if _is_rate_limit_error(exc):
535
+ with rate_limit_lock:
536
+ rate_limited_sources.add(source)
537
+ bundle.errors_by_source[source] = str(exc)
538
+ continue
539
+ # Retry once for transient 5xx errors
540
+ if _is_transient_error(exc):
541
+ time.sleep(3)
542
+ try:
543
+ raw_items, artifact = _retrieve_stream(
544
+ topic=topic, subquery=subquery, source=source,
545
+ config=config, depth=depth, date_range=(from_date, to_date),
546
+ runtime=runtime, mock=mock,
547
+ rate_limited_sources=rate_limited_sources,
548
+ rate_limit_lock=rate_limit_lock,
549
+ web_backend=web_backend,
550
+ raw_topic=topic,
551
+ subreddits=subreddits,
552
+ tiktok_hashtags=tiktok_hashtags,
553
+ tiktok_creators=tiktok_creators,
554
+ ig_creators=ig_creators,
555
+ )
556
+ except Exception as retry_exc:
557
+ bundle.errors_by_source[source] = f"{exc} (retried once, still failed: {retry_exc})"
558
+ continue
559
+ else:
560
+ bundle.errors_by_source[source] = str(exc)
561
+ continue
562
+ normalized = _normalize_score_dedupe(
563
+ source, raw_items, from_date, to_date,
564
+ freshness_mode=plan.freshness_mode,
565
+ ranking_query=subquery.ranking_query,
566
+ )
567
+ # Jobs is exempt from per_stream_limit: a careers board is a complete
568
+ # snapshot of open roles, and truncating it to the default 12 drops
569
+ # strategic postings (the whole point of hiring-signals coverage).
570
+ if source != "jobs":
571
+ normalized = normalized[: settings["per_stream_limit"]]
572
+ bundle.add_items(subquery.label, source, normalized)
573
+ if artifact:
574
+ bundle.artifacts.setdefault("grounding", []).append(artifact)
575
+
576
+ # Phase 2: supplemental entity-based searches
577
+ _run_supplemental_searches(
578
+ topic=topic,
579
+ bundle=bundle,
580
+ plan=plan,
581
+ config=config,
582
+ depth=depth,
583
+ date_range=(from_date, to_date),
584
+ runtime=runtime,
585
+ mock=mock,
586
+ rate_limited_sources=rate_limited_sources,
587
+ rate_limit_lock=rate_limit_lock,
588
+ x_handle=x_handle,
589
+ x_related=x_related,
590
+ )
591
+
592
+ # Phase 2b: retry thin sources with simplified query
593
+ # Note: _github_skip_sources tells the retry to not re-run GitHub keyword search
594
+ # when project-mode or person-mode already provided authoritative data.
595
+ _github_skip_retry = {"github"} if (_github_person_done or _github_custom_done) else set()
596
+ _retry_thin_sources(
597
+ topic=topic,
598
+ bundle=bundle,
599
+ plan=plan,
600
+ config=config,
601
+ depth=depth,
602
+ date_range=(from_date, to_date),
603
+ runtime=runtime,
604
+ mock=mock,
605
+ rate_limited_sources=rate_limited_sources,
606
+ rate_limit_lock=rate_limit_lock,
607
+ settings=settings,
608
+ web_backend=web_backend,
609
+ skip_sources=_github_skip_retry,
610
+ )
611
+
612
+ # Reclassify partial failures as DEGRADED instead of silently dropping them.
613
+ # A source that 429'd on one subquery but succeeded on another is not a hard
614
+ # failure, but it is not healthy either: it likely returned fewer results
615
+ # than it should have. Move it out of errors_by_source (so it isn't reported
616
+ # as "failed") and into degraded_by_source (so it survives into warnings),
617
+ # rather than deleting the signal outright as the engine used to.
618
+ degraded_by_source: dict[str, str] = {}
619
+ for source in list(bundle.errors_by_source):
620
+ if bundle.items_by_source.get(source):
621
+ degraded_by_source[source] = bundle.errors_by_source[source]
622
+ del bundle.errors_by_source[source]
623
+
624
+ hiring_summary = _apply_hiring_signal_gate(
625
+ bundle,
626
+ explicit=hiring_signals_mode,
627
+ topic=topic,
628
+ )
629
+ if hiring_summary:
630
+ bundle.artifacts["hiring_signals"] = hiring_summary
631
+
632
+ items_by_source = _finalize_items_by_source(
633
+ bundle.items_by_source, topic=topic, config=config, depth=depth, mock=mock,
634
+ )
635
+ candidates = weighted_rrf(bundle.items_by_source_and_query, plan, pool_limit=settings["pool_limit"])
636
+ # Normalized set of handles this run resolved for the topic. A candidate
637
+ # authored by one of these is first-party and is exempted from the
638
+ # entity-miss demotion in rerank (a post never repeats its own author's
639
+ # name, so the body-text grounding check would otherwise zero out the
640
+ # subject's own highest-signal posts).
641
+ resolved_handles = {
642
+ h.lstrip("@").strip().lower()
643
+ for h in ([x_handle, github_user, *(x_related or [])])
644
+ if h and h.strip()
645
+ }
646
+ ranked_candidates = rerank.rerank_candidates(
647
+ topic=topic,
648
+ plan=plan,
649
+ candidates=candidates,
650
+ provider=None if mock else reasoning_provider,
651
+ model=None if mock else runtime.rerank_model,
652
+ shortlist_size=settings["rerank_limit"],
653
+ resolved_handles=resolved_handles,
654
+ )
655
+ rerank.score_fun(
656
+ topic=topic,
657
+ candidates=ranked_candidates,
658
+ provider=None if mock else reasoning_provider,
659
+ model=None if mock else runtime.rerank_model,
660
+ )
661
+
662
+ # Phase 3: post-rerank GitHub star enrichment
663
+ if "github" in available and not mock:
664
+ github.enrich_candidates_with_stars(
665
+ ranked_candidates,
666
+ token=config.get("GITHUB_TOKEN"),
667
+ already_enriched=_github_enriched_repos,
668
+ )
669
+
670
+ clusters = cluster_candidates(ranked_candidates, plan)
671
+ warnings = _warnings(items_by_source, ranked_candidates, bundle.errors_by_source, degraded_by_source)
672
+
673
+ return schema.Report(
674
+ topic=topic,
675
+ range_from=from_date,
676
+ range_to=to_date,
677
+ generated_at=datetime.now(timezone.utc).isoformat(),
678
+ provider_runtime=runtime,
679
+ query_plan=plan,
680
+ clusters=clusters,
681
+ ranked_candidates=ranked_candidates,
682
+ items_by_source=items_by_source,
683
+ errors_by_source=bundle.errors_by_source,
684
+ warnings=warnings,
685
+ artifacts=bundle.artifacts,
686
+ )
687
+
688
+
689
+ def _normalize_score_dedupe(
690
+ source: str,
691
+ raw_items: list[dict],
692
+ from_date: str,
693
+ to_date: str,
694
+ freshness_mode: str,
695
+ ranking_query: str,
696
+ ) -> list[schema.SourceItem]:
697
+ """Normalize, annotate, prune, dedupe, and extract snippets for a batch of raw items."""
698
+ normalized = normalize.normalize_source_items(
699
+ source, raw_items, from_date, to_date,
700
+ freshness_mode=freshness_mode,
701
+ )
702
+ prepared_query = relevance.PreparedQuery(ranking_query)
703
+ lookback_window_days = (
704
+ datetime.strptime(to_date, "%Y-%m-%d").date()
705
+ - datetime.strptime(from_date, "%Y-%m-%d").date()
706
+ ).days
707
+ normalized = signals.annotate_stream(
708
+ normalized,
709
+ prepared_query,
710
+ freshness_mode,
711
+ reference_date=to_date,
712
+ max_days=lookback_window_days,
713
+ )
714
+ if source != "jobs":
715
+ normalized = signals.prune_low_relevance(normalized)
716
+ normalized = dedupe.dedupe_items(normalized)
717
+ for item in normalized:
718
+ item.snippet = snippet.extract_best_snippet(item, prepared_query)
719
+ return normalized
720
+
721
+
722
+ def _finalize_items_by_source(
723
+ items_by_source_raw: dict[str, list[schema.SourceItem]],
724
+ topic: str = "",
725
+ config: dict | None = None,
726
+ depth: str = "default",
727
+ mock: bool = False,
728
+ ) -> dict[str, list[schema.SourceItem]]:
729
+ finalized = {}
730
+ for source, items in items_by_source_raw.items():
731
+ items = sorted(items, key=lambda item: item.local_rank_score or 0.0, reverse=True)
732
+ items = dedupe.dedupe_items(items)
733
+ if source == "youtube" and items and not mock:
734
+ # Same budget-at-the-survivors principle as the digg branch
735
+ # below: retrieval-time transcripts go to each search's
736
+ # top-by-views candidates, while final selection ranks by
737
+ # relevance. Backfill survivors that arrived without one so the
738
+ # transcript budget lands on videos the brief actually shows
739
+ # (#542).
740
+ sc_token = (
741
+ config.get("SCRAPECREATORS_API_KEY")
742
+ if config and env.is_youtube_sc_available(config) else None
743
+ )
744
+ youtube_yt.backfill_transcripts(
745
+ items, topic=topic, depth=depth, token=sc_token,
746
+ )
747
+ # Post-merge topic-relevance filter for Polymarket: comparison queries
748
+ # fan out into per-entity subqueries ("Hermes", "OpenClaw") whose topic
749
+ # is too narrow for Gamma API to filter meaningfully. Re-validating the
750
+ # merged list against the full original topic drops off-topic markets
751
+ # (e.g., WTI crude oil, Elon tweet counts) before footer emission.
752
+ if source == "polymarket" and topic:
753
+ items = polymarket.filter_items_against_topic(topic, items)
754
+ # --polymarket-keywords (via config): additional keyword filter
755
+ # for ambiguous single-token topics (e.g., "Warriors" → nba,gsw).
756
+ keywords = config.get("_polymarket_keywords") if isinstance(config, dict) else None
757
+ if keywords:
758
+ items = polymarket.filter_items_against_keywords(items, keywords)
759
+ if source == "digg" and items:
760
+ # Pull top-ranked X posts only for the survivors that will appear
761
+ # in the brief. Spending the enrichment budget here (rather than
762
+ # at retrieval time) keeps the inline 'via Digg' quotes
763
+ # paired with the clusters dedupe actually kept.
764
+ digg.enrich_source_items(items, top_k=3)
765
+ finalized[source] = items
766
+ return finalized
767
+
768
+
769
+ def _apply_hiring_signal_gate(
770
+ bundle: schema.RetrievalBundle,
771
+ *,
772
+ explicit: bool,
773
+ topic: str,
774
+ ) -> dict[str, Any] | None:
775
+ jobs_items = bundle.items_by_source.get("jobs") or []
776
+ if not jobs_items:
777
+ if explicit:
778
+ return hiring_signals.analyze([], explicit=True, topic=topic)
779
+ return None
780
+
781
+ summary = hiring_signals.analyze(jobs_items, explicit=explicit, topic=topic)
782
+ if not explicit and not summary.get("include"):
783
+ bundle.items_by_source.pop("jobs", None)
784
+ for key in list(bundle.items_by_source_and_query):
785
+ if key[1] == "jobs":
786
+ del bundle.items_by_source_and_query[key]
787
+ return summary
788
+
789
+
790
+ def _ensure_jobs_in_plan(
791
+ plan: schema.QueryPlan,
792
+ available: list[str],
793
+ *,
794
+ explicit: bool,
795
+ topic: str,
796
+ ) -> None:
797
+ if "jobs" not in available:
798
+ return
799
+ if not (explicit or _company_topic_likely(topic)):
800
+ return
801
+ if "jobs" not in plan.source_weights:
802
+ plan.source_weights["jobs"] = 1.0
803
+ for subquery in plan.subqueries:
804
+ if "jobs" not in subquery.sources:
805
+ subquery.sources.append("jobs")
806
+
807
+
808
+ def _company_topic_likely(topic: str) -> bool:
809
+ text = topic.strip()
810
+ if not text:
811
+ return False
812
+ lower = text.lower()
813
+ if "?" in text or len(text.split()) > 4:
814
+ return False
815
+ generic = {
816
+ "how", "what", "why", "best", "top", "tutorial", "guide", "prompts",
817
+ "news", "latest", "ideas", "examples",
818
+ }
819
+ if any(word in generic for word in lower.split()):
820
+ return False
821
+ known_single_word_companies = {
822
+ "apple", "uber", "google", "microsoft", "amazon", "meta", "netflix",
823
+ "openai", "anthropic", "qualtrics", "stripe", "brex",
824
+ }
825
+ if " vs " in lower or " versus " in lower:
826
+ parts = re.split(r"\s+(?:vs|versus)\s+", text, maxsplit=1, flags=re.IGNORECASE)
827
+ if len(parts) != 2:
828
+ return False
829
+ return _comparison_side_company_like(parts[0], known_single_word_companies) or _comparison_side_company_like(
830
+ parts[1], known_single_word_companies
831
+ )
832
+ return bool(text[:1].isupper() or lower in known_single_word_companies)
833
+
834
+
835
+ def _comparison_side_company_like(side: str, known_companies: set[str]) -> bool:
836
+ token = re.sub(r"[^\w.+#-]", "", side.strip().split()[0] if side.strip() else "")
837
+ if not token:
838
+ return False
839
+ lower = token.lower()
840
+ common_tech_terms = {
841
+ "python", "ruby", "javascript", "typescript", "java", "go", "golang",
842
+ "rust", "php", "swift", "kotlin", "scala", "clojure", "elixir",
843
+ "react", "vue", "angular", "svelte", "node", "django", "rails",
844
+ "postgres", "mysql", "redis", "kubernetes", "docker",
845
+ }
846
+ if lower in common_tech_terms:
847
+ return False
848
+ return bool(token[:1].isupper() or lower in known_companies)
849
+
850
+
851
+ def _warnings(
852
+ items_by_source: dict[str, list[schema.SourceItem]],
853
+ candidates: list[schema.Candidate],
854
+ errors_by_source: dict[str, str],
855
+ degraded_by_source: dict[str, str] | None = None,
856
+ ) -> list[str]:
857
+ warnings: list[str] = []
858
+ if not candidates:
859
+ warnings.append("No candidates survived retrieval and ranking.")
860
+ if len(candidates) < 5:
861
+ warnings.append("Evidence is thin for this topic.")
862
+ top_sources = {
863
+ source
864
+ for candidate in candidates[:5]
865
+ for source in schema.candidate_sources(candidate)
866
+ }
867
+ if len(top_sources) <= 1 and len(candidates) >= 3:
868
+ warnings.append("Top evidence is highly concentrated in one source.")
869
+ if errors_by_source:
870
+ warnings.append(f"Some sources failed: {', '.join(sorted(errors_by_source))}")
871
+ if degraded_by_source:
872
+ # Partial failures: the source returned some items but errored/timed out
873
+ # on at least one subquery, so its coverage is likely incomplete. Kept
874
+ # distinct from hard failures so the signal is not silently dropped.
875
+ warnings.append(
876
+ f"Some sources returned partial results (degraded): {', '.join(sorted(degraded_by_source))}"
877
+ )
878
+ if not items_by_source:
879
+ warnings.append("No source returned usable items.")
880
+ return warnings
881
+
882
+
883
+ def _is_rate_limit_error(exc: Exception) -> bool:
884
+ """Detect 429 rate-limit errors by status code or message text."""
885
+ if hasattr(exc, "status_code") and getattr(exc, "status_code", None) == 429:
886
+ return True
887
+ return "429" in str(exc)
888
+
889
+
890
+ def _is_transient_error(exc: Exception) -> bool:
891
+ """Detect 5xx server errors that are worth retrying."""
892
+ status = getattr(exc, "status_code", None)
893
+ if isinstance(status, int) and 500 <= status < 600:
894
+ return True
895
+ msg = str(exc)
896
+ return any(code in msg for code in ("500", "502", "503", "504"))
897
+
898
+
899
+ def _run_supplemental_searches(
900
+ *,
901
+ topic: str,
902
+ bundle: schema.RetrievalBundle,
903
+ plan: schema.QueryPlan,
904
+ config: dict[str, Any],
905
+ depth: str,
906
+ date_range: tuple[str, str],
907
+ runtime: schema.ProviderRuntime,
908
+ mock: bool,
909
+ rate_limited_sources: set[str],
910
+ rate_limit_lock: threading.Lock,
911
+ x_handle: str | None = None,
912
+ x_related: list[str] | None = None,
913
+ ) -> None:
914
+ """Phase 2: extract entities from Phase 1 results, run targeted supplemental searches."""
915
+ if depth == "quick" or mock:
916
+ return
917
+
918
+ from_date, to_date = date_range
919
+
920
+ # Convert SourceItems to dicts for entity_extract. All X items (whatever
921
+ # backend fetched them — bird, xai, xurl, xquik) land under the single "x"
922
+ # slug, so this reads the whole X corpus.
923
+ x_dicts = [
924
+ {"author_handle": item.author or "", "text": item.body or ""}
925
+ for item in bundle.items_by_source.get("x", [])
926
+ ]
927
+ reddit_dicts = [
928
+ {
929
+ "subreddit": item.container or "",
930
+ "comment_insights": item.metadata.get("comment_insights", []),
931
+ "top_comments": [
932
+ {"excerpt": c.get("excerpt", c.get("text", ""))}
933
+ for c in (item.metadata.get("top_comments") or [])
934
+ if isinstance(c, dict)
935
+ ],
936
+ }
937
+ for item in bundle.items_by_source.get("reddit", [])
938
+ ]
939
+
940
+ if not x_dicts and not reddit_dicts and not x_handle and not x_related:
941
+ return
942
+
943
+ entities = entity_extract.extract_entities(
944
+ reddit_dicts, x_dicts,
945
+ max_handles=3, max_subreddits=3,
946
+ )
947
+
948
+ handles = entities.get("x_handles", [])
949
+
950
+ # Add explicit --x-handle if provided
951
+ if x_handle:
952
+ handle_clean = x_handle.lstrip("@").lower()
953
+ if handle_clean not in [h.lower() for h in handles]:
954
+ handles.insert(0, handle_clean)
955
+
956
+ # Collect related handles (searched separately with lower weight)
957
+ related_handles = []
958
+ if x_related:
959
+ primary_lower = x_handle.lstrip("@").lower() if x_handle else ""
960
+ for rh in x_related:
961
+ rh_clean = rh.lstrip("@").lower().strip()
962
+ if rh_clean and rh_clean != primary_lower and rh_clean not in [h.lower() for h in handles]:
963
+ related_handles.append(rh_clean)
964
+
965
+ if not handles and not related_handles:
966
+ return
967
+
968
+ # Pick the X handle-search backend: the first handle-capable backend in the
969
+ # chain (bird or xquik). These supplemental from:/mentions lanes are
970
+ # complementary to the topic search, so when the topic primary can't run
971
+ # them (xai/xurl have no handle-lane implementation) but a capable backend
972
+ # is available, use it rather than skipping Phase 2. bird scrapes X GraphQL
973
+ # with the user's browser cookies; xquik runs the same lanes over its REST
974
+ # API. All items land under the single "x" slug.
975
+ x_slug = "x"
976
+ chain = env.x_backend_chain(config)
977
+ # Trust an explicit runtime backend as the head of the chain.
978
+ pinned = runtime.x_search_backend
979
+ if pinned:
980
+ chain = [pinned] + [b for b in chain if b != pinned]
981
+ primary = next((b for b in chain if b in ("bird", "xquik")), None)
982
+
983
+ if primary == "bird":
984
+ def _from_lane(hs: list, count: int) -> list:
985
+ return bird_x.search_handles(hs, topic, from_date, count_per=count)
986
+
987
+ def _about_lane(hs: list, count: int) -> list:
988
+ return bird_x.search_mentions(hs, from_date, count_per=count)
989
+ elif primary == "xquik":
990
+ xquik_token = env.get_xquik_token(config)
991
+
992
+ def _from_lane(hs: list, count: int) -> list:
993
+ return xquik.search_handles(hs, topic, from_date, to_date, count_per=count, token=xquik_token)
994
+
995
+ def _about_lane(hs: list, count: int) -> list:
996
+ return xquik.search_mentions(hs, from_date, to_date, topic=topic, count_per=count, token=xquik_token)
997
+ else:
998
+ return # primary X backend has no handle-lane support (xai/xurl) or none configured
999
+
1000
+ # Skip if the X source is rate-limited.
1001
+ if x_slug in rate_limited_sources:
1002
+ return
1003
+
1004
+ # Collect existing URLs for deduplication
1005
+ existing_urls = {
1006
+ item.url
1007
+ for items in bundle.items_by_source.values()
1008
+ for item in items
1009
+ if item.url
1010
+ }
1011
+
1012
+ ranking_query = plan.subqueries[0].ranking_query if plan.subqueries else topic
1013
+ primary_label = plan.subqueries[0].label if plan.subqueries else "primary"
1014
+
1015
+ # Search primary handles (full weight): FROM lane (their own tweets) +
1016
+ # ABOUT lane (tweets mentioning them). Both engagement-weighted and deduped
1017
+ # by URL at normalize time.
1018
+ if handles:
1019
+ # Independent try/except per lane so a failure in one does not discard
1020
+ # the other's already-computed results.
1021
+ from_items: list = []
1022
+ about_items: list = []
1023
+ try:
1024
+ from_items = _from_lane(handles, FROM_LANE_COUNT_PER)
1025
+ except Exception as exc:
1026
+ print(f"[Pipeline] Phase 2 FROM-lane search failed: {exc}", file=sys.stderr)
1027
+ if not bundle.items_by_source.get(x_slug):
1028
+ bundle.errors_by_source[x_slug] = f"Phase 2 FROM-lane: {exc}"
1029
+ try:
1030
+ about_items = _about_lane(handles, MENTION_LANE_COUNT_PER)
1031
+ except Exception as exc:
1032
+ print(f"[Pipeline] Phase 2 ABOUT-lane search failed: {exc}", file=sys.stderr)
1033
+ raw_items = from_items + about_items
1034
+
1035
+ if raw_items:
1036
+ normalized = _normalize_score_dedupe(
1037
+ x_slug, raw_items, from_date, to_date,
1038
+ freshness_mode=plan.freshness_mode,
1039
+ ranking_query=ranking_query,
1040
+ )
1041
+ # Deduplicate against Phase 1 URLs
1042
+ normalized = [item for item in normalized if item.url not in existing_urls]
1043
+ if normalized:
1044
+ bundle.add_items(primary_label, x_slug, normalized)
1045
+ # Update existing URLs for related-handle dedup
1046
+ for item in normalized:
1047
+ if item.url:
1048
+ existing_urls.add(item.url)
1049
+
1050
+ # Search related handles with lower weight (0.3)
1051
+ if related_handles:
1052
+ try:
1053
+ raw_items = _from_lane(related_handles, RELATED_HANDLE_COUNT_PER)
1054
+ except Exception as exc:
1055
+ print(f"[Pipeline] Phase 2 related handle search failed: {exc}", file=sys.stderr)
1056
+ raw_items = []
1057
+
1058
+ if raw_items:
1059
+ normalized = _normalize_score_dedupe(
1060
+ x_slug, raw_items, from_date, to_date,
1061
+ freshness_mode=plan.freshness_mode,
1062
+ ranking_query=ranking_query,
1063
+ )
1064
+ # Deduplicate against all existing URLs (Phase 1 + primary handles)
1065
+ normalized = [item for item in normalized if item.url not in existing_urls]
1066
+ if normalized:
1067
+ # Use a separate subquery label with lower weight so RRF
1068
+ # scores related-handle results below primary results.
1069
+ bundle.add_items("supplemental-related", x_slug, normalized)
1070
+ # Register the supplemental-related label in the plan for fusion
1071
+ if not any(sq.label == "supplemental-related" for sq in plan.subqueries):
1072
+ plan.subqueries.append(
1073
+ schema.SubQuery(
1074
+ label="supplemental-related",
1075
+ search_query=", ".join(related_handles),
1076
+ ranking_query=ranking_query,
1077
+ sources=[x_slug],
1078
+ weight=0.3,
1079
+ )
1080
+ )
1081
+
1082
+
1083
+ def _retry_thin_sources(
1084
+ *,
1085
+ topic: str,
1086
+ bundle: schema.RetrievalBundle,
1087
+ plan: schema.QueryPlan,
1088
+ config: dict[str, Any],
1089
+ depth: str,
1090
+ date_range: tuple[str, str],
1091
+ runtime: schema.ProviderRuntime,
1092
+ mock: bool,
1093
+ rate_limited_sources: set[str],
1094
+ rate_limit_lock: threading.Lock,
1095
+ settings: dict[str, Any],
1096
+ web_backend: str = "auto",
1097
+ skip_sources: set[str] | None = None,
1098
+ ) -> None:
1099
+ """Retry sources with thin results using simplified core subject query."""
1100
+ if depth == "quick":
1101
+ return
1102
+
1103
+ planned_sources: list[str] = []
1104
+ for subquery in plan.subqueries:
1105
+ for source in subquery.sources:
1106
+ if source not in planned_sources:
1107
+ planned_sources.append(source)
1108
+ _skip = skip_sources or set()
1109
+ thin_sources = [
1110
+ source
1111
+ for source in planned_sources
1112
+ if len(bundle.items_by_source.get(source, [])) < 3
1113
+ and source not in bundle.errors_by_source
1114
+ and source not in _skip
1115
+ ]
1116
+
1117
+ if not thin_sources:
1118
+ return
1119
+
1120
+ core = query.extract_core_subject(topic, max_words=3)
1121
+ if not core:
1122
+ return
1123
+ # Note: we intentionally do NOT skip when core == topic. For short topics
1124
+ # like "Kanye West", the 3-word core IS the topic — but the planner may
1125
+ # have sent a different (worse) query to the source. Retrying with the
1126
+ # raw core subject is still valuable.
1127
+
1128
+ from_date, to_date = date_range
1129
+
1130
+ # Create a retry subquery with the simplified core subject
1131
+ retry_subquery = schema.SubQuery(
1132
+ label="retry",
1133
+ search_query=core,
1134
+ ranking_query=f"What recent evidence from the last 30 days matters for {core}?",
1135
+ sources=thin_sources,
1136
+ weight=0.3,
1137
+ )
1138
+
1139
+ def _retry_one_source(source: str) -> tuple[str, list[schema.SourceItem]]:
1140
+ raw_items, _artifact = _retrieve_stream(
1141
+ topic=topic,
1142
+ subquery=retry_subquery,
1143
+ source=source,
1144
+ config=config,
1145
+ depth=depth,
1146
+ date_range=date_range,
1147
+ runtime=runtime,
1148
+ mock=mock,
1149
+ rate_limited_sources=rate_limited_sources,
1150
+ rate_limit_lock=rate_limit_lock,
1151
+ web_backend=web_backend,
1152
+ raw_topic=topic,
1153
+ )
1154
+ normalized = _normalize_score_dedupe(
1155
+ source,
1156
+ raw_items,
1157
+ from_date,
1158
+ to_date,
1159
+ freshness_mode=plan.freshness_mode,
1160
+ ranking_query=retry_subquery.ranking_query,
1161
+ )
1162
+ if source == "jobs":
1163
+ return source, normalized
1164
+ return source, normalized[:settings["per_stream_limit"]]
1165
+
1166
+ retryable = [s for s in thin_sources if s not in rate_limited_sources]
1167
+
1168
+ from concurrent.futures import ThreadPoolExecutor, as_completed
1169
+ with ThreadPoolExecutor(max_workers=min(4, len(retryable) or 1)) as executor:
1170
+ futures = {executor.submit(_retry_one_source, s): s for s in retryable}
1171
+ for future in as_completed(futures):
1172
+ source = futures[future]
1173
+ try:
1174
+ source, normalized = future.result()
1175
+ existing_urls = {item.url for item in bundle.items_by_source.get(source, []) if item.url}
1176
+ new_items = [item for item in normalized if item.url not in existing_urls]
1177
+
1178
+ if new_items:
1179
+ bundle.items_by_source.setdefault(source, []).extend(new_items)
1180
+ primary_label = plan.subqueries[0].label if plan.subqueries else "primary"
1181
+ bundle.items_by_source_and_query.setdefault((primary_label, source), []).extend(new_items)
1182
+ except Exception as exc:
1183
+ print(f"[Pipeline] Retry failed for {source}: {type(exc).__name__}: {exc}", file=sys.stderr)
1184
+
1185
+
1186
+ def _fetch_x_backend(backend, subquery, from_date, to_date, depth, config):
1187
+ """Fetch X items from a single backend. Returns (items, error_str).
1188
+
1189
+ Backends are tried in priority order by the caller (env.x_backend_chain);
1190
+ a non-empty error_str signals a hard failure (auth/payment/etc.) so the
1191
+ caller can fail over to the next backend or surface the error honestly.
1192
+ """
1193
+ query = subquery.search_query
1194
+ if backend == "bird":
1195
+ result = bird_x.search_x(query, from_date, to_date, depth=depth)
1196
+ items = bird_x.parse_bird_response(result, query=query)
1197
+ elif backend == "xai":
1198
+ model = config.get("LAST30DAYS_X_MODEL") or config.get("XAI_MODEL_PIN") or providers.XAI_DEFAULT
1199
+ result = xai_x.search_x(config["XAI_API_KEY"], model, query, from_date, to_date, depth=depth)
1200
+ items = xai_x.parse_x_response(result)
1201
+ elif backend == "xurl":
1202
+ result = xurl_x.search_x(query, depth=depth)
1203
+ items = xurl_x.parse_x_response(result, topic=query)
1204
+ elif backend == "xquik":
1205
+ result = xquik.search_xquik(query, from_date, to_date, depth=depth, token=env.get_xquik_token(config))
1206
+ items = xquik.parse_xquik_response(result)
1207
+ else:
1208
+ return [], f"unknown X backend: {backend}"
1209
+ err = result.get("error") if isinstance(result, dict) else ""
1210
+ return items, (err or "")
1211
+
1212
+
1213
+ def _reddit_post_key(item: dict) -> str:
1214
+ """Stable per-thread dedupe key (base36 post id from the url/permalink)."""
1215
+ url = item.get("url") or item.get("permalink") or ""
1216
+ m = re.search(r"/comments/([A-Za-z0-9]+)", url)
1217
+ return m.group(1) if m else url
1218
+
1219
+
1220
+ def _merge_reddit_items(free: list[dict], sc: list[dict]) -> list[dict]:
1221
+ """Merge free + ScrapeCreators Reddit items, free first, deduped by post id.
1222
+
1223
+ Used when the thinness-floor trigger backfills a thin free run with SC, so a
1224
+ thread present in both is never double-listed.
1225
+ """
1226
+ merged = list(free)
1227
+ seen = {_reddit_post_key(it) for it in free}
1228
+ for it in sc:
1229
+ key = _reddit_post_key(it)
1230
+ if key and key not in seen:
1231
+ seen.add(key)
1232
+ merged.append(it)
1233
+ return merged
1234
+
1235
+
1236
+ def _retrieve_stream(
1237
+ *,
1238
+ topic: str,
1239
+ subquery: schema.SubQuery,
1240
+ source: str,
1241
+ config: dict[str, Any],
1242
+ depth: str,
1243
+ date_range: tuple[str, str],
1244
+ runtime: schema.ProviderRuntime,
1245
+ mock: bool,
1246
+ rate_limited_sources: set[str] | None = None,
1247
+ rate_limit_lock: threading.Lock | None = None,
1248
+ web_backend: str = "auto",
1249
+ raw_topic: str = "",
1250
+ subreddits: list[str] | None = None,
1251
+ tiktok_hashtags: list[str] | None = None,
1252
+ tiktok_creators: list[str] | None = None,
1253
+ ig_creators: list[str] | None = None,
1254
+ ) -> tuple[list[dict], dict]:
1255
+ # Early exit if source was rate-limited by a sibling future
1256
+ if rate_limited_sources is not None and source in rate_limited_sources:
1257
+ return [], {}
1258
+ from_date, to_date = date_range
1259
+ if mock:
1260
+ return _mock_stream_results(source, subquery)
1261
+ if source == "grounding":
1262
+ return grounding.web_search(
1263
+ subquery.search_query, date_range, config, backend=web_backend)
1264
+ if source == "jobs":
1265
+ return jobs.search_jobs(
1266
+ raw_topic or topic or subquery.search_query,
1267
+ date_range,
1268
+ config,
1269
+ depth=depth,
1270
+ web_backend=web_backend,
1271
+ explicit=bool(config.get("_hiring_signals_mode")),
1272
+ )
1273
+ if source == "reddit":
1274
+ # Use raw_topic so expand_reddit_queries() generates diverse variants
1275
+ # from the original user topic, not the planner's narrowed search_query.
1276
+ reddit_query = raw_topic or subquery.search_query
1277
+ dedicated_subreddits = config.get("_dedicated_subreddits") or None
1278
+ has_sc_key = bool(config.get("SCRAPECREATORS_API_KEY"))
1279
+ sc_first = (
1280
+ has_sc_key
1281
+ and (config.get("LAST30DAYS_REDDIT_BACKEND") or "").lower()
1282
+ == "scrapecreators"
1283
+ )
1284
+ if sc_first:
1285
+ # LAST30DAYS_REDDIT_BACKEND=scrapecreators: SC primary, public fallback
1286
+ try:
1287
+ result = reddit.search_and_enrich(
1288
+ reddit_query, from_date, to_date, depth=depth,
1289
+ token=config.get("SCRAPECREATORS_API_KEY"),
1290
+ subreddits=subreddits,
1291
+ )
1292
+ items = reddit.parse_reddit_response(result)
1293
+ if items:
1294
+ return items, {}
1295
+ sys.stderr.write(
1296
+ "[Reddit] ScrapeCreators primary returned no items, "
1297
+ "using public fallback\n"
1298
+ )
1299
+ except Exception as exc:
1300
+ sys.stderr.write(
1301
+ f"[Reddit] ScrapeCreators primary failed "
1302
+ f"({type(exc).__name__}: {exc}), using public fallback\n"
1303
+ )
1304
+ try:
1305
+ public_results = reddit_public.search_reddit_public(
1306
+ reddit_query, from_date, to_date, depth=depth,
1307
+ subreddits=subreddits,
1308
+ )
1309
+ if public_results:
1310
+ return public_results, {}
1311
+ sys.stderr.write(
1312
+ "[Reddit] Public fallback returned no items after "
1313
+ "ScrapeCreators primary miss\n"
1314
+ )
1315
+ except Exception as exc:
1316
+ sys.stderr.write(
1317
+ f"[Reddit] Public fallback also failed "
1318
+ f"({type(exc).__name__}: {exc})\n"
1319
+ )
1320
+ return [], {}
1321
+
1322
+ # Default: public Reddit first (free). ScrapeCreators backfills when the
1323
+ # free path is empty OR returns fewer than the configured thinness floor
1324
+ # (LAST30DAYS_REDDIT_SC_MIN_ITEMS, default 0 = empty-only — today's
1325
+ # behavior, no extra credit spend unless the user opts in).
1326
+ try:
1327
+ min_items = int(config.get("LAST30DAYS_REDDIT_SC_MIN_ITEMS") or 0)
1328
+ except (TypeError, ValueError):
1329
+ min_items = 0
1330
+ public_results: list[dict] = []
1331
+ try:
1332
+ public_results = reddit_public.search_reddit_public(
1333
+ reddit_query, from_date, to_date, depth=depth,
1334
+ subreddits=subreddits, dedicated_subreddits=dedicated_subreddits,
1335
+ ) or []
1336
+ except Exception as exc:
1337
+ sys.stderr.write(
1338
+ f"[Reddit] Public search failed ({type(exc).__name__}: {exc})"
1339
+ )
1340
+ if not has_sc_key:
1341
+ sys.stderr.write("\n")
1342
+ return [], {}
1343
+ sys.stderr.write(", using ScrapeCreators backup\n")
1344
+ # Enough free results, or no key to backfill with -> done. max(min_items,
1345
+ # 1) keeps the default (min_items=0) as empty-only AND treats exactly
1346
+ # `min_items` results as acceptable (no backfill) for min_items > 0.
1347
+ if len(public_results) >= max(min_items, 1) or not has_sc_key:
1348
+ return public_results, {}
1349
+ if public_results:
1350
+ sys.stderr.write(
1351
+ f"[Reddit] Free path returned {len(public_results)} "
1352
+ f"(below the {min_items}-item floor); backfilling with ScrapeCreators\n"
1353
+ )
1354
+ try:
1355
+ result = reddit.search_and_enrich(
1356
+ reddit_query, from_date, to_date, depth=depth,
1357
+ token=config.get("SCRAPECREATORS_API_KEY"),
1358
+ subreddits=subreddits,
1359
+ )
1360
+ sc_items = reddit.parse_reddit_response(result)
1361
+ except Exception as exc:
1362
+ sys.stderr.write(
1363
+ f"[Reddit] ScrapeCreators backup also failed "
1364
+ f"({type(exc).__name__}: {exc})\n"
1365
+ )
1366
+ return public_results, {}
1367
+ return _merge_reddit_items(public_results, sc_items), {}
1368
+ if source == "x":
1369
+ # One X source, an ordered chain of interchangeable backends. Try the
1370
+ # primary; fall through to the next only if it returns nothing or errors.
1371
+ chain = env.x_backend_chain(config)
1372
+ # Trust an explicit runtime backend as the primary (already resolved as
1373
+ # available), keeping the rest of the chain as failover backups.
1374
+ pinned = runtime.x_search_backend
1375
+ if pinned:
1376
+ chain = [pinned] + [b for b in chain if b != pinned]
1377
+ if not chain:
1378
+ raise RuntimeError("No X backend is available.")
1379
+ last_error = ""
1380
+ for i, backend in enumerate(chain):
1381
+ items, err = _fetch_x_backend(backend, subquery, from_date, to_date, depth, config)
1382
+ if items:
1383
+ if i > 0:
1384
+ print(f"[X] primary backend(s) returned nothing; used fallback '{backend}'", file=sys.stderr)
1385
+ return items, {}
1386
+ if err:
1387
+ last_error = f"{backend}: {err}"
1388
+ print(f"[X] backend '{backend}' failed ({err}); trying next", file=sys.stderr)
1389
+ if last_error:
1390
+ raise RuntimeError(f"All X backends failed — {last_error}")
1391
+ return [], {}
1392
+ if source == "youtube":
1393
+ # Use raw_topic so expand_youtube_queries() generates diverse variants
1394
+ # from the original user topic, not the planner's narrowed search_query.
1395
+ yt_query = raw_topic or subquery.search_query
1396
+ result = None
1397
+ # ScrapeCreators key (when present) is the default-on backup tier: it
1398
+ # powers the per-video transcript fallback, the SC search fallback, and
1399
+ # comment enrichment. None when no key, which keeps everything keyless.
1400
+ sc_token = (
1401
+ config.get("SCRAPECREATORS_API_KEY", "")
1402
+ if env.is_youtube_sc_available(config) else None
1403
+ )
1404
+ # Try yt-dlp first; the SC transcript fallback covers per-video failures.
1405
+ if which("yt-dlp"):
1406
+ try:
1407
+ result = youtube_yt.search_and_transcribe(
1408
+ yt_query, from_date, to_date, depth=depth, token=sc_token,
1409
+ )
1410
+ except Exception:
1411
+ result = None
1412
+ # Fall back to SC YouTube search if yt-dlp failed or isn't installed.
1413
+ if (result is None or not result.get("items")) and sc_token:
1414
+ result = youtube_yt.search_youtube_sc(
1415
+ yt_query, from_date, to_date, depth=depth, token=sc_token,
1416
+ )
1417
+ if result is None:
1418
+ result = {"items": []}
1419
+ # Enrich top videos with comments (default-on when a key is present).
1420
+ items = youtube_yt.parse_youtube_response(result)
1421
+ if items and env.is_youtube_comments_available(config):
1422
+ youtube_yt.enrich_with_comments(
1423
+ items, token=config.get("SCRAPECREATORS_API_KEY", ""),
1424
+ )
1425
+ return items, {}
1426
+ if source == "tiktok":
1427
+ # Use raw_topic so expand_tiktok_queries() generates diverse variants
1428
+ # from the original user topic, not the planner's narrowed search_query.
1429
+ tiktok_query = raw_topic or subquery.search_query
1430
+ result = tiktok.search_and_enrich(
1431
+ tiktok_query,
1432
+ from_date,
1433
+ to_date,
1434
+ depth=depth,
1435
+ token=env.get_tiktok_token(config),
1436
+ hashtags=tiktok_hashtags,
1437
+ creators=tiktok_creators,
1438
+ )
1439
+ items = tiktok.parse_tiktok_response(result)
1440
+ if items and env.is_tiktok_comments_available(config):
1441
+ sc_token = config.get("SCRAPECREATORS_API_KEY", "")
1442
+ tiktok.enrich_with_comments(items, token=sc_token)
1443
+ return items, {}
1444
+ if source == "instagram":
1445
+ # Use raw_topic so expand_instagram_queries() generates diverse variants
1446
+ # from the original user topic, not the planner's narrowed search_query.
1447
+ ig_query = raw_topic or subquery.search_query
1448
+ result = instagram.search_and_enrich(
1449
+ ig_query,
1450
+ from_date,
1451
+ to_date,
1452
+ depth=depth,
1453
+ token=env.get_instagram_token(config),
1454
+ ig_creators=ig_creators,
1455
+ )
1456
+ return instagram.parse_instagram_response(result), {}
1457
+ if source == "linkedin":
1458
+ token = config.get("SCRAPECREATORS_API_KEY", "")
1459
+ result = linkedin.search_linkedin(
1460
+ subquery.search_query,
1461
+ from_date,
1462
+ to_date,
1463
+ depth=depth,
1464
+ token=token,
1465
+ )
1466
+ items = linkedin.parse_linkedin_response(
1467
+ result, from_date=from_date, to_date=to_date
1468
+ )
1469
+ # Articles never appear in post search — surface them (high signal)
1470
+ # via a bounded profile-enrichment lane on person topics.
1471
+ items += linkedin.enrich_articles(
1472
+ items, raw_topic or topic, token, from_date=from_date, to_date=to_date
1473
+ )
1474
+ return items, {}
1475
+ if source == "hackernews":
1476
+ result = hackernews.search_hackernews(subquery.search_query, from_date, to_date, depth=depth)
1477
+ return hackernews.parse_hackernews_response(result, query=subquery.search_query), {}
1478
+ if source == "digg":
1479
+ result = digg.search_digg(subquery.search_query, from_date, to_date, depth=depth)
1480
+ items = digg.parse_digg_response(result, query=subquery.search_query)
1481
+ # Enrichment with attached X posts is deferred to
1482
+ # _finalize_items_by_source so it runs on the items that actually
1483
+ # survive dedupe rather than on top-K of the raw fanout.
1484
+ return items, {}
1485
+ if source == "arxiv":
1486
+ result = arxiv.search_arxiv(subquery.search_query, from_date, to_date, depth=depth)
1487
+ # Relevance keys off the stable research topic, not the per-subquery
1488
+ # search_query, so off-topic narrowing does not let weak matches through.
1489
+ relevance_topic = raw_topic or topic or subquery.search_query
1490
+ return arxiv.parse_arxiv_response(result, query=relevance_topic), {}
1491
+ if source == "techmeme":
1492
+ result = techmeme.search_techmeme(subquery.search_query, from_date, to_date, depth=depth)
1493
+ relevance_topic = raw_topic or topic or subquery.search_query
1494
+ return techmeme.parse_techmeme_response(result, query=relevance_topic), {}
1495
+ if source == "trustpilot":
1496
+ # Brand-shape gate keys off the stable research topic, not the narrowed
1497
+ # per-subquery search_query, so the company is detected consistently.
1498
+ relevance_topic = raw_topic or topic or subquery.search_query
1499
+ result = trustpilot.search_trustpilot(
1500
+ relevance_topic, from_date, to_date, depth=depth, config=config
1501
+ )
1502
+ return trustpilot.parse_trustpilot_response(result, query=relevance_topic), {}
1503
+ if source == "bluesky":
1504
+ result = bluesky.search_bluesky(subquery.search_query, from_date, to_date, depth=depth, config=config)
1505
+ return bluesky.parse_bluesky_response(result), {}
1506
+ if source == "threads":
1507
+ result = threads.search_threads(
1508
+ subquery.search_query, from_date, to_date,
1509
+ depth=depth,
1510
+ token=config.get("SCRAPECREATORS_API_KEY"),
1511
+ )
1512
+ return threads.parse_threads_response(result), {}
1513
+ if source == "truthsocial":
1514
+ result = truthsocial.search_truthsocial(subquery.search_query, from_date, to_date, depth=depth, config=config)
1515
+ return truthsocial.parse_truthsocial_response(result), {}
1516
+ if source == "polymarket":
1517
+ result = polymarket.search_polymarket(subquery.search_query, from_date, to_date, depth=depth)
1518
+ # Relevance filtering keys off the stable original research topic, not the
1519
+ # per-subquery search_query (which narrows differently on each fanout pass
1520
+ # and would let off-topic markets through on broad subqueries while dropping
1521
+ # everything on narrow ones).
1522
+ relevance_topic = raw_topic or topic or subquery.search_query
1523
+ return polymarket.parse_polymarket_response(result, topic=relevance_topic), {}
1524
+ if source == "github":
1525
+ # Resolve once at the pipeline boundary so search and enrich
1526
+ # share the result; otherwise each call would re-run the env
1527
+ # lookup and gh-CLI subprocess fallback (up to 5s timeout each).
1528
+ token = github.resolve_token(config.get("GITHUB_TOKEN"))
1529
+ response = github.search_github(subquery.search_query, from_date, to_date, depth=depth, token=token)
1530
+ items = github.parse_github_response(response)
1531
+ # Note: an unauth rate-limit (response["error"]) is expected on the
1532
+ # tokenless anon tier and returns empty here rather than raising — github
1533
+ # is now always eligible, so raising would spam "github failed" on every
1534
+ # tokenless run. The condition is logged in github.search_github.
1535
+ items = github.enrich_with_comments(items, depth=depth, token=token)
1536
+ return items, {}
1537
+ if source == "pinterest":
1538
+ result = pinterest.search_pinterest(
1539
+ subquery.search_query, from_date, to_date,
1540
+ depth=depth,
1541
+ token=env.get_pinterest_token(config),
1542
+ )
1543
+ return pinterest.parse_pinterest_response(result), {}
1544
+ if source == "xiaohongshu":
1545
+ return xiaohongshu_api.search_feeds(
1546
+ subquery.search_query,
1547
+ from_date,
1548
+ to_date,
1549
+ env.get_xiaohongshu_api_base(config),
1550
+ depth=depth,
1551
+ ), {}
1552
+ if source == "perplexity":
1553
+ return perplexity.search(subquery.search_query, date_range, config, deep=config.get("_deep_research", False))
1554
+ raise RuntimeError(f"Unsupported source: {source}")
1555
+
1556
+
1557
+ def _google_key(config: dict[str, Any]) -> str | None:
1558
+ return config.get("GOOGLE_API_KEY") or config.get("GEMINI_API_KEY") or config.get("GOOGLE_GENAI_API_KEY")
1559
+
1560
+
1561
+
1562
+
1563
+ def _mock_stream_results(source: str, subquery: schema.SubQuery) -> tuple[list[dict], dict]:
1564
+ payloads = {
1565
+ "reddit": [
1566
+ {
1567
+ "id": "R1",
1568
+ "title": f"{subquery.search_query} discussion thread",
1569
+ "url": "https://reddit.com/r/example/comments/1",
1570
+ "subreddit": "example",
1571
+ "date": dates.get_date_range(5)[0],
1572
+ "engagement": {"score": 120, "num_comments": 48, "upvote_ratio": 0.91},
1573
+ "selftext": f"Community discussion about {subquery.search_query}.",
1574
+ "top_comments": [{"excerpt": "Strong firsthand feedback from users."}],
1575
+ "relevance": 0.82,
1576
+ "why_relevant": "Mock Reddit result",
1577
+ }
1578
+ ],
1579
+ "x": [
1580
+ {
1581
+ "id": "X1",
1582
+ "text": f"People on X are discussing {subquery.search_query} right now.",
1583
+ "url": "https://x.com/example/status/1",
1584
+ "author_handle": "example",
1585
+ "date": dates.get_date_range(2)[0],
1586
+ "engagement": {"likes": 200, "reposts": 35, "replies": 18, "quotes": 4},
1587
+ "relevance": 0.79,
1588
+ "why_relevant": "Mock X result",
1589
+ }
1590
+ ],
1591
+ "grounding": [
1592
+ {
1593
+ "id": "WB1",
1594
+ "title": f"{subquery.search_query} article",
1595
+ "url": "https://example.com/article",
1596
+ "source_domain": "example.com",
1597
+ "snippet": f"Recent web reporting about {subquery.search_query}.",
1598
+ "date": dates.get_date_range(7)[0],
1599
+ "relevance": 0.88,
1600
+ "why_relevant": "Brave web search",
1601
+ }
1602
+ ],
1603
+ "digg": [
1604
+ {
1605
+ "id": "mock1abc",
1606
+ "title": f"Digg cluster about {subquery.search_query}",
1607
+ "url": "https://di.gg/ai/mock1abc",
1608
+ "tldr": f"Curated cluster summarizing recent {subquery.search_query} discussion across the AI 1000.",
1609
+ "author": "",
1610
+ "date": dates.get_date_range(3)[0],
1611
+ "engagement": {"postCount": 8, "uniqueAuthors": 5, "rank": 2, "rank_score": 49.0},
1612
+ "first_post_age": "3d",
1613
+ "posts": [
1614
+ {
1615
+ "username": "exampledev",
1616
+ "display_name": "Example Dev",
1617
+ "category": "Engineer",
1618
+ "rank": 142,
1619
+ "body": f"Quote from the AI 1000 about {subquery.search_query}.",
1620
+ "post_type": "tweet",
1621
+ "x_url": "https://x.com/exampledev/status/1",
1622
+ "posted_at": dates.get_date_range(3)[0],
1623
+ },
1624
+ ],
1625
+ "relevance": 0.84,
1626
+ "why_relevant": "Mock Digg cluster",
1627
+ },
1628
+ {
1629
+ "id": "mock2def",
1630
+ "title": f"Second Digg cluster on {subquery.search_query}",
1631
+ "url": "https://di.gg/ai/mock2def",
1632
+ "tldr": f"Another angle on {subquery.search_query}.",
1633
+ "author": "",
1634
+ "date": dates.get_date_range(8)[0],
1635
+ "engagement": {"postCount": 3, "uniqueAuthors": 2, "rank": 18, "rank_score": 33.0},
1636
+ "first_post_age": "8d",
1637
+ "posts": [],
1638
+ "relevance": 0.71,
1639
+ "why_relevant": "Mock Digg cluster",
1640
+ },
1641
+ ],
1642
+ "arxiv": [
1643
+ {
1644
+ "id": "http://arxiv.org/abs/2606.00001v1",
1645
+ "title": f"A Survey of {subquery.search_query}",
1646
+ "url": "https://arxiv.org/abs/2606.00001v1",
1647
+ "summary": f"We present a comprehensive study of {subquery.search_query} and its recent advances.",
1648
+ "author": "Ada Lovelace et al.",
1649
+ "authors": ["Ada Lovelace", "Alan Turing"],
1650
+ "date": dates.get_date_range(20)[0],
1651
+ "engagement": {},
1652
+ "relevance": 0.86,
1653
+ "why_relevant": "Mock arXiv paper",
1654
+ },
1655
+ ],
1656
+ "techmeme": [
1657
+ {
1658
+ "id": "https://www.techmeme.com/260627/p1",
1659
+ "title": f"Major development in {subquery.search_query} reshapes the industry",
1660
+ "url": "https://www.techmeme.com/260627/p1",
1661
+ "source_name": "techcrunch.com",
1662
+ "date": dates.get_date_range(1)[0],
1663
+ "engagement": {},
1664
+ "relevance": 0.83,
1665
+ "why_relevant": "Mock Techmeme headline",
1666
+ },
1667
+ ],
1668
+ "trustpilot": [
1669
+ {
1670
+ "id": "example.com",
1671
+ "title": f"{subquery.search_query}: TrustScore 3.4",
1672
+ "url": "https://www.trustpilot.com/review/example.com",
1673
+ "summary": f"Across recent reviews, customers were split on {subquery.search_query}: some praised support, others cited delays.",
1674
+ "name": subquery.search_query,
1675
+ "trustScore": 3.4,
1676
+ "reviewCount": 128,
1677
+ "date": dates.get_date_range(1)[0],
1678
+ "engagement": {"reviews": 128, "trustScore": 3.4},
1679
+ "relevance": 0.8,
1680
+ "why_relevant": "Mock Trustpilot sentiment",
1681
+ },
1682
+ ],
1683
+ "jobs": [
1684
+ {
1685
+ "id": "J1",
1686
+ "title": "Founding Enterprise Solutions Engineer",
1687
+ "url": "https://boards.greenhouse.io/example/jobs/1",
1688
+ "description": (
1689
+ f"Work with enterprise customers on SSO, SOC 2, security, "
1690
+ f"and procurement workflows for {subquery.search_query}."
1691
+ ),
1692
+ "department": "Sales",
1693
+ "location": "San Francisco, CA",
1694
+ "date": dates.get_date_range(4)[0],
1695
+ "provider": "mock",
1696
+ "relevance": 0.8,
1697
+ "why_relevant": "Mock public job posting",
1698
+ },
1699
+ {
1700
+ "id": "J2",
1701
+ "title": "Security Platform Engineer",
1702
+ "url": "https://boards.greenhouse.io/example/jobs/2",
1703
+ "description": "Build enterprise security, audit, and admin workflows.",
1704
+ "department": "Engineering",
1705
+ "location": "Remote",
1706
+ "date": dates.get_date_range(6)[0],
1707
+ "provider": "mock",
1708
+ "relevance": 0.78,
1709
+ "why_relevant": "Mock public job posting",
1710
+ },
1711
+ ],
1712
+ }
1713
+ if source == "grounding":
1714
+ return payloads.get(source, []), {
1715
+ "label": subquery.label,
1716
+ "mock": True,
1717
+ "webSearchQueries": [subquery.search_query],
1718
+ "resultCount": 1,
1719
+ }
1720
+ return payloads.get(source, []), {}