dsh-aris-panel 0.1.0

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 (442) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/README_CN.md +87 -0
  4. package/dsh/checkout.patch.yml +38 -0
  5. package/dsh/client.js +634 -0
  6. package/dsh/cordis.patch.yml +44 -0
  7. package/dsh/index.mjs +76 -0
  8. package/dsh/run-status.mjs +182 -0
  9. package/dsh/scope-limits.mjs +50 -0
  10. package/dsh/workbench.mjs +291 -0
  11. package/mcp-servers/claude-review/README.md +93 -0
  12. package/mcp-servers/claude-review/run_with_claude_aws.sh +49 -0
  13. package/mcp-servers/claude-review/server.py +718 -0
  14. package/mcp-servers/codex-image2/README.md +65 -0
  15. package/mcp-servers/codex-image2/server.py +893 -0
  16. package/mcp-servers/feishu-bridge/requirements.txt +1 -0
  17. package/mcp-servers/feishu-bridge/server.py +240 -0
  18. package/mcp-servers/gemini-review/README.md +171 -0
  19. package/mcp-servers/gemini-review/server.py +1856 -0
  20. package/mcp-servers/llm-chat/requirements.txt +1 -0
  21. package/mcp-servers/llm-chat/server.py +664 -0
  22. package/mcp-servers/manual-review/README.md +133 -0
  23. package/mcp-servers/manual-review/server.py +910 -0
  24. package/mcp-servers/manual-review/ui.html +279 -0
  25. package/mcp-servers/minimax-chat/requirements.txt +1 -0
  26. package/mcp-servers/minimax-chat/server.py +381 -0
  27. package/package.json +51 -0
  28. package/skills/ablation-planner/SKILL.md +123 -0
  29. package/skills/alphaxiv/SKILL.md +196 -0
  30. package/skills/analyze-results/SKILL.md +46 -0
  31. package/skills/arxiv/SKILL.md +248 -0
  32. package/skills/auto-paper-improvement-loop/SKILL.md +651 -0
  33. package/skills/auto-review-loop/SKILL.md +1137 -0
  34. package/skills/auto-review-loop-llm/SKILL.md +259 -0
  35. package/skills/auto-review-loop-minimax/SKILL.md +302 -0
  36. package/skills/citation-audit/SKILL.md +502 -0
  37. package/skills/claims-drafting/SKILL.md +227 -0
  38. package/skills/comm-lit-review/SKILL.md +297 -0
  39. package/skills/deepxiv/SKILL.md +263 -0
  40. package/skills/dse-loop/SKILL.md +296 -0
  41. package/skills/embodiment-description/SKILL.md +129 -0
  42. package/skills/exa-search/SKILL.md +205 -0
  43. package/skills/experiment-audit/SKILL.md +311 -0
  44. package/skills/experiment-bridge/SKILL.md +376 -0
  45. package/skills/experiment-plan/SKILL.md +249 -0
  46. package/skills/experiment-queue/SKILL.md +431 -0
  47. package/skills/experiment-queue/scripts/build_manifest.py +142 -0
  48. package/skills/experiment-queue/scripts/queue_manager.py +433 -0
  49. package/skills/feishu-notify/SKILL.md +156 -0
  50. package/skills/figure-description/SKILL.md +138 -0
  51. package/skills/figure-spec/SKILL.md +262 -0
  52. package/skills/figure-spec/scripts/figure_renderer.py +799 -0
  53. package/skills/formula-derivation/SKILL.md +280 -0
  54. package/skills/gemini-search/SKILL.md +231 -0
  55. package/skills/grant-proposal/SKILL.md +698 -0
  56. package/skills/idea-creator/SKILL.md +542 -0
  57. package/skills/idea-discovery/SKILL.md +521 -0
  58. package/skills/idea-discovery-robot/SKILL.md +363 -0
  59. package/skills/integrity-forensics/SKILL.md +284 -0
  60. package/skills/interview-cheatsheet/SKILL.md +245 -0
  61. package/skills/invention-structuring/SKILL.md +188 -0
  62. package/skills/jurisdiction-format/SKILL.md +192 -0
  63. package/skills/kill-argument/SKILL.md +437 -0
  64. package/skills/mermaid-diagram/SKILL.md +419 -0
  65. package/skills/meta-apply/SKILL.md +141 -0
  66. package/skills/meta-optimize/SKILL.md +437 -0
  67. package/skills/monitor-experiment/SKILL.md +140 -0
  68. package/skills/novelty-check/SKILL.md +101 -0
  69. package/skills/openalex/SKILL.md +237 -0
  70. package/skills/overleaf-sync/SKILL.md +220 -0
  71. package/skills/paper-claim-audit/SKILL.md +348 -0
  72. package/skills/paper-compile/SKILL.md +266 -0
  73. package/skills/paper-figure/SKILL.md +312 -0
  74. package/skills/paper-illustration/SKILL.md +736 -0
  75. package/skills/paper-illustration-image2/SKILL.md +391 -0
  76. package/skills/paper-illustration-image2/scripts/paper_illustration_image2.py +255 -0
  77. package/skills/paper-plan/SKILL.md +386 -0
  78. package/skills/paper-poster/SKILL.md +19 -0
  79. package/skills/paper-poster-html/DESIGN_FINAL.md +176 -0
  80. package/skills/paper-poster-html/IMPLEMENTATION_CONVENTIONS.md +161 -0
  81. package/skills/paper-poster-html/LICENSES/posterly-MIT.txt +21 -0
  82. package/skills/paper-poster-html/NOTICE.md +57 -0
  83. package/skills/paper-poster-html/SKILL.md +323 -0
  84. package/skills/paper-poster-html/scripts/_posterly/__init__.py +0 -0
  85. package/skills/paper-poster-html/scripts/_posterly/canvas.py +200 -0
  86. package/skills/paper-poster-html/scripts/_posterly/measure.py +588 -0
  87. package/skills/paper-poster-html/scripts/_posterly/polish.py +498 -0
  88. package/skills/paper-poster-html/scripts/_posterly/preflight.py +489 -0
  89. package/skills/paper-poster-html/scripts/_posterly/render.py +215 -0
  90. package/skills/paper-poster-html/scripts/_posterly/textutil.py +16 -0
  91. package/skills/paper-poster-html/scripts/_posterly/verify_final.py +171 -0
  92. package/skills/paper-poster-html/scripts/asset_check.py +897 -0
  93. package/skills/paper-poster-html/scripts/extract_pdf_figures.py +666 -0
  94. package/skills/paper-poster-html/scripts/poster_check.py +251 -0
  95. package/skills/paper-poster-html/scripts/preprocess_figures.py +238 -0
  96. package/skills/paper-poster-html/scripts/render_preview.py +217 -0
  97. package/skills/paper-poster-html/scripts/run_gates.py +556 -0
  98. package/skills/paper-poster-html/scripts/style_check.py +1324 -0
  99. package/skills/paper-poster-html/templates/COMPONENTS.md +462 -0
  100. package/skills/paper-poster-html/templates/README.md +170 -0
  101. package/skills/paper-poster-html/templates/landscape_4col.html +1032 -0
  102. package/skills/paper-poster-html/templates/landscape_hero.html +1046 -0
  103. package/skills/paper-poster-html/templates/portrait_2col.html +947 -0
  104. package/skills/paper-poster-html/templates/tokens/acl.json +9 -0
  105. package/skills/paper-poster-html/templates/tokens/cvpr.json +9 -0
  106. package/skills/paper-poster-html/templates/tokens/generic.json +9 -0
  107. package/skills/paper-poster-html/templates/tokens/iclr.json +9 -0
  108. package/skills/paper-poster-html/templates/tokens/icml.json +9 -0
  109. package/skills/paper-poster-html/templates/tokens/neurips.json +9 -0
  110. package/skills/paper-slides/SKILL.md +635 -0
  111. package/skills/paper-talk/SKILL.md +381 -0
  112. package/skills/paper-write/SKILL.md +604 -0
  113. package/skills/paper-write/templates/IEEEtran.bst +2409 -0
  114. package/skills/paper-write/templates/IEEEtran.cls +6347 -0
  115. package/skills/paper-write/templates/iclr2026.tex +84 -0
  116. package/skills/paper-write/templates/icml2025.tex +87 -0
  117. package/skills/paper-write/templates/ieee_conference.tex +89 -0
  118. package/skills/paper-write/templates/ieee_journal.tex +93 -0
  119. package/skills/paper-write/templates/math_commands.tex +48 -0
  120. package/skills/paper-write/templates/neurips2025.tex +80 -0
  121. package/skills/paper-writing/SKILL.md +916 -0
  122. package/skills/patent-novelty-check/SKILL.md +153 -0
  123. package/skills/patent-pipeline/SKILL.md +344 -0
  124. package/skills/patent-review/SKILL.md +203 -0
  125. package/skills/pixel-art/SKILL.md +137 -0
  126. package/skills/prior-art-search/SKILL.md +146 -0
  127. package/skills/proof-checker/SKILL.md +866 -0
  128. package/skills/proof-orchestrator/NOTICE.md +24 -0
  129. package/skills/proof-orchestrator/SKILL.md +254 -0
  130. package/skills/proof-orchestrator/references/audit-output-contract.md +126 -0
  131. package/skills/proof-orchestrator/references/deepseek-routing.md +74 -0
  132. package/skills/proof-orchestrator/references/dispatch-prompts.md +227 -0
  133. package/skills/proof-orchestrator/references/notation-audit.md +135 -0
  134. package/skills/proof-orchestrator/references/proof-audit-rubric.md +70 -0
  135. package/skills/proof-orchestrator/references/stress-tests.md +38 -0
  136. package/skills/proof-writer/SKILL.md +223 -0
  137. package/skills/qzcli/SKILL.md +324 -0
  138. package/skills/rebuttal/SKILL.md +376 -0
  139. package/skills/render-html/SKILL.md +316 -0
  140. package/skills/render-html/scripts/render_html.py +1006 -0
  141. package/skills/render-html/scripts/templates/academic.html +703 -0
  142. package/skills/render-html/scripts/templates/dashboard.html +333 -0
  143. package/skills/research-lit/SKILL.md +756 -0
  144. package/skills/research-pipeline/SKILL.md +384 -0
  145. package/skills/research-refine/SKILL.md +770 -0
  146. package/skills/research-refine-pipeline/SKILL.md +186 -0
  147. package/skills/research-review/SKILL.md +198 -0
  148. package/skills/research-wiki/SKILL.md +461 -0
  149. package/skills/resubmit-pipeline/SKILL.md +447 -0
  150. package/skills/result-to-claim/SKILL.md +311 -0
  151. package/skills/run-experiment/SKILL.md +313 -0
  152. package/skills/semantic-scholar/SKILL.md +236 -0
  153. package/skills/serverless-modal/SKILL.md +335 -0
  154. package/skills/shared-references/acceptance-gate.md +324 -0
  155. package/skills/shared-references/assurance-contract.md +248 -0
  156. package/skills/shared-references/capture-antipatterns.md +78 -0
  157. package/skills/shared-references/citation-discipline.md +583 -0
  158. package/skills/shared-references/compute-env-contract.md +163 -0
  159. package/skills/shared-references/effort-contract.md +183 -0
  160. package/skills/shared-references/evidence-precheck.md +65 -0
  161. package/skills/shared-references/experiment-integrity.md +49 -0
  162. package/skills/shared-references/external-cadence.md +326 -0
  163. package/skills/shared-references/fan-out-pattern.md +366 -0
  164. package/skills/shared-references/injection-hygiene.md +127 -0
  165. package/skills/shared-references/integration-contract.md +461 -0
  166. package/skills/shared-references/output-composition.md +93 -0
  167. package/skills/shared-references/output-language.md +45 -0
  168. package/skills/shared-references/output-manifest.md +49 -0
  169. package/skills/shared-references/output-versioning.md +111 -0
  170. package/skills/shared-references/patent-format-cn.md +199 -0
  171. package/skills/shared-references/patent-format-ep.md +173 -0
  172. package/skills/shared-references/patent-format-us.md +161 -0
  173. package/skills/shared-references/patent-writing-principles.md +197 -0
  174. package/skills/shared-references/prior-art-databases.md +141 -0
  175. package/skills/shared-references/resumable-runs.md +109 -0
  176. package/skills/shared-references/review-scope-limits.md +81 -0
  177. package/skills/shared-references/review-tracing.md +391 -0
  178. package/skills/shared-references/reviewer-independence.md +79 -0
  179. package/skills/shared-references/reviewer-routing.md +852 -0
  180. package/skills/shared-references/skill-governance.md +104 -0
  181. package/skills/shared-references/taste-calibration.md +85 -0
  182. package/skills/shared-references/venue-checklists.md +114 -0
  183. package/skills/shared-references/wiki-helper-resolution.md +134 -0
  184. package/skills/shared-references/writing-principles.md +525 -0
  185. package/skills/skills-codex/README.md +102 -0
  186. package/skills/skills-codex/README_CN.md +100 -0
  187. package/skills/skills-codex/ablation-planner/SKILL.md +126 -0
  188. package/skills/skills-codex/alphaxiv/SKILL.md +186 -0
  189. package/skills/skills-codex/analyze-results/SKILL.md +45 -0
  190. package/skills/skills-codex/arxiv/SKILL.md +210 -0
  191. package/skills/skills-codex/auto-paper-improvement-loop/SKILL.md +574 -0
  192. package/skills/skills-codex/auto-review-loop/SKILL.md +500 -0
  193. package/skills/skills-codex/auto-review-loop-llm/SKILL.md +247 -0
  194. package/skills/skills-codex/auto-review-loop-minimax/SKILL.md +290 -0
  195. package/skills/skills-codex/citation-audit/SKILL.md +504 -0
  196. package/skills/skills-codex/claims-drafting/SKILL.md +239 -0
  197. package/skills/skills-codex/comm-lit-review/SKILL.md +299 -0
  198. package/skills/skills-codex/comm-lit-review/references/domain-taxonomy.md +57 -0
  199. package/skills/skills-codex/comm-lit-review/references/output-template.md +37 -0
  200. package/skills/skills-codex/comm-lit-review/references/source-policy.md +99 -0
  201. package/skills/skills-codex/comm-lit-review/references/venue-tiering.md +112 -0
  202. package/skills/skills-codex/deepxiv/SKILL.md +142 -0
  203. package/skills/skills-codex/dse-loop/SKILL.md +285 -0
  204. package/skills/skills-codex/embodiment-description/SKILL.md +129 -0
  205. package/skills/skills-codex/exa-search/SKILL.md +192 -0
  206. package/skills/skills-codex/experiment-audit/SKILL.md +286 -0
  207. package/skills/skills-codex/experiment-bridge/SKILL.md +356 -0
  208. package/skills/skills-codex/experiment-plan/SKILL.md +249 -0
  209. package/skills/skills-codex/experiment-queue/SKILL.md +401 -0
  210. package/skills/skills-codex/feishu-notify/SKILL.md +155 -0
  211. package/skills/skills-codex/figure-description/SKILL.md +138 -0
  212. package/skills/skills-codex/figure-spec/SKILL.md +252 -0
  213. package/skills/skills-codex/formula-derivation/SKILL.md +280 -0
  214. package/skills/skills-codex/gemini-search/SKILL.md +205 -0
  215. package/skills/skills-codex/grant-proposal/SKILL.md +626 -0
  216. package/skills/skills-codex/idea-creator/SKILL.md +405 -0
  217. package/skills/skills-codex/idea-discovery/SKILL.md +475 -0
  218. package/skills/skills-codex/idea-discovery-robot/SKILL.md +362 -0
  219. package/skills/skills-codex/integrity-forensics/SKILL.md +106 -0
  220. package/skills/skills-codex/interview-cheatsheet/SKILL.md +245 -0
  221. package/skills/skills-codex/invention-structuring/SKILL.md +188 -0
  222. package/skills/skills-codex/jurisdiction-format/SKILL.md +192 -0
  223. package/skills/skills-codex/kill-argument/SKILL.md +403 -0
  224. package/skills/skills-codex/mermaid-diagram/SKILL.md +379 -0
  225. package/skills/skills-codex/meta-apply/SKILL.md +154 -0
  226. package/skills/skills-codex/meta-optimize/SKILL.md +348 -0
  227. package/skills/skills-codex/monitor-experiment/SKILL.md +98 -0
  228. package/skills/skills-codex/novelty-check/SKILL.md +89 -0
  229. package/skills/skills-codex/openalex/SKILL.md +228 -0
  230. package/skills/skills-codex/overleaf-sync/SKILL.md +220 -0
  231. package/skills/skills-codex/paper-claim-audit/SKILL.md +350 -0
  232. package/skills/skills-codex/paper-compile/SKILL.md +253 -0
  233. package/skills/skills-codex/paper-figure/SKILL.md +311 -0
  234. package/skills/skills-codex/paper-illustration/SKILL.md +690 -0
  235. package/skills/skills-codex/paper-illustration-image2/SKILL.md +383 -0
  236. package/skills/skills-codex/paper-illustration-image2/scripts/paper_illustration_image2.py +255 -0
  237. package/skills/skills-codex/paper-plan/SKILL.md +278 -0
  238. package/skills/skills-codex/paper-poster/SKILL.md +19 -0
  239. package/skills/skills-codex/paper-poster-html/SKILL.md +377 -0
  240. package/skills/skills-codex/paper-slides/SKILL.md +571 -0
  241. package/skills/skills-codex/paper-talk/SKILL.md +381 -0
  242. package/skills/skills-codex/paper-write/SKILL.md +411 -0
  243. package/skills/skills-codex/paper-write/templates/IEEEtran.bst +2409 -0
  244. package/skills/skills-codex/paper-write/templates/IEEEtran.cls +6347 -0
  245. package/skills/skills-codex/paper-write/templates/aaai2026.bst +1493 -0
  246. package/skills/skills-codex/paper-write/templates/aaai2026.sty +315 -0
  247. package/skills/skills-codex/paper-write/templates/aaai2026.tex +952 -0
  248. package/skills/skills-codex/paper-write/templates/acl.sty +312 -0
  249. package/skills/skills-codex/paper-write/templates/acl2026.tex +377 -0
  250. package/skills/skills-codex/paper-write/templates/acl_natbib.bst +1940 -0
  251. package/skills/skills-codex/paper-write/templates/acm.bst +3081 -0
  252. package/skills/skills-codex/paper-write/templates/acm_mm2026.tex +204 -0
  253. package/skills/skills-codex/paper-write/templates/acmart.cls +3520 -0
  254. package/skills/skills-codex/paper-write/templates/cvpr.bst +1448 -0
  255. package/skills/skills-codex/paper-write/templates/cvpr.sty +508 -0
  256. package/skills/skills-codex/paper-write/templates/cvpr2026.tex +63 -0
  257. package/skills/skills-codex/paper-write/templates/iclr2026.tex +84 -0
  258. package/skills/skills-codex/paper-write/templates/iclr2026_conference.bst +1440 -0
  259. package/skills/skills-codex/paper-write/templates/iclr2026_conference.sty +246 -0
  260. package/skills/skills-codex/paper-write/templates/icml2026.sty +767 -0
  261. package/skills/skills-codex/paper-write/templates/icml2026.tex +662 -0
  262. package/skills/skills-codex/paper-write/templates/ieee_conference.tex +89 -0
  263. package/skills/skills-codex/paper-write/templates/ieee_journal.tex +93 -0
  264. package/skills/skills-codex/paper-write/templates/math_commands.tex +48 -0
  265. package/skills/skills-codex/paper-write/templates/neurips2026.tex +493 -0
  266. package/skills/skills-codex/paper-write/templates/neurips_2026.sty +437 -0
  267. package/skills/skills-codex/paper-writing/SKILL.md +731 -0
  268. package/skills/skills-codex/patent-novelty-check/SKILL.md +153 -0
  269. package/skills/skills-codex/patent-pipeline/SKILL.md +344 -0
  270. package/skills/skills-codex/patent-review/SKILL.md +202 -0
  271. package/skills/skills-codex/pixel-art/SKILL.md +139 -0
  272. package/skills/skills-codex/prior-art-search/SKILL.md +146 -0
  273. package/skills/skills-codex/proof-checker/SKILL.md +554 -0
  274. package/skills/skills-codex/proof-orchestrator/SKILL.md +260 -0
  275. package/skills/skills-codex/proof-orchestrator/references/audit-output-contract.md +126 -0
  276. package/skills/skills-codex/proof-orchestrator/references/deepseek-routing.md +76 -0
  277. package/skills/skills-codex/proof-orchestrator/references/dispatch-prompts.md +227 -0
  278. package/skills/skills-codex/proof-orchestrator/references/notation-audit.md +135 -0
  279. package/skills/skills-codex/proof-orchestrator/references/proof-audit-rubric.md +70 -0
  280. package/skills/skills-codex/proof-orchestrator/references/stress-tests.md +38 -0
  281. package/skills/skills-codex/proof-writer/SKILL.md +222 -0
  282. package/skills/skills-codex/qzcli/SKILL.md +324 -0
  283. package/skills/skills-codex/rebuttal/SKILL.md +305 -0
  284. package/skills/skills-codex/render-html/SKILL.md +305 -0
  285. package/skills/skills-codex/render-html/scripts/__pycache__/render_html.cpython-314.pyc +0 -0
  286. package/skills/skills-codex/render-html/scripts/render_html.py +909 -0
  287. package/skills/skills-codex/render-html/scripts/templates/academic.html +342 -0
  288. package/skills/skills-codex/render-html/scripts/templates/dashboard.html +333 -0
  289. package/skills/skills-codex/research-lit/SKILL.md +464 -0
  290. package/skills/skills-codex/research-pipeline/SKILL.md +340 -0
  291. package/skills/skills-codex/research-refine/SKILL.md +721 -0
  292. package/skills/skills-codex/research-refine-pipeline/SKILL.md +186 -0
  293. package/skills/skills-codex/research-review/SKILL.md +135 -0
  294. package/skills/skills-codex/research-wiki/SKILL.md +421 -0
  295. package/skills/skills-codex/resubmit-pipeline/SKILL.md +444 -0
  296. package/skills/skills-codex/result-to-claim/SKILL.md +246 -0
  297. package/skills/skills-codex/run-experiment/SKILL.md +236 -0
  298. package/skills/skills-codex/semantic-scholar/SKILL.md +219 -0
  299. package/skills/skills-codex/serverless-modal/SKILL.md +335 -0
  300. package/skills/skills-codex/shared-references/acceptance-gate.md +336 -0
  301. package/skills/skills-codex/shared-references/assurance-contract.md +139 -0
  302. package/skills/skills-codex/shared-references/capture-antipatterns.md +84 -0
  303. package/skills/skills-codex/shared-references/citation-discipline.md +452 -0
  304. package/skills/skills-codex/shared-references/compute-env-contract.md +163 -0
  305. package/skills/skills-codex/shared-references/effort-contract.md +143 -0
  306. package/skills/skills-codex/shared-references/evidence-precheck.md +73 -0
  307. package/skills/skills-codex/shared-references/experiment-integrity.md +49 -0
  308. package/skills/skills-codex/shared-references/external-cadence.md +334 -0
  309. package/skills/skills-codex/shared-references/fan-out-pattern.md +375 -0
  310. package/skills/skills-codex/shared-references/injection-hygiene.md +132 -0
  311. package/skills/skills-codex/shared-references/integration-contract.md +372 -0
  312. package/skills/skills-codex/shared-references/output-composition.md +98 -0
  313. package/skills/skills-codex/shared-references/output-language.md +45 -0
  314. package/skills/skills-codex/shared-references/output-manifest.md +40 -0
  315. package/skills/skills-codex/shared-references/output-versioning.md +111 -0
  316. package/skills/skills-codex/shared-references/patent-format-cn.md +199 -0
  317. package/skills/skills-codex/shared-references/patent-format-ep.md +173 -0
  318. package/skills/skills-codex/shared-references/patent-format-us.md +161 -0
  319. package/skills/skills-codex/shared-references/patent-writing-principles.md +197 -0
  320. package/skills/skills-codex/shared-references/prior-art-databases.md +141 -0
  321. package/skills/skills-codex/shared-references/resumable-runs.md +125 -0
  322. package/skills/skills-codex/shared-references/review-scope-limits.md +81 -0
  323. package/skills/skills-codex/shared-references/review-tracing.md +144 -0
  324. package/skills/skills-codex/shared-references/reviewer-independence.md +66 -0
  325. package/skills/skills-codex/shared-references/reviewer-routing.md +128 -0
  326. package/skills/skills-codex/shared-references/skill-governance.md +119 -0
  327. package/skills/skills-codex/shared-references/taste-calibration.md +90 -0
  328. package/skills/skills-codex/shared-references/venue-checklists.md +73 -0
  329. package/skills/skills-codex/shared-references/wiki-helper-resolution.md +69 -0
  330. package/skills/skills-codex/shared-references/writing-principles.md +525 -0
  331. package/skills/skills-codex/slides-polish/SKILL.md +563 -0
  332. package/skills/skills-codex/specification-writing/SKILL.md +211 -0
  333. package/skills/skills-codex/system-profile/SKILL.md +103 -0
  334. package/skills/skills-codex/training-check/SKILL.md +83 -0
  335. package/skills/skills-codex/vast-gpu/SKILL.md +394 -0
  336. package/skills/skills-codex/web-debug-search/SKILL.md +334 -0
  337. package/skills/skills-codex/wiki-enrich/SKILL.md +255 -0
  338. package/skills/skills-codex/writing-systems-papers/SKILL.md +184 -0
  339. package/skills/skills-codex-claude-review/README.md +79 -0
  340. package/skills/skills-codex-claude-review/README_CN.md +78 -0
  341. package/skills/skills-codex-claude-review/auto-paper-improvement-loop/SKILL.md +581 -0
  342. package/skills/skills-codex-claude-review/auto-review-loop/SKILL.md +510 -0
  343. package/skills/skills-codex-claude-review/novelty-check/SKILL.md +102 -0
  344. package/skills/skills-codex-claude-review/paper-figure/SKILL.md +319 -0
  345. package/skills/skills-codex-claude-review/paper-plan/SKILL.md +287 -0
  346. package/skills/skills-codex-claude-review/paper-write/SKILL.md +420 -0
  347. package/skills/skills-codex-claude-review/research-refine/SKILL.md +732 -0
  348. package/skills/skills-codex-claude-review/research-review/SKILL.md +149 -0
  349. package/skills/skills-codex-gemini-review/README.md +176 -0
  350. package/skills/skills-codex-gemini-review/README_CN.md +175 -0
  351. package/skills/skills-codex-gemini-review/auto-paper-improvement-loop/SKILL.md +331 -0
  352. package/skills/skills-codex-gemini-review/auto-review-loop/SKILL.md +304 -0
  353. package/skills/skills-codex-gemini-review/grant-proposal/SKILL.md +630 -0
  354. package/skills/skills-codex-gemini-review/idea-creator/SKILL.md +263 -0
  355. package/skills/skills-codex-gemini-review/idea-discovery/SKILL.md +275 -0
  356. package/skills/skills-codex-gemini-review/idea-discovery-robot/SKILL.md +365 -0
  357. package/skills/skills-codex-gemini-review/novelty-check/SKILL.md +92 -0
  358. package/skills/skills-codex-gemini-review/paper-figure/SKILL.md +289 -0
  359. package/skills/skills-codex-gemini-review/paper-plan/SKILL.md +265 -0
  360. package/skills/skills-codex-gemini-review/paper-poster-html/SKILL.md +102 -0
  361. package/skills/skills-codex-gemini-review/paper-slides/SKILL.md +582 -0
  362. package/skills/skills-codex-gemini-review/paper-write/SKILL.md +346 -0
  363. package/skills/skills-codex-gemini-review/paper-writing/SKILL.md +312 -0
  364. package/skills/skills-codex-gemini-review/research-refine/SKILL.md +674 -0
  365. package/skills/skills-codex-gemini-review/research-review/SKILL.md +112 -0
  366. package/skills/slides-polish/SKILL.md +565 -0
  367. package/skills/specification-writing/SKILL.md +211 -0
  368. package/skills/system-profile/SKILL.md +103 -0
  369. package/skills/training-check/SKILL.md +132 -0
  370. package/skills/vast-gpu/SKILL.md +394 -0
  371. package/skills/web-debug-search/SKILL.md +334 -0
  372. package/skills/wiki-enrich/SKILL.md +257 -0
  373. package/skills/writing-systems-papers/SKILL.md +184 -0
  374. package/templates/CLAUDE_MD_TEMPLATE.md +29 -0
  375. package/templates/EXPERIMENT_LOG_TEMPLATE.md +47 -0
  376. package/templates/EXPERIMENT_PLAN_TEMPLATE.md +51 -0
  377. package/templates/EXPERIMENT_PLAN_TEMPLATE_CN.md +53 -0
  378. package/templates/FINDINGS_TEMPLATE.md +52 -0
  379. package/templates/IDEA_CANDIDATES_TEMPLATE.md +47 -0
  380. package/templates/IDEA_CANDIDATES_TEMPLATE_CN.md +47 -0
  381. package/templates/INVENTION_BRIEF_TEMPLATE.md +87 -0
  382. package/templates/MANIFEST_TEMPLATE.md +7 -0
  383. package/templates/NARRATIVE_REPORT_TEMPLATE.md +49 -0
  384. package/templates/PAPER_PLAN_TEMPLATE.md +47 -0
  385. package/templates/PATENT_CLAIMS_TEMPLATE.md +78 -0
  386. package/templates/PATENT_SPECIFICATION_TEMPLATE.md +68 -0
  387. package/templates/README.md +57 -0
  388. package/templates/RESEARCH_BRIEF_TEMPLATE.md +35 -0
  389. package/templates/RESEARCH_BRIEF_TEMPLATE_CN.md +41 -0
  390. package/templates/RESEARCH_CONTRACT_TEMPLATE.md +60 -0
  391. package/templates/claude-hooks/corpus_write_guard.json +16 -0
  392. package/templates/claude-hooks/corpus_write_guard.py +85 -0
  393. package/templates/claude-hooks/meta_logging.json +74 -0
  394. package/templates/gitignore-trace.txt +3 -0
  395. package/tools/__pycache__/check_skills_inventory.cpython-314.pyc +0 -0
  396. package/tools/arxiv_fetch.py +311 -0
  397. package/tools/capture_filter.py +126 -0
  398. package/tools/check_skills_inventory.py +273 -0
  399. package/tools/convert_skills_to_llm_chat.py +282 -0
  400. package/tools/copilot_native_evidence.py +818 -0
  401. package/tools/deepxiv_fetch.py +213 -0
  402. package/tools/evidence_check.py +212 -0
  403. package/tools/exa_search.py +425 -0
  404. package/tools/experiment_queue/README.md +118 -0
  405. package/tools/experiment_queue/build_manifest.py +44 -0
  406. package/tools/experiment_queue/queue_manager.py +44 -0
  407. package/tools/extract_paper_style.py +560 -0
  408. package/tools/figure_renderer.py +69 -0
  409. package/tools/forensics_gate.py +669 -0
  410. package/tools/generate_codex_claude_review_overrides.py +299 -0
  411. package/tools/idea_discovery_gate.py +256 -0
  412. package/tools/install_aris.ps1 +1372 -0
  413. package/tools/install_aris.sh +1370 -0
  414. package/tools/install_aris_codex.sh +1023 -0
  415. package/tools/install_aris_copilot.sh +1052 -0
  416. package/tools/iteration_log.py +143 -0
  417. package/tools/lint_skills_helpers.sh +84 -0
  418. package/tools/meta_opt/check_ready.sh +80 -0
  419. package/tools/meta_opt/log_event.sh +91 -0
  420. package/tools/meta_opt/trigger_eval.py +280 -0
  421. package/tools/meta_opt/trigger_evals.sample.json +28 -0
  422. package/tools/openalex_fetch.py +326 -0
  423. package/tools/overleaf_audit.sh +104 -0
  424. package/tools/overleaf_setup.sh +150 -0
  425. package/tools/paper_illustration_image2.py +62 -0
  426. package/tools/provenance.py +294 -0
  427. package/tools/research_wiki.py +1720 -0
  428. package/tools/review_gate.py +502 -0
  429. package/tools/run_state.py +399 -0
  430. package/tools/save_trace.sh +477 -0
  431. package/tools/semantic_scholar_fetch.py +438 -0
  432. package/tools/skill-groups.tsv +116 -0
  433. package/tools/skill_picker.py +238 -0
  434. package/tools/smart_update.ps1 +521 -0
  435. package/tools/smart_update.sh +591 -0
  436. package/tools/smart_update_codex.sh +419 -0
  437. package/tools/smart_update_copilot.sh +605 -0
  438. package/tools/threat_scan.py +222 -0
  439. package/tools/verify_paper_audits.sh +487 -0
  440. package/tools/verify_papers.py +613 -0
  441. package/tools/verify_wiki_coverage.sh +176 -0
  442. package/tools/watchdog.py +485 -0
