argo-search 1.0.1 → 2.9.8

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 (472) hide show
  1. package/LICENSE +201 -21
  2. package/README.en.md +735 -0
  3. package/README.es.md +777 -0
  4. package/README.ja.md +777 -0
  5. package/README.ko.md +777 -0
  6. package/README.md +778 -320
  7. package/SKILL.md +135 -0
  8. package/assets/readme/hero.svg +121 -0
  9. package/assets/readme/made-with-beautify.svg +10 -0
  10. package/assets/readme/proof-routes.svg +73 -0
  11. package/assets/readme/why-better.svg +104 -0
  12. package/assets/readme/workflow.svg +83 -0
  13. package/backends/domain_profiles.json +2356 -96
  14. package/backends/engine_registry.yaml +3428 -503
  15. package/backends/query_synonyms_cn.json +35 -0
  16. package/backends/quota_profiles.json +2488 -160
  17. package/backends/source_types_cn.json +109 -0
  18. package/backends/text_lexicon.json +44 -0
  19. package/bin/argo +772 -0
  20. package/bin/argo.js +102 -39
  21. package/config.yaml +5139 -367
  22. package/cordis.patch.yml +21 -0
  23. package/data/countries.json +40 -0
  24. package/data/lang_matrix/lang_capability.json +842 -0
  25. package/data/lang_matrix/matrix_2026-09-10.json +11921 -0
  26. package/data/regions_cn.json +1 -0
  27. package/docs/ADDING_NEW_ENGINE.md +184 -0
  28. package/docs/ARGO_INTRO.md +124 -0
  29. package/docs/DATA_SOURCE_GAPS.md +81 -0
  30. package/docs/DESIGN_LOCAL_DATA_FUSION.md +90 -0
  31. package/docs/ENGINE_CATALOG.md +575 -0
  32. package/docs/MCP_SETUP.md +108 -0
  33. package/docs/PLATFORM_PATHS.md +111 -0
  34. package/docs/RELEASE_NOTES_v2.8.0.md +71 -0
  35. package/docs/RELEASE_NOTES_v2.8.2.md +63 -0
  36. package/docs/RELEASE_NOTES_v2.8.3.md +55 -0
  37. package/docs/RELEASE_NOTES_v2.8.4.md +79 -0
  38. package/docs/RELEASE_NOTES_v2.8.5.md +102 -0
  39. package/docs/RELEASE_NOTES_v2.8.6.md +66 -0
  40. package/docs/RELEASE_NOTES_v2.8.7.md +65 -0
  41. package/docs/RELEASE_NOTES_v2.8.8.md +67 -0
  42. package/docs/RELEASE_NOTES_v2.8.9.md +67 -0
  43. package/docs/RELEASE_NOTES_v2.9.0.md +64 -0
  44. package/docs/RELEASE_NOTES_v2.9.8.md +73 -0
  45. package/docs/SEARCH_ARCHIVE.md +115 -0
  46. package/docs/assets/architecture.svg +104 -0
  47. package/docs/assets/hero.svg +98 -0
  48. package/docs/engines/bangumi.md +27 -0
  49. package/docs/engines/biorxiv.md +27 -0
  50. package/docs/engines/clawhub.md +27 -0
  51. package/docs/engines/crt_sh.md +27 -0
  52. package/docs/engines/datacite.md +27 -0
  53. package/docs/engines/deps_dev.md +27 -0
  54. package/docs/engines/douban_movie.md +27 -0
  55. package/docs/engines/endoflife.md +27 -0
  56. package/docs/engines/flk_law.md +27 -0
  57. package/docs/engines/fxtwitter.md +27 -0
  58. package/docs/engines/google_news.md +27 -0
  59. package/docs/engines/gov_regulations.md +27 -0
  60. package/docs/engines/guqian.md +27 -0
  61. package/docs/engines/hackernews.md +27 -0
  62. package/docs/engines/iplant.md +27 -0
  63. package/docs/engines/keenable.md +27 -0
  64. package/docs/engines/met_museum.md +27 -0
  65. package/docs/engines/nasa_images.md +27 -0
  66. package/docs/engines/netease_music.md +27 -0
  67. package/docs/engines/npm.md +27 -0
  68. package/docs/engines/nvd.md +27 -0
  69. package/docs/engines/openreview.md +27 -0
  70. package/docs/engines/openstd.md +27 -0
  71. package/docs/engines/parallel_free.md +27 -0
  72. package/docs/engines/people_daily.md +27 -0
  73. package/docs/engines/realtime_index.md +27 -0
  74. package/docs/engines/sec_edgar.md +27 -0
  75. package/docs/engines/seltz.md +27 -0
  76. package/docs/engines/skillsmp.md +27 -0
  77. package/docs/engines/sspai.md +27 -0
  78. package/docs/engines/std_samr.md +27 -0
  79. package/docs/engines/train.md +27 -0
  80. package/docs/engines/un_comtrade.md +27 -0
  81. package/docs/engines/wikisource.md +27 -0
  82. package/docs/engines/zdic.md +27 -0
  83. package/docs/engines/zenodo.md +27 -0
  84. package/docs/engines/zhihu_hot_app.md +27 -0
  85. package/docs//344/270/272/344/273/200/344/271/210/351/200/211/346/213/251argo.md +176 -0
  86. package/engines/README.md +10 -0
  87. package/engines/_template_cli.yaml +39 -0
  88. package/engines/_template_http.yaml +37 -0
  89. package/engines/plugins/_template_plugin.py +50 -0
  90. package/engines/specs/_cisa_kev.yaml +54 -0
  91. package/engines/specs/_osv.yaml +63 -0
  92. package/engines/specs/aljazeera.yaml +43 -0
  93. package/engines/specs/antara.yaml +43 -0
  94. package/engines/specs/artic.yaml +20 -0
  95. package/engines/specs/aviation_weather.yaml +39 -0
  96. package/engines/specs/bangumi.yaml +21 -0
  97. package/engines/specs/biorxiv.yaml +24 -0
  98. package/engines/specs/carbon_intensity.yaml +32 -0
  99. package/engines/specs/cfets.yaml +41 -0
  100. package/engines/specs/cinii.yaml +44 -0
  101. package/engines/specs/clawhub.yaml +49 -0
  102. package/engines/specs/cleveland.yaml +19 -0
  103. package/engines/specs/cn_ai_news.yaml +43 -0
  104. package/engines/specs/core.yaml +35 -0
  105. package/engines/specs/crt_sh.yaml +38 -0
  106. package/engines/specs/ct24.yaml +43 -0
  107. package/engines/specs/datacite.yaml +27 -0
  108. package/engines/specs/datagov_uk.yaml +41 -0
  109. package/engines/specs/ddgs_images.yaml +45 -0
  110. package/engines/specs/deepseek_search.yaml +74 -0
  111. package/engines/specs/deezer.yaml +20 -0
  112. package/engines/specs/defillama.yaml +38 -0
  113. package/engines/specs/deps_dev.yaml +21 -0
  114. package/engines/specs/digup.yaml +36 -0
  115. package/engines/specs/doi.yaml +39 -0
  116. package/engines/specs/douban_movie.yaml +22 -0
  117. package/engines/specs/dw_news.yaml +47 -0
  118. package/engines/specs/egov_law.yaml +21 -0
  119. package/engines/specs/elpais.yaml +43 -0
  120. package/engines/specs/endoflife.yaml +21 -0
  121. package/engines/specs/energy_charts.yaml +21 -0
  122. package/engines/specs/factcheck_org.yaml +52 -0
  123. package/engines/specs/faz.yaml +43 -0
  124. package/engines/specs/federal_register.yaml +57 -0
  125. package/engines/specs/firecrawl.yaml +53 -0
  126. package/engines/specs/flk_law.yaml +24 -0
  127. package/engines/specs/folha.yaml +43 -0
  128. package/engines/specs/france24.yaml +44 -0
  129. package/engines/specs/full_fact.yaml +47 -0
  130. package/engines/specs/fxtwitter.yaml +36 -0
  131. package/engines/specs/gbfs_nyc.yaml +32 -0
  132. package/engines/specs/gdacs.yaml +21 -0
  133. package/engines/specs/gdelt.yaml +64 -0
  134. package/engines/specs/gleif.yaml +55 -0
  135. package/engines/specs/google_news.yaml +22 -0
  136. package/engines/specs/gov_regulations.yaml +38 -0
  137. package/engines/specs/guardian_rss.yaml +59 -0
  138. package/engines/specs/hkex.yaml +40 -0
  139. package/engines/specs/hurriyet.yaml +43 -0
  140. package/engines/specs/iplant.yaml +25 -0
  141. package/engines/specs/jikan.yaml +24 -0
  142. package/engines/specs/jolpica.yaml +20 -0
  143. package/engines/specs/k10plus.yaml +20 -0
  144. package/engines/specs/keenable.yaml +44 -0
  145. package/engines/specs/keenable_free.yaml +36 -0
  146. package/engines/specs/lefigaro.yaml +43 -0
  147. package/engines/specs/listenbrainz.yaml +19 -0
  148. package/engines/specs/matichon.yaml +43 -0
  149. package/engines/specs/met_museum.yaml +21 -0
  150. package/engines/specs/nasa_images.yaml +45 -0
  151. package/engines/specs/netease_music.yaml +38 -0
  152. package/engines/specs/nhk.yaml +43 -0
  153. package/engines/specs/nhtsa_vpic.yaml +20 -0
  154. package/engines/specs/noaa_swpc.yaml +20 -0
  155. package/engines/specs/nvd.yaml +38 -0
  156. package/engines/specs/obis.yaml +20 -0
  157. package/engines/specs/opencitations.yaml +52 -0
  158. package/engines/specs/openf1.yaml +21 -0
  159. package/engines/specs/openfoodfacts.yaml +45 -0
  160. package/engines/specs/openligadb.yaml +20 -0
  161. package/engines/specs/openreview.yaml +37 -0
  162. package/engines/specs/openstd.yaml +21 -0
  163. package/engines/specs/people_daily.yaml +22 -0
  164. package/engines/specs/realtime_index.yaml +34 -0
  165. package/engines/specs/ror.yaml +20 -0
  166. package/engines/specs/satnogs.yaml +20 -0
  167. package/engines/specs/sec_edgar.yaml +46 -0
  168. package/engines/specs/shfe.yaml +39 -0
  169. package/engines/specs/skillsmp.yaml +50 -0
  170. package/engines/specs/sogou_weixin_browser.yaml +30 -0
  171. package/engines/specs/soilgrids.yaml +25 -0
  172. package/engines/specs/sspai.yaml +36 -0
  173. package/engines/specs/stackexchange.yaml +46 -0
  174. package/engines/specs/std_samr.yaml +21 -0
  175. package/engines/specs/tass.yaml +43 -0
  176. package/engines/specs/tavily_free.yaml +56 -0
  177. package/engines/specs/tle_mirror.yaml +21 -0
  178. package/engines/specs/train.yaml +37 -0
  179. package/engines/specs/tvmaze.yaml +20 -0
  180. package/engines/specs/twitter_syndication.yaml +31 -0
  181. package/engines/specs/un_comtrade.yaml +23 -0
  182. package/engines/specs/unpaywall.yaml +54 -0
  183. package/engines/specs/vnpress.yaml +43 -0
  184. package/engines/specs/weather.yaml +32 -0
  185. package/engines/specs/who_don.yaml +21 -0
  186. package/engines/specs/who_gho.yaml +20 -0
  187. package/engines/specs/wikimedia_commons.yaml +34 -0
  188. package/engines/specs/wikisource.yaml +22 -0
  189. package/engines/specs/worms.yaml +19 -0
  190. package/engines/specs/x_trending.yaml +34 -0
  191. package/engines/specs/yna.yaml +43 -0
  192. package/engines/specs/ynet.yaml +43 -0
  193. package/engines/specs/yuanbao.yaml +93 -0
  194. package/engines/specs/zbmath.yaml +40 -0
  195. package/engines/specs/zdic.yaml +22 -0
  196. package/engines/specs/zenodo.yaml +27 -0
  197. package/engines/specs/zhihu_hot_app.yaml +35 -0
  198. package/mcp/clients.yaml +106 -0
  199. package/package.json +55 -12
  200. package/packages/dsh-plugin/README.md +152 -0
  201. package/packages/dsh-plugin/cordis.patch.yml +48 -0
  202. package/packages/dsh-plugin/dsh/client.js +1533 -0
  203. package/packages/dsh-plugin/dsh/diagnostics.js +54 -0
  204. package/packages/dsh-plugin/dsh/index.js +527 -0
  205. package/packages/dsh-plugin/dsh/native-tools.mjs +749 -0
  206. package/packages/dsh-plugin/dsh/runner.js +666 -0
  207. package/packages/dsh-plugin/dsh/wide-research.js +878 -0
  208. package/packages/dsh-plugin/package.json +38 -0
  209. package/packages/dsh-plugin/scripts/sync-local.sh +70 -0
  210. package/packages/dsh-plugin/test/client.test.js +406 -0
  211. package/packages/dsh-plugin/test/config.test.js +30 -0
  212. package/packages/dsh-plugin/test/diagnostics.test.js +28 -0
  213. package/packages/dsh-plugin/test/index.test.js +1382 -0
  214. package/references/query-templates.md +96 -0
  215. package/references/query-templates.yaml +118 -0
  216. package/scripts/ab_eval.py +219 -0
  217. package/scripts/ab_eval_p0p1.py +266 -0
  218. package/scripts/adaptive.py +294 -70
  219. package/scripts/answer.py +154 -0
  220. package/scripts/archive_run.py +591 -0
  221. package/scripts/argo_engine_registry.py +172 -0
  222. package/scripts/argo_paths.py +728 -0
  223. package/scripts/article.py +136 -0
  224. package/scripts/article_meta.py +131 -0
  225. package/scripts/arxiv_source.py +201 -0
  226. package/scripts/badcase_ledger.py +252 -0
  227. package/scripts/batch_probe.py +241 -0
  228. package/scripts/benchmark.py +19 -0
  229. package/scripts/bounded_run.py +144 -0
  230. package/scripts/browser_auth.py +323 -0
  231. package/scripts/budget.py +247 -0
  232. package/scripts/budget_cli.py +93 -0
  233. package/scripts/build_release_assets.sh +65 -0
  234. package/scripts/burstiness_detector.py +147 -0
  235. package/scripts/cache.py +1066 -67
  236. package/scripts/cache_guard.py +271 -0
  237. package/scripts/cache_key_vdom.py +56 -0
  238. package/scripts/cache_similarity.py +170 -0
  239. package/scripts/caliber_isolation.py +195 -0
  240. package/scripts/calibrate_lowquality.py +210 -0
  241. package/scripts/candidate_envelope.py +366 -0
  242. package/scripts/chrome_cdp.py +871 -0
  243. package/scripts/ci_drift_gate.py +90 -0
  244. package/scripts/circuit_breaker.py +389 -0
  245. package/scripts/citations.py +334 -0
  246. package/scripts/cite_cli.py +59 -0
  247. package/scripts/cjk_tokens.py +95 -0
  248. package/scripts/clarify.py +47 -24
  249. package/scripts/cli_io.py +240 -0
  250. package/scripts/config.py +683 -45
  251. package/scripts/config_cache.py +402 -0
  252. package/scripts/conn_pool.py +220 -0
  253. package/scripts/content_boundary.py +46 -0
  254. package/scripts/content_security.py +685 -0
  255. package/scripts/content_signals.py +751 -0
  256. package/scripts/crawl.py +74 -35
  257. package/scripts/cross_lingual_trace.py +293 -0
  258. package/scripts/daemon.py +866 -0
  259. package/scripts/digup_bridge.py +214 -0
  260. package/scripts/dispatch_result.py +98 -0
  261. package/scripts/engine_admission.py +366 -0
  262. package/scripts/engine_dispatch.py +982 -0
  263. package/scripts/engine_env.py +539 -0
  264. package/scripts/engine_failure.py +411 -0
  265. package/scripts/engine_families.py +738 -0
  266. package/scripts/engine_policy.py +218 -0
  267. package/scripts/engine_requires.py +190 -0
  268. package/scripts/engine_status.py +322 -0
  269. package/scripts/engine_validate.py +638 -0
  270. package/scripts/engines.py +799 -294
  271. package/scripts/engines_base.py +1521 -0
  272. package/scripts/engines_builders.py +310 -0
  273. package/scripts/engines_builders_batch10.py +204 -0
  274. package/scripts/engines_builders_batch11.py +174 -0
  275. package/scripts/engines_builders_batch12.py +103 -0
  276. package/scripts/engines_builders_batch13.py +340 -0
  277. package/scripts/engines_builders_batch14.py +656 -0
  278. package/scripts/engines_builders_batch15.py +249 -0
  279. package/scripts/engines_builders_batch9.py +1468 -0
  280. package/scripts/engines_builders_cn.py +2283 -0
  281. package/scripts/engines_builders_data.py +2923 -0
  282. package/scripts/engines_builders_data_macro.py +775 -0
  283. package/scripts/engines_builders_feeds.py +255 -0
  284. package/scripts/engines_builders_intl.py +1239 -0
  285. package/scripts/engines_builders_keyless.py +111 -0
  286. package/scripts/engines_builders_search.py +440 -0
  287. package/scripts/engines_builders_tech.py +1359 -0
  288. package/scripts/evidence.py +620 -166
  289. package/scripts/evidence_loop.py +624 -0
  290. package/scripts/evidence_tier.py +298 -0
  291. package/scripts/except_sets.py +25 -0
  292. package/scripts/extract.py +55 -16
  293. package/scripts/fact_align.py +207 -0
  294. package/scripts/fact_crosscheck.py +230 -0
  295. package/scripts/fetch.py +25 -12
  296. package/scripts/fetch_browser.py +87 -0
  297. package/scripts/fetch_jsrun.py +122 -0
  298. package/scripts/fetch_quality.py +225 -0
  299. package/scripts/fetch_render_tinyfish.py +117 -0
  300. package/scripts/fetch_v3.py +1804 -0
  301. package/scripts/focus_extract.py +350 -0
  302. package/scripts/fulltext_store.py +169 -0
  303. package/scripts/fxtwitter_api.py +325 -0
  304. package/scripts/gen_engine_catalog.py +364 -0
  305. package/scripts/gen_native_tools.py +91 -0
  306. package/scripts/gen_routing_snapshot.py +195 -0
  307. package/scripts/health_check.py +140 -402
  308. package/scripts/health_probe.py +23 -8
  309. package/scripts/hot_state.py +134 -0
  310. package/scripts/http_client.py +1053 -0
  311. package/scripts/image_ops.py +430 -0
  312. package/scripts/image_vision/vision_probe.swift +126 -0
  313. package/scripts/install.ps1 +166 -0
  314. package/scripts/install.sh +221 -0
  315. package/scripts/job.py +1186 -0
  316. package/scripts/jump_resolver.py +235 -0
  317. package/scripts/lang_capability.py +150 -0
  318. package/scripts/lang_detect.py +289 -0
  319. package/scripts/lang_matrix_probe.py +329 -0
  320. package/scripts/lang_pref.py +311 -0
  321. package/scripts/link_source.py +340 -0
  322. package/scripts/local_image.py +970 -0
  323. package/scripts/local_seek.py +402 -0
  324. package/scripts/macro_countries.py +97 -0
  325. package/scripts/matrix_search_eval.py +1187 -0
  326. package/scripts/mcp_compact.py +234 -0
  327. package/scripts/mcp_diag.py +8 -3
  328. package/scripts/mcp_handlers.py +492 -0
  329. package/scripts/mcp_handlers_surface.py +157 -0
  330. package/scripts/mcp_launch.sh +124 -0
  331. package/scripts/mcp_local.py +241 -0
  332. package/scripts/mcp_server.py +125 -419
  333. package/scripts/mcp_setup.py +431 -0
  334. package/scripts/mcp_tools.py +331 -0
  335. package/scripts/mcp_transport.py +191 -0
  336. package/scripts/mcp_warm.py +127 -0
  337. package/scripts/min_version_scan.py +335 -0
  338. package/scripts/net_proxy.py +258 -0
  339. package/scripts/network_aware.py +152 -0
  340. package/scripts/output_transparency.py +200 -0
  341. package/scripts/paid_policy.py +98 -0
  342. package/scripts/paper.py +274 -0
  343. package/scripts/pdf_extract.py +518 -0
  344. package/scripts/phrase_filter.py +66 -0
  345. package/scripts/plan.py +422 -0
  346. package/scripts/postprocess_signals.py +150 -0
  347. package/scripts/pre-commit +48 -0
  348. package/scripts/query_enhance.py +157 -0
  349. package/scripts/query_operators.py +196 -0
  350. package/scripts/query_rewriter.py +116 -7
  351. package/scripts/query_signals.py +300 -0
  352. package/scripts/query_templates.py +167 -0
  353. package/scripts/query_understanding.py +411 -0
  354. package/scripts/query_variants.py +298 -0
  355. package/scripts/quota.py +633 -76
  356. package/scripts/rank_signals.py +775 -0
  357. package/scripts/ranking_eval.py +319 -0
  358. package/scripts/readability_extract.py +433 -0
  359. package/scripts/recompute.py +415 -0
  360. package/scripts/recovery.py +616 -0
  361. package/scripts/recovery_l0.py +160 -0
  362. package/scripts/recovery_runner.py +150 -0
  363. package/scripts/redskill/redskill_engine.py +332 -0
  364. package/scripts/regression_p0p1.py +318 -0
  365. package/scripts/replay_eval.py +658 -0
  366. package/scripts/rerank_tfidf.py +225 -0
  367. package/scripts/research.py +607 -403
  368. package/scripts/research_cli.py +361 -0
  369. package/scripts/research_dossier.py +492 -0
  370. package/scripts/research_expand.py +169 -0
  371. package/scripts/research_gates.py +272 -0
  372. package/scripts/research_report.py +209 -0
  373. package/scripts/research_strategy.py +37 -0
  374. package/scripts/research_work_packages.py +195 -0
  375. package/scripts/result_lang.py +311 -0
  376. package/scripts/retry_policy.py +76 -0
  377. package/scripts/robots_guard.py +259 -0
  378. package/scripts/route.py +921 -254
  379. package/scripts/route_bangs.py +40 -0
  380. package/scripts/route_cache.py +243 -0
  381. package/scripts/route_combo.py +917 -0
  382. package/scripts/route_domains.py +341 -0
  383. package/scripts/route_lang.py +605 -0
  384. package/scripts/route_log.py +63 -0
  385. package/scripts/route_policy.py +250 -0
  386. package/scripts/rrf_classic.py +66 -0
  387. package/scripts/screenshot.py +73 -0
  388. package/scripts/search.py +886 -406
  389. package/scripts/search_attribution.py +185 -0
  390. package/scripts/search_benchmark.py +407 -0
  391. package/scripts/search_cli.py +640 -0
  392. package/scripts/search_entry.py +395 -0
  393. package/scripts/search_output.py +690 -0
  394. package/scripts/search_pipeline.py +926 -0
  395. package/scripts/search_rank.py +991 -0
  396. package/scripts/search_types.py +129 -16
  397. package/scripts/seek_locator.py +68 -0
  398. package/scripts/semantic_evidence.py +391 -0
  399. package/scripts/serp_guard.py +336 -0
  400. package/scripts/serp_kill_log.py +74 -0
  401. package/scripts/single_flight.py +120 -0
  402. package/scripts/social_engines/__init__.py +10 -0
  403. package/scripts/social_engines/bilibili_engine.py +131 -0
  404. package/scripts/social_engines/hackernews_engine.py +113 -0
  405. package/scripts/social_engines/reddit_engine.py +172 -0
  406. package/scripts/social_engines/twitter_engine.py +397 -0
  407. package/scripts/social_engines/v2ex_engine.py +101 -0
  408. package/scripts/social_engines/weibo_engine.py +128 -0
  409. package/scripts/social_engines/xiaohongshu_engine.py +137 -0
  410. package/scripts/social_engines/zhihu_engine.py +139 -0
  411. package/scripts/social_research.py +201 -0
  412. package/scripts/sogou_weixin_browser.py +318 -0
  413. package/scripts/source_ledger.py +249 -0
  414. package/scripts/stage_timing.py +58 -0
  415. package/scripts/stats_cli.py +107 -0
  416. package/scripts/straggler_policy.py +60 -0
  417. package/scripts/stylometry_detector.py +231 -0
  418. package/scripts/sync_backends.py +353 -0
  419. package/scripts/sync_installs.py +29 -0
  420. package/scripts/template_cli.py +127 -0
  421. package/scripts/text_lexicon.py +72 -0
  422. package/scripts/tfidf_router.py +109 -37
  423. package/scripts/time_utils.py +233 -0
  424. package/scripts/topic_research_profiles.py +643 -0
  425. package/scripts/train.py +737 -0
  426. package/scripts/tweet.py +596 -0
  427. package/scripts/url_canon.py +217 -0
  428. package/scripts/url_optimize.py +56 -0
  429. package/scripts/url_safety.py +236 -0
  430. package/scripts/usage_log.py +116 -0
  431. package/scripts/usage_log_clean.py +113 -0
  432. package/scripts/v2ex_nodes.py +311 -0
  433. package/scripts/variant_recall.py +188 -0
  434. package/scripts/watch.py +205 -0
  435. package/scripts/wechat_article.py +258 -0
  436. package/scripts/wide_research.py +447 -0
  437. package/scripts/wx.py +349 -0
  438. package/scripts/x_article_fetch.py +141 -0
  439. package/scripts/x_search.py +234 -0
  440. package/scripts/x_trending.py +189 -0
  441. package/scripts/yaml_load.py +67 -0
  442. package/scripts/zhihu_quota.py +139 -0
  443. package/sub-skills/ego-search/SKILL.md +253 -0
  444. package/sub-skills/ego-search/scripts/ego_search.py +800 -0
  445. package/sub-skills/ego-search/scripts/merge.py +226 -0
  446. package/sub-skills/ego-search/scripts/quality.py +64 -0
  447. package/sub-skills/ego-search/scripts/runtime.py +172 -0
  448. package/sub-skills/ego-search/scripts/safety.py +109 -0
  449. package/sub-skills/ego-search/scripts/serp_spec.py +109 -0
  450. package/sub-skills/ego-search/scripts/webbridge_adapter.py +374 -0
  451. package/sub-skills/js-run/SKILL.md +67 -0
  452. package/sub-skills/js-run/scripts/jsrun.py +112 -0
  453. package/sub-skills/js-run/scripts/shim.js +188 -0
  454. package/sub-skills/local-search/SKILL.md +50 -53
  455. package/sub-skills/local-search/bm25.py +186 -0
  456. package/sub-skills/local-search/cli_runner.py +331 -0
  457. package/sub-skills/local-search/config.yaml +152 -55
  458. package/sub-skills/local-search/engine_registry.py +91 -26
  459. package/sub-skills/local-search/{health_check.py → local_health_check.py} +140 -10
  460. package/sub-skills/local-search/local_search_adapter.py +13 -2
  461. package/sub-skills/local-search/parse_maps.yaml +40 -42
  462. package/sub-skills/local-search/parsers.py +328 -0
  463. package/sub-skills/local-search/search_v3.py +716 -242
  464. package/sub-skills/local-search/smart_router.py +47 -7
  465. package/sub-skills/local-seek/SKILL.md +110 -0
  466. package/sub-skills/local-seek/config/domains.yaml +20 -0
  467. package/sub-skills/local-seek/scripts/eval_rank.py +216 -0
  468. package/sub-skills/local-seek/scripts/eval_seek.py +224 -0
  469. package/sub-skills/local-seek/scripts/seek.py +1465 -0
  470. package/sub-skills/local-seek/scripts/seek_frecency.py +126 -0
  471. package/sub-skills/local-seek/scripts/seek_fts.py +430 -0
  472. package/sub-skills/local-seek/scripts/seek_rank.py +197 -0
@@ -0,0 +1,447 @@
1
+ #!/usr/bin/env python3
2
+ """wide_research.py — 广泛研究的机器侧原语(skill 形态)。
3
+
4
+ 与 DSH 插件 `@taxueseek/argo-dsh` 的 wide_research 共用同一套证据语义,
5
+ 但宿主能力不同:插件靠 ctx.subagents 派 worker、outputSchema 拿结构化
6
+ 结果;skill 形态的宿主只有 bash,所以这里只做**确定性机器侧**:
7
+
8
+ stage — 轨道按 depends_on 分阶段(复用 research_work_packages 的分层)
9
+ merge — 各轨道 worker 产出 → 跨轨道来源账本(URL 去重、http(s) 门)
10
+ → 宽表门禁(failures→low / warnings→medium / 干净→high)
11
+ → Markdown 报告落盘
12
+
13
+ 规划(拆轨道)、worker 取证、综合写作这三步是模型判断,不属于机器,
14
+ 协议见 references/research-protocol.md 的「广泛研究(skill 形态)」一节。
15
+
16
+ 用法:
17
+ python3 scripts/wide_research.py stage --tracks '<json 数组>'
18
+ python3 scripts/wide_research.py merge --run-dir <dir> [--question ...] [--write-report]
19
+
20
+ merge 的输入是 <run-dir>/tracks/<track-id>.json,每个文件形如:
21
+ {"track_id": "...", "summary": "...", "findings": [...],
22
+ "sources": [{"title","url","source_type","claim","excerpt",
23
+ "confidence","limitations"}],
24
+ "disagreements": [...], "gaps": [...], "error": 可选}
25
+ 输出(stdout,--json):账本 + stats + quality_gate_results + report_path。
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import hashlib
32
+ import json
33
+ import re
34
+ import sys
35
+ import time
36
+ from pathlib import Path
37
+ from typing import Any
38
+ from urllib.parse import urlparse
39
+
40
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
41
+
42
+ from cli_io import dumps # noqa: E402
43
+ from research_work_packages import stage_work_packages # noqa: E402
44
+
45
+ MAX_SOURCES_PER_TRACK = 8
46
+ MAX_FINDINGS_PER_TRACK = 8
47
+ MAX_DISAGREEMENTS = 6
48
+ MAX_GAPS = 6
49
+ CLIP = {
50
+ "title": 240,
51
+ "url": 2000,
52
+ "source_type": 80,
53
+ "claim": 1000,
54
+ "excerpt": 1200,
55
+ "limitations": 480,
56
+ "summary": 2500,
57
+ "finding": 1200,
58
+ "disagreement": 1000,
59
+ "gap": 1000,
60
+ }
61
+
62
+
63
+ def _clip(value: Any, limit: int, default: str = "") -> str:
64
+ text = str(value if value is not None else default).strip()
65
+ return text[:limit]
66
+
67
+
68
+ def normalize_track(raw: Any, index: int) -> dict[str, Any] | None:
69
+ """对齐插件 normalizeTrack:id/title/question 必需,rationale/depends_on 可选。"""
70
+ if not isinstance(raw, dict):
71
+ return None
72
+ title = _clip(raw.get("title"), 120)
73
+ question = _clip(raw.get("question"), 800)
74
+ if not title or not question:
75
+ return None
76
+ track_id = _clip(raw.get("id") or f"track-{index + 1}", 64)
77
+ track_id = re.sub(r"[^a-z0-9_-]+", "-", track_id.lower()).strip("-") or f"track-{index + 1}"
78
+ return {
79
+ "id": track_id,
80
+ "title": title,
81
+ "question": question,
82
+ "rationale": _clip(raw.get("rationale"), 360, "Independent evidence-collection angle."),
83
+ "depends_on": [str(d).strip() for d in (raw.get("depends_on") or []) if str(d).strip()][:8],
84
+ }
85
+
86
+
87
+ def normalize_tracks(raw: Any, maximum: int = 9) -> list[dict[str, Any]]:
88
+ """接受 list / JSON 字符串 / {"tracks": [...]},去重、截断到 maximum。"""
89
+ if raw is None or raw == "":
90
+ return []
91
+ if isinstance(raw, str):
92
+ text = raw.strip()
93
+ if not text:
94
+ return []
95
+ try:
96
+ raw = json.loads(text)
97
+ except json.JSONDecodeError as exc:
98
+ raise ValueError(f"tracks 不是合法 JSON: {exc}") from exc
99
+ if isinstance(raw, dict):
100
+ raw = raw.get("tracks") or []
101
+ if not isinstance(raw, list):
102
+ raise ValueError("tracks 须为数组")
103
+ seen: set[str] = set()
104
+ out: list[dict[str, Any]] = []
105
+ for i, item in enumerate(raw):
106
+ if len(out) >= maximum:
107
+ break
108
+ track = normalize_track(item, i)
109
+ if not track or track["id"] in seen:
110
+ continue
111
+ seen.add(track["id"])
112
+ out.append(track)
113
+ return out
114
+
115
+
116
+ def build_cross_lingual_tracks(question: str, maximum: int = 4) -> list[dict[str, Any]]:
117
+ """为溯源与多语言任务自动构建分阶段跨语言长程轨道。"""
118
+ q = (question or "").strip()
119
+ has_tracing = bool(re.search(
120
+ r"(?:溯源|起源|出处|最初|源头|提出者|发明|草案|历史沿革|origin|provenance|root cause|first proposed|specification|rfc|cve)",
121
+ q, re.I
122
+ ))
123
+ tracks = []
124
+ tracks.append({
125
+ "id": "track-primary-lang",
126
+ "title": "原语言背景与核心定义",
127
+ "question": f"{q} 背景 定义 现状",
128
+ "rationale": "Primary language overview and conceptual framing.",
129
+ "depends_on": [],
130
+ })
131
+ if has_tracing or len(tracks) < maximum:
132
+ tracks.append({
133
+ "id": "track-origin-trace",
134
+ "title": "源头出处与权威一手规范长程溯源",
135
+ "question": f"{q} original paper inception specification first proposed RFC official repository",
136
+ "rationale": "Cross-lingual long-range tracing for primary sources, papers, or official specifications.",
137
+ "depends_on": ["track-primary-lang"],
138
+ })
139
+ tracks.append({
140
+ "id": "track-cross-reconciliation",
141
+ "title": "多语言证据交叉对账与互补验证",
142
+ "question": f"{q} milestone benchmark evaluation comparison timeline",
143
+ "rationale": "Cross-lingual reconciliation of evidence, timeline differences, and complementary facts.",
144
+ "depends_on": ["track-origin-trace"],
145
+ })
146
+ return tracks[:maximum]
147
+
148
+
149
+
150
+ def stage_tracks(tracks: list[dict[str, Any]]) -> tuple[list[list[dict[str, Any]]], list[str]]:
151
+ """轨道分层。work package 的分层逻辑与轨道同构:id/question/depends_on。"""
152
+ packages = [
153
+ {"id": t["id"], "question": t["question"], "depends_on": t["depends_on"]}
154
+ for t in tracks
155
+ ]
156
+ stages, warnings = stage_work_packages(packages)
157
+ by_id = {t["id"]: t for t in tracks}
158
+ return [[by_id[p["id"]] for p in stage if p["id"] in by_id] for stage in stages], warnings
159
+
160
+
161
+ def normalize_source(raw: Any) -> dict[str, Any] | None:
162
+ """对齐插件 normalizeSource:title/url/claim 必需,仅 http(s) 入账。"""
163
+ if not isinstance(raw, dict):
164
+ return None
165
+ title = _clip(raw.get("title"), CLIP["title"])
166
+ url = _clip(raw.get("url"), CLIP["url"])
167
+ claim = _clip(raw.get("claim"), CLIP["claim"])
168
+ if not title or not url or not claim:
169
+ return None
170
+ parsed = urlparse(url)
171
+ if parsed.scheme not in ("http", "https"):
172
+ return None
173
+ confidence = str(raw.get("confidence") or "low").lower()
174
+ if confidence not in ("high", "medium"):
175
+ confidence = "low"
176
+ return {
177
+ "title": title,
178
+ "url": url,
179
+ "source_type": _clip(raw.get("source_type") or raw.get("sourceType"), CLIP["source_type"], "web"),
180
+ "claim": claim,
181
+ "excerpt": _clip(raw.get("excerpt"), CLIP["excerpt"], "No excerpt supplied."),
182
+ "confidence": confidence,
183
+ "limitations": _clip(raw.get("limitations"), CLIP["limitations"],
184
+ "Not independently verified by the orchestrator."),
185
+ }
186
+
187
+
188
+ def normalize_track_result(raw: Any, track_id: str) -> dict[str, Any]:
189
+ """对齐插件 normalizeResearch 的字段与截断。error 轨道保留错误信息。"""
190
+ record = raw if isinstance(raw, dict) else {}
191
+ sources: list[dict[str, Any]] = []
192
+ for item in (record.get("sources") or [])[:MAX_SOURCES_PER_TRACK]:
193
+ source = normalize_source(item)
194
+ if source:
195
+ sources.append(source)
196
+ return {
197
+ "track_id": track_id,
198
+ "summary": _clip(record.get("summary"), CLIP["summary"], "No usable summary returned."),
199
+ "findings": [_clip(f, CLIP["finding"]) for f in (record.get("findings") or [])[:MAX_FINDINGS_PER_TRACK]],
200
+ "sources": sources,
201
+ "disagreements": [_clip(d, CLIP["disagreement"]) for d in (record.get("disagreements") or [])[:MAX_DISAGREEMENTS]],
202
+ "gaps": [_clip(g, CLIP["gap"]) for g in (record.get("gaps") or [])[:MAX_GAPS]],
203
+ "error": _clip(record.get("error"), 800) or None,
204
+ }
205
+
206
+
207
+ def load_track_results(run_dir: Path, tracks: list[dict[str, Any]] | None = None) -> list[dict[str, Any]]:
208
+ """读 <run-dir>/tracks/*.json。给了 --tracks 时交叉校验:缺失产出的轨道记失败行,
209
+ 防 worker 静默丢失(没写文件 = 没完成,门禁必须看见)。"""
210
+ tracks_dir = run_dir / "tracks"
211
+ results: list[dict[str, Any]] = []
212
+ seen: set[str] = set()
213
+ if tracks_dir.is_dir():
214
+ for path in sorted(tracks_dir.glob("*.json")):
215
+ try:
216
+ raw = json.loads(path.read_text(encoding="utf-8"))
217
+ except (json.JSONDecodeError, OSError) as exc:
218
+ results.append(normalize_track_result({"error": f"轨道文件不可读: {exc}"}, path.stem))
219
+ seen.add(path.stem)
220
+ continue
221
+ track_id = str(raw.get("track_id") or path.stem)
222
+ seen.add(track_id)
223
+ results.append(normalize_track_result(raw, track_id))
224
+ elif tracks is None:
225
+ raise FileNotFoundError(f"找不到轨道产出目录: {tracks_dir}")
226
+ if tracks:
227
+ for track in tracks:
228
+ if track["id"] not in seen:
229
+ results.append(normalize_track_result(
230
+ {"error": f"worker 未产出结果文件({tracks_dir}/{track['id']}.json 不存在)"},
231
+ track["id"],
232
+ ))
233
+ if not results:
234
+ raise FileNotFoundError(f"{tracks_dir} 下没有任何轨道产出 (*.json)")
235
+ return results
236
+
237
+
238
+ def build_ledger(results: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
239
+ """跨轨道来源账本:URL 归一去重,S1..Sn 全局编号。返回 (sources, ledger)。"""
240
+ sources: list[dict[str, Any]] = []
241
+ seen: set[str] = set()
242
+ ledger: list[dict[str, Any]] = []
243
+ for result in results:
244
+ keys: list[str] = []
245
+ for source in result["sources"]:
246
+ normalized = source["url"].lower().split("#", 1)[0].rstrip("/")
247
+ if normalized in seen:
248
+ continue
249
+ seen.add(normalized)
250
+ key = f"S{len(sources) + 1}"
251
+ sources.append({**source, "key": key})
252
+ keys.append(key)
253
+ ledger.append({
254
+ "track_id": result["track_id"],
255
+ "summary": result["summary"],
256
+ "findings": result["findings"],
257
+ "source_keys": keys,
258
+ "disagreements": result["disagreements"],
259
+ "gaps": result["gaps"],
260
+ "error": result["error"],
261
+ })
262
+ return sources, ledger
263
+
264
+
265
+ def evaluate_gates(
266
+ sources: list[dict[str, Any]],
267
+ ledger: list[dict[str, Any]],
268
+ caveats: list[str],
269
+ unanswered: list[str],
270
+ ) -> dict[str, Any]:
271
+ """对齐插件 evaluateGates 的可判定谓词(recompute 门由 research.py 工作包链路负责)。"""
272
+ failures: list[dict[str, str]] = []
273
+ warnings: list[dict[str, str]] = []
274
+ completed = [row for row in ledger if not row["error"]]
275
+ failed = [row for row in ledger if row["error"]]
276
+
277
+ if not sources:
278
+ failures.append({"id": "no_sources", "detail": "No usable sources with real URLs were returned."})
279
+ if not completed:
280
+ failures.append({"id": "no_completed_tracks", "detail": "No research track completed."})
281
+ if failed and completed:
282
+ warnings.append({
283
+ "id": "partial_track_failure",
284
+ "detail": f"{len(failed)} of {len(ledger)} track(s) failed; report covers completed tracks only.",
285
+ })
286
+ if sources and all(s["confidence"] != "high" for s in sources):
287
+ warnings.append({"id": "no_high_confidence_sources",
288
+ "detail": "No source reached high confidence; treat claims as unverified."})
289
+ if len(caveats) + len(unanswered) >= max(3, (len(sources) + 1) // 2):
290
+ warnings.append({"id": "high_uncertainty",
291
+ "detail": f"{len(caveats)} caveats and {len(unanswered)} unanswered questions exceed the evidence threshold."})
292
+
293
+ # 内容农场与镜像站占优检查
294
+ farm_markers = ("ctolib.com", "coder.work", "voidcn.com", "it1352.com", "oommake.com", "py4u.net",
295
+ "wenku.zone", "chinadmd.com", "jb51.net", "pc6.com", "onlinedown.net")
296
+ farm_hits = [s for s in sources if any(m in str(s.get("url", "")).lower() for m in farm_markers)]
297
+ if farm_hits and len(farm_hits) >= (len(sources) + 1) // 2:
298
+ warnings.append({
299
+ "id": "content_farm_dominant",
300
+ "detail": f"{len(farm_hits)} sources originate from content farms or SEO scraper mirrors; evidence quality severely degraded.",
301
+ })
302
+
303
+ cap = "low" if failures else ("medium" if warnings else "high")
304
+ return {"passed": not failures, "conclusion_cap": cap, "failures": failures, "warnings": warnings}
305
+
306
+
307
+ def render_report(question: str, summary: str, answer: str,
308
+ sources: list[dict[str, Any]], warnings: list[str]) -> str:
309
+ """对齐插件 renderReport 的报告骨架。"""
310
+ bibliography = "\n".join(f"- [{s['key']}] {s['title']} — {s['url']}" for s in sources) \
311
+ or "- No valid source entries were returned."
312
+ warning_block = ""
313
+ if warnings:
314
+ warning_block = "\n\n## Execution warnings\n" + "\n".join(f"- {w}" for w in warnings)
315
+ return (f"# Wide Research Report\n\n## Executive summary\n{summary}\n\n"
316
+ f"{answer}\n\n## Evidence ledger\n{bibliography}{warning_block}")
317
+
318
+
319
+ def persist_report(run_dir: Path, question: str, text: str) -> str:
320
+ slug = re.sub(r"[^\w\u4e00-\u9fa5-]+", "_", question[:40]).strip("_") or "research"
321
+ ts = time.strftime("%Y%m%d-%H%M%S")
322
+ path = run_dir / f"{ts}-{slug}.md"
323
+ path.write_text(text, encoding="utf-8")
324
+ return str(path)
325
+
326
+
327
+ def cmd_stage(args: argparse.Namespace) -> int:
328
+ tracks = normalize_tracks(args.tracks, args.max_tracks)
329
+ if len(tracks) < 2:
330
+ print("wide_research: 至少需要两条有效轨道(id/title/question)", file=sys.stderr)
331
+ return 2
332
+ stages, warnings = stage_tracks(tracks)
333
+ print(dumps({
334
+ "stages": [[t["id"] for t in stage] for stage in stages],
335
+ "tracks": tracks,
336
+ "warnings": warnings,
337
+ "hint": "每个阶段内的轨道并行取证;下一阶段等上一阶段完成。worker 产出写 <run-dir>/tracks/<track-id>.json。",
338
+ }))
339
+ return 0
340
+
341
+
342
+ def _parse_lines(raw: str) -> list[str]:
343
+ """接受 JSON 数组或每行一条;给 agent 留最低门槛的输入方式。"""
344
+ raw = (raw or "").strip()
345
+ if not raw:
346
+ return []
347
+ if raw[0] == "[":
348
+ try:
349
+ return [str(c) for c in json.loads(raw)][:12]
350
+ except json.JSONDecodeError:
351
+ pass
352
+ return [line.strip() for line in raw.splitlines() if line.strip()][:12]
353
+
354
+
355
+ def cmd_merge(args: argparse.Namespace) -> int:
356
+ tracks = normalize_tracks(args.tracks, 9) if args.tracks else []
357
+ run_dir = Path(args.run_dir).expanduser()
358
+ results = load_track_results(run_dir, tracks or None)
359
+ sources, ledger = build_ledger(results)
360
+
361
+ caveats: list[str] = []
362
+ unanswered: list[str] = []
363
+ summary = args.summary or ""
364
+ answer = args.answer or ""
365
+ if args.synthesis:
366
+ try:
367
+ raw = Path(args.synthesis).expanduser().read_text(encoding="utf-8")
368
+ except OSError as exc:
369
+ print(f"wide_research: synthesis 文件不可读: {exc}", file=sys.stderr)
370
+ return 2
371
+ try:
372
+ synth = json.loads(raw)
373
+ except json.JSONDecodeError as exc:
374
+ print(f"wide_research: synthesis 不是合法 JSON: {exc}", file=sys.stderr)
375
+ return 2
376
+ summary = _clip(synth.get("executive_summary") or synth.get("executiveSummary"), CLIP["summary"]) or summary
377
+ answer = str(synth.get("answer") or answer)
378
+ caveats = [str(c)[:CLIP["disagreement"]] for c in (synth.get("caveats") or [])[:12]]
379
+ unanswered = [str(c)[:CLIP["gap"]] for c in (synth.get("unanswered_questions") or synth.get("unansweredQuestions") or [])[:12]]
380
+ else:
381
+ caveats = _parse_lines(args.caveats)
382
+ unanswered = _parse_lines(args.unanswered)
383
+
384
+ gates = evaluate_gates(sources, ledger, caveats, unanswered)
385
+ warnings = [w["detail"] for w in gates["warnings"]]
386
+ report = ""
387
+ report_path = None
388
+ if args.write_report:
389
+ if not answer:
390
+ # 没有综合正文时,报告至少承载账本与警告,供人工/后续核验。
391
+ answer = "\n\n".join(
392
+ f"### {row['track_id']}\n{row['summary']}"
393
+ + ("".join(f"\n- {f}" for f in row["findings"]))
394
+ for row in ledger if not row["error"]
395
+ )
396
+ report = render_report(args.question or "Wide Research", summary, answer, sources, warnings)
397
+ report_path = persist_report(run_dir, args.question or "wide-research", report)
398
+
399
+ print(dumps({
400
+ "question": args.question or "",
401
+ "stats": {
402
+ "plannedTracks": len(ledger),
403
+ "completedTracks": sum(1 for r in ledger if not r["error"]),
404
+ "failedTracks": sum(1 for r in ledger if r["error"]),
405
+ "sourceCount": len(sources),
406
+ },
407
+ "sources": sources,
408
+ "ledger": ledger,
409
+ "caveats": caveats,
410
+ "unanswered_questions": unanswered,
411
+ "warnings": warnings,
412
+ "quality_gate_results": gates,
413
+ "report": report,
414
+ "report_path": report_path,
415
+ }))
416
+ return 0
417
+
418
+
419
+ def main(argv: list[str] | None = None) -> int:
420
+ parser = argparse.ArgumentParser(prog="wide_research", description=__doc__.splitlines()[0])
421
+ sub = parser.add_subparsers(dest="command", required=True)
422
+
423
+ p_stage = sub.add_parser("stage", help="轨道按 depends_on 分阶段")
424
+ p_stage.add_argument("--tracks", required=True, help="轨道 JSON 数组(内联或文件路径)")
425
+ p_stage.add_argument("--max-tracks", type=int, default=9)
426
+ p_stage.add_argument("--json", action="store_true", help="兼容仓库 CLI 惯例;输出本就是 JSON")
427
+ p_stage.set_defaults(func=cmd_stage)
428
+
429
+ p_merge = sub.add_parser("merge", help="轨道产出 → 账本 + 门禁 + 报告")
430
+ p_merge.add_argument("--run-dir", required=True, help="本次研究运行目录(含 tracks/*.json)")
431
+ p_merge.add_argument("--tracks", default="", help="轨道 JSON(与 stage 同输入),用于交叉校验缺失产出")
432
+ p_merge.add_argument("--question", default="")
433
+ p_merge.add_argument("--summary", default="", help="执行摘要(模型写)")
434
+ p_merge.add_argument("--answer", default="", help="综合正文 Markdown(模型写)")
435
+ p_merge.add_argument("--synthesis", default="", help="综合 JSON 文件(answer/executive_summary/caveats/unanswered_questions)")
436
+ p_merge.add_argument("--caveats", default="", help="JSON 数组或每行一条(无 --synthesis 时生效)")
437
+ p_merge.add_argument("--unanswered", default="", help="JSON 数组或每行一条(无 --synthesis 时生效)")
438
+ p_merge.add_argument("--write-report", action="store_true", help="报告落盘 <run-dir>/<ts>-<slug>.md")
439
+ p_merge.add_argument("--json", action="store_true", help="兼容仓库 CLI 惯例;输出本就是 JSON")
440
+ p_merge.set_defaults(func=cmd_merge)
441
+
442
+ args = parser.parse_args(argv)
443
+ return int(args.func(args))
444
+
445
+
446
+ if __name__ == "__main__":
447
+ raise SystemExit(main())