@@ -0,0 +1,910 @@
1
+ #!/usr/bin/env python3
2
+ """Manual Review MCP Server for ARIS.
3
+
4
+ A human-in-the-loop reviewer bridge: when the pipeline needs cross-model
5
+ review, this server opens a browser page (or writes a file on headless Linux)
6
+ where the user can copy the prompt to a different-family model and paste the
7
+ response back.
8
+
9
+ Zero API cost. The reviewer must be a model this server can classify by family
10
+ (OpenAI, Anthropic, Google, DeepSeek, Moonshot/Kimi, Qwen) — an unclassifiable
11
+ name cannot be shown to differ from the executor's, so it cannot acquit.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import http.server
17
+ import json
18
+ import os
19
+ import re
20
+ import socketserver
21
+ import sys
22
+ import threading
23
+ import time
24
+ import uuid
25
+ import webbrowser
26
+ from datetime import datetime, timezone
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ # --- stdio setup (deferred to main() to allow safe import for testing) ---
31
+ _stdio_initialized = False
32
+
33
+
34
+ def _init_stdio():
35
+ global _stdio_initialized
36
+ if _stdio_initialized:
37
+ return
38
+ sys.stdout = os.fdopen(sys.stdout.fileno(), "wb", buffering=0)
39
+ sys.stdin = os.fdopen(sys.stdin.fileno(), "rb", buffering=0)
40
+ _stdio_initialized = True
41
+
42
+ # --- Configuration ---
43
+ SERVER_NAME = os.environ.get("MANUAL_REVIEW_SERVER_NAME", "manual-review")
44
+ DEFAULT_TIMEOUT_SEC = int(os.environ.get("MANUAL_REVIEW_TIMEOUT_SEC", "86400"))
45
+ MODE = os.environ.get("MANUAL_REVIEW_MODE", "browser") # "browser" or "file"
46
+ AUTO_OPEN = os.environ.get("MANUAL_REVIEW_AUTO_OPEN", "true").lower() in {"1", "true", "yes"}
47
+ PENDING_DIR = Path(os.environ.get("MANUAL_REVIEW_PENDING_DIR", ".aris/pending_review"))
48
+ DEBUG_LOG_RAW = os.environ.get("MANUAL_REVIEW_DEBUG_LOG", "").strip()
49
+ DEBUG_LOG = Path(DEBUG_LOG_RAW).expanduser() if DEBUG_LOG_RAW else None
50
+ DEFAULT_PORT = int(os.environ.get("MANUAL_REVIEW_PORT", "17900"))
51
+ MAX_PORT_ATTEMPTS = 10
52
+
53
+ # File-mode stability: require content unchanged across two reads with this gap
54
+ FILE_STABLE_INTERVAL_SEC = 3
55
+ FILE_POLL_INTERVAL_SEC = 2
56
+
57
+ # --- MCP Protocol ---
58
+ _use_ndjson = False
59
+
60
+ # --- Thread storage (in-memory, lives as long as the MCP server process) ---
61
+ _threads: dict[str, list[dict[str, str]]] = {}
62
+
63
+ # --- UI HTML (loaded once) ---
64
+ _UI_HTML: str | None = None
65
+
66
+
67
+ def debug_log(message: str) -> None:
68
+ if DEBUG_LOG is None:
69
+ return
70
+ try:
71
+ DEBUG_LOG.parent.mkdir(parents=True, exist_ok=True)
72
+ with DEBUG_LOG.open("a", encoding="utf-8") as fh:
73
+ fh.write(f"[{utc_now()}] {message}\n")
74
+ except OSError:
75
+ pass
76
+
77
+
78
+ def utc_now() -> str:
79
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
80
+
81
+
82
+ def model_family(model: str) -> str:
83
+ """Derive a known provider family from a model identity, failing closed."""
84
+ name = (model or "").strip().lower()
85
+ families: set[str] = set()
86
+ if re.search(r"(^|[^a-z0-9])(gpt|chatgpt|codex|oracle|o1|o3|o4)([^a-z0-9]|$)", name):
87
+ families.add("openai")
88
+ if re.search(r"(^|[^a-z0-9])(claude|sonnet|opus|haiku|anthropic)([^a-z0-9]|$)", name):
89
+ families.add("anthropic")
90
+ if re.search(r"(^|[^a-z0-9])(gemini|google)([^a-z0-9]|$)", name):
91
+ families.add("google")
92
+ # The models MANUAL_REVIEW_GUIDE.md actually recommends to non-GPT users.
93
+ # [0-9.]* so versioned names (qwen3-max, qwen2.5-72b) still match.
94
+ if re.search(r"(^|[^a-z0-9])(deepseek)[0-9.]*([^a-z0-9]|$)", name):
95
+ families.add("deepseek")
96
+ if re.search(r"(^|[^a-z0-9])(kimi|moonshot)[0-9.]*([^a-z0-9]|$)", name):
97
+ families.add("moonshot")
98
+ if re.search(r"(^|[^a-z0-9])(qwen|tongyi)[0-9.]*([^a-z0-9]|$)", name):
99
+ families.add("qwen")
100
+ return next(iter(families)) if len(families) == 1 else "unknown"
101
+
102
+
103
+ def reviewer_model_from_response(response: str) -> str | None:
104
+ """Read the mandatory first-line identity without trusting prose later on."""
105
+ first_line = response.splitlines()[0].strip() if response.splitlines() else ""
106
+ match = re.fullmatch(r"Reviewer-Model:\s*(\S(?:.*\S)?)", first_line, re.IGNORECASE)
107
+ return match.group(1).strip() if match else None
108
+
109
+
110
+ def validate_reviewer_identity(response: str, config: dict) -> str | None:
111
+ """Return an error when strict cross-family manual review cannot be proven."""
112
+ if not config.get("require_reviewer_model"):
113
+ return None
114
+ executor_model = str(config.get("executor_model") or "").strip()
115
+ executor_family = model_family(executor_model)
116
+ if executor_family == "unknown":
117
+ return "Cannot verify manual review: executor_model is missing or has an unknown family"
118
+ reviewer_model = reviewer_model_from_response(response)
119
+ if not reviewer_model:
120
+ return "Manual response must begin with: Reviewer-Model: <exact-model-id>"
121
+ reviewer_family = model_family(reviewer_model)
122
+ if reviewer_family == "unknown":
123
+ return f"Cannot verify manual reviewer model family: {reviewer_model}"
124
+ if reviewer_family == executor_family:
125
+ return (
126
+ "Manual reviewer must use a different model family: "
127
+ f"executor={executor_family}, reviewer={reviewer_family}"
128
+ )
129
+ return None
130
+
131
+
132
+ def load_ui_html() -> str:
133
+ global _UI_HTML
134
+ if _UI_HTML is None:
135
+ ui_path = Path(__file__).parent / "ui.html"
136
+ _UI_HTML = ui_path.read_text(encoding="utf-8")
137
+ return _UI_HTML
138
+
139
+
140
+ # --- MCP stdio transport ---
141
+
142
+ _send_lock = threading.Lock()
143
+
144
+
145
+ def send_response(response: dict[str, Any]) -> None:
146
+ global _use_ndjson
147
+ payload = json.dumps(response, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
148
+ debug_log(f"SEND {payload.decode('utf-8', errors='replace')[:200]}")
149
+ with _send_lock:
150
+ if _use_ndjson:
151
+ sys.stdout.write(payload + b"\n")
152
+ else:
153
+ header = f"Content-Length: {len(payload)}\r\n\r\n".encode("utf-8")
154
+ sys.stdout.write(header + payload)
155
+ sys.stdout.flush()
156
+
157
+
158
+ def read_message() -> dict[str, Any] | None:
159
+ global _use_ndjson
160
+ line = sys.stdin.readline()
161
+ if not line:
162
+ return None
163
+ line_text = line.decode("utf-8").rstrip("\r\n")
164
+ if line_text.lower().startswith("content-length:"):
165
+ try:
166
+ content_length = int(line_text.split(":", 1)[1].strip())
167
+ except ValueError:
168
+ return None
169
+ while True:
170
+ header_line = sys.stdin.readline()
171
+ if not header_line:
172
+ return None
173
+ if header_line in {b"\r\n", b"\n"}:
174
+ break
175
+ body = sys.stdin.read(content_length)
176
+ try:
177
+ return json.loads(body.decode("utf-8"))
178
+ except json.JSONDecodeError:
179
+ return None
180
+ if line_text.startswith("{") or line_text.startswith("["):
181
+ _use_ndjson = True
182
+ try:
183
+ return json.loads(line_text)
184
+ except json.JSONDecodeError:
185
+ return None
186
+ return None
187
+
188
+
189
+ # --- Thread management ---
190
+
191
+ def create_thread() -> str:
192
+ thread_id = uuid.uuid4().hex[:12]
193
+ _threads[thread_id] = []
194
+ return thread_id
195
+
196
+
197
+ def append_exchange(thread_id: str, role: str, content: str) -> None:
198
+ if thread_id not in _threads:
199
+ _threads[thread_id] = []
200
+ _threads[thread_id].append({"role": role, "content": content})
201
+
202
+
203
+ def get_history(thread_id: str) -> list[dict[str, str]]:
204
+ return _threads.get(thread_id, [])
205
+
206
+
207
+ # --- Pending state file ---
208
+
209
+ def _pending_dir_for(thread_id: str) -> Path:
210
+ """Per-thread pending directory to avoid clobbering in concurrent calls."""
211
+ return PENDING_DIR / thread_id
212
+
213
+
214
+ _pending_state_lock = threading.Lock()
215
+
216
+
217
+ def write_pending_state(url: str | None, thread_id: str, prompt_file: str | None) -> None:
218
+ pdir = _pending_dir_for(thread_id)
219
+ state = {
220
+ "status": "waiting",
221
+ "url": url,
222
+ "prompt_file": prompt_file,
223
+ "response_file": str(pdir / "response.md") if prompt_file else None,
224
+ "thread_id": thread_id,
225
+ "created_at": utc_now(),
226
+ }
227
+ pdir.mkdir(parents=True, exist_ok=True)
228
+ state_path = pdir / "pending_review.json"
229
+ state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
230
+ with _pending_state_lock:
231
+ PENDING_DIR.mkdir(parents=True, exist_ok=True)
232
+ (PENDING_DIR / "pending_review.json").write_text(
233
+ json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8"
234
+ )
235
+
236
+
237
+ def clear_pending_state(thread_id: str | None = None,
238
+ keep_thread_dir: bool = False) -> None:
239
+ """Idempotent, thread-safe pending state cleanup.
240
+
241
+ keep_thread_dir preserves the per-thread directory when the round ended on a
242
+ fixable authoring error: in file mode that directory holds the prompt AND the
243
+ reviewer response the user just pasted, and deleting it would throw away
244
+ their work over a wrong first line. It does not make the call resumable — the
245
+ retry is a new call with a new directory.
246
+ """
247
+ # Clear per-thread dir
248
+ if thread_id and not keep_thread_dir:
249
+ pdir = _pending_dir_for(thread_id)
250
+ if pdir.exists():
251
+ import shutil
252
+ shutil.rmtree(pdir, ignore_errors=True)
253
+ # Clear top-level pointer — tolerate already-deleted
254
+ with _pending_state_lock:
255
+ try:
256
+ (PENDING_DIR / "pending_review.json").unlink()
257
+ except FileNotFoundError:
258
+ pass
259
+
260
+
261
+
262
+ # --- Browser mode: HTTP server ---
263
+
264
+ class _ReviewSession:
265
+ """Holds state for one review interaction."""
266
+ def __init__(self, prompt: str, config: dict, thread_id: str, history: list):
267
+ self.prompt = prompt
268
+ self.config = config
269
+ self.thread_id = thread_id
270
+ self.history = history
271
+ self.response: str | None = None
272
+ self.done = threading.Event()
273
+
274
+
275
+ class _PendingCall:
276
+ """Per-call state for the active blocking review call. Replaces global
277
+ _pending_call_cancelled with per-call cancellation that cannot be
278
+ cleared by a new call."""
279
+ def __init__(self, request_id: Any):
280
+ self.request_id = request_id
281
+ self.thread: threading.Thread | None = None
282
+ self.cancel_event = threading.Event()
283
+ self.suppress_response = False
284
+ self.cancel_reason = "Manual review request was cancelled"
285
+
286
+
287
+ _current_session: _ReviewSession | None = None
288
+ _active_server: socketserver.TCPServer | None = None
289
+ _active_server_lock = threading.Lock()
290
+ _auth_token: str | None = None
291
+ _pending_call: _PendingCall | None = None
292
+
293
+
294
+ def _generate_token() -> str:
295
+ """Generate a one-shot auth token for this review session."""
296
+ return uuid.uuid4().hex
297
+
298
+
299
+ def _check_token(handler) -> bool:
300
+ """Validate auth token from query string or header. Returns True if valid."""
301
+ from urllib.parse import urlparse, parse_qs
302
+ parsed = urlparse(handler.path)
303
+ params = parse_qs(parsed.query)
304
+ token_values = params.get("token", [])
305
+ if token_values and token_values[0] == _auth_token:
306
+ return True
307
+ header_token = handler.headers.get("X-Review-Token", "")
308
+ if header_token == _auth_token:
309
+ return True
310
+ return False
311
+
312
+
313
+ def _check_origin(handler) -> bool:
314
+ """Defense-in-depth: reject browser requests with suspicious cross-site headers.
315
+ Token auth is still required; this is an additional layer."""
316
+ origin = handler.headers.get("Origin", "")
317
+ if origin:
318
+ expected = f"http://127.0.0.1:{handler.server.server_address[1]}"
319
+ if origin != expected:
320
+ return False
321
+ sec_fetch_site = handler.headers.get("Sec-Fetch-Site", "")
322
+ if sec_fetch_site and sec_fetch_site not in {"same-origin", "none"}:
323
+ return False
324
+ return True
325
+
326
+
327
+ class _ReviewHandler(http.server.BaseHTTPRequestHandler):
328
+ def log_message(self, format, *args):
329
+ debug_log(f"HTTP {format % args}")
330
+
331
+ def _get_clean_path(self) -> str:
332
+ from urllib.parse import urlparse
333
+ return urlparse(self.path).path
334
+
335
+ def do_GET(self):
336
+ path = self._get_clean_path()
337
+ if path == "/":
338
+ if not _check_token(self):
339
+ self.send_error(403, "Invalid or missing token")
340
+ return
341
+ if not _check_origin(self):
342
+ self.send_error(403, "Cross-origin request blocked")
343
+ return
344
+ html = load_ui_html()
345
+ self.send_response(200)
346
+ self.send_header("Content-Type", "text/html; charset=utf-8")
347
+ self.end_headers()
348
+ self.wfile.write(html.encode("utf-8"))
349
+ elif path == "/api/context":
350
+ if not _check_token(self):
351
+ self.send_error(403, "Invalid or missing token")
352
+ return
353
+ if not _check_origin(self):
354
+ self.send_error(403, "Cross-origin request blocked")
355
+ return
356
+ session = _current_session
357
+ ctx = {
358
+ "prompt": session.prompt if session else "",
359
+ "config": session.config if session else {},
360
+ "threadId": session.thread_id if session else "",
361
+ "history": session.history if session else [],
362
+ }
363
+ self.send_response(200)
364
+ self.send_header("Content-Type", "application/json; charset=utf-8")
365
+ self.end_headers()
366
+ self.wfile.write(json.dumps(ctx, ensure_ascii=False).encode("utf-8"))
367
+ else:
368
+ self.send_error(404)
369
+
370
+ def do_POST(self):
371
+ path = self._get_clean_path()
372
+ if path == "/api/submit":
373
+ if not _check_token(self):
374
+ self.send_error(403, "Invalid or missing token")
375
+ return
376
+ if not _check_origin(self):
377
+ self.send_error(403, "Cross-origin request blocked")
378
+ return
379
+ length = int(self.headers.get("Content-Length", 0))
380
+ body = self.rfile.read(length).decode("utf-8")
381
+ try:
382
+ data = json.loads(body)
383
+ except json.JSONDecodeError:
384
+ self.send_error(400, "Invalid JSON")
385
+ return
386
+ response_text = data.get("response", "").strip()
387
+ if not response_text:
388
+ self.send_error(400, "Empty response")
389
+ return
390
+ session = _current_session
391
+ if session:
392
+ identity_error = validate_reviewer_identity(response_text, session.config)
393
+ if identity_error:
394
+ # JSON, not send_error's HTML page: the browser shows this text
395
+ # verbatim so a rejected paste is fixable in place.
396
+ payload = json.dumps({"error": identity_error}).encode("utf-8")
397
+ self.send_response(400)
398
+ self.send_header("Content-Type", "application/json; charset=utf-8")
399
+ self.send_header("Content-Length", str(len(payload)))
400
+ self.end_headers()
401
+ self.wfile.write(payload)
402
+ return
403
+ session.response = response_text
404
+ session.done.set()
405
+ self.send_response(200)
406
+ self.send_header("Content-Type", "application/json; charset=utf-8")
407
+ self.end_headers()
408
+ self.wfile.write(b'{"ok":true}')
409
+ else:
410
+ self.send_error(404)
411
+
412
+ def do_OPTIONS(self):
413
+ self.send_error(403, "CORS not allowed")
414
+
415
+
416
+ FILE_MODE_WARNING = """# ARIS Manual Review - Cross-Model Warning
417
+
418
+ Use a reviewer from a DIFFERENT model family than the executor. A same-family response cannot satisfy an ARIS acceptance gate.
419
+
420
+ 请使用与执行器不同模型家族的评审模型;同家族回复不能通过 ARIS 验收门。
421
+
422
+ ---
423
+
424
+ """
425
+
426
+
427
+ def file_mode_warning(config: dict) -> str:
428
+ header = FILE_MODE_WARNING
429
+ if config.get("require_reviewer_model"):
430
+ executor_model = str(config.get("executor_model") or "unknown")
431
+ header += (
432
+ f"Executor model: `{executor_model}` (derived family: `{model_family(executor_model)}`).\n\n"
433
+ "The response MUST begin with `Reviewer-Model: <exact-model-id>`.\n\n---\n\n"
434
+ )
435
+ return header
436
+
437
+
438
+ def wait_for_browser_response(prompt: str, config: dict, thread_id: str,
439
+ history: list, cancel_event: threading.Event,
440
+ cancel_reason: str) -> tuple[str | None, str | None]:
441
+ global _current_session, _active_server, _auth_token
442
+
443
+ # Cleanup any leftover server from a previous interrupted call
444
+ with _active_server_lock:
445
+ if _active_server is not None:
446
+ try:
447
+ _active_server.shutdown()
448
+ _active_server.server_close()
449
+ except Exception:
450
+ pass
451
+ _active_server = None
452
+ session = _ReviewSession(prompt, config, thread_id, history)
453
+ _current_session = session
454
+ _auth_token = _generate_token()
455
+
456
+ # Try fixed port, increment on conflict
457
+ server = None
458
+ port = DEFAULT_PORT
459
+ for attempt in range(MAX_PORT_ATTEMPTS):
460
+ try:
461
+ socketserver.TCPServer.allow_reuse_address = True
462
+ srv = socketserver.TCPServer(("127.0.0.1", port), _ReviewHandler)
463
+ server = srv
464
+ break
465
+ except OSError:
466
+ port += 1
467
+ if server is None:
468
+ _current_session = None
469
+ return None, f"Could not bind to any port in range {DEFAULT_PORT}-{DEFAULT_PORT + MAX_PORT_ATTEMPTS - 1}"
470
+
471
+ with _active_server_lock:
472
+ _active_server = server
473
+
474
+ url = f"http://127.0.0.1:{port}?token={_auth_token}"
475
+
476
+ write_pending_state(url=url, thread_id=thread_id, prompt_file=None)
477
+ debug_log(f"HTTP server started on {url}")
478
+
479
+ server_thread = threading.Thread(target=server.serve_forever, daemon=True)
480
+ server_thread.start()
481
+
482
+ if AUTO_OPEN:
483
+ try:
484
+ webbrowser.open(url)
485
+ except Exception:
486
+ pass
487
+
488
+ response: str | None = None
489
+ error: str | None = None
490
+
491
+ try:
492
+ # Poll with short intervals; check cancel BEFORE success
493
+ deadline = time.monotonic() + DEFAULT_TIMEOUT_SEC
494
+ while time.monotonic() < deadline:
495
+ if cancel_event.is_set():
496
+ error = cancel_reason
497
+ break
498
+ if session.done.wait(timeout=1.0):
499
+ response = session.response
500
+ break
501
+
502
+ if error is None and response is None:
503
+ if cancel_event.is_set():
504
+ error = cancel_reason
505
+ elif not session.done.is_set():
506
+ error = f"Timed out after {DEFAULT_TIMEOUT_SEC}s waiting for manual review response"
507
+ finally:
508
+ # Worker thread owns cleanup: server, session, pending state
509
+ try:
510
+ server.shutdown()
511
+ except Exception:
512
+ pass
513
+ try:
514
+ server.server_close()
515
+ except Exception:
516
+ pass
517
+ with _active_server_lock:
518
+ if _active_server is server:
519
+ _active_server = None
520
+ if _current_session is session:
521
+ _current_session = None
522
+ clear_pending_state(thread_id)
523
+
524
+ return response, error
525
+
526
+
527
+ # --- File mode: prompt.md / response.md ---
528
+
529
+ def wait_for_file_response(prompt: str, config: dict, thread_id: str,
530
+ history: list, cancel_event: threading.Event,
531
+ cancel_reason: str) -> tuple[str | None, str | None]:
532
+ pdir = _pending_dir_for(thread_id)
533
+ pdir.mkdir(parents=True, exist_ok=True)
534
+ prompt_path = pdir / "prompt.md"
535
+ response_path = pdir / "response.md"
536
+ identity_error = None
537
+
538
+ # Clean up any stale response file
539
+ if response_path.exists():
540
+ response_path.unlink()
541
+
542
+ # Write prompt with cross-model warning
543
+ header = file_mode_warning(config)
544
+ header += f"<!-- thread: {thread_id} | config: {json.dumps(config)} -->\n\n"
545
+ if history:
546
+ header += "## Previous Exchanges\n\n"
547
+ for i, ex in enumerate(history):
548
+ header += f"### {'Prompt' if ex['role'] == 'user' else 'Response'} (Round {i // 2 + 1})\n\n"
549
+ header += ex["content"][:500] + ("..." if len(ex["content"]) > 500 else "") + "\n\n"
550
+ header += "---\n\n## Current Prompt\n\n"
551
+ # Atomic write (tmp + rename): watchers poll for prompt.md by NAME, and a
552
+ # bare write_text has an exists-but-empty window between create and flush —
553
+ # a reader (human tooling or the test suite) that wins that race sees "".
554
+ # os.replace is atomic on POSIX/Windows: the name appears only with full content.
555
+ prompt_tmp = pdir / ".prompt.md.tmp"
556
+ prompt_tmp.write_text(header + prompt, encoding="utf-8")
557
+ os.replace(prompt_tmp, prompt_path)
558
+
559
+ write_pending_state(url=None, thread_id=thread_id, prompt_file=str(prompt_path))
560
+ debug_log(f"File mode: prompt written to {prompt_path}, waiting for {response_path}")
561
+
562
+ # Poll for response file with stability check
563
+ deadline = time.monotonic() + DEFAULT_TIMEOUT_SEC
564
+ prev_content: str | None = None
565
+ response: str | None = None
566
+ error: str | None = None
567
+
568
+ try:
569
+ while time.monotonic() < deadline:
570
+ if cancel_event.is_set():
571
+ error = cancel_reason
572
+ break
573
+
574
+ time.sleep(FILE_POLL_INTERVAL_SEC)
575
+
576
+ if cancel_event.is_set():
577
+ error = cancel_reason
578
+ break
579
+
580
+ if not response_path.exists():
581
+ prev_content = None
582
+ continue
583
+ try:
584
+ content = response_path.read_text(encoding="utf-8").strip()
585
+ except OSError:
586
+ prev_content = None
587
+ continue
588
+ if not content:
589
+ prev_content = None
590
+ continue
591
+ if content == prev_content:
592
+ identity_error = validate_reviewer_identity(content, config)
593
+ if identity_error:
594
+ error = identity_error
595
+ break
596
+ response = content
597
+ break
598
+ prev_content = content
599
+ time.sleep(FILE_STABLE_INTERVAL_SEC)
600
+
601
+ if cancel_event.is_set():
602
+ error = cancel_reason
603
+ break
604
+
605
+ try:
606
+ content2 = response_path.read_text(encoding="utf-8").strip()
607
+ except OSError:
608
+ prev_content = None
609
+ continue
610
+ if content2 == content and content2:
611
+ identity_error = validate_reviewer_identity(content2, config)
612
+ if identity_error:
613
+ error = identity_error
614
+ break
615
+ response = content2
616
+ break
617
+ prev_content = content2
618
+
619
+ if error is None and response is None and not cancel_event.is_set():
620
+ error = f"Timed out after {DEFAULT_TIMEOUT_SEC}s waiting for {response_path}"
621
+ finally:
622
+ if identity_error is not None and response_path.exists():
623
+ # The paste is almost right — only its first line is wrong. Park it under
624
+ # a name the next round will not clear (a retried review_reply reuses this
625
+ # same directory and unlinks response.md), so the text survives to be
626
+ # corrected and re-pasted. A second rejection replaces this file: the
627
+ # newest attempt is the one worth keeping, and rotating copies would be
628
+ # scaffolding for a case nobody wants back.
629
+ response_path.replace(pdir / "response.rejected.md")
630
+ clear_pending_state(thread_id, keep_thread_dir=identity_error is not None)
631
+
632
+ return response, error
633
+
634
+
635
+
636
+ # --- Unified dispatch ---
637
+
638
+ def do_review(prompt: str, config: dict, thread_id: str, history: list,
639
+ cancel_event: threading.Event, cancel_reason: str) -> tuple[str | None, str | None]:
640
+ if MODE == "file":
641
+ return wait_for_file_response(prompt, config, thread_id, history, cancel_event, cancel_reason)
642
+ return wait_for_browser_response(prompt, config, thread_id, history, cancel_event, cancel_reason)
643
+
644
+
645
+ # --- MCP tool handlers ---
646
+
647
+ def tool_success(request_id: Any, payload: dict[str, Any]) -> dict[str, Any]:
648
+ return {
649
+ "jsonrpc": "2.0",
650
+ "id": request_id,
651
+ "result": {
652
+ "content": [{"type": "text", "text": json.dumps(payload, ensure_ascii=False)}],
653
+ },
654
+ }
655
+
656
+
657
+ def tool_error(request_id: Any, message: str) -> dict[str, Any]:
658
+ return {
659
+ "jsonrpc": "2.0",
660
+ "id": request_id,
661
+ "result": {
662
+ "content": [{"type": "text", "text": json.dumps({"error": message}, ensure_ascii=False)}],
663
+ "isError": True,
664
+ },
665
+ }
666
+
667
+
668
+ def handle_review(args: dict, request_id: Any, cancel_event: threading.Event,
669
+ cancel_reason: str) -> dict[str, Any]:
670
+ prompt = str(args.get("prompt", "")).strip()
671
+ if not prompt:
672
+ return tool_error(request_id, "prompt is required")
673
+ config = args.get("config", {})
674
+ if not isinstance(config, dict):
675
+ config = {}
676
+
677
+ thread_id = create_thread()
678
+ append_exchange(thread_id, "user", prompt)
679
+
680
+ response, error = do_review(prompt, config, thread_id, [], cancel_event, cancel_reason)
681
+ if error:
682
+ return tool_error(request_id, error)
683
+
684
+ append_exchange(thread_id, "assistant", response)
685
+ return tool_success(request_id, {"threadId": thread_id, "content": response})
686
+
687
+
688
+ def handle_review_reply(args: dict, request_id: Any, cancel_event: threading.Event,
689
+ cancel_reason: str) -> dict[str, Any]:
690
+ thread_id = str(args.get("threadId", "")).strip()
691
+ if not thread_id:
692
+ return tool_error(request_id, "threadId is required")
693
+ if thread_id not in _threads:
694
+ return tool_error(request_id, f"Unknown threadId: {thread_id}")
695
+
696
+ prompt = str(args.get("prompt", "")).strip()
697
+ if not prompt:
698
+ return tool_error(request_id, "prompt is required")
699
+ config = args.get("config", {})
700
+ if not isinstance(config, dict):
701
+ config = {}
702
+
703
+ history = get_history(thread_id)
704
+ append_exchange(thread_id, "user", prompt)
705
+
706
+ response, error = do_review(prompt, config, thread_id, history, cancel_event, cancel_reason)
707
+ if error:
708
+ return tool_error(request_id, error)
709
+
710
+ append_exchange(thread_id, "assistant", response)
711
+ return tool_success(request_id, {"threadId": thread_id, "content": response})
712
+
713
+
714
+ # --- MCP request router ---
715
+
716
+ def handle_request(request: dict[str, Any]) -> dict[str, Any] | None:
717
+ request_id = request.get("id")
718
+ method = request.get("method", "")
719
+ params = request.get("params", {})
720
+
721
+ if request_id is None:
722
+ return None
723
+
724
+ if method == "initialize":
725
+ return {
726
+ "jsonrpc": "2.0",
727
+ "id": request_id,
728
+ "result": {
729
+ "protocolVersion": "2024-11-05",
730
+ "capabilities": {"tools": {}},
731
+ "serverInfo": {"name": SERVER_NAME, "version": "0.1.0"},
732
+ },
733
+ }
734
+
735
+ if method == "ping":
736
+ return {"jsonrpc": "2.0", "id": request_id, "result": {}}
737
+
738
+ if method in {"resources/list", "resources/templates/list"}:
739
+ return {"jsonrpc": "2.0", "id": request_id, "result": {"resources": []}}
740
+
741
+ if method in {"notifications/initialized", "initialized"}:
742
+ return {"jsonrpc": "2.0", "id": request_id, "result": {}}
743
+
744
+ if method == "tools/list":
745
+ return {
746
+ "jsonrpc": "2.0",
747
+ "id": request_id,
748
+ "result": {
749
+ "tools": [
750
+ {
751
+ "name": "review",
752
+ "description": "Start a new manual review session. Opens a browser page where the user copies the prompt to any AI model and pastes the response back.",
753
+ "inputSchema": {
754
+ "type": "object",
755
+ "properties": {
756
+ "prompt": {"type": "string", "description": "The full review prompt to show the user"},
757
+ "config": {"type": "object", "description": "Config hints (e.g. model_reasoning_effort)"},
758
+ },
759
+ "required": ["prompt"],
760
+ },
761
+ },
762
+ {
763
+ "name": "review_reply",
764
+ "description": "Continue a review conversation in an existing thread. Shows previous exchanges for context.",
765
+ "inputSchema": {
766
+ "type": "object",
767
+ "properties": {
768
+ "threadId": {"type": "string", "description": "Thread ID from a previous review call"},
769
+ "prompt": {"type": "string", "description": "Follow-up prompt"},
770
+ "config": {"type": "object", "description": "Config hints"},
771
+ },
772
+ "required": ["threadId", "prompt"],
773
+ },
774
+ },
775
+ ],
776
+ },
777
+ }
778
+
779
+ if method == "tools/call":
780
+ name = params.get("name", "")
781
+ args = params.get("arguments", {})
782
+ if not isinstance(args, dict):
783
+ return tool_error(request_id, "tool arguments must be an object")
784
+ if name == "review":
785
+ return handle_review(args, request_id, threading.Event(), "")
786
+ if name == "review_reply":
787
+ return handle_review_reply(args, request_id, threading.Event(), "")
788
+ return tool_error(request_id, f"unknown tool: {name}")
789
+
790
+ return {
791
+ "jsonrpc": "2.0",
792
+ "id": request_id,
793
+ "error": {"code": -32601, "message": f"Unknown method: {method}"},
794
+ }
795
+
796
+
797
+ # --- Main loop (non-blocking, fail-fast for concurrent calls) ---
798
+
799
+ def _run_blocking_tool(handler, args, request_id, pending: _PendingCall):
800
+ """Run a blocking tool handler in background, send response when done."""
801
+ global _pending_call
802
+ try:
803
+ response = handler(args, request_id, pending.cancel_event, pending.cancel_reason)
804
+ if not pending.suppress_response and not pending.cancel_event.is_set():
805
+ # Clear _pending_call BEFORE sending so the next request doesn't
806
+ # fail-fast on a thread that's about to exit
807
+ if _pending_call is pending:
808
+ _pending_call = None
809
+ send_response(response)
810
+ except Exception:
811
+ debug_log(f"Unhandled exception in tool handler for request {request_id}")
812
+ if _pending_call is pending:
813
+ _pending_call = None
814
+ if not pending.suppress_response and not pending.cancel_event.is_set():
815
+ send_response(tool_error(request_id, "Internal error in manual review handler"))
816
+ finally:
817
+ if _pending_call is pending:
818
+ _pending_call = None
819
+
820
+
821
+ def _cancel_active_call(request_id: Any, reason: str) -> None:
822
+ """Cancel the active pending call. Only sets signals — the worker thread's
823
+ finally block handles server shutdown, port release, and pending state cleanup."""
824
+ global _pending_call, _current_session, _active_server
825
+ if _pending_call is None:
826
+ return
827
+ if _pending_call.request_id != request_id:
828
+ debug_log(f"Ignoring cancel for request {request_id}; active is {_pending_call.request_id}")
829
+ return
830
+ _pending_call.cancel_reason = reason
831
+ _pending_call.suppress_response = True
832
+ _pending_call.cancel_event.set()
833
+ # Unblock browser wait loop
834
+ if _current_session is not None:
835
+ _current_session.done.set()
836
+ # Shut down HTTP server to unblock serve_forever (worker handles server_close)
837
+ with _active_server_lock:
838
+ if _active_server is not None:
839
+ try:
840
+ _active_server.shutdown()
841
+ except Exception:
842
+ pass
843
+ # Worker thread owns cleanup; do not call clear_pending_state or server_close here
844
+
845
+
846
+ def main() -> int:
847
+ global _pending_call
848
+ _init_stdio()
849
+ debug_log(f"Server starting: mode={MODE}, timeout={DEFAULT_TIMEOUT_SEC}s")
850
+ while True:
851
+ request = read_message()
852
+ if request is None:
853
+ return 0
854
+
855
+ request_id = request.get("id")
856
+ method = request.get("method", "")
857
+ params = request.get("params", {})
858
+
859
+ # Handle cancellation notification (MCP spec)
860
+ if method == "notifications/cancelled":
861
+ try:
862
+ cancelled_request_id = params.get("requestId")
863
+ reason = params.get("reason", "Client cancelled request")
864
+ _cancel_active_call(cancelled_request_id, reason)
865
+ except Exception:
866
+ debug_log("Exception during cancel notification (ignored)")
867
+ continue # notification — no response
868
+
869
+ # For tool calls that block (review, review_reply), run in background
870
+ if method == "tools/call":
871
+ name = params.get("name", "")
872
+ if name in ("review", "review_reply"):
873
+ # Fail-fast: if another review is already active, reject
874
+ if _pending_call is not None:
875
+ if _pending_call.thread is not None and _pending_call.thread.is_alive():
876
+ send_response(tool_error(
877
+ request_id,
878
+ "Another manual review is already in progress. "
879
+ "Finish it in the browser/file response path, "
880
+ "or cancel the previous tool call before starting a new one.",
881
+ ))
882
+ continue
883
+ else:
884
+ # Stale reference — clean up
885
+ _pending_call = None
886
+
887
+ args = params.get("arguments", {})
888
+ if not isinstance(args, dict):
889
+ send_response(tool_error(request_id, "tool arguments must be an object"))
890
+ continue
891
+
892
+ pending = _PendingCall(request_id)
893
+ _pending_call = pending
894
+ handler = handle_review if name == "review" else handle_review_reply
895
+ pending.thread = threading.Thread(
896
+ target=_run_blocking_tool,
897
+ args=(handler, args, request_id, pending),
898
+ daemon=True,
899
+ )
900
+ pending.thread.start()
901
+ continue
902
+
903
+ # Non-blocking requests handled synchronously
904
+ response = handle_request(request)
905
+ if response is not None:
906
+ send_response(response)
907
+
908
+
909
+ if __name__ == "__main__":
910
+ raise SystemExit(main())